AWSアカウント10個で管理が崩壊した話|Control Tower + AFTで立て直した6ヶ月の記録

「このアカウント何用だっけ」が誰も答えられない状態、経験ありませんか?Control Tower + AFTを本番導入して手作業地獄から抜け出した実装記録。移行中のハマりポイントも正直に共有します。

うちのチームでAWSアカウントが気づいたら10個を超えていたとき、正直パニックになった。新規アカウントを作るたびに手動でSCPを割り当てて、CloudTrailを有効化して、セキュリティベースラインを設定して……という作業が積み重なって、チームの誰も「このアカウント何用だっけ」が答えられない状態になっていた。

そこで2025年末から本格的にControl TowerとAccount Factory for Terraform(AFT)を導入して、2026年6月現在で約6ヶ月運用してきた。結論から言うと「なぜもっと早く入れなかったのか」という感想しかない。ただ、移行中にいくつかハマりポイントもあったので、同じ轍を踏まないよう共有したい。


導入前の状況と、なぜControl Towerを選んだか

そもそもAWS Organizationsは以前から使っていた。SCPも設定していたし、StackSetsでリソースを展開することもやっていた。以前AWS Organizationsの設計失敗談を書いた記事でも触れたけど、SCPとIAMの組み合わせで権限地獄に陥った経験がある。

問題は「アカウントそのもののライフサイクル管理」だった。新規アカウントの払い出しに最低2〜3時間かかっていたし、担当者によってベースラインの設定内容がバラバラだった。セキュリティ監査のたびに「このアカウントにはGuardDutyが入っていない」「CloudTrailのログがS3に流れていない」といった指摘が出る。正直、管理コストがじわじわと上がってきていた。

Control Towerを選んだ理由は三つある。まとめると以下のとおりだ。

理由詳細
Landing Zoneの設計思想ガードレールがAWSのベストプラクティスに基づいていて、考え方が整理されている
AFTによるIaC化手続き型の作業をTerraformのコードに落とせる
既存アカウントEnrollの改善2025年後半のアップデートで、現実的な移行パスが見えた

最後の点が個人的には一番大きかった。Control TowerはグリーンフィールドなOrganizations向けという印象があったけど、既存アカウントを段階的に取り込めるようになったことで「あ、これなら今の構成から移行できる」と思えた。


Control Tower全体のアーキテクチャと構成図

実際に構築した構成を示す。Management AccountとLog Archive、Audit(Security Tooling)の三つはControl Towerが作るOUに配置して、残りのアカウントはWorkloads OUの下に整理した。

graph TB
    subgraph ROOT["AWS Organizations Root"]
        subgraph SECURITY_OU["Security OU (Control Tower管理)"]
            MGMT["Management Account\n(Control Tower)"]
            LOG["Log Archive Account\n(CloudTrail・Config集約)"]
            AUDIT["Audit Account\n(SecurityHub・GuardDuty集約)"]
        end

        subgraph WORKLOADS_OU["Workloads OU"]
            subgraph PROD_OU["Production OU"]
                PROD_A["Prod-AppA Account"]
                PROD_B["Prod-AppB Account"]
                PROD_SHARED["Prod-Shared Account\n(VPC Transit Gateway)"]
            end

            subgraph STG_OU["Staging OU"]
                STG_A["Staging-AppA Account"]
                STG_B["Staging-AppB Account"]
            end

            subgraph DEV_OU["Dev OU"]
                DEV_SANDBOX["Dev-Sandbox Account"]
                DEV_CI["Dev-CI Account"]
            end
        end

        subgraph AFT_OU["AFT管理OU"]
            AFT_MGMT["AFT Management Account\n(CodePipeline・Terraform State)"]
        end
    end

    MGMT -->|SCP適用| PROD_OU
    MGMT -->|SCP適用| STG_OU
    MGMT -->|SCP適用| DEV_OU
    LOG -->|ログ集約| PROD_A
    LOG -->|ログ集約| PROD_B
    AUDIT -->|GuardDuty集約| PROD_A
    AUDIT -->|GuardDuty集約| PROD_B
    AFT_MGMT -->|アカウント払い出し| PROD_OU
    AFT_MGMT -->|アカウント払い出し| STG_OU

この構成になるまでに、OUの階層設計で1週間くらい悩んだ。最初は環境(Prod/Staging/Dev)でOU分けするか、アプリケーション単位でOU分けするか迷ったけど、SCPの粒度と環境の特性が合致するという理由で環境単位にした。ProductionにはS3パブリックアクセス拒否やルートユーザーアクション拒否を強制するSCPを適用し、DevにはSandbox的に緩いポリシーを許容する構成にしている。


AFT(Account Factory for Terraform)の実装パターン

AFTの仕組みをざっくり言うと、GitリポジトリへのPushをトリガーにCodePipelineが動いてアカウントをプロビジョニングする、という流れだ。2026年時点ではTerraform 1.8以降との互換性が確認されていて、backendブロックの記述も整理されている。

実際に使っているリポジトリ構成はこんな感じ。

