マルチアカウントIaC、単一アカウントからの3年の失敗と工夫

AWSを単一アカウントで運用していて限界を感じた経験ありませんか?3年かけてマルチアカウント化した実体験から、アカウント分割の判断基準と本当に機能した設計を本音で語ります。

最初は単一アカウントで十分だと思ってた

先日チームで「なぜマルチアカウント化したのか」という振り返り会をやってて、自分たちの失敗の歴史が一気に蘇った。うちが単一アカウント環境から複数アカウントに移行したのは3年前。当初は開発・本番分けぐらいで十分だと思ってたんですよ。

ところが本番環境が大きくなるにつれて、IAMポリシーが複雑化して、セキュリティグループの設定がもう何が何やら。さらに費用感もぐちゃぐちゃになって、どのプロジェクトがいくら使ってるのかすら把握できない状態になっちゃった。CloudTrailログも全部同じアカウントに混在するから、監査が地獄。SOC2対応の時期でもあったし、「もう限界」って感じで本格的にマルチアカウント化に踏み切ったんです。

そこからの3年間、いろんなパターンを試して、失敗して、ようやく「これなら本番運用できる」って形に収まった。正直、最初は教科書通りにやろうとして盛大にコケました。

アカウント分割の判断基準──正解より失敗から学ぶ

マルチアカウント化する時って「環境別(dev/stg/prod)と機能別(billing/logging/security)」みたいな図をよく見ますよね。うちもそれをそのまま採用したんですが、半年で破綻しました。

理由は単純で、実際のプロジェクト数と管理の粒度が合ってなかったんです。3つの独立したマイクロサービスがあったんですが、それぞれのdev環境を別アカウントにしたら、アカウント数が異常に増えちゃって、IAM権限管理だけで1日潰れる日も出てきた。

そこで学んだのが「アカウント分割は目的から逆算する」ってこと。うちの場合は以下のような感じで落ち着いたんです。

  • Platform Account(1つ):VPC、ロードバランサー、共有ネットワークリソース
  • Project Account:プロジェクトごと(例:モバイルAPI、データパイプライン)に1アカウント
  • 環境分割:各プロジェクトアカウント内で dev/stg/prod を論理分離(VPC + タグで管理)
  • Shared Service Account:ロギング、監視、Artifact Registry
  • Billing Account:AWS Organizations の管理アカウント

重要なのは「アカウント = 課金と権限の境界」っていう認識。環境分割までアカウントで分けたら、コスト集計がクソになるし、リソース作成の手間も倍増する。正直、最初の判断を間違えると後処理が地獄ですよ。

クロスアカウントアクセス設計で最初に踏んだ地雷

graph TD
    A["AWS Organizations"] --> B["Billing Account (Root)"]
    B --> |管理| C["Organizations管理<br/>Consolidated Billing<br/>Config Aggregator"]
    
    B --> D["Platform Account"]
    B --> E["Project Account 1"]
    B --> F["Project Account 2"]
    
    D --> D1["VPC<br/>Transit GW<br/>ECR Repo"]
    E --> E1["App 1<br/>KMS Key<br/>Secrets"]
    F --> F1["App 2<br/>RDS<br/>ElastiCache"]
    
    B --> G["Shared Service Account"]
    G --> G1["CloudWatch Logs<br/>CloudTrail<br/>SNS Topics"]
    
    D -.->|assume role| G
    E -.->|assume role| G
    F -.->|assume role| G

クロスアカウントアクセスは、IAMロールの assume role で実現するんですが、ここで最初にやった失敗が「信頼関係(Trust Relationship)の設計が甘かった」こと。

最初は「Platform Account のリソースを Project Account から使えれば OK」って感じで、こんな感じで設定してたんです:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::PROJECT_ACCOUNT:root"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

でもこれだと「Project Account のすべてのプリンシパルが Platform Account のロールを assume できる」っていう状態になって、セキュリティ監査で一発で指摘されました。正しくは Principal を特定のロール or サービスに限定すべき:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::PROJECT_ACCOUNT:role/DeploymentRole"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "unique-external-id-12345"
        }
      }
    }
  ]
}

External ID を付けることで、Principal が本当に信頼できるかを二段階で検証できる。小ネタみたいだけど、本番セキュリティではマジで重要です。

Terraformでマルチアカウント管理──Backend構成が全てを決める

