メインコンテンツへスキップ
友田 陽大
Google Cloud Run 本番運用
GCP
Cloud Run
サーバーレス
コンテナ
インフラ
コスト最適化
可観測性
Terraform

Google Cloud Run 本番運用ガイド:コンテナ契約・並行性・オートスケール・デプロイ・コスト・セキュリティを実コードで

Google Cloud公式ドキュメントに忠実なCloud Runの本番運用ガイド。コンテナ契約(PORT/SIGTERM)、並行性(既定80・最大1000)、スケールトゥゼロ、リクエスト課金とインスタンス課金、リビジョンによるトラフィック分割(Blue/Green・カナリア)、ヘルスチェック、最小権限サービスアカウントとSecret Manager、Direct VPC egressまでを、gcloud・Terraform・FastAPI/Nodeの実コードで体系化します。

公開日
読了時間
25分
著者
友田 陽大
シェア
目次

「コンテナは本番で動かしたい。でもKubernetesクラスタのノード管理やパッチ当てに時間を割けない」——スタートアップや少人数開発でGCP上にコンテナ基盤を組むとき、ほぼ必ずここに行き着きます。その答えが Google Cloud Run です。

私は実際に、国内大手放送事業者の社内AIプラットフォームをGCP上にTerraformでIaC構築し、その本番運用を担ってきました(ケーススタディ)。FastAPIのAPI群、放送品質の音声合成、テロップ誤字検出のOCR×音声認識パイプライン、アップロード素材のClamAVマルウェアスキャナ——これらを Cloud Run のサービスとジョブで動かし、データは Cloud SQL / Memorystore / Firestore / Cloud Storage に集約、入口は Cloud Armor、CI/CD は Workload Identity Federation で鍵レス、本番リージョンは常時1インスタンスを温め副リージョンはゼロスケールでDR——という構成で、専用VMもKubernetesも持たずに回しています。

この記事は、Cloud Run公式ドキュメントに忠実でありながら、公式よりわかりやすく、かつ「どの場面でどう使うか」を実コードで示すことを目的にしています。コンテナ契約・リソース設計・並行性・スケール・デプロイ・回復性・セキュリティ・コストまで、本番に出すために必要なことを一気通貫で扱います。

技術選定そのもの(Cloud Run か GKE か App Engine か)は GCPコンテナ技術選定ガイド に、並行性・課金・コスト最適化の深掘りは Cloud Run オートスケール・課金・コスト最適化ガイド に分けました。本稿は 「Cloud Runを選んだ後、本番でどう作るか」 に集中します。


Cloud Run とは何か:公式の定義

公式の定義はシンプルです。

Cloud Run is a fully managed application platform for running your code, function, or container on top of Google's highly scalable infrastructure.(— What is Cloud Run

つまり Cloud Run は、サーバーの構成・OSのパッチ・オーケストレーション・スケーリングをすべてプラットフォームに任せ、コンテナを動かすことだけに集中するためのサーバーレス基盤です。重要な特徴を公式から拾うと——

  • デプロイ単位は常にコンテナイメージ。自分でビルドしてもいいし、ソースコード(Go / Node.js / Python / Java / .NET / Ruby など)を渡せば buildpacks が自動でコンテナ化します。
  • 任意の言語・バイナリが動く。公式いわく「コンテナイメージをビルドできるなら、どんな言語で書いたコードでもCloud Runにデプロイできる(You can deploy code written in any programming language on Cloud Run if you can build a container image from it)」。
  • 3つのリソース形態を持ちます。
形態役割代表的な使い所
Services(サービス)安定したHTTPSエンドポイントでリクエストを受け、トラフィックに応じて自動スケールREST/GraphQL API、Webアプリ、Webhook受け
Jobs(ジョブ)実行して完了したら止まる。手動/スケジュール起動、並列タスクバッチ、DBマイグレーション、長時間の一括処理
Worker Pools(ワーカープール)常駐するバックグラウンド処理Pub/Sub のpullサブスクライバ、Kafkaコンシューマ

本稿は主に Services を扱い、Jobs / Worker Pools は後半で「いつ使い分けるか」を示します。実プロジェクトでは、HTTP APIは Services、テロップ誤字検出やマルウェアスキャンのような重い長時間処理は Jobs、という分担で運用していました。


