G-gen の佐々木です。当記事では、Cloud Run の最小インスタンス数を、特定の時間帯で自動的にスケーリングさせる処理を実装していきます。
前提知識
Cloud Run について
Cloud Run には Cloud Run services と Cloud Run jobs、そして Cloud Functions から統合された Cloud Run functions の3種類がありますが、当記事における Cloud Run は Cloud Run services を指すものとし、また Cloud Run functions についてはそのまま Cloud Run functions と記載します。
当記事は Cloud Run の応用的な使い方を解説するため、Cloud Run そのものに関する解説はしません。
Cloud Run の解説については以下の記事をご一読ください。
Cloud Run のコールドスタート
最小インスタンス数を0に設定した Cloud Run では、リクエストを受信するとコンテナインスタンスが起動し、処理を行います。
このように最小インスタンス数が0の場合、リクエストがない間は CPU、メモリなどのコンピュートリソースを使用しないため、リソース使用量に応じた料金が発生せず、コストを非常に安く抑えることができます。
その反面、常にコンピュートリソースを使用する場合と比べて、インスタンスを起動する時間だけレイテンシが発生します。これをコールドスタートといいます。
Cloud Run で実行しているアプリケーションでコールドスタートによるレイテンシが許容できない場合、最小インスタンス数を1以上に設定し、リクエストを処理できるコンピュートリソースを常に確保しておきます。
この場合、当然ながらコンピュートリソースの料金は、最小インスタンスの数だけ常に発生してしまいます。
また、コールドスタートを回避できるのは、最小インスタンス数として設定した数のコンテナインスタンス(常時起動されているインスタンス)だけです。
リクエストの増加によりコンテナインスタンスのスケールアウトが起こると、新たに起動したコンテナインスタンスに送信されたリクエストはコールドスタートの影響を受けます。
したがって、多くのリクエストを処理しなければならない、かつコールドスタートが許容できないアプリケーションでは、予測されるリクエストの規模に応じて最小インスタンス数を調整する(もしくは Cloud Run を使用しない)ことになります。
このように、Cloud Run におけるコストの削減とコールドスタートの回避はトレードオフの関係にあり、アプリケーション実行基盤として Cloud Run を選択する際のノックアウトファクターとなり得ます。
サービスレベルの最小インスタンス数の設定
従来の Cloud Run では、最小インスタンス数はリビジョンレベルで設定を行い、最小インスタンスを変更するたびに新たなリビジョンをデプロイする必要がありました。
しかし、2024年3月のアップデートにて、サービスレベルの最小インスタンス数の設定として、新たなリビジョンをデプロイすることなく最小インスタンス数の設定を変更できるようになりました。
構成
当記事では、Cloud Run の最小インスタンス数に対して、スケジュールベースの自動スケーリングを行う処理を実装していきます。
ユースケースとして、特定の時間帯にリクエストが増加することがわかっているサービスを想定します。
リクエストが増加する時間帯は Cloud Run の最小インスタンス数を増加させることでコールドスタートの影響を無くし、リクエストが少ない時間帯は最小インスタンス数を0にしてコストを節約します。
当記事では、以下のように時間帯によって最小インスタンス数を変更するようにします。
この場合、スケールアウトは 12:00
、スケールインは 19:00
に行います。
時間帯 | 最小インスタンス数 |
---|---|
0:00~12:00 | 0 |
12:00~19:00 | 3 |
19:00~0:00 | 0 |
実装には、以下のサービスを使用していきます。
スケーリングの対象となる Cloud Run サービスのデプロイ
まず、スケジュールベースのスケーリングを設定する対象となる Cloud Run サービスを作成します。
当記事では、このサービス自体の処理内容は重要ではないので、Google Cloud が提供するサンプルのコンテナイメージを使用してデプロイします。
# スケーリングの対象となる Cloud Run サービスを作成 $ gcloud run deploy hello \ --image us-docker.pkg.dev/cloudrun/container/hello \ --region=${LOCATION} \ --allow-unauthenticated
このコマンドでサービスをデプロイした場合、サービス名は hello
となります。
最小インスタンス数は、デフォルトで0に設定されています。
各種ファイルの準備
作成するファイルについて
当記事では、ワーキングディレクトリに以下のファイルを作成していきます。
. ├── scalein.json ├── scaleout.json └── src ├── main.py └── requirements.txt
Cloud Run functions にデプロイするファイル
ディレクトリの作成
Cloud Run functions にデプロイする各種ファイルを配置するディレクトリを作成します。
ディレクトリ名はなんでもよいですが、当記事では src
とします。
# src ディレクトリを作成 $ mkdir src
main.py
当記事では Python 3.12
を使用していきます。
# Python のバージョン $ python -V Python 3.12.0
src
ディレクトリ内に main.py
ファイルを作成します。
ファイルの中身は以下のようにします。
# src/main.py from google.cloud.run_v2.services.services import ServicesClient import functions_framework import base64, json client = ServicesClient() @functions_framework.cloud_event def update_service(cloud_event): # Cloud Scheduler から送信されてきたメッセージを処理 data = json.loads(base64.b64decode(cloud_event.data["message"]["data"]).decode()) project_id = data["project_id"] location = data["location"] service_name = data["service_name"] min_instance_count = int(data["min_instance_count"]) # スケーリング対象の Cloud Run サービスの情報を取得 service = client.get_service(name=f"projects/{project_id}/locations/{location}/services/{service_name}") # Cloud Run サービスの最小インスタンス数を更新 service.scaling.min_instance_count = min_instance_count # リビジョンを更新せず、サービスに対して最小インスタンス数を設定 # Cloud Run サービスの更新 client.update_service(service=service)
main.py
には、Cloud Scheduler から(Pub/Sub を介して)スケーリング対象の Cloud Run サービスの名前や、スケーリング後の最小インスタンス数などの情報を受け取り、それを元に最小インスタンス数を変更する処理を実装します。
requirements.txt
src
ディレクトリ内に requirements.txt
を作成し、使用する外部ライブラリを記載します。
当記事では以下のライブラリを使用していきます。
# src/requirements.txt functions-framework==3.5.0 google-cloud-run==0.10.5
Cloud Scheduler で使用するファイル
Cloud Scheduler のジョブを作成する際に使用する、Cloud Run functions に送信するメッセージを記述したファイルを作成します。
最小インスタンス数のスケールアウトとスケールインのそれぞれに対応したファイルを作成していきます。
scaleout.json
ワーキングディレクトリ内に scaleout.json
を作成し、以下のように記述します。
{ "project_id":"{Cloud Runが存在するプロジェクトID}", "location":"asia-northeast1", "service_name":"hello", "min_instance_count":"3" }
このメッセージを受信した Cloud Run functions は、対象となる Cloud Run サービスの最小インスタンス数を 3
に変更します。
scalein.json
ワーキングディレクトリ内に scalein.json
を作成し、以下のように記述します。
{ "project_id":"{Cloud Runが存在するプロジェクトID}", "location":"asia-northeast1", "service_name":"hello", "min_instance_count":"0" }
このメッセージを受信した Cloud Run functions は、対象となる Cloud Run サービスの最小インスタンス数を 0
に変更します。
各種サービスの作成
シェル変数の設定
シェル変数として、PROJECT
変数にサービスを作成するプロジェクト、 LOCATION
変数にサービスを作成するリージョンを設定しておきます。
当記事では asia-northeast1
リージョンを使用していきます。
PROJECT=myproject LOCATION=asia-northeast1
Pub/Sub トピック
Cloud Scheduler からのメッセージを中継する Pub/Sub のトピックを作成します。
当記事では topic-run-scaler
という名前で作成します。
# Pub/Sub トピックの作成
$ gcloud pubsub topics create topic-run-scaler
サービスアカウント
Cloud Run functions 用サービスアカウント
Cloud Run functions に紐付けるサービスアカウントを作成します。
当記事では sa-run-scaler
という名前で作成します。
# Cloud Run functions 用サービスアカウントの作成
$ gcloud iam service-accounts create sa-run-scaler
サービスアカウントに対して、Cloud Run の設定を変更するための roles/run.developer
ロールを設定します。
# サービスアカウントに Cloud Run の編集権限を付与 $ gcloud run services add-iam-policy-binding hello \ --region=${LOCATION} \ --member="serviceAccount:sa-run-scaler@${PROJECT}.iam.gserviceaccount.com" \ --role="roles/run.developer"
また、Cloud Run functions のコードからサービスアカウントの認証情報を取得できるように、roles/iam.serviceAccountUser
ロールも設定します。
# サービスアカウントにサービスアカウントユーザー権限を付与 $ gcloud projects add-iam-policy-binding ${PROJECT} \ --member="serviceAccount:sa-run-scaler@${PROJECT}.iam.gserviceaccount.com" \ --role="roles/iam.serviceAccountUser"
Pub/Sub 用サービスアカウント
Cloud Scheduler と Cloud Run functions を中継する Pub/Sub 用のサービスアカウントを作成します。
このサービスアカウントには Cloud Run を呼び出すための権限を付与しますが、対象の Cloud Run がまだ作成されていないので、一旦は作成だけしておきます。
当記事では sa-run-scaler-trigger
という名前で作成します。
# Pub/Sub 用 サービスアカウントの作成
$ gcloud iam service-accounts create sa-run-scaler-trigger
Cloud Run functions
src
ディレクトリ内のファイルを使用して Cloud Run functions の関数をデプロイします。関数の名前は run-scacler
とします。
関数のトリガーとして先ほど作成した Pub/Sub トピックを設定し、Pub/Sub 用に作成したサービスアカウントも --trigger-service-account
として指定します。
# Cloud Run functions のデプロイ $ gcloud functions deploy run-scaler \ --gen2 \ --runtime=python312 \ --entry-point="update_service" \ --region=${LOCATION} \ --source=./src \ --run-service-account="sa-run-scaler@${PROJECT}.iam.gserviceaccount.com" \ --trigger-topic=topic-run-scaler \ --trigger-service-account="sa-run-scaler-trigger@${PROJECT}.iam.gserviceaccount.com"
Cloud Run functions のデプロイが完了したら、Pub/Sub 用のサービスアカウントに Cloud Run functions を呼び出す権限を付与します。
gCloud Run functions add-invoker-policy-binding
コマンドを使用することで、必要な権限を付与することができます。
# サービスアカウントに Cloud Run functions の起動元権限を付与 $ gcloud functions add-invoker-policy-binding run-scaler \ --region=${LOCATION} \ --member="serviceAccount:sa-run-scaler-trigger@${PROJECT}.iam.gserviceaccount.com"
Cloud Scheduler ジョブ
作成するジョブについて
Cloud Run functions のトリガーとなる Cloud Scheduler ジョブを作成していきます。
スケールアウトとスケールインで異なる設定値が必要になるため、ジョブは2つ作成します。
Cloud Run functions は Cloud Scheduler から送信されたメッセージの内容に応じた処理を行うため、各ジョブは同一の Pub/Sub を経由して、同一の Cloud Run functions 関数にメッセージを送ります。
スケールアウト用ジョブ
スケールアウト用のジョブは scaleout.json
ファイルに記述したメッセージを送信するように設定します。
このメッセージを受信した Cloud Run functions は、対象となる Cloud Run サービスの最小インスタンス数を 3
に変更します。
ジョブの実行タイミングは --schedule
に unix-cron 文字列形式で設定します。
当記事では、毎日12時(0 12 * * *
)のタイミングでスケールアウトを実行するようにします。
# Cloud Run のスケールアウト用のジョブをトリガーするスケジュール $ gcloud scheduler jobs create pubsub schedule-run-scaler-scaleout \ --location=${LOCATION} \ --schedule="0 12 * * *" \ --topic=topic-run-scaler \ --message-body-from-file=./scaleout.json \ --time-zone="Asia/Tokyo"
スケールイン用ジョブ
スケールイン用のジョブは scalein.json
を使用して作成します。
このファイルに記述したメッセージを受信した Cloud Run functions は、対象となる Cloud Run サービスの最小インスタンス数を 0
に変更します。
ジョブの実行タイミングは、毎日19時(0 19 * * *
)に設定します。
# Cloud Run のスケールイン用のジョブをトリガーするスケジュール $ gcloud scheduler jobs create pubsub schedule-run-scaler-scalein \ --location=${LOCATION} \ --schedule="0 19 * * *" \ --topic=topic-run-scaler \ --message-body-from-file=./scalein.json \ --time-zone="Asia/Tokyo"
動作確認
Cloud Run のコンソールから「コンテナ インスタンス数」の指標を確認すると、設定した時間にジョブが実行され、Cloud Run サービスの最小インスタンス数の変更が行われていることがわかります。
12:00 にスケールアウトのジョブが実行され、最小インスタンス数が3に設定されています。
3つのコンテナインスタンスが常に idle もしくは active になっており、この3つで処理できるリクエスト量であれば、コールドスタートの影響を受けることはありません。
19:00 にスケールインのジョブが実行され、翌日の 12:00 までは最小インスタンス数が 0 になります。
idle もしくは active 状態のインスタンスがないときにリクエストがあると、そのリクエストに対する処理はコールドスタートの影響を受けますが、コンピュートリソースの使用コストを最小限に抑えることができます。
「課金対象のコンテナ インスタンス時間」の指標を確認すると、最小インスタンス数が3(1以上)になっている時間帯のみ、インスタンスのリソース使用料が発生し続けていることがわかります。
佐々木 駿太 (記事一覧)
G-gen最北端、北海道在住のクラウドソリューション部エンジニア
2022年6月にG-genにジョイン。Google Cloud Partner Top Engineer 2025 Fellowに選出。好きなGoogle CloudプロダクトはCloud Run。
趣味はコーヒー、小説(SF、ミステリ)、カラオケなど。
Follow @sasashun0805