AWS Control TowerのAccount Factory for Terraform(AFT)は、GitOpsモデルに基づいたアカウントプロビジョニングを実現するソリューションです。本記事では、AFTが提供する3種類のカスタマイズ機能(グローバル、アカウント固有、プロビジョニング時)の実践的な活用例を、具体的なTerraformコードとともに解説します。

AFTカスタマイズの概要

AFTでは、アカウントのプロビジョニング後に自動的にカスタマイズを適用できます。これにより、セキュリティ設定、ネットワーク構成、必須リソースの作成などを標準化し、すべてのアカウントで一貫したベースライン構成を維持できます。

AFTのカスタマイズ体系

AFTは3種類のカスタマイズリポジトリを使用して、異なるスコープと目的のカスタマイズを管理します。

flowchart TB
    subgraph "AFTカスタマイズ体系"
        subgraph "1. Provisioning Customizations"
            PC[プロビジョニング時カスタマイズ]
            PC_DESC[Step Functionsベース<br/>アカウント作成時に1回実行]
        end
        
        subgraph "2. Global Customizations"
            GC[グローバルカスタマイズ]
            GC_DESC[全アカウント共通<br/>ベースライン設定]
        end
        
        subgraph "3. Account Customizations"
            AC[アカウント固有カスタマイズ]
            AC_DESC[アカウント種別ごと<br/>個別設定]
        end
    end
    
    subgraph "適用順序"
        O1[1. Provisioning] --> O2[2. Global] --> O3[3. Account]
    end
    
    PC --> O1
    GC --> O2
    AC --> O3

カスタマイズの種類と特徴

カスタマイズ種別 適用タイミング 対象範囲 主な用途
プロビジョニング時 アカウント作成時(1回のみ) 全アカウント 初期設定、外部システム連携
グローバル プロビジョニング後(継続的) 全アカウント セキュリティベースライン
アカウント固有 プロビジョニング後(継続的) 指定アカウント 環境別設定、ワークロード固有設定

AFTリポジトリ構成

AFTは4つのGitリポジトリを使用してアカウント管理とカスタマイズを行います。

flowchart LR
    subgraph "AFTリポジトリ"
        REQ[aft-account-request<br/>アカウントリクエスト]
        PROV[aft-account-provisioning-<br/>customizations<br/>プロビジョニング時]
        GLOBAL[aft-global-customizations<br/>グローバル]
        ACCT[aft-account-customizations<br/>アカウント固有]
    end
    
    subgraph "CodePipeline"
        CP[AFT Pipeline]
    end
    
    subgraph "新規アカウント"
        NEW[作成されたアカウント]
    end
    
    REQ --> CP
    PROV --> CP
    GLOBAL --> CP
    ACCT --> CP
    CP --> NEW

リポジトリ構成例

各リポジトリは特定のディレクトリ構造に従う必要があります。

aft-account-request/
├── terraform/
│   ├── main.tf              # アカウントリクエスト定義
│   ├── aft-providers.jinja  # プロバイダー設定(AFT生成)
│   ├── backend.jinja        # バックエンド設定(AFT生成)
│   └── modules/
│       └── aft-account-request/
│           ├── ddb.tf
│           ├── variables.tf
│           └── versions.tf

aft-global-customizations/
├── api_helpers/
│   ├── pre-api-helpers.sh   # Terraform実行前スクリプト
│   ├── post-api-helpers.sh  # Terraform実行後スクリプト
│   └── python/
│       └── requirements.txt
└── terraform/
    ├── aft-providers.jinja
    ├── backend.jinja
    └── main.tf              # グローバルカスタマイズ

aft-account-customizations/
├── sandbox/                  # サンドボックス環境用
│   ├── api_helpers/
│   │   ├── pre-api-helpers.sh
│   │   └── post-api-helpers.sh
│   └── terraform/
│       └── main.tf
├── production/               # 本番環境用
│   ├── api_helpers/
│   └── terraform/
│       └── main.tf
└── development/              # 開発環境用
    ├── api_helpers/
    └── terraform/
        └── main.tf

アカウントリクエストの作成