IaCでマルチアカウント管理するなら、State 管理の設計が最重要。うちは最初 Terraform のディレクトリ構成を こんな感じにしてました:

terraform/
├── accounts/
│   ├── platform/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfstate
│   ├── project-1/
│   │   ├── main.tf
│   │   └── terraform.tfstate
│   └── project-2/
├── modules/
│   ├── vpc/
│   ├── iam/
│   └── logging/
└── shared/
    ├── outputs.tf
    └── data sources...

ディレクトリレベルではまあ良かったんですが、State ファイルの場所がボトルネックになった。ローカルに tfstate が散乱してて、チームメンバーが同時に apply すると競合するし、誰かが tfstate を git に commit しちゃったり。3ヶ月で改善して、以下の構成に変えました:

# accounts/platform/main.tf
terraform {
  required_version = ">= 1.6"
  
  backend "s3" {
    bucket         = "terraform-state-platform-us-east-1"
    key            = "platform/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-lock"
    encrypt        = true
  }
  
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
  
  assume_role {
    role_arn = "arn:aws:iam::${var.platform_account_id}:role/TerraformRole"
  }
}

重要なポイントは以下の通り:

  1. S3 backend はアカウント別に分ける:State ファイル自体も権限で保護できるし、アカウント削除時の影響範囲を最小化できるんです
  2. DynamoDB で Lock 管理:同時 apply を防げる
  3. KMS 暗号化:State には平文で credentials とか秘密情報が入るので必須
  4. assume_role を Provider 設定に含める:Plan 時点で権限検証できる

これで「CI/CD パイプラインから各アカウントに apply できる」状態になりました。

CDK でマルチアカウント化する時の工夫

うちが Terraform をメインで使ってますが、一部のプロジェクトで CDK も試してみたんですよ。CDK はコード度が高い分、マルチアカウント対応が楽だったり難しかったり。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';

interface CrossAccountStackProps extends cdk.StackProps {
  sourceAccountId: string;
  targetAccountId: string;
  roleName: string;
}

export class CrossAccountStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: CrossAccountStackProps) {
    super(scope, id, {
      ...props,
      env: {
        account: props.targetAccountId,
        region: 'us-east-1'
      }
    });

    // Source Account が assume できるロールを作成
    const crossAccountRole = new cdk.aws_iam.Role(this, 'CrossAccountRole', {
      assumedBy: new cdk.aws_iam.AccountPrincipal(props.sourceAccountId),
      roleName: props.roleName
    });

    crossAccountRole.addToPrincipalPolicy(
      new cdk.aws_iam.PolicyStatement({
        effect: cdk.aws_iam.Effect.ALLOW,
        actions: ['sts:AssumeRole'],
        resources: [`arn:aws:iam::${props.targetAccountId}:role/${props.roleName}`]
      })
    );
  }
}

// デプロイ側
const app = new cdk.App();
const sourceAccountId = '111111111111';
const targetAccountId = '222222222222';

new CrossAccountStack(app, 'CrossAccountStack', {
  sourceAccountId,
  targetAccountId,
  roleName: 'CrossAccountDeployRole'
});

CDK の良い点は TypeScript の型チェックが効くので、アカウント ID を誤入力するリスクが低いこと。ただし Terraform と混在すると、State 管理が複雑になるので注意した方が良い。

ドリフト対策──IaC が本当に信頼できるか

ここからが本番で地獄だった部分です。マルチアカウント環境になると、誰かが手動で AWS Console からリソース作成したり設定変更したりするリスクが爆発的に上がるんですよ。

「Terraform で管理してる」はずの VPC が、実は Console で Subnet が足されてたとか。RDS のスナップショット自動設定が無効になってたとか。そういう「気づかない変更」が本当に怖い。

うちが導入した対策は以下の通り:

1. Config Rules で自動チェック

resource "aws_config_configuration_aggregator" "main" {
  name = "central-aggregator"

  account_aggregation_sources {
    account_ids = var.project_account_ids
    regions     = ["us-east-1", "us-west-2"]
  }
}

resource "aws_config_config_rule" "terraform_managed" {
  name = "terraform-managed-resources"

  source {
    owner             = "CUSTOM_LAMBDA"
    source_identifier = aws_lambda_function.drift_detector.arn
    source_details {
      event_source = "aws.config"
      message_type = "ConfigurationItemChangeNotification"
    }
  }
}

