マルチアカウントIaCで3年間地獄を見た話|辿り着いた設計と運用パターン
アカウント増えるたびにStateが散乱、CIが混乱、terraform destroyで冷や汗……そんな経験ありませんか?3年の失敗から安定運用に辿り着いたTerraform・CDK・Control Towerの実践パターンを公開します。
マルチアカウントIaC戦略2026|3年の失敗から辿り着いた設計と運用パターン
正直に言う。2023年に「マルチアカウントをちゃんとIaCで管理しよう」と決意して最初の半年は地獄だった。アカウントが増えるたびにStateファイルが散乱して、CIが「どのアカウントに何をデプロイするんだっけ」状態になって、チームメンバーがterraform destroyをローカルから叩いてlsして冷や汗をかいてた。
2026年時点でようやく「これで安定して回せてる」という形に辿り着いたので、その過程で踏んだ地雷と現在の構成を包み隠さず書いていく。同じ苦労をしているエンジニアの参考になれば。
なぜマルチアカウントIaCはこんなに難しいのか
単一アカウントのIaCと比べて、マルチアカウントで難しいのは「Stateの分離」「権限の委任」「デプロイ順序の制御」の3つが複雑に絡み合うことだと思っている。
単一アカウントならterraform applyを叩けばそれで終わり。でもアカウントが10を超えてくると、問題が一気に噴き出す。どのアカウントのStateをどこに置くか。CI/CDパイプラインが各アカウントにどう認証するか。共通のセキュリティベースラインをどう全アカウントに展開するか。新しいアカウントをオンボーディングするときのフローをどう自動化するか。この4つが同時に押し寄せてくる感じ、経験した人にはわかってもらえると思う。
AWS Organizationsで10個のアカウントをまとめた話でも少し触れたけど、組織構造の設計が甘いままIaCを積み上げると後で全部作り直しになる。うちのチームは実際にそれをやった。
2026年時点での主な選択肢比較
| ツール | 強み | 弱み | 向いてるケース |
|---|---|---|---|
| Terraform 1.9+ | エコシステム成熟・State管理柔軟 | Stacks機能まだ発展途上 | 大規模・既存資産多い |
| AWS CDK v2 | TypeScriptで型安全・Constructs豊富 | 学習コスト高め | AWSネイティブ重視 |
| Pulumi 3.x | 汎用言語使用可能・マルチクラウド | ステート管理コスト | マルチクラウド対応が必要 |
| OpenTofu 1.8 | Terraform互換・OSS | エコシステム差 | Terraform代替検討中 |
| CloudFormation StackSets | AWS純正・Organizations連携 | 記述量多い・デバッグ辛い | セキュリティベースライン配布 |
うちのチームでは現在、Terraform 1.9 + CDK v2の二段構成で運用している。「どちらかに統一しろ」というツッコミはわかるんだけど、実際にやってみると役割が明確に分かれるので意外とうまく機能している。その詳細は後述する。
全体アーキテクチャと構成図
現在運用している構成を図にするとこうなる。
graph TB
subgraph Management["Management Account"]
ORG[AWS Organizations]
CT[Control Tower]
AFT[Account Factory<br/>for Terraform]
PIPE_MGMT[CodePipeline<br/>Infra Orchestrator]
end
subgraph SecurityOU["Security OU"]
subgraph SecurityAcc["Security Tooling Account"]
CT_CTRL[Config Aggregator]
SIEM[Security Hub]
LOG_ARCHIVE[Log Archive]
end
end
subgraph SharedOU["Shared Services OU"]
subgraph SharedAcc["Shared Services Account"]
ECR_SHARED[ECR Shared]
TF_STATE[Terraform State<br/>S3 + DynamoDB]
OIDC[OIDC Provider<br/>GitHub Actions]
end
end
subgraph WorkloadOU["Workload OU"]
subgraph ProdAcc["Production Account"]
subgraph VPC_PROD["VPC prod-vpc"]
subgraph AZ1_PROD["AZ ap-northeast-1a"]
ECS_PROD[ECS Fargate]
RDS_PROD[RDS Aurora]
end
subgraph AZ2_PROD["AZ ap-northeast-1c"]
ECS_PROD2[ECS Fargate]
RDS_PROD2[RDS Aurora Replica]
end
ALB_PROD[ALB]
end
end
subgraph StagingAcc["Staging Account"]
subgraph VPC_STG["VPC stg-vpc"]
ECS_STG[ECS Fargate]
RDS_STG[RDS Aurora]
end
end
subgraph DevAcc["Dev Account"]
subgraph VPC_DEV["VPC dev-vpc"]
ECS_DEV[ECS Fargate]
end
end
end
ORG --> CT
CT --> AFT
AFT -->|Account Vending| ProdAcc
AFT -->|Account Vending| StagingAcc
AFT -->|Account Vending| DevAcc
PIPE_MGMT -->|AssumeRole| ProdAcc
PIPE_MGMT -->|AssumeRole| StagingAcc
PIPE_MGMT -->|AssumeRole| DevAcc
TF_STATE -->|State Lock| PIPE_MGMT
OIDC -->|OIDC Auth| PIPE_MGMT
LOG_ARCHIVE -->|Log Aggregation| ProdAcc
LOG_ARCHIVE -->|Log Aggregation| StagingAcc
SIEM -->|Findings| SecurityAcc
ポイントをいくつか説明すると:
Shared Services AccountにStateを集約している。各チームがそれぞれのアカウントにStateバケットを持つのは最初に試してやめた。アカウントが増えるとバケット管理だけで疲弊する。集約した方がBackend設定のシンプルさと、State Lockのロック状態確認が格段に楽になる。
OIDC認証でCI/CDを接続している。以前はIAMユーザーのアクセスキーをCIに登録していたが、キーローテーションの管理とセキュリティリスクでしんどくなった。GitHub ActionsのOIDCプロバイダーをShared AccountとManagementに登録して、そこからAssumeRoleで各アカウントに入る構成に変えた。
Terraform + CDKの役割分担、うちが辿り着いたパターン
「TerraformとCDKを混在させるなんて混乱する」という意見はわかる。最初は僕もそう思ってた。でも2年運用してみると、それぞれが得意なことが明確に違うんですよね。
Terraformが担当するレイヤー:
- Organizations構造・OU設計
- アカウントレベルのベースライン(CloudTrail・Config・SecurityHub有効化)
- ネットワーク基盤(VPC・Transit Gateway・PrivateLink)
- IAMロール定義(Cross-AccountのRoleは特に)
- コスト管理系(Budgets・Cost Anomaly Detection)
CDK v2が担当するレイヤー:
- アプリケーションワークロード(ECS・Lambda・API Gateway)
- データストア(RDS・DynamoDB・Elasticache)
- アプリ固有のCIパイプライン
- L3 Constructsを活用した標準化パターンの配布
Terraformは「組織の基盤」、CDKは「アプリケーションの構築」という分担にすると、担当チームも分かれやすくて運用しやすい。インフラチームがTerraformを、アプリ開発チームがCDKを、というオーナーシップが自然と生まれる。個人的には、この分担を言語化できたのが運用安定の転換点だったと思っている。それまでは「なんとなくTerraformでいけそうなやつはTerraformで」みたいな雰囲気だったから。
Terraform Stateの設計
# backend.tf - Shared Servicesアカウントに集約
terraform {
backend "s3" {
bucket = "shared-tfstate-ap-northeast-1"
key = "${var.account_name}/${var.layer}/terraform.tfstate"
region = "ap-northeast-1"
dynamodb_table = "tfstate-lock"
encrypt = true
# Shared ServicesアカウントのRole経由でアクセス
role_arn = "arn:aws:iam::SHARED_ACCOUNT_ID:role/TerraformStateRole"
}
}
キーの命名規則は{account_name}/{layer}/terraform.tfstateにしている。layerはnetwork/security/computeのように機能別に分けることで、Stateが巨大になってlockが詰まる問題を回避できる。地味に便利なのが、このパス構造にしておくとS3コンソール上でアカウントとレイヤーがディレクトリ的に見えて、どこに何があるかが一目でわかること。
Terraformで3年分の失敗から学んだ、State管理とAI検証の正解にも詳しく書いたけど、Stateの粒度設計は本当に後から変えるのがキツいので、最初に設計しきることを強くすすめる。
CDKマルチアカウントのbootstrap問題
CDKをマルチアカウントで使う場合、cdk bootstrapを各アカウントに対して実行する必要がある。これをAFT(Account Factory for Terraform)のカスタマイゼーションで自動化している。
# AFTカスタマイゼーション内で実行するスクリプト
import subprocess
import boto3
def bootstrap_cdk(account_id: str, region: str = "ap-northeast-1"):
"""新規アカウントをCDK bootstrapする"""
trusted_account = "MANAGEMENT_ACCOUNT_ID"
cmd = [
"cdk", "bootstrap",
f"aws://{account_id}/{region}",
"--trust", trusted_account,
"--trust-for-lookup", trusted_account,
"--cloudformation-execution-policies",
"arn:aws:iam::aws:policy/AdministratorAccess",
"--qualifier", "myorg", # 独自qualifierで管理
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"CDK bootstrap failed: {result.stderr}")
print(f"✅ CDK bootstrapped for account {account_id} in {region}")
return result.stdout
--qualifierを組織固有のものにしておくと、デフォルトのqualifierとの衝突を防げる。うちはmyorgにしているが、組織名の略称などを使うといい。これを最初にやっておかないと、後で複数のqualifierが混在してbootstrapスタックが二重になる事故が起きる。実際に一回やらかした。
CI/CDパイプラインの設計
CI/CDパイプラインの設計で3回くらい作り直した。最終的に落ち着いた構成を説明する。
flowchart LR
PR[Pull Request] -->|Trigger| CI_CHECK
subgraph CI_CHECK["CI Check Phase"]
PLAN[terraform plan<br/>全アカウント]
LINT[tflint +<br/>tfsec]
COST[infracost<br/>差分表示]
CDK_SYNTH[cdk synth<br/>+ cdk-nag]
end
CI_CHECK -->|PR Comment| REVIEW[Code Review]
REVIEW -->|Merge to main| CD_DEPLOY
subgraph CD_DEPLOY["CD Deploy Phase"]
direction TB
BASELINE[1. Security Baseline<br/>全アカウント並列]
NETWORK[2. Network Layer<br/>Transit GW → VPC]
COMPUTE[3. Compute Layer<br/>環境別順次]
APP[4. Application<br/>dev → stg → prod]
end
BASELINE --> NETWORK
NETWORK --> COMPUTE
COMPUTE --> APP
APP -->|Slack通知| NOTIFY[デプロイ完了通知]
デプロイを4フェーズに分けているのがポイントで、それぞれに依存関係がある。Baselineが壊れてからComputeを立ち上げても意味ないし、逆にComputeが一切なくてもBaselineだけ先に適用できる。この構造にしてから「Computeのデプロイが失敗したけどBaselineは正常」みたいな切り分けがすごく楽になった。
GitHub Actions設定の実例
# .github/workflows/terraform-deploy.yml
name: Terraform Deploy
on:
push:
branches: [main]
paths:
- 'terraform/**'
permissions:
id-token: write # OIDC必須
contents: read
jobs:
deploy-baseline:
name: Deploy Security Baseline
runs-on: ubuntu-latest
strategy:
matrix:
account:
- name: prod
account_id: "111111111111"
- name: staging
account_id: "222222222222"
- name: dev
account_id: "333333333333"
max-parallel: 3 # 並列で全アカウントに適用
steps:
- uses: actions/checkout@v4
- name: Configure AWS Credentials (Shared Services)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::SHARED_ACCOUNT_ID:role/GitHubActionsRole
aws-region: ap-northeast-1
- name: Assume Target Account Role
run: |
CREDS=$(aws sts assume-role \
--role-arn "arn:aws:iam::${{ matrix.account.account_id }}:role/TerraformDeployRole" \
--role-session-name "github-actions-${{ github.run_id }}" \
--output json)
echo "AWS_ACCESS_KEY_ID=$(echo $CREDS | jq -r .Credentials.AccessKeyId)" >> $GITHUB_ENV
echo "AWS_SECRET_ACCESS_KEY=$(echo $CREDS | jq -r .Credentials.SecretAccessKey)" >> $GITHUB_ENV
echo "AWS_SESSION_TOKEN=$(echo $CREDS | jq -r .Credentials.SessionToken)" >> $GITHUB_ENV
- name: Terraform Apply - Baseline
working-directory: terraform/layers/security-baseline
run: |
terraform init -backend-config="key=${{ matrix.account.name }}/security-baseline/terraform.tfstate"
terraform apply -auto-approve \
-var="account_name=${{ matrix.account.name }}" \
-var="account_id=${{ matrix.account.account_id }}"
ここで重要なのが「Shared Services AccountのRoleを経由して各アカウントのRoleにAssumeRoleする」二段構成。GitHub ActionsのOIDCはShared Services Accountだけに登録して、そこから各ワークロードアカウントに権限委任する。これでOIDCプロバイダーの管理が一箇所に集約される。
新規アカウントのオンボーディング自動化
アカウントが増えるにつれ、新規アカウントのセットアップを手動でやることへの恐怖感が出てきた。「あのアカウント、CloudTrail有効化したっけ?」みたいな会話が起きるのが一番怖い。コンプライアンス的にアウトになるリスクもあるし、なにより精神衛生がよくない。
2026年現在は**Control Tower + AFT(Account Factory for Terraform)**でアカウントのプロビジョニングから基本設定まで全自動化している。AWSアカウント10個で管理が崩壊した話|Control Tower + AFTで立て直した6ヶ月の記録で詳しく書いたが、AFTの概念をTerraformで書ける点が個人的にはかなり気に入っている。
# AFTでアカウント定義
module "new_team_account" {
source = "./modules/aft-account-request"
control_tower_parameters = {
AccountEmail = "team-new@mycompany.com"
AccountName = "team-new-prod"
ManagedOrganizationalUnit = "Workloads/Production"
SSOUserEmail = "admin@mycompany.com"
SSOUserFirstName = "Platform"
SSOUserLastName = "Team"
}
account_tags = {
Environment = "production"
Team = "new-team"
CostCenter = "CC-12345"
DataClass = "internal"
}
change_management_parameters = {
change_requested_by = "platform-team"
change_reason = "new product launch"
}
# アカウント作成後に自動実行されるカスタマイゼーション
account_customizations_name = "standard-workload-baseline"
}
これをPRに出してマージするだけで、数十分後にアカウントが作られてCloudTrail・Config・SecurityHubが有効化されて、CDKのBootstrapまで済んだ状態で渡せる。最初にAFTの設定に1ヶ月かけたけど、その後の手作業ゼロは本当にマジで助かっている。
オンボーディング工数:AFT導入前後の比較
AFT導入前後でどれだけ工数が変わったか、タスク別に並べるとこうなる。
xychart-beta
title "アカウントオンボーディング 工数削減効果(分)"
x-axis ["Organizations登録", "CloudTrail有効化", "Config設定", "SecurityHub有効化", "CDK Bootstrap", "IAMロール作成", "コスト通知設定"]
y-axis "作業時間(分)" 0 --> 60
bar [2, 0, 0, 0, 0, 0, 0]
line [45, 30, 20, 15, 10, 25, 20]
棒グラフが現在(AFT導入後)、折れ線が以前の手作業時間。Organizations登録だけ2分かかっているのはPR作成とマージの時間で、それ以外は全部0分(自動)。合計165分 → 2分になった。正直これが一番インパクト大きかった。
運用で気づいたこと・今でも悩んでいること
「完璧な構成が完成した」と言いたいところだけど、正直まだ改善中の部分もある。うまくいっていることと、まだ課題に感じていることを整理すると以下のとおりだ。
| カテゴリ | 内容 |
|---|---|
| ✅ うまくいっている | アカウントのオンボーディングが完全自動化された |
| ✅ うまくいっている | StateファイルのパスがアカウントとLayerで構造化されて見通しが良い |
| ✅ うまくいっている | CI/CDでのPRコメントにinfracostの差分が出るので、コストインパクトが事前に見える |
| ✅ うまくいっている | SCPとRCPの組み合わせで意図しないリソース作成を防止できている |
| ⚠️ まだ課題 | Terraform PlanがCI上で全アカウント分走るので、大規模PRだと15分待ちになる(Atroposなどの差分検出を検討中) |
| ⚠️ まだ課題 | CDKとTerraformのドリフト検出が統一されていない(CDK側はcdk diffを手動で叩くことになりがち) |
| ⚠️ まだ課題 | アカウントが20を超えてきたあたりから、OU構造の見直し議論が定期的に発生する |
「OrganizationsのOU設計は最初が肝心」とはよく言われるが、本当にそう。プロダクトが増えるたびに「このアカウントはどのOUに入れるべきか」という議論が必ず起きる。SCPの適用範囲の影響もあって、一回決めたOU構造を変えるのは心理的コストが高い。ここだけは「後でいいや」を絶対に許してはいけないと思っている。
マルチクラウドの文脈でこの設計をどう拡張するかについてはマルチクラウド戦略2026|最適構成とベストプラクティスも参考になるかもしれない。
また、このインフラ基盤の上に乗っかるSLI/SLO設計2026完全ガイドも合わせて読んでもらえると、マルチアカウントの信頼性設計の全体像がつかめると思う。
まとめ
3年かけて辿り着いたマルチアカウントIaC戦略の要点をまとめる。
-
Stateの集約は早めに決める。Shared Services Accountに集約して、キーはアカウント名とLayerで構造化する。後から変えるのは地獄。
-
TerraformとCDKは役割分担できる。基盤(ネットワーク・セキュリティ・Organizations)はTerraform、アプリケーション層はCDKという分担は意外とうまく機能する。無理に統一しなくていい。
-
新規アカウントオンボーディングの自動化が最大のROI。AFTで自動化すると手作業165分 → 2分になった。ここへの投資は絶対に報われる。
-
CI/CDはOIDC + 二段AssumeRoleで。IAMアクセスキーのCI登録は今すぐやめるべき。OIDCプロバイダーをShared Servicesに集約してRoleを委任する構成が管理しやすい。
-
OU設計だけは最初に時間をかけろ。一度決めたOU構造を変えるのはSCPの影響範囲確認など心理的コストが高い。プロダクトの成長シナリオを考慮した設計を最初に議論しきること。
次のアクションとしては、まず手元の環境でTerraformのStateパス設計を見直すことから始めるのがいいと思う。State設計が整ったら、AFTかAFTに類するアカウント自動化の仕組みを入れる。この2つだけでも運用の安定度が全然変わる。
皆さんはマルチアカウントのIaC、どんな構成で運用してますか?特にOU設計とStateの管理方法はチームによって全然違うと思うので、知見があったらぜひ聞かせてください。