新規アカウントをプロビジョニングするには、aft-account-requestリポジトリにアカウント定義を追加します。

基本的なアカウントリクエスト

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# terraform/main.tf

module "production_workload_account" {
  source = "./modules/aft-account-request"

  control_tower_parameters = {
    AccountEmail              = "aws-prod-workload@example.com"
    AccountName               = "prod-workload-001"
    ManagedOrganizationalUnit = "Production"
    SSOUserEmail              = "admin@example.com"
    SSOUserFirstName          = "Admin"
    SSOUserLastName           = "User"
  }

  account_tags = {
    "Environment"  = "production"
    "CostCenter"   = "engineering"
    "Project"      = "core-platform"
    "Owner"        = "platform-team"
    "Compliance"   = "pci-dss"
  }

  change_management_parameters = {
    change_requested_by = "Platform Team"
    change_reason       = "New production workload account for core platform services"
  }

  custom_fields = {
    environment     = "production"
    data_classification = "confidential"
    backup_required = "true"
  }

  account_customizations_name = "production"
}

複数アカウントの一括リクエスト

開発、ステージング、本番の3環境を一括でリクエストする例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# terraform/main.tf

locals {
  environments = {
    development = {
      ou           = "Development"
      email_suffix = "dev"
      cost_center  = "dev-ops"
    }
    staging = {
      ou           = "Staging"
      email_suffix = "stg"
      cost_center  = "dev-ops"
    }
    production = {
      ou           = "Production"
      email_suffix = "prod"
      cost_center  = "engineering"
    }
  }
}

module "workload_accounts" {
  source   = "./modules/aft-account-request"
  for_each = local.environments

  control_tower_parameters = {
    AccountEmail              = "aws-${each.value.email_suffix}-app@example.com"
    AccountName               = "app-${each.key}"
    ManagedOrganizationalUnit = each.value.ou
    SSOUserEmail              = "admin@example.com"
    SSOUserFirstName          = "Admin"
    SSOUserLastName           = "User"
  }

  account_tags = {
    "Environment" = each.key
    "CostCenter"  = each.value.cost_center
    "Project"     = "my-application"
    "ManagedBy"   = "AFT"
  }

  change_management_parameters = {
    change_requested_by = "DevOps Team"
    change_reason       = "Provisioning ${each.key} environment for my-application"
  }

  custom_fields = {
    environment = each.key
  }

  account_customizations_name = each.key
}

グローバルカスタマイズの実装

グローバルカスタマイズは、AFTでプロビジョニングされるすべてのアカウントに適用されます。セキュリティベースライン、監査設定、必須タグポリシーなど、組織全体で統一すべき設定を定義します。

セキュリティベースラインの実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# aft-global-customizations/terraform/main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.0.0"
    }
  }
}

