「デプロイが怖い」——本番のコンテナ基盤で最も避けたい感覚です。怖さの正体は、戻せないことと、何が変わったか分からないこと。Cloud RunのCI/CDは、この2つを構造的に潰せます。リビジョンが不変だから再ビルドなしで即座に戻せるし、責務を分離すれば何が変わったかが常に明確になります。
私は放送事業者向けプラットフォームをGCPで運用する中で、Cloud Buildでstg/prodを出し分け、Terraformは『インフラ構成』・Cloud Buildは『イメージと最新env』と責務を分離し、DBマイグレーションは専用ジョブに切り出し、CI/CDはWorkload Identity Federationで鍵レス——という構成で、止まらない社内プラットフォームを回していました。本記事はその設計を、Google Cloud公式ドキュメントに忠実に、実コードで再現します。
本番運用の全体像は Cloud Run 本番運用ガイド、長時間ジョブそのものの設計は Jobs / Workflows ガイド を参照してください。
設計原則:3つの責務を分ける
Cloud RunのCI/CDで事故が起きるのは、たいてい責務が混ざっているときです。最初に境界を引きます。
| 責務 | 担うもの | 真実源 |
|---|---|---|
| アプリの中身 | コンテナイメージ(CIでビルド → Artifact Registry) | Git(コミットSHA=イメージタグ) |
| インフラ構成 | サービス・SA・VPC・スケール設定(Terraform) | Terraform state |
| どのリビジョンに流すか | トラフィック配分(不変リビジョン) | Cloud Runのトラフィック設定 |
この分離が効くのは——「イメージタグ=コミットSHA」にすれば、本番で動いているものがどのコミットか一意に追跡でき、インフラ変更(Terraform)とアプリ変更(イメージ)が混ざらないから。latest タグは使いません(何が動いているか分からなくなる)。
Artifact Registry:イメージの置き場所
イメージは Artifact Registry(旧Container Registry)に置きます。まずリポジトリを作ります。
gcloud artifacts repositories create app \
--repository-format=docker \
--location=asia-northeast1 \
--description="app container images"
# イメージURLの形:asia-northeast1-docker.pkg.dev/PROJECT_ID/app/api:GIT_SHA
経路A:Cloud Build(GCPネイティブで完結)
GCPだけで完結させたいなら Cloud Build。cloudbuild.yaml に「ビルド → プッシュ → デプロイ」を宣言します。
# cloudbuild.yaml — push trigger で起動。$SHORT_SHA はCloud Buildが注入する。
steps:
# 1. ビルド(コミットSHAをタグに)
- name: "gcr.io/cloud-builders/docker"
args:
["build", "-t",
"${_REGION}-docker.pkg.dev/$PROJECT_ID/app/api:$SHORT_SHA", "."]
# 2. Artifact Registry へプッシュ
- name: "gcr.io/cloud-builders/docker"
args:
["push",
"${_REGION}-docker.pkg.dev/$PROJECT_ID/app/api:$SHORT_SHA"]
# 3. トラフィックを流さずにデプロイ(タグURLで検証してから昇格する)
- name: "gcr.io/google.com/cloudsdktool/cloud-sdk"
entrypoint: gcloud
args:
["run", "deploy", "api",
"--image", "${_REGION}-docker.pkg.dev/$PROJECT_ID/app/api:$SHORT_SHA",
"--region", "${_REGION}",
"--no-traffic", "--tag", "sha-$SHORT_SHA"]
images:
- "${_REGION}-docker.pkg.dev/$PROJECT_ID/app/api:$SHORT_SHA"
substitutions:
_REGION: asia-northeast1
options:
logging: CLOUD_LOGGING_ONLY
GitHubリポジトリに push トリガーを接続すれば、コミットごとに自動でビルド・デプロイ(トラフィックは流さない)まで走ります。
gcloud builds triggers create github \
--repo-name=app --repo-owner=YOUR_ORG \
--branch-pattern="^main$" \
--build-config=cloudbuild.yaml
経路B:GitHub Actions × Workload Identity(鍵レス)
既存CIがGitHub Actionsなら、こちらが自然です。サービスアカウント鍵を発行せず、Workload Identity Federation(WIF)でGCPに認証します。
# .github/workflows/deploy.yml
name: deploy
on:
push:
branches: [main]
permissions:
contents: read
id-token: write # これが無いとGitHubはOIDCトークンを注入せず、認証が失敗する
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# 鍵レス認証:プールとプロバイダはWIFで事前設定(下記リンク参照)
- id: auth
uses: google-github-actions/auth@v3
with:
# ★プロジェクト「番号」を含むフルパス。プロジェクトIDではない。
workload_identity_provider: "projects/123456789/locations/global/workloadIdentityPools/github/providers/app-repo"
service_account: "deployer@PROJECT_ID.iam.gserviceaccount.com"
- uses: google-github-actions/deploy-cloudrun@v3
with:
service: api
region: asia-northeast1
image: asia-northeast1-docker.pkg.dev/PROJECT_ID/app/api:${{ github.sha }}
flags: "--no-traffic --tag=sha-${{ github.sha }}"
WIFのプール/プロバイダ設定(Attribute Conditionで自分のリポジトリだけ許可する等)は本記事では繰り返しません。 設定の勘所——
assertion.repositoryの一致を必ず付ける・subをワイルドカードにしない——は専用記事 GitHub Actionsを鍵レスにする にまとめています(DRY)。デプロイ用SAには最小権限(roles/run.developer+Artifact Registry読み取り+ランタイムSAへのroles/iam.serviceAccountUser)だけを与えます。
安全な出荷:検証 → カナリア → Blue/Green → 即時ロールバック
CIは「トラフィックを流さずにデプロイ」までで止めるのが肝です。人間(または自動チェック)が検証してから昇格します。リビジョンが不変だからこそ、この段階制御が安全に効きます。
# 1. タグURLで隔離検証(本番トラフィックに影響しない)
# → https://sha-abc123---api-xxxxx.a.run.app をスモークテスト
curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
https://sha-abc123---api-xxxxx.a.run.app/healthz
# 2. 健全なら5%だけカナリア
gcloud run services update-traffic api --region asia-northeast1 \
--to-tags sha-abc123=5
# 3. エラー率・レイテンシを監視しつつ段階引き上げ(5 → 25 → 50%)
gcloud run services update-traffic api --region asia-northeast1 \
--to-tags sha-abc123=50
# 4. 問題なければ100%へ(Blue/Green切替)
gcloud run services update-traffic api --region asia-northeast1 --to-latest
# ── 異常を検知したら、旧リビジョンへ即時ロールバック(再ビルド不要)──
gcloud run services update-traffic api --region asia-northeast1 \
--to-revisions api-00021-prev=100
ロールバックが「旧リビジョンに100%戻すだけ」で完結するのがCloud Run最大の安全装置です。イメージの作り直しもデプロイのやり直しも要りません。これを CI/CD の標準手順に組み込んでおけば、夜間の障害でも数十秒で平常へ戻せます。
DBマイグレーションはデプロイから分離する
最も事故りやすいのがスキーマ変更です。アプリのロールアウトとマイグレーションを同じステップに混ぜると、ロールバックしたときに「コードは古いがスキーマは新しい」不整合に陥ります。正解は専用のCloud Run Jobに切り出し、前方/後方互換のある段階適用にすること。
# マイグレーション専用ジョブを用意し、デプロイとは独立に実行する
gcloud run jobs deploy db-migrate \
--image asia-northeast1-docker.pkg.dev/PROJECT_ID/app/migrate:${GIT_SHA} \
--region asia-northeast1 \
--service-account migrator@PROJECT_ID.iam.gserviceaccount.com \
--max-retries 0 # マイグレーションは安易にリトライさせない
gcloud run jobs execute db-migrate --region asia-northeast1 --wait
ゼロダウンタイムのスキーマ変更は「①互換性のある列追加 → ②新旧両対応のコードをデプロイ → ③バックフィル → ④古い参照を消すコード → ⑤旧列削除」という多段リリースにします。設計の詳細は ゼロダウンタイムのスキーマ移行 を参照してください(DBはCloud SQL/PostgreSQLでも原則は同じ)。ジョブの作り込み自体は Jobs / Workflows ガイド へ。
Cloud Build と GitHub Actions、どちらを選ぶか
| Cloud Build | GitHub Actions | |
|---|---|---|
| 認証 | GCP内なのでネイティブに簡単 | WIFで鍵レス(設定は要る) |
| エコシステム | GCPに最適化 | 広い(Lint/テスト/他クラウドと統合しやすい) |
| 向くチーム | GCP中心・インフラもCloud Buildに寄せたい | 既にGitHub Actionsが標準 |
| ビルド環境 | マネージド・並列・キャッシュ | ランナー(self-hosted可) |
正解は「チームの既存CIに寄せる」こと。どちらも --no-traffic+タグ検証→カナリア→Blue/Greenという出荷フローは同じに作れます。私のプロジェクトでは、ビルド/デプロイのコアは Cloud Build に集約しつつ、GitHub 側で CodeQL・依存更新・テストを回す併用構成にしていました。
本番投入チェックリスト
- イメージタグは コミットSHA(
latestを使わない) - Terraform=インフラ / イメージ=アプリ で責務分離
- CI/CDは WIFで鍵レス(
id-token: writeを付ける) - デプロイ用SAは 最小権限(
run.developer+AR読み取り+serviceAccountUser) - CIは
--no-traffic+--tagまで。昇格は検証後 - カナリア → Blue/Green の段階出荷をスクリプト化
- 即時ロールバック手順(旧リビジョンへ100%)を runbook に
- DBマイグレーションは専用ジョブに分離し、段階適用にする
- stg環境で本番同等の検証(WAF含む)を先に通す
まとめ:デプロイを「怖くない」作業にする
Cloud RunのCI/CDは、責務分離(イメージ/インフラ/トラフィック)と不変リビジョンによる段階出荷で、「戻せない・何が変わったか分からない」という恐怖を構造的に消せます。鍵レス(WIF)で認証情報の漏洩リスクも断ち、マイグレーションを分離して不整合も防ぐ。これで少人数でも、本番デプロイを淡々とこなせるようになります。
全体設計は Cloud Run 本番運用ガイド、コストは 並行性・課金ガイド、長時間処理は Jobs / Workflows ガイド へ。GCPのCI/CD整備や鍵レス化の伴走が必要なら、実運用の知見を踏まえてお手伝いします。