TL;DR
前々回で Claude Code に Terraform を書かせ、前回でセキュリティ脆弱性を修正した。今回は実際に本番環境にデプロイする。
10ステップの手順を実行する中で、Claude Code が解決できない問題が3つあった。 ブートストラップ問題、DNS 伝播の待ち時間、GitHub Secrets の対応表管理。
最終回では、全体を通じた「AI に任せていいこと・ダメなこと」のスコアカードを提示する。
デプロイ前に: 全体像の確認
Claude Code と一緒に作ったインフラの最終構成:
┌───────────────────────────────────────────────────────┐
│ GitHub │
│ │
│ PR作成 → terraform plan → PRコメント │
│ main merge → terraform apply → インフラ更新 │
│ git tag v* → build → migrate → deploy → smoke-test │
│ │ │
│ WIF (OIDC) ← SA キー不要 │
└────────────────────┼────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────┐
│ GCP (myapp-production) │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ VPC │ │
│ │ Cloud Run ←→ Cloud SQL (Private IP) │ │
│ └──────┬───────────────────────────────┘ │
│ ↓ Cloud Tasks API │
│ Cloud Tasks → Cloud Run (invoke) │
│ │
│ 5 SA (app/deploy/migrate/tasks/terraform) │
│ WIF (GitHub Actions 限定、ワークフロー単位制限) │
│ Artifact Registry (immutable tags) │
│ Cloud DNS + SSL + LB (条件付き作成) │
└───────────────────────────────────────────────────────┘
コードは完成した。ここからは「実際にリソースを作って動かす」フェーズ。
10ステップの本番デプロイ
全体フロー
Step 1 GCS バケット作成
↓
Step 2 ローカルから terraform apply(ブートストラップ) ← AI に解決できない問題①
↓
Step 3 terraform output で値を確認
↓
Step 4 State を GCS に移行
↓
Step 5 GitHub Secrets/Variables を登録 ← AI に解決できない問題②
↓
Step 6 Google OAuth クライアント設定
↓
Step 7 DNS ネームサーバー設定
↓
Step 8 SSL 証明書の伝播待ち ← AI に解決できない問題③
↓
Step 9 git tag v1.0.0 → 自動デプロイ
↓
Step 10 smoke-test 確認
Step 1: GCS バケット作成(Claude Code: ★★☆)
gcloud storage buckets create gs://myapp-tfstate-production --location=asia-northeast1
gcloud storage buckets update gs://myapp-tfstate-production --versioning
Claude Code に「State 保存用の GCS バケット作って」と言ったら、コマンドは正確に出てきた。ただし、なぜ Terraform の外で作るのかは聞かないと教えてくれなかった。
理由: Terraform State を保存するバケットを Terraform で管理すると、terraform destroy でバケットが消えて State も消える。鶏が先か卵が先か、の亜種。
Q: このバケットも Terraform で管理したほうがいいのでは?
A: State バケットだけは Terraform の外で作る。
理由: Terraform がこのバケットを壊すと、
自分自身の State を失って復旧不能になるため。
Step 2: ローカルから terraform apply ── AI に解決できない問題①
ブートストラップ問題。 このシリーズで一番の山場。
何が問題か
Terraform で WIF を作りたい
→ Terraform を GitHub Actions で実行したい
→ GitHub Actions → GCP のアクセスに WIF が必要
→ でも WIF はまだない ← 循環依存
Claude Code にこの問題を聞いてみた:
WIF をまだ作っていない状態で、GitHub Actions から
terraform apply を実行するにはどうすればいい?
Claude Code の回答は「SA キーを一時的に使って初回 apply する」だった。
これは間違いではないが、ベストではない。 SA キーを作ると、削除し忘れのリスクがある。より安全な方法は:
# 人間が判断した方法: ローカルから gcloud 認証で apply
# 1. ローカルで GCP 認証(Owner 権限のアカウント)
gcloud auth application-default login
# 2. backend.tf の backend ブロックをコメントアウト
# (GCS バケットに State を保存する設定をスキップ)
# 3. init
cd infra/terraform/environments/production
terraform init
# 4. DB パスワードを生成
DB_PASSWORD=$(openssl rand -hex 24)
echo "控えておく: ${DB_PASSWORD}"
# 5. apply(全リソースが一括作成される)
terraform apply \
-var-file=production.tfvars \
-var="project_id=myapp-production" \
-var="github_org=my-org" \
-var="github_repo=myapp" \
-var="database_password=${DB_PASSWORD}"
ここでの判断:
-
openssl rand -hex 24で URL 安全な48文字の16進文字列を生成(-base64だと+/=が含まれて DATABASE_URL でエスケープ問題が起きる) - Owner 権限はこの初回 apply でしか使わない。以降は WIF 経由の最小権限 SA に切り替わる
- SA キーは作らない。
gcloud auth application-default loginで十分
パスワードと State の関係: Cloud SQL のユーザーリソースでは Terraform 1.11+ の write_only 引数を使っている:
resource "google_sql_user" "app" {
instance = google_sql_database_instance.main.name
name = "app"
password_wo = var.database_password # State に保存されない
password_wo_version = var.database_password_version # 変更時にインクリメント
}
password_wo は apply 時に GCP に送信されるが、State ファイルには記録されない。 従来の password 引数だと State に平文で残り、GCS バケットへのアクセス権がある人は全員パスワードを見られてしまう。write_only によってこのリスクを排除している。
パスワードを変更したい場合は、新しいパスワードを -var で渡しつつ database_password_version をインクリメントする。Terraform は password_wo の値が State にないため変更を検知できないが、password_wo_version の変更をトリガーにして更新を実行する。
Owner 権限と最小権限原則の矛盾: このシリーズを通じて「最小権限」を強調してきたが、ブートストラップでは Owner 権限を使っている。本来はブートストラップ用に必要なロール(roles/iam.workloadIdentityPoolAdmin、roles/compute.networkAdmin、roles/cloudsql.admin 等)だけを持つアカウントを使うのが理想。ただし初回 apply では作成するリソースの種類が多く、必要なロールの洗い出し自体にコストがかかる。ここは「初回のみ Owner を使い、以降は WIF 経由の最小権限 SA に切り替える」というトレードオフを取った。Owner 権限の利用がこの1回限りであることが重要。
この「種火」作業は、AI がどれだけ優秀でも人間がやるしかない。 GCP のコンソールにログインして認証するのは物理的に人間の作業。
apply の結果
Apply complete! Resources: 47 added, 0 changed, 0 destroyed.
Outputs:
wif_provider = "projects/123456/locations/global/..."
deploy_service_account = "myapp-deploy@myapp-production.iam..."
app_service_account = "myapp-app@myapp-production.iam..."
migrate_service_account = "myapp-migrate@myapp-production.iam..."
cloud_sql_private_ip = "10.x.x.x"
cloud_sql_connection_name = "myapp-production:asia-northeast1:..."
47リソースが一括作成。VPC、Cloud SQL、Artifact Registry、WIF、SA、全部入り。所要時間は約10分(Cloud SQL のプロビジョニングが一番遅い)。
Step 3-4: outputs 確認 & State 移行(Claude Code: ★★★★☆)
# outputs の確認
terraform output
# backend.tf のコメントを外す(GCS backend を有効化)
# → 手動でファイルを編集
# State を GCS に移行
terraform init -migrate-state
# "Do you want to copy existing state to the new backend?" → yes
ここは Claude Code に聞けば手順が正確に出てくる。ただし「backend.tf のコメントを外す」は手動作業。
Step 5: GitHub Secrets/Variables ── AI に解決できない問題②
一番面倒な作業。 Terraform の outputs と GitHub Secrets の対応表を見ながら、1つずつ手動登録する。
Terraform outputs → GitHub Secrets
| GitHub Secret 名 | Terraform output | 値の例 |
|---|---|---|
PROD_WIF_PROVIDER |
wif_provider |
projects/123/locations/global/... |
PROD_DEPLOY_SA |
deploy_service_account |
myapp-deploy@...iam... |
PROD_CLOUD_SQL_PRIVATE_IP |
cloud_sql_private_ip |
10.x.x.x |
PROD_MIGRATE_SA |
migrate_service_account |
myapp-migrate@...iam... |
PROD_APP_SA |
app_service_account |
myapp-app@...iam... |
PROD_CLOUD_SQL_CONNECTION_NAME |
cloud_sql_connection_name |
myapp-production:... |
手動生成 → GitHub Secrets
| GitHub Secret 名 | 生成方法 |
|---|---|
PROD_DATABASE_PASSWORD |
Step 2 で生成済み |
PROD_BETTER_AUTH_SECRET |
openssl rand -base64 32 |
PROD_GOOGLE_CLIENT_ID |
Step 6 で GCP Console から取得 |
PROD_GOOGLE_CLIENT_SECRET |
同上 |
PROD_RESEND_API_KEY |
メール配信サービスから取得 |
PROD_PII_ENCRYPTION_KEY |
openssl rand -base64 32 |
PROD_PII_HASH_SALT |
openssl rand -base64 32 |
PROD_OPTOUT_HMAC_SECRET |
openssl rand -base64 32 |
PROD_TRACKING_HMAC_SECRET |
openssl rand -base64 32 |
Variables(公開値)
| Variable 名 | 値 |
|---|---|
PROD_APP_URL |
https://myapp.example.com |
PROD_EMAIL_FROM |
noreply@myapp.example.com |
PROD_DOMAIN |
myapp.example.com |
PROD_CLOUD_TASKS_SA |
tasks_service_account_email の値 |
合計 19 個の Secrets/Variables を手動で登録する。
Claude Code にこの作業を「やって」と言っても、GitHub のウェブ UI を操作することはできない。gh secret set コマンドは使えるが、値は人間が確認して渡す必要がある。
# gh CLI で一括登録する方法(Claude Code が教えてくれた)
# ※ --env production を使う場合、GitHub リポジトリに
# "production" Environment が事前に作成されている必要がある。
# Environment を使わない場合は --env を省略(リポジトリレベルの Secret になる)
gh secret set PROD_WIF_PROVIDER --body "$(terraform output -raw wif_provider)" --env production
gh secret set PROD_DEPLOY_SA --body "$(terraform output -raw deploy_service_account)" --env production
# ...
これは便利だった。 手動で19個コピペするよりはるかに安全(コピペミスがない)。ただし terraform output の結果が正しいことの確認は人間がやる。
落とし穴: 対応表のズレ
最初に Claude Code が生成した対応表と、実際の Terraform outputs の名前が微妙にズレていた:
設計書: PROD_MIGRATE_SA → migrate_sa ← 間違い
実際: PROD_MIGRATE_SA → migrate_service_account ← 正しい
output 名を確認せずにコピペしたら、deploy ワークフローが認証エラーで止まるところだった。対応表は必ず terraform output の実際の出力と突き合わせる。
Step 6-7: OAuth 設定 & DNS(Claude Code: ★☆☆)
GCP Console での OAuth クライアント作成と、ドメインレジストラでの NS レコード設定。どちらもウェブ UI での作業で、Claude Code にはできない。
Claude Code が役立ったのは「何をどこで設定するか」の手順書を作る部分:
Google OAuth:
GCP Console → APIs & Services → Credentials
→ Create OAuth 2.0 Client ID
→ Application type: Web application
→ Authorized redirect URIs: https://myapp.example.com/api/auth/callback/google
DNS:
terraform output cloud_dns_name_servers でネームサーバーを確認
→ レジストラの管理画面で NS レコードを設定
Step 8: SSL 証明書の伝播待ち ── AI に解決できない問題③
gcloud compute ssl-certificates describe myapp-ssl-cert \
--format='value(managed.status)'
# PROVISIONING → まだ待ち
# ACTIVE → OK
DNS 設定後、GCP の Managed SSL Certificate が発行されるまで最大 24 時間待つ。 早ければ 10 分で終わるが、保証はない。
Claude Code に「あとどれくらいかかりますか?」と聞いても「最大24時間です」としか答えられない。DNS 伝播は人間にも AI にも制御できない。
この待ち時間が精神的に一番きつかった。デプロイまであと少しなのに、何もできない。
Step 9-10: タグ push → 自動デプロイ(Claude Code: ★★★★★)
git tag v1.0.0
git push origin v1.0.0
タグを push した瞬間、GitHub Actions が起動:
build-and-push → Docker イメージビルド & push (3分)
↓
migrate → Cloud Run Jobs で DB マイグレーション (1分)
↓
deploy → Cloud Run サービスを新イメージで更新 (2分)
↓
smoke-test → /health に GET → 200 OK (30秒)
ここは完全に自動。 Claude Code が書いたワークフロー(修正後)がそのまま動いた。
smoke-test:
Health check passed (attempt 1)
Response: {"status":"ok","version":"1.0.0"}
所要時間とAI貢献度のスコアカード
| Step | 作業 | 所要時間 | AI 貢献度 | 備考 |
|---|---|---|---|---|
| 1 | GCS バケット作成 | 1分 | ★★☆☆☆ | コマンドは教えてくれる |
| 2 | ローカル apply | 15分 | ★☆☆☆☆ | ブートストラップは人間の仕事 |
| 3 | outputs 確認 | 2分 | ★★★★☆ | 手順は正確 |
| 4 | State 移行 | 2分 | ★★★★☆ | 手順は正確 |
| 5 | Secrets 登録 | 30分 | ★★★☆☆ | gh CLI は助かったが確認は人間 |
| 6 | OAuth 設定 | 10分 | ★☆☆☆☆ | GCP Console の操作 |
| 7 | DNS 設定 | 5分 | ★☆☆☆☆ | レジストラの操作 |
| 8 | SSL 伝播待ち | 0〜24時間 | ☆☆☆☆☆ | 待つしかない |
| 9 | タグ push | 1分 | ★★★★★ | 完全自動 |
| 10 | smoke-test | 自動 | ★★★★★ | 完全自動 |
合計作業時間: 約1時間(SSL 伝播待ちを除く)
そのうち AI が貢献した時間: 約20%(コード生成と手順書で時間短縮)
人間にしかできなかった時間: 約80%(ブートストラップ、Secrets 登録、DNS 設定)
※ この比率はデプロイフェーズのみの話。コード生成フェーズ(記事1)では AI の貢献度が圧倒的に高い。シリーズ全体を通じた AI の価値は「叩き台の生成速度」にある。
コスト設計: 段階的にスケールする
Claude Code は初期構成として production に db-custom-2-8192(~$130/月)を提案してきた。これは悪くないが、サービスの成長フェーズを考慮すると段階的にスケールすべき:
Phase 0 (PoC): ~$7/月
db-f1-micro, Cloud Run min=0
→ 社内テスト、機能検証
※ db-f1-micro は共有 vCPU。GCP は本番ワークロード非推奨。外部トラフィックを受けるなら Phase 1 から
Phase 1 (MVP): ~$35/月
db-g1-small, Cloud Run min=0, LB+SSL 追加
→ 初期ユーザーへの提供開始
Phase 2 (Growth): ~$180/月
db-custom-2-8192, Cloud Run min=1, バックアップ+PITR
→ セール時のトラフィック対応
Terraform の変数化のおかげで、スケールアップは tfvars の値を変えるだけ:
- database_tier = "db-f1-micro"
+ database_tier = "db-custom-2-8192"
- database_backup_enabled = false
+ database_backup_enabled = true
Claude Code に「Phase 0 から Phase 2 までのコスト試算を出して」と言ったら、GCP の料金表をベースに概算を出してくれた。ただし「いつ Phase 1 に移行すべきか」の判断は、ビジネスメトリクス(DAU、リクエスト数、レスポンスタイム)に依存するため、AI には答えられない。
シリーズ全体の振り返り
AI 貢献度の最終スコアカード
| カテゴリ | AI 貢献度 | 具体的にできたこと | できなかったこと |
|---|---|---|---|
| Terraform 構文 | ★★★★★ | HCL 構文、リソース定義、validate 一発 OK | - |
| モジュール設計 | ★★★★☆ | modules/environments 分離、tfvars 設計 | 安全なデフォルト値の設計方針 |
| IAM 設計 | ★★★☆☆ | SA 定義、ロール付与 | 5SA 分離の判断、actAs スコープ |
| WIF | ★★★☆☆ | WIF の HCL、attribute_mapping | attribute_condition、ワークフロー制限 |
| CI/CD | ★★★★☆ | ワークフロー構文、ジョブ依存関係 | Script Injection 回避、TOCTOU 対策 |
| Cloud SQL | ★★★★★ | バックアップ、PITR、Query Insights | trivy 推奨の database_flags 不足 |
| テスト戦略 | ★★☆☆☆ | validate が通るコード生成 | テスト戦略の設計、trivy/checkov の CI 組み込み |
| デプロイ手順 | ★★☆☆☆ | コマンドの構文 | ブートストラップ判断、DNS 待ち |
| コスト設計 | ★★★☆☆ | 料金概算 | フェーズ移行のタイミング判断 |
パターンの法則
3記事を通じて見えた法則:
AI が強い領域:
構文 × 定型パターン = ほぼ完璧
(HCL 構文、YAML 構文、コマンドの引数)
AI が弱い領域:
設計判断 × セキュリティコンテキスト = 人間が必要
(最小権限、攻撃シナリオ、循環依存の解決)
AI が意外に強かった領域:
知識の引き出し = 人間より速い場合がある
(check ブロックの存在、gh CLI の使い方、GCP の料金体系)
最も価値があった AI の使い方
- 「叩き台」としてのコード生成: ゼロから書くより、AI の出力を修正するほうが3倍速い
-
知らない API の発見:
checkブロックは AI に教えてもらった - 手順書の生成: デプロイ手順の対応表を作るのが楽になった
-
構文チェック:
terraform validateの前に、AI が構文エラーをほぼゼロにしてくれる
最も危険だった AI の使い方
- セキュリティ判断を AI に委ねる: Script Injection、IAM スコープは人間がレビュー必須
-
「動いたから OK」: AI のコードは
planは通るが、攻撃シナリオを考慮していない - 対応表を AI に任せきる: output 名のズレに気づかず、デプロイが止まるところだった
これから本番環境を作る人へ
チェックリスト(このシリーズの要点)
□ 認証方式
□ SA キーではなく WIF を使っているか
□ WIF に attribute_condition があるか
□ SA ごとにワークフロー制限があるか
□ IAM 設計
□ SA が用途ごとに分離されているか
□ 各 SA のロールが最小限か
□ actAs のスコープが SA レベルか
□ CI/CD セキュリティ
□ ${{ }} に外部入力が直接展開されていないか
□ plan ファイルを artifact で保存しているか
□ Docker イメージに immutable tags を使っているか
□ Cloud SQL
□ Public IP が無効になっているか
□ バックアップ + PITR が有効か
□ 削除保護が有効か
□ デフォルト値
□ production のデフォルトが安全側に倒れているか
□ cross-variable バリデーション(variable validation ブロック、Terraform 1.9+)があるか
□ テスト・検証
□ CI に terraform validate + fmt -check が入っているか
□ tflint が実行されているか
□ trivy / checkov でセキュリティスキャンしているか
□ モジュール単位の terraform test があるか(Terraform 1.6+)
□ デプロイ
□ ブートストラップ手順が文書化されているか
□ GitHub Secrets の対応表が terraform output と一致しているか
最後に
AI にコードを書かせることの価値は「速さ」にある。47リソースの Terraform を人間がゼロから書いたら数日かかる。Claude Code なら30分で叩き台が出てくる。
でも 叩き台を本番品質にするのは人間の仕事。 そのために必要なのは:
- 「何が危険か」を知っていること(セキュリティの知識)
- 「なぜそうするのか」を説明できること(設計の判断力)
- 「動くけど使えない」を見分けること(運用の経験)
コードを書く能力と、サービスを運用する能力は別物。AI 時代だからこそ、後者の価値が相対的に上がっている。
参考
- Workload Identity Federation
- Terraform GCP Provider
- GitHub Actions Security Hardening
- Cloud SQL バックアップと復元
- GitHub Actions Script Injection
シリーズ記事
- Claude Code に「GCP で本番環境作って」と言ったら、4箇所にセキュリティ脆弱性があった
- Claude Code が書いた Terraform を本番品質にするまでの全修正記録
- Claude Code と一緒に本番デプロイして学んだ、AI に任せていいこと・ダメなこと(この記事)