aft/
├── aft-account-request/          # アカウント払い出し定義
│   └── terraform/
│       └── main.tf
├── aft-account-customizations/   # アカウント固有カスタマイズ
│   └── ACCOUNT_NAME/
│       └── terraform/
│           ├── main.tf
│           └── variables.tf
├── aft-global-customizations/    # 全アカウント共通カスタマイズ
│   └── terraform/
│       ├── cloudtrail.tf
│       ├── securityhub.tf
│       └── iam_baseline.tf
└── aft-provisioning-customizations/  # プロビジョニング時のカスタマイズ

新規アカウントを払い出すときはaft-account-requestにPRを出すだけ。実際のコードはこんな感じになる。

# aft-account-request/terraform/main.tf
module "prod_app_c" {
  source = "./modules/aft-account-request"

  control_tower_parameters = {
    AccountEmail              = "aws+prod-appc@example.com"
    AccountName               = "Prod-AppC"
    ManagedOrganizationalUnit = "Workloads/Production"
    SSOUserEmail              = "admin@example.com"
    SSOUserFirstName          = "Admin"
    SSOUserLastName           = "User"
  }

  account_tags = {
    Environment = "production"
    Application = "app-c"
    Owner       = "platform-team"
    CostCenter  = "cc-1234"
  }

  change_management_parameters = {
    change_requested_by = "platform-team"
    change_reason       = "新規アプリケーションのProduction環境構築"
  }

  account_customizations_name = "prod-standard"
}

PRがマージされるとCodePipelineが起動して、だいたい20〜30分でアカウントが払い出される。以前の手作業2〜3時間と比べると、地味に感動する変化だ。

Global Customizationsで必ず入れているベースライン

全アカウントに共通で適用しているリソースのうち、特に重要なものをピックアップする。

# aft-global-customizations/terraform/securityhub.tf
resource "aws_securityhub_account" "main" {}

resource "aws_securityhub_standards_subscription" "cis" {
  standards_arn = "arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.4.0"
  depends_on    = [aws_securityhub_account.main]
}

resource "aws_securityhub_standards_subscription" "aws_foundational" {
  standards_arn = "arn:aws:securityhub:ap-northeast-1::standards/aws-foundational-security-best-practices/v/1.0.0"
  depends_on    = [aws_securityhub_account.main]
}

# GuardDuty有効化(Audit AccountをDelegated Adminに設定済みの前提)
resource "aws_guardduty_detector" "main" {
  enable = true

  datasources {
    s3_logs {
      enable = true
    }
    kubernetes {
      audit_logs {
        enable = true
      }
    }
    malware_protection {
      scan_ec2_instance_with_findings {
        ebs_volumes {
          enable = true
        }
      }
    }
  }
}

これで新規アカウントが払い出されると同時に、SecurityHubとGuardDutyが自動で有効化される。GuardDuty導入3ヶ月の失敗談でも書いたけど、False Positiveへの対応はまた別の話で、ベースラインの有効化自体はAFTで自動化できる。


SCP設計で失敗した話と現在の設計

最初にSCPを設計したとき、「強いガードレールを最初から入れよう」という方針で、かなり制限の厳しいSCPを本番OUに適用した。結果、開発チームから「RDSのバックアップが取れない」「CloudFormationのStackが作れない」という障害報告が続出して、SCPの見直しに1週間かかった。これは純粋に設計ミスだったと思っている。

SCPの管理フローはこんな構造にしている。

flowchart TD
    A[SCP変更PR作成] --> B{対象OU確認}
    B -->|Production OU| C[Staging環境で1週間検証]
    B -->|Dev OU| D[即日適用可能]
    C --> E{障害報告なし?}
    E -->|Yes| F[Production OU適用]
    E -->|No| G[SCP修正]
    G --> C
    F --> H[CloudTrailで適用確認]
    D --> H
    H --> I[Slack通知]

SCPそのものはTerraformで管理していて、aws_organizations_policyaws_organizations_policy_attachmentを組み合わせている。正直まだ検証中の部分もあるけど、現在の基本セットはこんな感じだ。

# SCPの基本セット(Production OU用)
data "aws_iam_policy_document" "production_guardrails" {
  # ルートユーザーのアクション禁止
  statement {
    sid    = "DenyRootUserActions"
    effect = "Deny"
    actions = ["*"]
    resources = ["*"]
    condition {
      test     = "StringLike"
      variable = "aws:PrincipalArn"
      values   = ["arn:aws:iam::*:root"]
    }
  }

  # 指定リージョン以外のリソース作成禁止
  statement {
    sid    = "DenyNonApprovedRegions"
    effect = "Deny"
    not_actions = [
      "iam:*",
      "organizations:*",
      "route53:*",
      "budgets:*",
      "waf:*",
      "cloudfront:*",
      "sts:*",
      "support:*",
      "trustedadvisor:*"
    ]
    resources = ["*"]
    condition {
      test     = "StringNotEquals"
      variable = "aws:RequestedRegion"
      values   = ["ap-northeast-1", "us-east-1"]
    }
  }

  # S3パブリックアクセスブロック設定の変更禁止
  statement {
    sid    = "DenyS3PublicAccessModification"
    effect = "Deny"
    actions = [
      "s3:PutBucketPublicAccessBlock",
      "s3:DeletePublicAccessBlock"
    ]
    resources = ["*"]
  }
}