# S3パブリックアクセスブロックの有効化
resource "aws_s3_account_public_access_block" "block_public_access" {
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# EBSデフォルト暗号化の有効化
resource "aws_ebs_encryption_by_default" "enabled" {
  enabled = true
}

# デフォルトKMSキーの設定(EBS暗号化用)
resource "aws_ebs_default_kms_key" "default" {
  key_arn = aws_kms_key.ebs_default.arn
}

resource "aws_kms_key" "ebs_default" {
  description             = "Default KMS key for EBS encryption"
  deletion_window_in_days = 30
  enable_key_rotation     = true

  tags = {
    Name      = "ebs-default-key"
    ManagedBy = "AFT"
  }
}

resource "aws_kms_alias" "ebs_default" {
  name          = "alias/ebs-default"
  target_key_id = aws_kms_key.ebs_default.key_id
}

IAMパスワードポリシーの強化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# aft-global-customizations/terraform/iam_password_policy.tf

resource "aws_iam_account_password_policy" "strict" {
  minimum_password_length        = 14
  require_lowercase_characters   = true
  require_uppercase_characters   = true
  require_numbers                = true
  require_symbols                = true
  allow_users_to_change_password = true
  max_password_age               = 90
  password_reuse_prevention      = 24
  hard_expiry                    = false
}

SecurityHub有効化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# aft-global-customizations/terraform/security_hub.tf

resource "aws_securityhub_account" "main" {}

# CIS AWS Foundations Benchmark標準を有効化
resource "aws_securityhub_standards_subscription" "cis" {
  depends_on    = [aws_securityhub_account.main]
  standards_arn = "arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.4.0"
}

# AWS Foundational Security Best Practices標準を有効化
resource "aws_securityhub_standards_subscription" "aws_foundational" {
  depends_on    = [aws_securityhub_account.main]
  standards_arn = "arn:aws:securityhub:${data.aws_region.current.name}::standards/aws-foundational-security-best-practices/v/1.0.0"
}

data "aws_region" "current" {}

GuardDuty有効化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# aft-global-customizations/terraform/guardduty.tf

resource "aws_guardduty_detector" "main" {
  enable                       = true
  finding_publishing_frequency = "FIFTEEN_MINUTES"

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

  tags = {
    Name      = "guardduty-detector"
    ManagedBy = "AFT"
  }
}

CloudWatch Logs保持期間の設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# aft-global-customizations/terraform/cloudwatch.tf

# 既存のロググループの保持期間を設定するLambda
resource "aws_lambda_function" "set_log_retention" {
  filename         = data.archive_file.lambda_zip.output_path
  function_name    = "aft-set-log-retention"
  role             = aws_iam_role.lambda_role.arn
  handler          = "index.handler"
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256
  runtime          = "python3.11"
  timeout          = 300

  environment {
    variables = {
      RETENTION_DAYS = "90"
    }
  }

  tags = {
    Name      = "aft-set-log-retention"
    ManagedBy = "AFT"
  }
}

resource "aws_iam_role" "lambda_role" {
  name = "aft-log-retention-lambda-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy" "lambda_policy" {
  name = "log-retention-policy"
  role = aws_iam_role.lambda_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "logs:DescribeLogGroups",
          "logs:PutRetentionPolicy"
        ]
        Resource = "*"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "lambda_basic" {
  role       = aws_iam_role.lambda_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

data "archive_file" "lambda_zip" {
  type        = "zip"
  source_dir  = "${path.module}/lambda/set_log_retention"
  output_path = "${path.module}/lambda/set_log_retention.zip"
}

pre-api-helpers.shの活用例

Terraform実行前に実行されるシェルスクリプトの例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#!/bin/bash
# aft-global-customizations/api_helpers/pre-api-helpers.sh

set -e

echo "=== Pre-API Helpers Start ==="

# 環境変数の確認
echo "Account ID: ${AWS_ACCOUNT_ID}"
echo "Region: ${AWS_REGION}"

# デフォルトVPCの削除(AFTのfeature flagでも可能だが、カスタムロジックが必要な場合)
echo "Checking for default VPC..."

DEFAULT_VPC_ID=$(aws ec2 describe-vpcs \
  --filters "Name=isDefault,Values=true" \
  --query "Vpcs[0].VpcId" \
  --output text 2>/dev/null || echo "None")

if [ "$DEFAULT_VPC_ID" != "None" ] && [ "$DEFAULT_VPC_ID" != "null" ]; then
  echo "Default VPC found: $DEFAULT_VPC_ID"
  
  # インターネットゲートウェイのデタッチと削除
  IGW_ID=$(aws ec2 describe-internet-gateways \
    --filters "Name=attachment.vpc-id,Values=$DEFAULT_VPC_ID" \
    --query "InternetGateways[0].InternetGatewayId" \
    --output text 2>/dev/null || echo "None")
  
  if [ "$IGW_ID" != "None" ] && [ "$IGW_ID" != "null" ]; then
    echo "Detaching and deleting Internet Gateway: $IGW_ID"
    aws ec2 detach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$DEFAULT_VPC_ID"
    aws ec2 delete-internet-gateway --internet-gateway-id "$IGW_ID"
  fi
  
  # サブネットの削除
  SUBNET_IDS=$(aws ec2 describe-subnets \
    --filters "Name=vpc-id,Values=$DEFAULT_VPC_ID" \
    --query "Subnets[*].SubnetId" \
    --output text 2>/dev/null || echo "")
  
  for SUBNET_ID in $SUBNET_IDS; do
    echo "Deleting subnet: $SUBNET_ID"
    aws ec2 delete-subnet --subnet-id "$SUBNET_ID"
  done
  
  # デフォルトVPCの削除
  echo "Deleting default VPC: $DEFAULT_VPC_ID"
  aws ec2 delete-vpc --vpc-id "$DEFAULT_VPC_ID"
  echo "Default VPC deleted successfully"
else
  echo "No default VPC found"
fi

echo "=== Pre-API Helpers Complete ==="

アカウント固有カスタマイズの実装

アカウント固有カスタマイズは、特定のアカウント種別にのみ適用される設定を定義します。本番環境、開発環境、サンドボックスなど、用途に応じた異なる構成を適用できます。

本番環境用カスタマイズ

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# aft-account-customizations/production/terraform/main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.0.0"
    }
  }
}

