Skip to content

デプロイ運用ガイド (Runbook)

「何を変えたら、どこを反映すれば本番に届くのか」を 1 枚で把握するためのページ。 個別の詳細手順は Terraform 設計/運用開発フロー/マイグレ踏んだ落とし穴 に譲り、ここでは 責務の境界判断フローに絞る。

大原則: 何を誰が管理するか

レイヤー管理するもの唯一の管理者反映トリガー
Web (apps/web)フロントNetlifymain push で自動 build/deploy
API イメージコンテナ imageGitHub Actions (deploy-staging.yml)main push で gcloud run deploy --image
API の env / secretALLOWED_ORIGINS / DATABASE_URL / Stripe 等Terraform (infra/terraform)terraform apply (手動)
インフラ全般Cloud SQL / Secret Manager / IAM / WIF / ARTerraformterraform apply (手動)
DB スキーマテーブル定義drizzle migration手動 (Cloud SQL Auth Proxy 経由)

最重要原則

Cloud Run の env と secret は Terraform が唯一の管理者。CI の gcloud run deploy--image だけ更新し、env には一切触らない (既存 revision の env を引き継ぐだけ)。

env を変えたら必ず terraform applygcloud run deploy --set-env-vars で 個別に上書きしてはいけない (Secret Manager 参照が平文に化けて壊れる)。

変更種別ごとの「やること」

変更したもの必要なアクション自動 / 手動
apps/web/**git pushNetlify が自動
apps/api/** (コードのみ)git pushCI が image を deploy
packages/* (db / shared)git push (web は依存変更検知に注意 → 落とし穴⑯)CI / Netlify
Cloud Run の env (cloud_run.tf / tfvars の allowed_originsauth_base_url)terraform apply手動 (忘れると起動失敗)
Secret 値 (Stripe / SendGrid 等)gcloud secrets versions add <NAME>_<env> --data-file=-Cloud Run 再 deploy (新 version を読ませる)手動
Cloud SQL 設定 (cloud_sql.tf)terraform apply手動
DB スキーマmigration 生成 → Auth Proxy 経由で migrate (dev-workflow)手動
新しい env / secret 名をコードで読み始めたcloud_run.tf の env / secrets.tfapp_secret_names に追加 → terraform apply手動

⚠ 今回の事故 (2026-05-21) と教訓

症状: Cloud Run デプロイが「コンテナが PORT=8080 で listen できず起動失敗」。

真因: CORS の fail-fast (ALLOWED_ORIGINS 未設定なら NODE_ENV=production で起動時に throw) をコードに追加し、cloud_run.tf にも ALLOWED_ORIGINS env 定義を追加したが、 terraform apply を回していなかった。そのため Cloud Run の実 revision には ALLOWED_ORIGINS env が無いまま → アプリが起動時 throw → クラッシュ。

envs/staging.tfvars には allowed_origins = ["https://staging--liga.netlify.app"]値は入っていた。足りなかったのは「apply する」という操作だけ。

教訓:

  1. env を要求するコード変更インフラ側の env 設定は必ずセットで terraform apply
  2. CI (image deploy) と Terraform (env/infra) は責務が別。CI を直しても env は入らない。
  3. gcloud run deploy --set-env-vars で逃げない (一度この方法を試みたが、Secret Manager 参照を壊すので撤回した)。

デプロイが起動失敗したときの復旧フロー

sh
# 1. ログで起動時の例外を確認
gcloud run services logs read liga-api-staging --region=asia-northeast1 --limit=50
#    → "ALLOWED_ORIGINS が未設定です" 等の throw が出ていれば env 反映漏れ

# 2. Terraform の差分を確認 (env 追加などが出るはず)
cd infra/terraform
terraform init -reconfigure \
  -backend-config="bucket=${PROJECT_ID}-tf-state" \
  -backend-config="prefix=staging"
terraform plan -var-file=envs/staging.tfvars

# 3. 反映
terraform apply -var-file=envs/staging.tfvars

# 4. 新 revision に env が入ったか確認
gcloud run services describe liga-api-staging --region=asia-northeast1 \
  --format='value(spec.template.spec.containers[0].env)'

CI の image はそのまま使われる (Terraform は image を ignore_changes)。

本番 (prod) を立ち上げるときの想定

詳細手順は Terraform 運用 → prod を増やすとき を参照。同 GCP project に env=prod suffix で別リソースを立てる方針 (backend prefix を prod に切替)。

prod 化チェックリスト

  • [ ] envs/prod.tfvarsauth_base_url / allowed_origins本番ドメイン
  • [ ] terraform init -reconfigure ... -backend-config="prefix=prod" で state を分離
  • [ ] Cloud SQL を Private IP 化cloud_sql.tf は現状 staging を 0.0.0.0/0 で公開 しており (Netlify が public IP 経由で繋ぐ都合)、prod ではこれをやめて Private IP + Serverless VPC Access に切り替える (未実装。下記「セキュリティ宿題」)
  • [ ] deletion_protection = true (env=prod で自動)
  • [ ] availability_type = REGIONAL (HA、env=prod で自動)
  • [ ] cloud_run_min_instances >= 1 (cold start 回避、warm)
  • [ ] Secret をprod 用の値で投入 (BETTER_AUTH_SECRET 等は staging と別値、Stripe は live key)
  • [ ] Cloud Run に Cloud Armor / API ゲートウェイ を被せる検討 (現状 allUsers 公開)
  • [ ] LINE ログインを使うなら secrets.tfapp_secret_namesLINE_CLIENT_ID / LINE_CLIENT_SECRET を追加 + cloud_run.tf に env、NEXT_PUBLIC_LINE_LOGIN_ENABLED を Netlify env に (現状 Terraform 未登録なので prod でも LINE は無効のまま)
  • [ ] Netlify の prod context に env を投入 (落とし穴⑪⑭)
  • [ ] Stripe Webhook を prod エンドポイントで再登録

セキュリティ宿題 (staging の現状リスク)

cloud_sql.tfstaging の Cloud SQL を authorized_networks = 0.0.0.0/0 で全世界に公開 している (Netlify=AWS Lambda が固定 IP を持たず、public IP 経由で繋ぐ都合)。

実際に外部 IP からの postgres ユーザへの brute force (短時間で数百回の認証失敗) を 観測済み。postgres デフォルトユーザは Terraform 管理外 (パスワード未設定) で晒されている。

対応の方向 (どれを採るかは別途判断):

  1. Private IP + Serverless VPC Access に移行し public IP を閉じる (本筋。Netlify からは Cloud SQL Auth Proxy or 別経路に)
  2. authorized_networks を絞る (Netlify は IP 不定なので限定が難しい → 1 が現実的)
  3. 当面の緩和: postgres デフォルトユーザに強固なパスkeyを設定 / 無効化、攻撃元 IP の遮断

関連ドキュメント