「全部Denyで書いてホワイトリスト的に許可していく」か「最小限のDenyで運用する」かは、組織のセキュリティ要件と運用コストのトレードオフだと思っている。うちは後者に落ち着いた。最初の失敗を経て「シンプルに始める」を徹底するようになった。


6ヶ月運用してわかった効果と現実的な課題

数字で見ると効果は明確だった。

指標導入前導入後(6ヶ月平均)
新規アカウント払い出し時間2〜3時間25〜35分
セキュリティベースライン設定漏れ月3〜5件0件
アカウント棚卸し作業時間(月次)4時間30分(自動レポート)
Control Towerガードレール違反検出手動(随時)リアルタイム

払い出し時間のインパクトをグラフで見るとよりわかりやすい。

xychart-beta
    title "アカウント払い出し時間の比較(分)"
    x-axis ["手動(以前)", "AFT導入後"]
    y-axis "時間(分)" 0 --> 200
    bar [150, 30]

ただ、課題もある。正直まだ解決しきれていないものもある。

1. AFTのパイプライン失敗時の再実行が面倒

TerraformのStateが壊れたり、タイムアウトしたりすると、手動でDynamoDBのStateロックを解除したり、CodePipelineを再実行したりする必要がある。失敗ログを追うのが難しいので、アラートとRunbookを整備することで対処している。SSM Automationと組み合わせたRunbook自動化も参考になるかもしれない。

2. 既存アカウントのEnroll時に設定が上書きされる問題

既存アカウントをControl Towerに登録(Enroll)すると、一部のリソース(CloudTrail、Config)が上書きされる。事前の棚卸しが必須で、うちはSOC2審査でCloudTrailの設定が泥沼になった経験がある。既存アカウント移行は慎重に進めるべきだ。

3. Control Towerのバージョンアップによる設定変更

2026年に入ってからControl Towerの機能アップデートが複数あって、そのたびにLanding Zoneのアップデートが求められる。これを先延ばしにすると積み重なって大変になるので、月次でアップデートを確認する運用にしている。

Account Factory絡みの話では、「Control TowerとAccount Factoryで10アカウント統制したら、手作業が15分から5分になった話」という記事も以前書いているので、そちらも参考にしてほしい(あちらは初期導入の話、この記事は6ヶ月運用後の知見が中心)。


2026年時点のAFT最新機能と今後の展望

2025年後半〜2026年前半にかけていくつか重要なアップデートがあった。

Account Factory Customization(AFC)のGUI改善

Control TowerのコンソールからAFCのテンプレートをある程度GUI操作できるようになった。Terraformに不慣れなチームメンバーがカスタマイズの内容を確認できるようになったのは地味に便利で、「コードを見ないとわからない」という状況が減った。

AFT v1.12でのTerraform 1.9対応

Terraform 1.9で導入されたInput Variables in provider configurationへの対応が進んで、Providerの設定がすっきり書けるようになった。

マルチリージョン展開の改善

AFTのグローバルカスタマイズをマルチリージョンに展開するときの設定が整理されて、ap-northeast-1us-east-1を同時にベースライン適用する構成が組みやすくなった。マルチクラウド・マルチリージョン戦略を検討しているチームには特に恩恵が大きいと思う。

今後やろうと思っているのは、アカウント削除のライフサイクル自動化だ。現状、不要になったアカウントの削除はまだ半手動なので、Service CatalogやEventBridgeと組み合わせて自動化したい。Service Catalog導入9ヶ月の経験でも学んだけど、ライフサイクル管理は作成より削除のほうが難しい。これは次の大きなテーマになりそうだ。


まとめ

6ヶ月運用してみて得た知見を整理するとこうなる。

ポイント内容
AFTによる払い出し自動化アカウント数が5つを超えたあたりから費用対効果が高い。2〜3時間→25分に短縮
SCPはシンプルに始める最初から厳格すぎると開発チームへの影響大。見直しコストがかかる
Global Customizationsの徹底GuardDuty・SecurityHubの設定漏れがゼロに。ROIが一番高い施策
既存アカウントのEnrollは慎重にCloudTrailやConfigの上書き問題あり。事前の棚卸しと影響調査が必須
パイプライン失敗対応Runbookを最初から障害対応が格段に楽になる。後回しにしがち、でも後悔する

Control TowerはOSSツールと比べると「AWSに依存する」感じがして最初は懐疑的だったけど、マルチアカウントの統制に割く時間が明らかに減ったので、今は純粋におすすめできる。

皆さんのチームはアカウント払い出しどうやって管理してますか?もし手動でやっていてしんどくなってきたなら、まずControl TowerのLanding Zoneだけ作ってみるところから始めるのが一番ハードルが低いと思う。

U

Untanbaby

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

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

関連記事