# 本番環境用VPC
module "production_vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "production-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway     = true
  single_nat_gateway     = false
  one_nat_gateway_per_az = true

  enable_vpn_gateway = false

  enable_dns_hostnames = true
  enable_dns_support   = true

  # VPCフローログの有効化
  enable_flow_log                      = true
  create_flow_log_cloudwatch_log_group = true
  create_flow_log_cloudwatch_iam_role  = true
  flow_log_max_aggregation_interval    = 60

  tags = {
    Environment = "production"
    ManagedBy   = "AFT"
  }

  vpc_tags = {
    Name = "production-vpc"
  }
}

# AWS Backupの設定
resource "aws_backup_vault" "production" {
  name = "production-backup-vault"

  tags = {
    Environment = "production"
    ManagedBy   = "AFT"
  }
}

resource "aws_backup_plan" "production" {
  name = "production-backup-plan"

  rule {
    rule_name         = "daily-backup"
    target_vault_name = aws_backup_vault.production.name
    schedule          = "cron(0 5 ? * * *)"  # 毎日14:00 JST

    lifecycle {
      delete_after = 35  # 35日間保持
    }

    copy_action {
      destination_vault_arn = aws_backup_vault.production.arn
      lifecycle {
        delete_after = 90
      }
    }
  }

  rule {
    rule_name         = "weekly-backup"
    target_vault_name = aws_backup_vault.production.name
    schedule          = "cron(0 5 ? * 1 *)"  # 毎週月曜14:00 JST

    lifecycle {
      delete_after = 90  # 90日間保持
    }
  }

  tags = {
    Environment = "production"
    ManagedBy   = "AFT"
  }
}

resource "aws_backup_selection" "production" {
  name         = "production-backup-selection"
  iam_role_arn = aws_iam_role.backup_role.arn
  plan_id      = aws_backup_plan.production.id

  selection_tag {
    type  = "STRINGEQUALS"
    key   = "Backup"
    value = "true"
  }
}

resource "aws_iam_role" "backup_role" {
  name = "aws-backup-service-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "backup.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "backup_policy" {
  role       = aws_iam_role.backup_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup"
}

resource "aws_iam_role_policy_attachment" "restore_policy" {
  role       = aws_iam_role.backup_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForRestores"
}

開発環境用カスタマイズ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# aft-account-customizations/development/terraform/main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.0.0"
    }
  }
}

# 開発環境用VPC(コスト最適化)
module "development_vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "development-vpc"
  cidr = "10.1.0.0/16"

  azs             = ["ap-northeast-1a", "ap-northeast-1c"]
  private_subnets = ["10.1.1.0/24", "10.1.2.0/24"]
  public_subnets  = ["10.1.101.0/24", "10.1.102.0/24"]

  # コスト削減のため単一NATゲートウェイ
  enable_nat_gateway     = true
  single_nat_gateway     = true
  one_nat_gateway_per_az = false

  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Environment = "development"
    ManagedBy   = "AFT"
  }
}

