⚠️ なぜ「準備」が最も重要なのか
Terraform で最も多いトラブルは、準備不足のまま terraform apply を実行することで起きます。
よくある失敗パターン:
- 権限不足で apply 失敗 → 中途半端なリソースが残る
- API が有効化されておらず
403 SERVICE_DISABLED - State の保存先を決めずにローカルで実行 → チームで共有できない
- 手動で作ったリソースと Terraform が衝突する
コードを書く前に、環境を整える。 これが Terraform 運用の鉄則です。
🗺️ 準備の全体像
本記事はシリーズ 「Terraform IaC 実践ガイド」第 2 回(準備編) です。第 1 回 概要 で示したマルチクラウド基盤を、コードを書く前に設計で固める段階を扱います。
Terraform を導入する前に、以下を整理します。
0. bootstrap(鶏と卵の解決) ← State バケットと土台プロジェクトを手で作る
↓
1. クラウドアカウント設計
↓
2. 認証方式の選定(WIF)
↓
3. 権限(IAM)設計
↓
4. State 管理の設計
↓
5. Secret Manager で変数を集約
↓
6. ディレクトリ構成の設計
順番が重要です。上から順に決めないと、後工程で手戻りが発生します。
🥚 0. bootstrap — 鶏と卵を解く
Terraform で全部を管理したい。しかし State の保存先(GCS バケット)は、Terraform を動かす前に存在していないといけない。これが「鶏と卵」問題です。
解決策は、最小限の土台だけを Terraform 管理外で手作業で作ること。
# State バケットだけは手で作る(Terraform 管理外・ローカル実行)
gsutil mb -l asia-northeast1 gs://my-project-tfstate
gsutil versioning set on gs://my-project-tfstate
実運用では、この土台を bootstrap プロジェクトとして 1 つ用意し、そこに以下を集約します。
| bootstrap が持つもの | 役割 |
|---|---|
| State バケット | 全スタックの State を集約(プレフィックスで分離) |
| WIF Pool / Provider | CI/CD のキーレス認証の入口 |
| CI 用サービスアカウント | Secret Manager 読み取り・各スタック SA への橋渡し |
| Secret Manager | 各スタックの変数(tfvars)を集約 |
落とし穴: bootstrap プロジェクトは「最初の 1 回だけローカルから apply」する特別なスタックです。ここを CI に載せようとすると、また鶏と卵になります。bootstrap だけはローカル apply を許容し、それ以外は CI 経由、と割り切ります。
☁️ 1. クラウドアカウント設計
環境分離の原則
本番・ステージング・開発は、プロジェクト(アカウント)レベルで分離します。
| クラウド | 分離単位 | 例 |
|---|---|---|
| Google Cloud | プロジェクト | myapp-prod, myapp-stg |
| AWS | アカウント | 本番アカウント、検証アカウント |
| Azure | サブスクリプション | 本番サブスクリプション、検証サブスクリプション |
同一プロジェクト内で環境を分けると、本番リソースを誤って削除するリスクが生まれます。
マルチクラウドの場合
クラウドごとに役割を明確にします。
例:
Google Cloud → メインクラウド(Cloud Run, Cloud SQL 等)
AWS → 認証基盤のみ(Cognito)
「両方で同じことをやる」のではなく、各クラウドの得意領域を活かす設計にします。
🔐 2. 認証方式の選定
サービスアカウントキーは使わない
Terraform の認証で最も避けるべきは、サービスアカウントキー(JSON キー)の利用です。
| 方式 | セキュリティ | 運用コスト | 推奨 |
|---|---|---|---|
| サービスアカウントキー | 漏洩リスクあり | キーのローテーション必要 | NG |
| Workload Identity Federation (WIF) | キーレス | ローテーション不要 | 推奨 |
| gcloud auth(ローカル開発) | 個人認証 | 手動ログイン | 開発のみ |
Workload Identity Federation(WIF)
CI/CD パイプラインからの認証は WIF を使います。GitLab / GitHub Actions の OIDC トークンで、キーなしに Google Cloud や AWS に認証できます。
CI/CD ランナー
↓ OIDC トークン
Workload Identity Pool
↓ トークン交換
サービスアカウント(権限あり)
↓ 認証済み
Terraform 実行
attribute mapping と最小権限
WIF Provider では、OIDC トークンの主張(assertion)を GCP 側の属性にマッピングし、どのリポジトリ・どのブランチからの実行を許すかを絞ります。マッピングする主な属性は「リポジトリ識別子・ブランチ/タグ・参照種別・保護ブランチか否か」です。
これにより、本番スタックは保護ブランチからのみ・検証スタックは MR からも回せる、といった環境別の絞り込みができます(条件分岐の考え方は第 5 回 運用編)。
設計判断: WIF Provider はリポジトリ単位で分けます。1 つの Provider を全社共有にすると条件が緩くなり最小権限が崩れるため、クライアント・リポジトリごとに切るのが安全です。
クロスクラウド認証
Google Cloud と AWS の両方を Terraform で管理する場合:
GitLab Runner
├→ GCP WIF で Google Cloud に認証
└→ GCP SA の OIDC トークン → AWS STS AssumeRoleWithWebIdentity
→ AWS IAM Role で AWS に認証
キーの受け渡しなしで、クラウド間の認証が成立します。
AWS の落とし穴: OIDC Provider は AWS アカウントに 1 つしか作れません。stg と prod が同一 AWS アカウントを共有する場合、各スタックで
resourceとして作るとEntityAlreadyExistsで衝突します。OIDC Provider は bootstrap 側で 1 度だけ作り、各スタックはdataソースで参照するだけにします。詳細は GCP → AWS キーレス認証の落とし穴。
🛡️ 3. 権限(IAM)設計
最小権限の原則
Terraform 用のサービスアカウントには、必要最小限の権限だけを付与します。
✗ roles/owner(何でもできる → 危険)
✗ roles/editor(広すぎる)
○ 必要なロールだけを個別に付与
サービスアカウントの分離
1 つのサービスアカウントで全部やらず、用途別に分離します。実運用では命名規約を定めたうえで、おおむね 3 系統に分けています。
| サービスアカウント | 用途 | 権限 |
|---|---|---|
| Terraform 実行用 | インフラ構築・変更 | リソース作成・変更 |
| CI デプロイ用 | アプリのデプロイ | Cloud Run デプロイのみ |
| ランタイム用 | 実行時のサービス | 実行に必要な最小権限 |
「Terraform を動かす権限」と「アプリが実行時に使う権限」を分けるのが肝です。これを混ぜると、アプリ起動時の権限事故が Terraform 全体に波及します。命名は環境・用途が一目で分かる規約をプロジェクトごとに定めておきます。
API の有効化を忘れない(両プロジェクトで)
Terraform でリソースを作る前に、対象の API を有効化します。
# Google Cloud の場合
gcloud services enable run.googleapis.com # Cloud Run
gcloud services enable sqladmin.googleapis.com # Cloud SQL
gcloud services enable secretmanager.googleapis.com # Secret Manager
gcloud services enable iamcredentials.googleapis.com # WIF の SA impersonation
これを忘れると 403 SERVICE_DISABLED になります。
マルチプロジェクトの落とし穴: 対象プロジェクトで API を有効化しても、bootstrap プロジェクト側でも同じ API が必要になるケースがあります(SA impersonation や横断操作)。「対象側は有効なのに 403 が出る」ときは、bootstrap 側の API を疑ってください(第 5 回 運用編 の障害 3)。
組織ポリシーの事前確認
GCP の組織ポリシーは、デフォルトで allUsers(公開アクセス)をブロックすることがあります。公開エンドポイントを作る予定があるなら、Tag Binding を使った解放の仕組みを準備段階で設計しておきます(実装手順は第 5 回 運用編 の障害 4)。準備段階で気付かないと、apply 時に does not belong to a permitted customer で詰まります。
💾 4. State 管理の設計
State とは
Terraform の State は、「Terraform が管理しているリソースの一覧」 です。
State ファイル(terraform.tfstate)
├ Cloud Run サービス A → 実際の GCP リソース
├ Cloud SQL インスタンス B → 実際の GCP リソース
└ S3 バケット C → 実際の AWS リソース
State がないと、Terraform はどのリソースを自分が作ったのか判別できません。
リモートバックエンド必須
State はリモートに保存します。ローカルに置くと、チームで共有できません。
GCS に統合する
マルチクラウド構成(GCP + AWS + Azure)の場合、State の保存先をクラウドごとに分けると管理が煩雑になります。GCS(Cloud Storage)に統合すると、State の管理対象が 1 箇所にまとまり、運用がシンプルになります。
# GCP のスタックも、AWS のスタックも、同じ GCS バックエンド
terraform {
backend "gcs" {
bucket = "my-project-tfstate"
}
}
State バケットの作成
State 保存用のバケットは Terraform 管理外で作成します(鶏と卵の問題)。
gsutil mb -l asia-northeast1 gs://my-project-tfstate
gsutil versioning set on gs://my-project-tfstate
バージョニングは必ず有効化します。State を壊した場合の復旧に必要です。
🗝️ 5. Secret Manager で変数を集約する
スタックごとの変数(プロジェクト ID・WIF audience・SA・バックエンド設定など)を GitLab Variables に散らすのではなく、Secret Manager に集約します。CI/CD は実行時に bootstrap 認証で Secret Manager から読み出します。
ポイントは 2 つだけです。
- スタック × 環境を一意に特定できる命名規約を決める(スタックが増えても破綻しない)
- 実値は Secret Manager 側にのみ置き、リポジトリや GitLab Variables には持たせない
狙い: 認証情報の置き場所を 1 箇所に集約し、GitLab 側には「Secret Manager を読む最小権限」だけを持たせます。スタック追加 = Secret を 1 つ足すだけ、という運用にします。命名規約の具体形はプロジェクト内で定義します。
📁 6. ディレクトリ構成の設計
module / environments 分離パターン
terraform/
├── environments/ # 環境別エントリーポイント(薄い)
│ ├── stg/
│ │ ├── client-a/ # クライアントスタック
│ │ └── auth-stack/ # 認証スタック(AWS Cognito + GCP)
│ └── prod/
│ ├── client-a/
│ ├── client-b/
│ └── auth-stack/
├── infra/
│ └── prod/infra-bootstrap/ # bootstrap(State バケット・WIF Pool 等/ローカル実行)
└── modules/
├── infra/ # 基盤プリミティブ(1 リソース群 = 1 モジュール)
│ ├── gcp/ # cloud-run, cloud-sql, load-balancer, dns-zone, gitlab-wif, ...
│ └── aws/ # cognito, ses, lightsail, ...
├── clients/ # クライアント単位の構成(infra を束ねる)
│ ├── client-a/
│ └── client-b/
└── services/ # 横断サービス({業務ドメイン}/{サービス})
└── app-domain/
└── shared-svc/
設計原則
| 原則 | 説明 |
|---|---|
| 単一責任 | 各モジュールは 1 つの責務のみ |
| 環境差分は変数で | コードの重複を排除 |
| State はスタック単位 | environments/prod/my-app ごとに 1 つの State |
モジュールに環境固有の値をハードコードしてはいけません。すべて変数で渡すのが原則です。
🚫 やってはいけないこと
| 禁止事項 | 理由 |
|---|---|
| CLI で直接リソース作成 | State と実態が不一致になる |
| ローカルから本番に直接 apply | 事故のリスク、CI/CD を通すこと |
| State ファイルをコミット | 機密情報が含まれる |
| サービスアカウントキーの利用 | 漏洩リスク、WIF を使う |
本番環境で terraform destroy | 多層防御で禁止すべき |
特に CLI で直接リソースを作ると、State との不整合(State drift) が発生します。Terraform で管理すると決めたら、すべて Terraform 経由で変更してください。
📚 シリーズ記事
| # | タイトル | 内容 |
|---|---|---|
| 1 | 概要 | マルチクラウド構成の全体像と AI 駆動運用 |
| 2 | 準備編(本記事) | アカウント設計・WIF 認証・bootstrap・State 管理 |
| 3 | モジュール設計編 | 再利用可能なモジュールの設計原則 |
| 4 | CI/CD パイプライン編 | 動的 child pipeline・WIF キーレス認証・GCP↔AWS |
| 5 | 運用編 | State drift 解消・障害対応・AI 運用ループ |
関連記事
- WIF でキーレス認証 — 本記事の WIF 設計の詳細
- GCP → AWS キーレス認証の落とし穴 — クロスクラウド認証で踏む罠
- ステアリング駆動開発とは:概要 — 本基盤を AI と組んで開発する手法