いつ使うか:ひと目の判断軸(詳細は技術選定ガイドへ)

「コンテナを動かす」選択肢はGCPに複数あります。深い比較は 技術選定ガイド に譲りますが、最初の判断軸だけ示します。

サービス一言でいうと選ぶべき場面
Cloud Runサーバーレスのコンテナ/マイクロサービス基盤ステートレスなコンテナをK8s運用なしで動かす。迷ったらここ。
Cloud Run functions(旧Cloud Functions)イベント駆動のFaaS単一トリガー(HTTP/Pub/Sub/Storage等)に応答する関数。Cloud Run基盤上で動く。
GKE / GKE AutopilotマネージドKubernetesDaemonSet・CRD・Operator・サービスメッシュ等のK8s固有機能が要る。
App Engine旧来のPaaS既存資産。新規はCloud Run推奨(後述)。
Compute EngineVMコンテナ化できない/OSレベルの制御やGPU常駐が要る。

公式(App Engineの移行ガイド)は新規開発について明言しています。

For new Google Cloud users, we recommend using Cloud Run as the preferred alternative over App Engine.(— Compare App Engine and Cloud Run

迷ったら Cloud Run。これが2026年のGCPにおける既定の答えです。


コンテナ契約(runtime contract):守るべき5つの約束

Cloud Runに載せるコンテナは、コンテナ契約(container runtime contract) を満たさなければなりません。ここを外すと「ローカルでは動くのに本番で起動しない」が起きます。最重要の約束を5つに整理します。

1. $PORT0.0.0.0 で待ち受ける

The ingress container within an instance must listen for requests on 0.0.0.0 on the port to which requests are sent.(— Container runtime contract

ポートは環境変数 PORT(既定 8080)で渡されます。localhost/127.0.0.1 で待ち受けると外部から到達できず起動失敗します。必ず 0.0.0.0 で、$PORT を読んで 待ち受けます。

# FastAPI(uvicorn)。PORT を読み、0.0.0.0 で待ち受ける。
import os
import uvicorn
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def root():
    return {"ok": True}

if __name__ == "__main__":
    # 既定 8080。Cloud Run は PORT を注入するので必ず環境変数から読む。
    port = int(os.environ.get("PORT", "8080"))
    uvicorn.run(app, host="0.0.0.0", port=port)
// Node.js(Express)。同じく PORT を読み、0.0.0.0 で待ち受ける。
import express from "express";

const app = express();
app.get("/", (_req, res) => res.json({ ok: true }));

const port = Number(process.env.PORT ?? 8080);
app.listen(port, "0.0.0.0", () => console.log(`listening on ${port}`));

2. ステートレスである

インスタンスはいつでも増減・破棄されます。状態(セッション・カウンタ・アップロード中ファイル)をインスタンスのメモリやローカルディスクに永続化してはいけません。状態は Cloud SQL / Memorystore / Firestore / Cloud Storage などの外部に置きます。

3. ファイルシステムはメモリである

the in-memory filesystem ... writing too much data can crash the instance.

書き込み可能なファイルシステムはインメモリで、書いた分だけインスタンスのメモリを消費します。大きな一時ファイルを書くとOOMでクラッシュします。一時データは小さく保つか、Cloud Storage にストリーミングします(後述のマルウェアスキャナはまさにこの理由で「バッファせずストリーミング検査」しています)。

4. SIGTERM を受けて10秒以内に後始末する

Before shutting down an instance, Cloud Run sends a SIGTERM signal to all the containers in an instance, indicating the start of a 10 second period before the actual shutdown occurs, at which point Cloud Run sends a SIGKILL signal.(— Container runtime contract

スケールイン・デプロイ・リビジョン切替のたびにインスタンスは落とされます。SIGTERM を受けたら、処理中リクエストの完了・接続のクローズ・バッファのフラッシュを10秒以内に終えます。詳細なコードはグレースフルシャットダウンの節で示します。

5. タイムアウト内にレスポンスを返す

レスポンスがリクエストタイムアウト(既定300秒)内に完了しないと、クライアントには 504 が返ります。長時間処理は同期HTTPで抱え込まず、ジョブやワークフローに切り離します。


最初のデプロイ:ソースから or コンテナから

最短はソースデプロイです。Dockerfile すら要りません(buildpacksが面倒を見ます)。

# ソースから直接デプロイ(buildpacksが自動でコンテナ化 → Artifact Registry → Cloud Run)
gcloud run deploy api \
  --source . \
  --region asia-northeast1 \
  --no-allow-unauthenticated   # まず認証必須で公開(後述)

# 自前ビルドのイメージからデプロイ(本番はこちらを推奨:再現性が高い)
gcloud run deploy api \
  --image asia-northeast1-docker.pkg.dev/PROJECT_ID/repo/api:GIT_SHA \
  --region asia-northeast1 \
  --no-allow-unauthenticated

--no-allow-unauthenticated を最初に付ける癖をつけてください。--allow-unauthenticated は「インターネット全体に無認証公開」です。社内ツールやサービス間呼び出しは認証必須にし、本当に公開が必要なものだけ明示的に開けます(無認証は無駄なリクエストでコストも増やします)。

本番では「イメージはCI(Cloud Build / GitHub Actions)でビルドし、Cloud Runにはタグ付きイメージをデプロイする」のが定石です。私のプロジェクトでも Terraformが『インフラ構成』、Cloud Buildが『イメージと最新env』 と責務を分け、ドリフトを防いでいました。CI側の鍵レス化は Workload Identity Federationの記事 を参照してください。


リソース設計:CPUとメモリの"組み合わせ"を理解する

CPUとメモリは独立に決められますが、CPU値ごとにメモリの上限・下限が決まっていますConfigure CPU limits)。

vCPUメモリの範囲
0.08〜512 MiB
0.5〜1 GiB
1〜4 GiB
2〜8 GiB
42〜16 GiB
64〜24 GiB
84〜32 GiB
  • vCPUは 0.08〜8。1未満は 0.001刻みの小数(例 0.25)、1以上は 1, 2, 4, 6, 8 の整数のみ。
  • 小さく始めて、メトリクスで右サイズ化するのが原則。最初から大きく取るとそのまま課金に乗ります。
gcloud run deploy api \
  --image IMAGE_URL --region asia-northeast1 \
  --cpu 1 --memory 512Mi \
  --cpu-boost           # 起動時だけCPUを増やして冷起動を速くする

起動時CPUブースト(Startup CPU boost)

--cpu-boost を付けると、インスタンス起動中だけCPUを一時的に増やします(例:1 vCPUなら起動中は2 vCPU相当)。JVMやNode/Pythonの重い初期化を持つアプリの冷起動を縮めるのに有効で、追加費用に対して効果が大きい定番設定です。

実行環境:gen1 と gen2

Cloud Runには2世代の実行環境があります(About execution environments)。

gen1gen2
基盤gVisormicroVM
冷起動速い一部のサービスでやや遅い
Linux互換多くのsyscallをエミュレート(一部非対応)完全なLinux互換
ネットワークファイルシステム×○(NFS等)
CPU/ネットワーク性能標準高速
メモリ下限512 MiB未満も可512 MiB以上
  • 既定は unspecified(プラットフォームが自動選択)
  • 冷起動最優先・軽量なAPIなら gen1完全なLinux互換・NFS・VPC egress・CPU集約ワークロードなら gen2
  • Jobs と Worker Pools は常に gen2
gcloud run deploy api --execution-environment gen2 --region asia-northeast1 ...

並行性(concurrency):1インスタンスが同時に捌くリクエスト数

Cloud Runの最重要パラメータが 並行性(concurrency) です。「1つのインスタンスが同時に何リクエストまで捌くか」を決めます。

The maximum concurrency ... is 80 (Console) / 80 times the number of vCPUs (CLI/Terraform). The maximum value is 1000.(— About concurrency

  • 既定は 80(gcloud/TerraformではvCPU数×80が上限の既定)。最大は 1000
  • 並行性が低いほど、同じ負荷を捌くのに多くのインスタンスが要る=冷起動が増え原価も上がりやすい。
  • 公式は「並行性 1 はスケール性能を著しく落とす(many instances will have to start up to handle a spike)」と明言。スパイクに弱くなります。
  • アプリがリクエストごとに大量のCPU/メモリを使うなら並行性を下げ、I/O待ちが多い(DB・外部API)なら並行性を上げて密度を稼ぐ——というチューニングになります。
gcloud run deploy api --concurrency 80 --region asia-northeast1 ...

並行性がスケールと課金をどう動かすかは、専用記事 並行性・オートスケール・課金 で原価計算まで踏み込んで解説します。ここでは「並行性は性能とコストの中心ダイヤル」とだけ覚えてください。


オートスケール:スケールトゥゼロと最小/最大インスタンス

Cloud Runはリクエストが来なければゼロまで縮み(scale to zero)、来れば自動で増えます。スケールの頭脳は——

The autoscaler ... targets ... 60% CPU utilization / 60% concurrency utilization by default.(— About instance autoscaling

  • 既定で 60% の使用率を目標にインスタンス数を調整(CPU使用率と並行使用率の両面)。
  • リクエスト処理後、最大15分(GPUは10分)はインスタンスを温存して冷起動を減らす。
  • 最小インスタンス(min instances) で常時温めて冷起動を消せる。最大インスタンス(max instances・既定100) で暴走時のコスト上限を作る。
gcloud run deploy api \
  --min-instances 1 \      # 本番の入口は1台温めて冷起動を消す
  --max-instances 10 \     # コストの安全弁。スパイクでも10台で頭打ち
  --region asia-northeast1 ...

実プロジェクトでは 本番リージョンは min-instances=1 で常時温め、副リージョンは min-instances=0(ゼロスケール)でDR用 という非対称構成にして、平常時コストを抑えつつ障害時の回復性を確保していました。「全リージョンを温める」必要はありません。

スケールの設計(冷起動対策・最小/最大の決め方・60%目標の意味)は オートスケール記事 で深掘りします。


リクエストタイムアウト:長時間処理はジョブへ

Default timeout: 5 minutes (300 seconds). Maximum timeout: 60 minutes (3,600 seconds).(— Request timeout

  • 既定300秒・最大60分--timeout 1m20s のように期間でも指定可。
  • これを超える処理(動画処理・大規模バッチ・LLMの長い推論)を同期HTTPで抱え込んではいけません。クライアントが切れても処理は止められず、リトライで多重実行も起きます。

正解は 「受付(Service)と実行(Job/Workflow)を分ける」 こと。

# 長時間処理は Cloud Run Jobs に切り出す(HTTPから切り離す)
gcloud run jobs create telop-ocr \
  --image IMAGE_URL --region asia-northeast1 \
  --tasks 10 --parallelism 5 \       # 10タスクを最大5並列で
  --max-retries 3 --task-timeout 3600s
gcloud run jobs execute telop-ocr --region asia-northeast1

私のテロップ誤字検出パイプラインは、まさにこの形でした。HTTPのAPIは「ジョブを起動して即時に受付IDを返す」だけにし、OCRと音声認識の重い処理は Cloud Run Jobs + Cloud Workflows で並列実行。進捗は Firestore のスナップショット購読+SSEでUIに準リアルタイム配信し、逐次18分→並列13分(約30%短縮) を実現しました。長時間処理は必ず「冪等・再開可能」に設計します(セグメントIDを決定的に採番し、再実行でも結果が一意に収束するように)。


ヘルスチェック:startup と liveness

Cloud Runは2種類のプローブを持ちます(Configure health checks)。

  • Startup probe(起動プローブ):起動完了を判定。成功するまでトラフィックを流さない。新規サービスは既定でコンテナポートへのTCPプローブtimeoutSeconds: 240 / periodSeconds: 240 / failureThreshold: 1)。
  • Liveness probe(生存プローブ):起動後に継続監視。失敗するとコンテナを再起動failureThreshold × periodSeconds 以内に成功しなければ SIGKILL → 新インスタンス起動)。

HTTPプローブは 2XX/3XX が成功、それ以外は失敗。アプリに /healthz を実装し、「自分が生きているか」だけを軽く返すのが基本です(重い依存先チェックを毎回やるとプローブが詰まり、巻き添えで再起動が起きます)。

from fastapi import FastAPI, Response
app = FastAPI()

@app.get("/healthz")
def liveness():
    # liveness は「自分のプロセスが応答可能か」だけを軽く返す。
    # 依存先(DB/Redis)の不調で再起動ループに入れないため、依存チェックは入れない。
    return {"status": "ok"}

Terraformでの設定例は後半のIaC節に示します。「起動が遅いアプリ」は startup probe の failure_threshold × period_seconds起動に十分な余裕まで広げるのが正解です(既定のTCPプローブはほぼ即時成功を前提にしているため)。


グレースフルシャットダウン:SIGTERMと冪等性

コンテナ契約のとおり、SIGTERMから10秒でSIGKILLです。この10秒で「処理中リクエストの完了」「コネクションのクローズ」「バッファのフラッシュ」を終えます。

# FastAPI(uvicorn)。SIGTERM を捕まえて後始末する。
import signal, asyncio, logging
from contextlib import asynccontextmanager
from fastapi import FastAPI

log = logging.getLogger("app")

@asynccontextmanager
async def lifespan(app: FastAPI):
    # 起動時:プールやクライアントを確保
    app.state.pool = await create_pool()
    yield
    # 終了時(SIGTERM経由でlifespanのshutdownが走る):確実に閉じる
    log.info("draining: closing pool within the 10s grace window")
    await app.state.pool.close()

app = FastAPI(lifespan=lifespan)
// Node.js。SIGTERM で新規受付を止め、処理中を待ってから終了。
const server = app.listen(Number(process.env.PORT ?? 8080), "0.0.0.0");

process.on("SIGTERM", () => {
  console.log("SIGTERM received: draining connections");
  server.close(async () => {
    await pool.end();          // DBプールを閉じる
    process.exit(0);           // 10秒以内に抜ける
  });
});

ここで本質的に重要なのは 冪等性 です。SIGTERMで途中まで進んだ処理は、別インスタンスでリトライされて多重実行され得ます。私が本番二重課金0件の決済基盤で徹底したのと同じ原則——「同じ操作が2回来ても結果が変わらない」よう、冪等性キー+一意制約で構造的に多重実行を不可能にする——をCloud Runでも守ります。「SIGTERMハンドラで丁寧に閉じる」だけでは不十分で、処理側が冪等であって初めて安全です。冪等な非同期処理の設計は SQS/Lambdaの冪等処理Transactional Outbox も参考になります(クラウドは違えど原則は同じです)。


リビジョンとデプロイ:Blue/Green・カナリア・即時ロールバック

Cloud Runのデプロイ戦略は リビジョン(revision) の上に成り立ちます。リビジョンはコードと設定の不変スナップショットで、トラフィックを各リビジョンにパーセント単位で振り分けられます。

トラフィックを流さずにデプロイ → タグ付きURLで検証

# 新リビジョンをデプロイするが、トラフィックは流さない。タグ付きURLだけ発行。
gcloud run deploy api \
  --image IMAGE_URL --region asia-northeast1 \
  --no-traffic --tag green
# → https://green---api-xxxxx.a.run.app で、本番トラフィックと隔離して検証できる

カナリア → Blue/Green(段階的切替)

# 新リビジョン(latest)に5%だけ流す(カナリア)。残り95%は現行が捌く。
gcloud run services update-traffic api --region asia-northeast1 \
  --to-revisions LATEST=5

# メトリクスが健全なら段階的に引き上げ、最後に100%へ(Blue/Green切替)
gcloud run services update-traffic api --region asia-northeast1 --to-latest

即時ロールバック

# 問題が出たら、健全な旧リビジョンに100%戻すだけ。再ビルド不要。
gcloud run services update-traffic api --region asia-northeast1 \
  --to-revisions api-00021-abc=100

ロールバックが「旧リビジョンへトラフィックを100%戻すだけ」で完結するのがCloud Runの強みです。再デプロイもイメージの作り直しも不要。なお公式の注意点として、切替は瞬時ではなく、処理中リクエストは元のリビジョンで完了します。セッションアフィニティを有効にしている場合は、戻ってきたユーザーの振り分けにアフィニティが影響する点にも注意します。


セキュリティ:最小権限のサービスアカウントと秘密管理

サービスごとに専用のサービスアカウントを割り当てる

これはCloud Runで最初にやるべき設定です。何も指定しないと、サービスは Compute Engineのデフォルトサービスアカウントで動き、これは多くの場合広すぎるEditor権限を持っています。

We strongly recommend that you disable the automatic role grant by enforcing the iam.automaticIamGrantsForDefaultServiceAccounts organization policy constraint.(— Service identity

正解は、サービスごとに最小権限のユーザー管理サービスアカウントを作り、--service-account で割り当てること。

# このサービス専用のSAを作り、必要な権限だけを付与(最小権限)
gcloud iam service-accounts create api-runtime --display-name "api runtime"

# 例:このSAに Secret Manager の参照権限だけ与える
gcloud projects add-iam-policy-binding PROJECT_ID \
  --member "serviceAccount:api-runtime@PROJECT_ID.iam.gserviceaccount.com" \
  --role "roles/secretmanager.secretAccessor"

gcloud run deploy api --region asia-northeast1 \
  --service-account api-runtime@PROJECT_ID.iam.gserviceaccount.com ...

私の放送事業者向けプラットフォームでは、各サービスに専用SAを割り当て、Cloud SQLはIAM認証・TLS必須(ENCRYPTED_ONLY)・プライベートIPで運用し、認証情報をコードからもネットワークからも極力消していました。

秘密は Secret Manager から:環境変数 vs ボリューム

秘密(APIキー・DBパスワード)はイメージにも環境変数の平文にも置かず、Secret Manager から注入します(Configure secrets)。注入方法は2つあり、意味が違います

方法値の解決向いている用途
環境変数インスタンス起動時に固定。実行中は変わらないバージョンを固定したい秘密(latestではなく具体バージョンを指定推奨
ボリュームマウント常に最新版を取得(ファイルとして)ローテーションする秘密(次回読み取りで新しい値に追従)
# 環境変数として注入(バージョンを固定)。SAに roles/secretmanager.secretAccessor が必要。
gcloud run deploy api --region asia-northeast1 \
  --set-secrets "DB_PASSWORD=db-password:3"

# ボリュームとしてマウント(常に最新=ローテーション向き)
gcloud run deploy api --region asia-northeast1 \
  --set-secrets "/etc/secrets/db/password=db-password:latest"

入口を固める:認証 + Cloud Armor

  • サービス間呼び出し・社内ツールは認証必須--no-allow-unauthenticated)。呼び出し側のSAに roles/run.invoker を与え、IDトークンで呼びます。
  • 公開エンドポイントは 外部HTTP(S)ロードバランサ + Cloud Armor を前段に置き、WAF(OWASPルール)・レート制限・適応型DDoS防御をかけます。私のプラットフォームでは Cloud Armor(OWASP CRS 3.3+適応型DDoS+レート制限) を入口に置き、stgでWAFを全面有効化して本番前に誤検知を潰す運用にしていました。多層防御の考え方は WAF多層防御ガイド も参照してください。

ネットワーキング:Direct VPC egress を既定に

Cloud Runから**VPC内のリソース(Cloud SQLのプライベートIP、Memorystore、内部API)**へ出るには2つの方式があります。公式は新しい方式を推奨しています(Networking best practices)。

方式特徴
Direct VPC egress(推奨・GA)コネクタVM不要。アイドル課金なし・低レイテンシ・高スループット。サブネットのIP枠が必要
Serverless VPC Access コネクタ旧方式。コネクタVMの常駐コスト・運用が乗る
# Direct VPC egress:コネクタを介さず直接VPCへ出る
gcloud run deploy api --region asia-northeast1 \
  --network projects/PROJECT_ID/global/networks/my-vpc \
  --subnet projects/PROJECT_ID/regions/asia-northeast1/subnetworks/run-subnet \
  --vpc-egress private-ranges-only

新規は迷わず Direct VPC egress。コネクタの常駐コストとアイドル課金が消えるため、コスト面でも有利です。Ingress制御・IAM認証・Cloud SQLプライベートIP接続・Cloud Armorによる多層防御の作り込みは、ネットワーキングとセキュリティ ガイド に詳述しました。


ジョブとワーカープール:HTTPに向かない処理の置き場所

「Servicesは同期HTTPを捌くもの」と割り切ると、本番設計が一気に整理されます。タスク分割・冪等・再開設計・Cloud Workflowsによるオーケストレーションの作り込みは、専用記事 Cloud Run Jobs と Cloud Workflows ガイド に体系化しました。

  • Cloud Run Jobs実行して完了したら止まる処理。DBマイグレーション、定期バッチ、長時間の一括処理。--tasks/--parallelism で並列分割、--max-retries でリトライ。Cloud Schedulerでcron起動、Eventarcでイベント起動も可能。
  • Worker Pools常駐するバックグラウンド処理。Pub/Subのpullサブスクライバ、Kafkaコンシューマなど、HTTPリクエストを受けずに動き続けるワークロード。
# 素材のマルウェアスキャンを Eventarc(GCSイベント)で起動する例
gcloud eventarc triggers create scan-on-upload \
  --location asia-northeast1 \
  --destination-run-service malware-scanner \
  --event-filters "type=google.cloud.storage.object.v1.finalized" \
  --event-filters "bucket=uploads-raw" \
  --service-account eventarc-invoker@PROJECT_ID.iam.gserviceaccount.com

私のプラットフォームのマルウェアスキャナは、GCSへのアップロードをEventarcで受けてClamAV(Cloud Run)に渡し、最大10GiBの素材をバッファせずストリーミング検査してクリーン/隔離バケットへ振り分けていました。File.move の原子性で再試行に冪等にし、ゼロ長・アップロード中・削除済みは安全に無視。「重い処理はHTTPから切り離す」「冪等にする」——この2点がCloud Run本番運用の背骨です。


IaC:Terraformで宣言的に作る

本番は手作業の gcloud ではなく、Terraform(google_cloud_run_v2_service)で宣言的に作ります。ここまでの設定(並行性・スケール・タイムアウト・プローブ・SA・課金モード・実行環境)が1か所に集約されます。

resource "google_cloud_run_v2_service" "api" {
  name     = "api"
  location = "asia-northeast1"
  ingress  = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" # 入口はLB+Cloud Armor経由に限定

  template {
    service_account                  = google_service_account.api_runtime.email
    max_instance_request_concurrency = 80
    timeout                          = "300s"
    execution_environment            = "EXECUTION_ENVIRONMENT_GEN2"

    scaling {
      min_instance_count = 1   # 本番の入口は温める
      max_instance_count = 10  # コストの安全弁
    }

    containers {
      image = "asia-northeast1-docker.pkg.dev/${var.project_id}/repo/api:${var.image_tag}"
      ports { container_port = 8080 }

      resources {
        limits            = { cpu = "1", memory = "512Mi" }
        cpu_idle          = true   # true=リクエスト課金(アイドル時CPU停止)/ false=インスタンス課金
        startup_cpu_boost = true   # 冷起動を速くする
      }

      startup_probe {
        tcp_socket { port = 8080 }
        failure_threshold = 10     # 起動が遅いアプリは余裕を持たせる
        period_seconds    = 5
        timeout_seconds   = 3
      }
      liveness_probe {
        http_get { path = "/healthz" }
        period_seconds = 10
      }

      # 秘密は Secret Manager から注入(バージョン固定)
      env {
        name = "DB_PASSWORD"
        value_source {
          secret_key_ref {
            secret  = google_secret_manager_secret.db_password.secret_id
            version = "3"
          }
        }
      }
    }

    # VPC内リソース(Cloud SQLプライベートIP等)へは Direct VPC egress
    vpc_access {
      network_interfaces {
        subnetwork = google_compute_subnetwork.run.id
      }
      egress = "PRIVATE_RANGES_ONLY"
    }
  }

  # 最新リビジョンに100%(カナリア時はここを複数 traffic ブロックに分割)
  traffic {
    type    = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
    percent = 100
  }
}

cpu_idle = trueリクエスト課金(アイドル時にCPUを止める)falseインスタンス課金(CPU常時確保) に対応します。この選択がコストを大きく左右します(課金記事で詳述)。

CI/CDの鍵レス化(Workload Identity Federation)は別記事に詳しくまとめています:GitHub Actionsを鍵レスにする。私のプロジェクトでは GCP全体を約71のTerraformモジュールでコード化し、stg/prodのstateを分離して運用していました。


可観測性:標準出力に構造化ログを吐くだけ

Cloud Runは標準出力・標準エラーをそのまま Cloud Logging に取り込みます。アプリはファイルではなくstdout/stderrにJSONで構造化ログを吐けば、追加のエージェント無しでログが集まります。

import json, logging, sys

class JsonFormatter(logging.Formatter):
    def format(self, record):
        # Cloud Logging は severity / trace を解釈する。相関のため trace を載せる。
        return json.dumps({
            "severity": record.levelname,
            "message": record.getMessage(),
            "logging.googleapis.com/trace": getattr(record, "trace", None),
        })

handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter())
logging.basicConfig(level=logging.INFO, handlers=[handler])
  • メトリクス(リクエスト数・レイテンシ・インスタンス数・CPU/メモリ使用率)は Cloud Monitoring に自動で出ます。SLOやアラートはここに紐づけます。
  • トレースは OpenTelemetry で計装し、Cloud Trace / Cloud Monitoring に送ります。私のマルウェアスキャナもスキャン結果をOpenTelemetryでCloud Monitoringに送出していました。可観測性の設計思想は OpenTelemetry実践ガイド を参照してください。

本番投入チェックリスト

  • $PORT0.0.0.0 で待ち受けている(コンテナ契約)
  • SIGTERMハンドラで10秒以内に後始末し、処理は冪等である
  • 状態をインスタンスに持たない(外部ストアに集約)
  • 並行性を負荷特性に合わせて設定(既定80。むやみに1にしない)
  • min/max instances を設定(入口は温め、コスト上限を作る)
  • リクエストタイムアウトを見直し、超える処理は Jobs/Workflows
  • startup/liveness probe を設定(起動が遅いなら閾値を広げる)
  • 専用の最小権限サービスアカウントを割り当て、デフォルトSAを使わない
  • 秘密は Secret Manager(固定したいなら環境変数+バージョン、ローテーションならボリューム)
  • 公開は本当に必要なものだけ(既定は --no-allow-unauthenticated)、公開面は Cloud Armor
  • VPC接続は Direct VPC egress
  • Terraform で宣言的に管理し、CI/CDは Workload Identity Federation で鍵レス
  • ログは stdout/stderrに構造化JSON、メトリクス/トレースで可観測に

まとめ:サーバーレスコンテナの勘所は移植できる

Cloud Runは「コンテナを動かすことだけに集中する」ためのサーバーレス基盤です。本番品質の鍵は、特別な魔法ではなく契約を守ることにあります——$PORTで待ち受け、SIGTERMで丁寧に閉じ、状態を持たず、重い処理はジョブに切り離し、並行性とスケールでコストを制御し、最小権限と秘密管理で固める。

これらはAWS Fargateでも、Azure Container Appsでも変わらないサーバーレスコンテナ共通の勘所です。私は放送事業者向けプラットフォームをGCP・Cloud Runで、決済基盤や木材流通DXをAWS・Fargateで本番運用してきました。クラウドが変わっても、「壊れない・安い・安全に」コンテナを本番で回す設計原則は地続きです。

技術選定で迷っているなら GCPコンテナ技術選定ガイド を、コストを詰めたいなら 並行性・オートスケール・課金ガイド を続けてどうぞ。

友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・ 実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

この記事の実装を、案件として承ります

GCP / Cloud Run のコンテナ基盤を、設計から本番運用・コスト最適化まで

Cloud Run(サービス+ジョブ)でのコンテナ基盤構築、AWS/オンプレからの移行、CI/CD(Workload Identityで鍵レス)、Cloud Armor・最小権限の多層防御、並行性と課金モデルのコスト最適化まで。放送事業者向けプラットフォームをGCPにIaCで構築・運用した知見で、速く・安く・安全に伴走します。

プロジェクト単位(請負)・技術顧問のどちらにも対応可能です。まずは30分の無料技術相談から。

あわせて読みたい