マルチアカウント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 v2TypeScriptで型安全・Constructs豊富学習コスト高めAWSネイティブ重視
Pulumi 3.x汎用言語使用可能・マルチクラウドステート管理コストマルチクラウド対応が必要
OpenTofu 1.8Terraform互換・OSSエコシステム差Terraform代替検討中
CloudFormation StackSetsAWS純正・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戦略の要点をまとめる。

  1. Stateの集約は早めに決める。Shared Services Accountに集約して、キーはアカウント名とLayerで構造化する。後から変えるのは地獄。

  2. TerraformとCDKは役割分担できる。基盤(ネットワーク・セキュリティ・Organizations)はTerraform、アプリケーション層はCDKという分担は意外とうまく機能する。無理に統一しなくていい。

  3. 新規アカウントオンボーディングの自動化が最大のROI。AFTで自動化すると手作業165分 → 2分になった。ここへの投資は絶対に報われる。

  4. CI/CDはOIDC + 二段AssumeRoleで。IAMアクセスキーのCI登録は今すぐやめるべき。OIDCプロバイダーをShared Servicesに集約してRoleを委任する構成が管理しやすい。

  5. OU設計だけは最初に時間をかけろ。一度決めたOU構造を変えるのはSCPの影響範囲確認など心理的コストが高い。プロダクトの成長シナリオを考慮した設計を最初に議論しきること。

次のアクションとしては、まず手元の環境でTerraformのStateパス設計を見直すことから始めるのがいいと思う。State設計が整ったら、AFTかAFTに類するアカウント自動化の仕組みを入れる。この2つだけでも運用の安定度が全然変わる。

皆さんはマルチアカウントのIaC、どんな構成で運用してますか?特にOU設計とStateの管理方法はチームによって全然違うと思うので、知見があったらぜひ聞かせてください。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事