# 開発者用IAMロール
resource "aws_iam_role" "developer" {
  name = "DeveloperRole"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
        }
        Condition = {
          Bool = {
            "aws:MultiFactorAuthPresent" = "true"
          }
        }
      }
    ]
  })

  tags = {
    Environment = "development"
    ManagedBy   = "AFT"
  }
}

resource "aws_iam_role_policy_attachment" "developer_poweruser" {
  role       = aws_iam_role.developer.name
  policy_arn = "arn:aws:iam::aws:policy/PowerUserAccess"
}

# 開発環境用のコスト制限
resource "aws_budgets_budget" "development" {
  name              = "development-monthly-budget"
  budget_type       = "COST"
  limit_amount      = "500"
  limit_unit        = "USD"
  time_unit         = "MONTHLY"
  time_period_start = "2026-01-01_00:00"

  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 80
    threshold_type             = "PERCENTAGE"
    notification_type          = "FORECASTED"
    subscriber_email_addresses = ["devops@example.com"]
  }

  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 100
    threshold_type             = "PERCENTAGE"
    notification_type          = "ACTUAL"
    subscriber_email_addresses = ["devops@example.com"]
  }
}

data "aws_caller_identity" "current" {}

サンドボックス環境用カスタマイズ

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# aft-account-customizations/sandbox/terraform/main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.0.0"
    }
  }
}

# サンドボックス用実験バケット
resource "aws_s3_bucket" "sandbox_experiments" {
  bucket_prefix = "sandbox-experiments-"

  tags = {
    Environment = "sandbox"
    ManagedBy   = "AFT"
    Purpose     = "experimentation"
  }
}

resource "aws_s3_bucket_lifecycle_configuration" "sandbox_experiments" {
  bucket = aws_s3_bucket.sandbox_experiments.id

  rule {
    id     = "auto-delete-old-objects"
    status = "Enabled"

    expiration {
      days = 30  # 30日後に自動削除
    }

    noncurrent_version_expiration {
      noncurrent_days = 7
    }
  }
}

resource "aws_s3_bucket_public_access_block" "sandbox_experiments" {
  bucket = aws_s3_bucket.sandbox_experiments.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# サンドボックス用の厳格な予算制限
resource "aws_budgets_budget" "sandbox" {
  name              = "sandbox-monthly-budget"
  budget_type       = "COST"
  limit_amount      = "100"
  limit_unit        = "USD"
  time_unit         = "MONTHLY"
  time_period_start = "2026-01-01_00:00"

  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 50
    threshold_type             = "PERCENTAGE"
    notification_type          = "FORECASTED"
    subscriber_email_addresses = ["devops@example.com"]
  }

  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 80
    threshold_type             = "PERCENTAGE"
    notification_type          = "ACTUAL"
    subscriber_email_addresses = ["devops@example.com"]
  }

  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 100
    threshold_type             = "PERCENTAGE"
    notification_type          = "ACTUAL"
    subscriber_email_addresses = ["devops@example.com", "finance@example.com"]
  }
}

# リソース自動停止のEventBridgeルール(夜間のEC2停止)
resource "aws_cloudwatch_event_rule" "stop_instances" {
  name                = "stop-sandbox-instances"
  description         = "Stop all sandbox EC2 instances at 21:00 JST"
  schedule_expression = "cron(0 12 * * ? *)"  # 21:00 JST

  tags = {
    Environment = "sandbox"
    ManagedBy   = "AFT"
  }
}

resource "aws_cloudwatch_event_target" "stop_instances" {
  rule     = aws_cloudwatch_event_rule.stop_instances.name
  arn      = "arn:aws:ssm:${data.aws_region.current.name}::automation-definition/AWS-StopEC2Instance"
  role_arn = aws_iam_role.eventbridge_automation.arn

  input = jsonencode({
    InstanceId = ["*"]
    AutomationAssumeRole = [aws_iam_role.ssm_automation.arn]
  })
}

resource "aws_iam_role" "eventbridge_automation" {
  name = "eventbridge-automation-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "events.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role" "ssm_automation" {
  name = "ssm-automation-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ssm.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "ssm_automation" {
  role       = aws_iam_role.ssm_automation.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonSSMAutomationRole"
}

