1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code と一緒に本番デプロイして学んだ、AI に任せていいこと・ダメなこと

1
Posted at

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.workloadIdentityPoolAdminroles/compute.networkAdminroles/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 の使い方

  1. 「叩き台」としてのコード生成: ゼロから書くより、AI の出力を修正するほうが3倍速い
  2. 知らない API の発見: check ブロックは AI に教えてもらった
  3. 手順書の生成: デプロイ手順の対応表を作るのが楽になった
  4. 構文チェック: terraform validate の前に、AI が構文エラーをほぼゼロにしてくれる

最も危険だった AI の使い方

  1. セキュリティ判断を AI に委ねる: Script Injection、IAM スコープは人間がレビュー必須
  2. 「動いたから OK」: AI のコードは plan は通るが、攻撃シナリオを考慮していない
  3. 対応表を 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分で叩き台が出てくる。

でも 叩き台を本番品質にするのは人間の仕事。 そのために必要なのは:

  1. 「何が危険か」を知っていること(セキュリティの知識)
  2. 「なぜそうするのか」を説明できること(設計の判断力)
  3. 「動くけど使えない」を見分けること(運用の経験)

コードを書く能力と、サービスを運用する能力は別物。AI 時代だからこそ、後者の価値が相対的に上がっている。


参考


シリーズ記事

  1. Claude Code に「GCP で本番環境作って」と言ったら、4箇所にセキュリティ脆弱性があった
  2. Claude Code が書いた Terraform を本番品質にするまでの全修正記録
  3. Claude Code と一緒に本番デプロイして学んだ、AI に任せていいこと・ダメなこと(この記事)
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?