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

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

- 公開日: 2026-06-28
- 著者: 友田 陽大
- タグ: GCP, Cloud Run, サーバーレス, コンテナ, インフラ, コスト最適化, 可観測性, Terraform
- URL: https://tomodahinata.com/blog/google-cloud-run-production-guide
- カテゴリ: Google Cloud Run 本番運用

## 要点

- Cloud Runは『コード・関数・コンテナをGoogleのインフラ上で動かす』フルマネージドのサーバーレス基盤。デプロイ単位は常にコンテナイメージで、ソースからbuildpacksで自動ビルドもできる。Services（HTTP）・Jobs（実行完了型）・Worker Pools（常駐バックグラウンド）の3形態がある
- 本番品質の鍵は『コンテナ契約（$PORTで0.0.0.0待受・SIGTERMで10秒以内に後始末）』『並行性（既定80・最大1000）が原価とスケールを決める』『スケールトゥゼロと最小インスタンスの使い分け』『リビジョンの不変性を使ったトラフィック分割でBlue/Green・カナリア』の4点
- 課金はリクエスト課金（--cpu-throttling・既定）とインスタンス課金（--no-cpu-throttling）の2モード。前者はリクエスト処理中のみCPU課金で散発トラフィック向き、後者はライフサイクル全体で課金されレスポンス後のバックグラウンド処理が可能
- セキュリティの初手は『サービスごとに最小権限のユーザー管理サービスアカウントを割り当て、デフォルトのCompute Engineサービスアカウントを使わない』『秘密はSecret Manager（環境変数=起動時固定 / ボリューム=常に最新）』『認証必須（--no-allow-unauthenticated）』
- リクエストタイムアウトは既定300秒・最大60分。これを超える処理はCloud Run Jobs / Cloud WorkflowsへHTTPから切り離す。長時間ジョブは冪等・再開可能に設計する

---

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