data "aws_region" "current" {}

プロビジョニング時カスタマイズ

プロビジョニング時カスタマイズは、AWS Step Functionsを使用してアカウント作成時に1回だけ実行される処理を定義します。外部システムとの連携やワンタイムの初期設定に適しています。

Step Functionsステートマシン定義例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
{
  "Comment": "AFT Account Provisioning Customizations",
  "StartAt": "NotifyAccountCreation",
  "States": {
    "NotifyAccountCreation": {
      "Type": "Task",
      "Resource": "arn:aws:states:::sns:publish",
      "Parameters": {
        "TopicArn.$": "$.sns_topic_arn",
        "Message": {
          "event": "account_created",
          "account_id.$": "$.account_info.id",
          "account_name.$": "$.account_info.name",
          "account_email.$": "$.account_info.email"
        }
      },
      "Next": "RegisterInCMDB"
    },
    "RegisterInCMDB": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "aft-register-account-cmdb",
        "Payload.$": "$"
      },
      "Retry": [
        {
          "ErrorEquals": ["Lambda.ServiceException"],
          "IntervalSeconds": 2,
          "MaxAttempts": 3,
          "BackoffRate": 2
        }
      ],
      "Next": "ConfigureNetworkConnectivity"
    },
    "ConfigureNetworkConnectivity": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "aft-configure-transit-gateway",
        "Payload.$": "$"
      },
      "Next": "Success"
    },
    "Success": {
      "Type": "Succeed"
    }
  }
}

CMDB登録用Lambda関数例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# aft-account-provisioning-customizations/lambda/register_cmdb/index.py

import json
import boto3
import requests
import os

def handler(event, context):
    """
    新規アカウントをCMDB(構成管理データベース)に登録する
    """
    account_info = event.get('account_info', {})
    
    account_id = account_info.get('id')
    account_name = account_info.get('name')
    account_email = account_info.get('email')
    custom_fields = event.get('custom_fields', {})
    
    # CMDBへの登録データを構築
    cmdb_record = {
        'ci_type': 'aws_account',
        'ci_name': account_name,
        'attributes': {
            'account_id': account_id,
            'account_email': account_email,
            'environment': custom_fields.get('environment', 'unknown'),
            'data_classification': custom_fields.get('data_classification', 'internal'),
            'cost_center': custom_fields.get('cost_center', 'unassigned'),
            'status': 'active',
            'managed_by': 'AFT'
        }
    }
    
    # CMDBのAPIエンドポイント(環境変数から取得)
    cmdb_endpoint = os.environ.get('CMDB_API_ENDPOINT')
    cmdb_api_key = get_secret('cmdb-api-key')
    
    try:
        response = requests.post(
            f"{cmdb_endpoint}/api/v1/configuration-items",
            json=cmdb_record,
            headers={
                'Authorization': f'Bearer {cmdb_api_key}',
                'Content-Type': 'application/json'
            },
            timeout=30
        )
        response.raise_for_status()
        
        return {
            'statusCode': 200,
            'body': json.dumps({
                'message': 'Account registered in CMDB successfully',
                'cmdb_id': response.json().get('id')
            })
        }
    except requests.exceptions.RequestException as e:
        print(f"Error registering account in CMDB: {e}")
        raise

def get_secret(secret_name):
    """Secrets Managerからシークレットを取得"""
    client = boto3.client('secretsmanager')
    response = client.get_secret_value(SecretId=secret_name)
    return response['SecretString']

カスタマイズのベストプラクティス

AFTカスタマイズを効果的に運用するためのベストプラクティスを紹介します。

モジュール化と再利用

flowchart TB
    subgraph "共通モジュールリポジトリ"
        MOD1[vpc-module]
        MOD2[security-baseline-module]
        MOD3[logging-module]
        MOD4[backup-module]
    end
    
    subgraph "グローバルカスタマイズ"
        GC[main.tf]
        GC --> MOD2
        GC --> MOD3
    end
    
    subgraph "本番アカウントカスタマイズ"
        PC[main.tf]
        PC --> MOD1
        PC --> MOD4
    end
    
    subgraph "開発アカウントカスタマイズ"
        DC[main.tf]
        DC --> MOD1
    end