2. CloudTrail 監視 + Lambda で異常検知

Console から何か作成されたら Slack に通知するようにしてるんです。完全に自動化はできないけど、「あ、誰かが何かした」ってことに気づけるだけでも違う。

3. Terraform Refresh & Plan を定期実行

CI/CD パイプラインで 1 日 1 回、全アカウントの terraform plan を実行。差分が出たらアラートする仕組みにしてます。

# GitHub Actions例
name: Detect Drift
on:
  schedule:
    - cron: '0 9 * * *'  # 毎日9時

jobs:
  drift-detection:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2
      
      - name: Terraform Plan (All Accounts)
        run: |
          for account in platform project-1 project-2; do
            cd accounts/$account
            terraform init
            terraform plan -no-color > /tmp/${account}.plan
            if grep -q "Plan:" /tmp/${account}.plan; then
              echo "::notice::Drift detected in $account"
            fi
          done
      
      - name: Notify Slack
        if: failure()
        uses: slackapi/slack-github-action@v1

正直、完全にドリフト排除することは無理です。でも「気づく仕組み」があるだけで、修復の優先度を判断できるんですよ。

チーム運用で工夫していること

マルチアカウント環境になると、デプロイ権限の管理が複雑化します。「Project 1 のエンジニアが Platform Account の VPC を変更できる」みたいなことが起きないようにしないと、とんでもないことになる。

うちが運用してるのは RBAC + Cross Account Role の組み合わせです:

# Platform Account に、各プロジェクトの権限を分ける
resource "aws_iam_role" "project_1_deployment" {
  name = "project-1-deployment-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${var.project_1_account}:role/GitHubActionsRole"
        }
        Action   = "sts:AssumeRole"
        Condition = {
          StringEquals = {
            "sts:ExternalId" = var.external_id
          }
        }
      }
    ]
  })
}

resource "aws_iam_role_policy" "project_1_vpc_access" {
  role = aws_iam_role.project_1_deployment.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ec2:DescribeVpcs",
          "ec2:DescribeSubnets",
          "ec2:CreateSecurityGroup",
          "ec2:ModifySecurityGroup*"
        ]
        Resource = "*"
        Condition = {
          StringEquals = {
            "ec2:vpc" = aws_vpc.project_1.arn
          }
        }
      }
    ]
  })
}

こうすることで「Project 1 は自分たちの VPC だけ変更できる」って制限ができるんです。ただし、このポリシー管理が増えるにつれて複雑化するのは避けられません。CDK や Terraform で共通のモジュール化をしておくことが本当に重要。

2026年時点での推奨スタック

最後に、いま新しくマルチアカウント環境を設計するなら、こんな感じにするかな:

項目選択理由
IaCTerraform 5.x + OpenTofuState 管理が成熟。マルチアカウント対応ツールが豊富
BackendS3 + DynamoDB + KMS業界標準。アカウント分割で権限管理も簡単
MonitoringCloudWatch + GrafanaCloudWatch Logs Insights が便利。コスト効率も良好
ConfigAWS Config + Lambdaドリフト検知の標準手段
RBACIAM Identity Center + TerraformSAML 連携で AD/Okta と同期。権限管理が一元化
CI/CDCodePipeline V2 + CodeBuild Fleetマルチアカウント対応が改善。ビルド待機時間が短い

実装としては、CodePipeline V2 × CodeBuild Fleet を参考にするといいですよ。うちも今これで運用してます。

まとめ

マルチアカウント IaC 戦略で大事なのは、教科書通りに作ることじゃなく、自分たちのチーム・プロジェクト規模に合わせることなんです。

  • アカウント分割は目的から逆算する:課金と権限の境界を明確に
  • Cross Account ロール設計は External ID を必ず付ける:セキュリティ監査対策になる
  • State 管理は S3 + DynamoDB に集約:アカウント別の backend 構成で権限分離ができる
  • ドリフト対策は気づく仕組みから:完全排除は無理。Config + CloudTrail 監視で十分
  • チーム運用は RBAC + Condition で粒度を上げる:権限漏れを防ぐ

次のアクション:今マルチアカウント化を検討してるなら、まずアカウント分割の軸を明確にしてから IaC ツール選びをすること。State 管理の設計だけで成功 or 失敗が 8 割決まります。正直、ここを手抜きするとあとで本当に苦しいですよ。

U

Untanbaby

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

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

関連記事