私は実際に、**国内大手放送事業者の社内AIプラットフォームをGCP上にTerraformでIaC構築**し、その本番運用を担ってきました（[ケーススタディ](/case-studies/broadcaster-ai-content-platform)）。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公式ドキュメント](https://docs.cloud.google.com/run/docs/overview/what-is-cloud-run)に忠実でありながら、公式よりわかりやすく**、かつ「どの場面でどう使うか」を実コードで示すことを目的にしています。コンテナ契約・リソース設計・並行性・スケール・デプロイ・回復性・セキュリティ・コストまで、本番に出すために必要なことを一気通貫で扱います。

技術選定そのもの（Cloud Run か GKE か App Engine か）は [GCPコンテナ技術選定ガイド](/blog/google-cloud-run-vs-gke-app-engine-cloud-run-functions-compute-selection-guide) に、並行性・課金・コスト最適化の深掘りは [Cloud Run オートスケール・課金・コスト最適化ガイド](/blog/google-cloud-run-autoscaling-concurrency-billing-cost-optimization-guide) に分けました。本稿は **「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](https://docs.cloud.google.com/run/docs/overview/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に複数あります。深い比較は [技術選定ガイド](/blog/google-cloud-run-vs-gke-app-engine-cloud-run-functions-compute-selection-guide) に譲りますが、最初の判断軸だけ示します。

| サービス | 一言でいうと | 選ぶべき場面 |
|---------|------------|------------|
| **Cloud Run** | サーバーレスのコンテナ／マイクロサービス基盤 | ステートレスなコンテナを**K8s運用なしで**動かす。**迷ったらここ。** |
| **Cloud Run functions**（旧Cloud Functions） | イベント駆動のFaaS | 単一トリガー（HTTP/Pub/Sub/Storage等）に応答する**関数**。Cloud Run基盤上で動く。 |
| **GKE / GKE Autopilot** | マネージドKubernetes | DaemonSet・CRD・Operator・サービスメッシュ等の**K8s固有機能**が要る。 |
| **App Engine** | 旧来のPaaS | 既存資産。**新規はCloud Run推奨**（後述）。 |
| **Compute Engine** | VM | コンテナ化できない／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](https://docs.cloud.google.com/appengine/migration-center/run/compare-gae-with-run)）

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

---

## コンテナ契約（runtime contract）：守るべき5つの約束

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

### 1. `$PORT`・`0.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](https://docs.cloud.google.com/run/docs/container-contract)）

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

```python
# 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)
```

```javascript
// 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](https://docs.cloud.google.com/run/docs/container-contract)）

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

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

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

---

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

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

```bash
# ソースから直接デプロイ（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の記事](/blog/github-actions-oidc-keyless-cicd-aws-gcp-guide) を参照してください。

---

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

CPUとメモリは独立に決められますが、**CPU値ごとにメモリの上限・下限が決まっています**（[Configure CPU limits](https://docs.cloud.google.com/run/docs/configuring/services/cpu)）。

| vCPU | メモリの範囲 |
|------|------------|
| 0.08 | 〜512 MiB |
| 0.5  | 〜1 GiB |
| 1    | 〜4 GiB |
| 2    | 〜8 GiB |
| 4    | 2〜16 GiB |
| 6    | 4〜24 GiB |
| 8    | 4〜32 GiB |

- **vCPUは 0.08〜8**。1未満は 0.001刻みの小数（例 `0.25`）、1以上は `1, 2, 4, 6, 8` の整数のみ。
- **小さく始めて、メトリクスで右サイズ化する**のが原則。最初から大きく取るとそのまま課金に乗ります。

```bash
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](https://docs.cloud.google.com/run/docs/about-execution-environments)）。

| | **gen1** | **gen2** |
|---|---------|---------|
| 基盤 | gVisor | microVM |
| 冷起動 | **速い** | 一部のサービスでやや遅い |
| Linux互換 | 多くのsyscallをエミュレート（一部非対応） | **完全なLinux互換** |
| ネットワークファイルシステム | × | **○（NFS等）** |
| CPU/ネットワーク性能 | 標準 | **高速** |
| メモリ下限 | 512 MiB未満も可 | 512 MiB以上 |

- 既定は **unspecified（プラットフォームが自動選択）**。
- **冷起動最優先・軽量なAPIなら gen1**、**完全なLinux互換・NFS・VPC egress・CPU集約ワークロードなら gen2**。
- **Jobs と Worker Pools は常に gen2**。

```bash
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](https://docs.cloud.google.com/run/docs/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）なら並行性を上げて密度を稼ぐ——というチューニングになります。

```bash
gcloud run deploy api --concurrency 80 --region asia-northeast1 ...
```

並行性が**スケールと課金をどう動かすか**は、専用記事 [並行性・オートスケール・課金](/blog/google-cloud-run-autoscaling-concurrency-billing-cost-optimization-guide) で原価計算まで踏み込んで解説します。ここでは「並行性は性能とコストの中心ダイヤル」とだけ覚えてください。

---

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

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

> The autoscaler ... targets ... 60% CPU utilization / 60% concurrency utilization by default.（— [About instance autoscaling](https://docs.cloud.google.com/run/docs/about-instance-autoscaling)）

- **既定で 60% の使用率を目標**にインスタンス数を調整（CPU使用率と並行使用率の両面）。
- リクエスト処理後、**最大15分（GPUは10分）はインスタンスを温存**して冷起動を減らす。
- **最小インスタンス（min instances）** で常時温めて冷起動を消せる。**最大インスタンス（max instances・既定100）** で暴走時のコスト上限を作る。

```bash
gcloud run deploy api \
  --min-instances 1 \      # 本番の入口は1台温めて冷起動を消す
  --max-instances 10 \     # コストの安全弁。スパイクでも10台で頭打ち
  --region asia-northeast1 ...
```

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

スケールの設計（冷起動対策・最小/最大の決め方・60%目標の意味）は [オートスケール記事](/blog/google-cloud-run-autoscaling-concurrency-billing-cost-optimization-guide) で深掘りします。

---

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

> Default timeout: 5 minutes (300 seconds). Maximum timeout: 60 minutes (3,600 seconds).（— [Request timeout](https://docs.cloud.google.com/run/docs/configuring/request-timeout)）

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

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

```bash
# 長時間処理は 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](https://docs.cloud.google.com/run/docs/configuring/healthchecks)）。

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

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

```python
from fastapi import FastAPI, Response
app = FastAPI()

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

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

---

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

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

```python
# 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)
```

```javascript
// 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件の決済基盤](/case-studies/payment-platform-reliability)で徹底したのと同じ原則——**「同じ操作が2回来ても結果が変わらない」よう、冪等性キー＋一意制約で構造的に多重実行を不可能にする**——をCloud Runでも守ります。「SIGTERMハンドラで丁寧に閉じる」だけでは不十分で、**処理側が冪等**であって初めて安全です。冪等な非同期処理の設計は [SQS/Lambdaの冪等処理](/blog/aws-sqs-lambda-eventbridge-idempotent-async-processing-guide) と [Transactional Outbox](/blog/transactional-outbox-pattern-reliable-event-publishing-guide) も参考になります（クラウドは違えど原則は同じです）。

---

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

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

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

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

### カナリア → Blue/Green（段階的切替）

```bash
# 新リビジョン（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
```

### 即時ロールバック

```bash
# 問題が出たら、健全な旧リビジョンに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](https://docs.cloud.google.com/run/docs/securing/service-identity)）

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

```bash
# このサービス専用の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](https://docs.cloud.google.com/run/docs/configuring/services/secrets)）。注入方法は2つあり、**意味が違います**。

| 方法 | 値の解決 | 向いている用途 |
|------|---------|--------------|
| **環境変数** | **インスタンス起動時に固定**。実行中は変わらない | バージョンを固定したい秘密（**`latest`ではなく具体バージョンを指定推奨**） |
| **ボリュームマウント** | **常に最新版を取得**（ファイルとして） | **ローテーションする秘密**（次回読み取りで新しい値に追従） |

```bash
# 環境変数として注入（バージョンを固定）。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多層防御ガイド](/blog/waf-defense-in-depth-aws-waf-cloud-armor-owasp-guide) も参照してください。

---

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

Cloud Runから**VPC内のリソース（Cloud SQLのプライベートIP、Memorystore、内部API）**へ出るには2つの方式があります。公式は新しい方式を推奨しています（[Networking best practices](https://docs.cloud.google.com/run/docs/configuring/networking-best-practices)）。

| 方式 | 特徴 |
|------|------|
| **Direct VPC egress（推奨・GA）** | **コネクタVM不要**。アイドル課金なし・低レイテンシ・高スループット。サブネットのIP枠が必要 |
| **Serverless VPC Access コネクタ** | 旧方式。コネクタVMの常駐コスト・運用が乗る |

```bash
# 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による多層防御の作り込みは、[ネットワーキングとセキュリティ ガイド](/blog/google-cloud-run-networking-security-vpc-egress-cloud-armor-iam-ingress-guide) に詳述しました。

---

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

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

- **Cloud Run Jobs**：**実行して完了したら止まる**処理。DBマイグレーション、定期バッチ、長時間の一括処理。`--tasks`/`--parallelism` で並列分割、`--max-retries` でリトライ。Cloud Schedulerでcron起動、Eventarcでイベント起動も可能。
- **Worker Pools**：**常駐するバックグラウンド処理**。Pub/Subのpullサブスクライバ、Kafkaコンシューマなど、HTTPリクエストを受けずに動き続けるワークロード。

```bash
# 素材のマルウェアスキャンを 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か所に集約されます。

```hcl
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常時確保）** に対応します。この選択がコストを大きく左右します（[課金記事](/blog/google-cloud-run-autoscaling-concurrency-billing-cost-optimization-guide)で詳述）。

CI/CDの鍵レス化（Workload Identity Federation）は別記事に詳しくまとめています：[GitHub Actionsを鍵レスにする](/blog/github-actions-oidc-keyless-cicd-aws-gcp-guide)。私のプロジェクトでは **GCP全体を約71のTerraformモジュールでコード化し、stg/prodのstateを分離**して運用していました。

---

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

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

```python
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実践ガイド](/blog/opentelemetry-observability-production-tracing-metrics-logs) を参照してください。

---

## 本番投入チェックリスト

- [ ] `$PORT`・`0.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コンテナ技術選定ガイド](/blog/google-cloud-run-vs-gke-app-engine-cloud-run-functions-compute-selection-guide) を、コストを詰めたいなら [並行性・オートスケール・課金ガイド](/blog/google-cloud-run-autoscaling-concurrency-billing-cost-optimization-guide) を続けてどうぞ。