共通モジュールを使用したカスタマイズの例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# aft-global-customizations/terraform/main.tf

module "security_baseline" {
  source  = "git::https://github.com/your-org/terraform-modules.git//security-baseline?ref=v1.2.0"
  
  enable_security_hub = true
  enable_guardduty    = true
  enable_config       = true
  
  security_hub_standards = [
    "aws-foundational-security-best-practices",
    "cis-aws-foundations-benchmark"
  ]
}

module "logging" {
  source  = "git::https://github.com/your-org/terraform-modules.git//centralized-logging?ref=v1.2.0"
  
  log_archive_account_id = var.log_archive_account_id
  retention_days         = 90
}

テスト戦略

AFTカスタマイズのテスト戦略として、以下のアプローチが推奨されます。

テストレベル 方法 目的
構文検証 terraform validate HCL構文の正確性確認
静的解析 tflint, checkov セキュリティ・ベストプラクティス準拠
Plan検証 terraform plan 意図した変更の確認
サンドボックステスト 専用テストアカウント 実環境での動作確認

CI/CDパイプラインの構築

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# .github/workflows/aft-customizations-ci.yml

name: AFT Customizations CI

on:
  pull_request:
    branches: [main]
    paths:
      - 'terraform/**'
      - 'api_helpers/**'

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.6.0
      
      - name: Terraform Format Check
        run: terraform fmt -check -recursive
        working-directory: terraform
      
      - name: Terraform Init
        run: terraform init -backend=false
        working-directory: terraform
      
      - name: Terraform Validate
        run: terraform validate
        working-directory: terraform
      
      - name: Run tflint
        uses: terraform-linters/setup-tflint@v4
      
      - name: tflint
        run: tflint --recursive
        working-directory: terraform
      
      - name: Run Checkov
        uses: bridgecrewio/checkov-action@v12
        with:
          directory: terraform
          framework: terraform
          soft_fail: false

  shellcheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Run ShellCheck
        uses: ludeeus/action-shellcheck@master
        with:
          scandir: './api_helpers'

トラブルシューティング

AFTカスタマイズで発生しやすい問題と解決方法を紹介します。

よくある問題と対処法

問題 原因 対処法
カスタマイズが適用されない リポジトリ構造の不備 terraform/ディレクトリの存在確認
Terraform実行エラー プロバイダーバージョン不整合 versions.tfでバージョン固定
権限エラー AFT実行ロールの権限不足 IAMポリシーの追加
タイムアウト 長時間実行処理 CodeBuildタイムアウト値の調整

ログの確認方法

AFTの実行ログは以下の場所で確認できます。

flowchart LR
    subgraph "AFT管理アカウント"
        CB[CodeBuild<br/>ビルドログ]
        CP[CodePipeline<br/>パイプライン履歴]
        SF[Step Functions<br/>実行履歴]
        CW[CloudWatch Logs<br/>Lambda/CodeBuildログ]
    end
    
    CB --> CW
    CP --> CB
    SF --> CW

CloudWatch Logs Insightsでのログ検索クエリ例です。

fields @timestamp, @message
| filter @logStream like /aft-/
| filter @message like /ERROR|FAILED/
| sort @timestamp desc
| limit 100

まとめ

AFT(Account Factory for Terraform)のカスタマイズ機能を活用することで、以下のメリットが得られます。

  • 一貫性のあるアカウント構成:グローバルカスタマイズにより、すべてのアカウントで同一のセキュリティベースラインを維持できます
  • 柔軟な環境別設定:アカウント固有カスタマイズにより、本番・開発・サンドボックスなど用途に応じた構成を自動適用できます
  • GitOpsによる変更管理:すべてのカスタマイズがGitリポジトリで管理され、変更履歴の追跡とレビューが容易になります
  • Infrastructure as Codeの実践:Terraformによるインフラ定義により、再現性と自動化を実現できます

AFTカスタマイズを効果的に活用し、スケーラブルで安全なマルチアカウント環境を構築してください。

参考リンク