【AWS re:Invent 2025】IAM Policy Autopilot によるポリシー生成をやってみた(2人目)

記事タイトルとURLをコピーする

みなさん、こんにちは。AWS CLI が好きな AWS サポート課の市野です。

現地参加しながら、良さそうだな、と思っていた IAM Policy Autopilot を試してみました。
とはいえ、普段私が bash によるシェルスクリプトくらいしか書かないので、Kiro CLI によるプログラム言語の変更なども活用しながら試してみました。

IAM Policy Autopilot とは

以下の AWS 公式ブログでも紹介されていますが、MCP サーバとして動作する IAM Policy Autopilot はアプリケーションのコードを分析して、IAM ポリシーを自動生成するのを支援してくれるとあります。

aws.amazon.com

プログラム自体は GitHub で管理されており、Apache License 2.0 で提供されるオープンソースツールとなっています。
なお、IAM Policy Autopilot に限らずですが、GitHub で公開されているソリューションは AWS サポートでのサポート対象外となる点にご留意が必要 です。
当該のツールを利用した結果の AWS リソースに関する問い合わせは受け付けられる場合がありますが、ツールそのものについてのお問い合わせは、各ソースコードのリポジトリ宛への問い合わせが必要となります。

github.com

また、すでに兼安さんも試していて、ブログにしてくれていますのでご一読ください。

blog.serverworks.co.jp

やってみた

今回、実行環境は AWS CloudShell のみを利用しています。
また今回は iam-policy-autopilot コマンドとしての実行をします。

なお、後述の通り IAM Policy Autopilot は MCP サーバーとして利用可能なため、Claude Desktop や Kiro IDE、Kiro CLI 向けに MCP サーバー設定として定義しておくことも可能です。

環境の確認

~ $ cat /etc/os-release 
NAME="Amazon Linux"
VERSION="2023"
ID="amzn"
ID_LIKE="fedora"
VERSION_ID="2023"
PLATFORM_ID="platform:al2023"
PRETTY_NAME="Amazon Linux 2023.9.20251014"
ANSI_COLOR="0;33"
CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2023"
HOME_URL="https://aws.amazon.com/linux/amazon-linux-2023/"
DOCUMENTATION_URL="https://docs.aws.amazon.com/linux/"
SUPPORT_URL="https://aws.amazon.com/premiumsupport/"
BUG_REPORT_URL="https://github.com/amazonlinux/amazon-linux-2023"
VENDOR_NAME="AWS"
VENDOR_URL="https://aws.amazon.com/"
SUPPORT_END="2029-06-30"

iam-policy-autopilot のインストール

推奨では uv を実行可能な環境で uvx iam-policy-autopilot での実行とされていますが、AWS CloudShell 環境で試したので、サクッと pip インストールでやってみます。

~ $ pip install iam-policy-autopilot
Defaulting to user installation because normal site-packages is not writeable
Collecting iam-policy-autopilot
  Downloading iam_policy_autopilot-0.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (14.9 MB)
     |████████████████████████████████| 14.9 MB 13.0 MB/s            
Installing collected packages: iam-policy-autopilot
Successfully installed iam-policy-autopilot-0.1.1
~ $ 

iam-policy-autopilot によるポリシー生成(失敗するパターン)

IAM Policy Autopilot は現時点で Python、TypeScript、Go のアプリケーションにのみ対応しています。
そのためシェルスクリプトを与えてみると以下のようなエラーが発生しました。

エラーメッセージで読み取れるように IAM Policy Autopilot として対応できる言語で書かれたファイルとして認識していないことがわかります。

~ $ iam-policy-autopilot generate-policies EnableControlTower.sh 
Error: Failed to process source files
  Caused by: Failed to detect or validate programming language consistency
  Caused by: Validation error: Unable to detect language for file: EnableControlTower.sh

シェルスクリプトの中身

Landing Zone 4.0 に対応するための Control Tower 有効化スクリプトを検討していたときのものを流用しています。

# ログアーカイブアカウント用のメールアドレスの定義
LOG_ARCHIVE_EMAIL=foo@example.com

# 監査アカウント用のメールアドレスの定義
AUDIT_EMAIL=bar@example.com

# Control Tower のホームリージョンの定義
AWS_HOME_REGION=ap-northeast-1

# アカウントのエイリアスの固定文字列の定義
ALIAS=hoge

# 各種 OU の名称の定義
SECURITY_OU="Security"
SANDBOX_OU="Sandbox"

# アカウントエイリアスの希望の有無によるエイリアス文字列の決定
if [ "${ALIAS}" != "" ]; then
  ALIAS=${ALIAS}"-"
else
  ALIAS=""
fi

# AWS Organizations の有効化
aws organizations create-organization

# ログアーカイブアカウント、監査アカウントの作成
aws organizations create-account \
  --email "${LOG_ARCHIVE_EMAIL}" \
  --account-name "${ALIAS}log"
aws organizations create-account \
  --email "${AUDIT_EMAIL}" \
  --account-name "${ALIAS}sec"

# サービスロール AWSControlTowerAdmin の作成
cat <<EOF >controltower_trust.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "controltower.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
aws iam create-role \
  --role-name AWSControlTowerAdmin \
  --path /service-role/ \
  --assume-role-policy-document file://controltower_trust.json
  
# サービスロール AWSControlTowerAdmin へのポリシーのアタッチ
cat <<EOF >ct_admin_role_policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ec2:DescribeAvailabilityZones",
            "Resource": "*"
        }
    ]
}
EOF
aws iam put-role-policy \
  --role-name AWSControlTowerAdmin \
  --policy-name AWSControlTowerAdminPolicy \
  --policy-document file://ct_admin_role_policy.json
aws iam attach-role-policy \
  --role-name AWSControlTowerAdmin \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSControlTowerServiceRolePolicy

# サービスロール AWSControlTowerCloudTrailRole の作成
cat <<EOF >cloudtrail_trust.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudtrail.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
EOF
aws iam create-role \
  --role-name AWSControlTowerCloudTrailRole \
  --path /service-role/ \
  --assume-role-policy-document file://cloudtrail_trust.json

# サービスロール AWSControlTowerCloudTrailRole へのポリシーのアタッチ
AWS_CONTROLTOWER_CLOUDTRAIL_ROLE_POLICY_ARN=$(aws iam list-policies --scope AWS --query "Policies[?contains(PolicyName, 'AWSControlTowerCloudTrailRolePolicy')].Arn" --output text)
aws iam attach-role-policy \
  --role-name AWSControlTowerCloudTrailRole \
  --policy-arn ${AWS_CONTROLTOWER_CLOUDTRAIL_ROLE_POLICY_ARN}

# サービスロール AWSControlTowerStackSetRole の作成
cat <<EOF >cloudformation_trust.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudformation.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
EOF
aws iam create-role \
  --role-name AWSControlTowerStackSetRole \
  --path /service-role/ \
  --assume-role-policy-document file://cloudformation_trust.json

# サービスロール AWSControlTowerStackSetRole へのポリシーのアタッチ
cat <<EOF >stackset_role_policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "sts:AssumeRole"
            ],
            "Resource": [
                "arn:aws:iam::*:role/AWSControlTowerExecution"
            ],
            "Effect": "Allow"
        }
    ]
}
EOF
aws iam put-role-policy \
  --role-name AWSControlTowerStackSetRole \
  --policy-name AWSControlTowerStackSetRolePolicy \
  --policy-document file://stackset_role_policy.json

# サービスロール AWSControlTowerConfigAggregatorRoleForOrganizations の作成
cat <<EOF >config_trust.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "config.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
EOF
aws iam create-role \
  --role-name AWSControlTowerConfigAggregatorRoleForOrganizations \
  --path /service-role/ --assume-role-policy-document file://config_trust.json
aws iam attach-role-policy\
  --role-name AWSControlTowerConfigAggregatorRoleForOrganizations \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSConfigRoleForOrganizations

# 作成済みログアーカイブアカウントのアカウント ID の取得
LOG_ARCHIVE_ACCOUNT_ID=$(aws organizations list-accounts \
  --query "Accounts[?contains(Name, '-log')].Id" \
  --output text)
# 作成済み監査アカウントのアカウント ID の取得
AUDIT_ACCOUNT_ID=$(aws organizations list-accounts \
  --query "Accounts[?contains(Name, '-sec')].Id" \
  --output text)

# AWS Organizations Root ID の取得
ROOT_ID=$(aws organizations list-roots --query "Roots[].Id" --output text)

# OU の作成
aws organizations create-organizational-unit --parent-id ${ROOT_ID} --name "${SECURITY_OU}"
aws organizations create-organizational-unit --parent-id ${ROOT_ID} --name "${SANDBOX_OU}"

SECURITY_OU_ID=$(aws organizations list-organizational-units-for-parent --parent-id ${ROOT_ID} --query "OrganizationalUnits[?contains(Name, 'Security')].Id" --output text)

aws organizations move-account --account-id ${LOG_ARCHIVE_ACCOUNT_ID} --source-parent-id ${ROOT_ID} --destination-parent-id ${SECURITY_OU_ID}
aws organizations move-account --account-id ${AUDIT_ACCOUNT_ID} --source-parent-id ${ROOT_ID} --destination-parent-id ${SECURITY_OU_ID}

# ランディングゾーン マニフェストファイルの作成
cat <<EOF >landing_zone_manifest.json
{
    "accessManagement": {
        "enabled": true
    },
    "backup": {
        "enabled": false
    },
    "centralizedLogging": {
        "accountId": "${LOG_ARCHIVE_ACCOUNT_ID}",
        "enabled": true,
        "configurations": {
            "accessLoggingBucket": {
                "retentionDays": 1
            },
            "loggingBucket": {
                "retentionDays": 1
            }
        }
    },
    "config": {
        "accountId": "${AUDIT_ACCOUNT_ID}",
        "enabled": true,
        "configurations": {
            "accessLoggingBucket": {
                "retentionDays": 1
            },
            "loggingBucket": {
                "retentionDays": 1
            }
        }
    },
    "securityRoles": {
        "enabled": true,
        "accountId": "${AUDIT_ACCOUNT_ID}"
    }
}
EOF

# ランディングゾーンの作成
aws controltower create-landing-zone \
  --manifest file://landing_zone_manifest.json \
  --landing-zone-version "4.0" \
  --region ${AWS_HOME_REGION}

iam-policy-autopilot によるポリシー生成(Python プログラムに対して実行)

Kiro CLI によるシェルスクリプトの Python 化

執筆時点の 12月12日 に起動した AWS CloudShell 環境ではすでに Kiro CLI が入っていました。
そのまま利用してみます。

~ $ which kiro-cli
/usr/local/bin/kiro-cli

~ $ kiro-cli --version
kiro-cli 1.20.0

ちょっと長いので折り畳みます

~ $ kiro-cli chat "EnableControlTower.sh を Python3 を利用したプログラムに改修したい"
⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀     ⢀⣴⣶⣶⣦⡀⠀⠀⠀⢀⣴⣶⣦⣄⡀⠀⠀⢀⣴⣶⣶⣦⡀⠀⠀⢀⣴⣶⣶⣶⣶⣶⣶⣶⣶⣶⣦⣄⡀⠀⠀⠀⠀⠀⠀⢀⣠⣴⣶⣶⣶⣶⣶⣦⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀    ⢰⣿⠋⠁⠈⠙⣿⡆⠀⢀⣾⡿⠁⠀⠈⢻⡆⢰⣿⠋⠁⠈⠙⣿⡆⢰⣿⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠻⣦⠀⠀⠀⠀⣴⡿⠟⠋⠁⠀⠀⠀⠈⠙⠻⢿⣦⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀    ⢸⣿⠀⠀⠀⠀⣿⣇⣴⡿⠋⠀⠀⠀⢀⣼⠇⢸⣿⠀⠀⠀⠀⣿⡇⢸⣿⠀⠀⠀⢠⣤⣤⣤⣤⣄⠀⠀⠀⠀⣿⡆⠀⠀⣼⡟⠀⠀⠀⠀⣀⣀⣀⠀⠀⠀⠀⢻⣧⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀    ⢸⣿⠀⠀⠀⠀⣿⡿⠋⠀⠀⠀⢀⣾⡿⠁⠀⢸⣿⠀⠀⠀⠀⣿⡇⢸⣿⠀⠀⠀⢸⣿⠉⠉⠉⣿⡇⠀⠀⠀⣿⡇⠀⣼⡟⠀⠀⠀⣰⡿⠟⠛⠻⢿⣆⠀⠀⠀⢻⣧⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀    ⢸⣿⠀⠀⠀⠀⠙⠁⠀⠀⢀⣼⡟⠁⠀⠀⠀⢸⣿⠀⠀⠀⠀⣿⡇⢸⣿⠀⠀⠀⢸⣿⣶⣶⡶⠋⠀⠀⠀⠀⣿⠇⢰⣿⠀⠀⠀⢰⣿⠀⠀⠀⠀⠀⣿⡆⠀⠀⠀⣿⡆
⠀⠀⠀⠀⠀⠀⠀    ⢸⣿⠀⠀⠀⠀⠀⠀⠀⠀⠹⣷⡀⠀⠀⠀⠀⢸⣿⠀⠀⠀⠀⣿⡇⢸⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣼⠟⠀⢸⣿⠀⠀⠀⢸⣿⠀⠀⠀⠀⠀⣿⡇⠀⠀⠀⣿⡇
⠀⠀⠀⠀⠀⠀⠀    ⢸⣿⠀⠀⠀⠀⠀⣠⡀⠀⠀⠹⣷⡄⠀⠀⠀⢸⣿⠀⠀⠀⠀⣿⡇⢸⣿⠀⠀⠀⠀⣤⣄⠀⠀⠀⠀⠹⣿⡅⠀⠀⠸⣿⠀⠀⠀⠸⣿⠀⠀⠀⠀⠀⣿⠇⠀⠀⠀⣿⠇
⠀⠀⠀⠀⠀⠀⠀    ⢸⣿⠀⠀⠀⠀⣾⡟⣷⡀⠀⠀⠘⣿⣆⠀⠀⢸⣿⠀⠀⠀⠀⣿⡇⢸⣿⠀⠀⠀⠀⣿⡟⣷⡀⠀⠀⠀⠘⣿⣆⠀⠀⢻⣧⠀⠀⠀⠹⣷⣦⣤⣤⣾⠏⠀⠀⠀⣼⡟
⠀⠀⠀⠀⠀⠀⠀    ⢸⣿⠀⠀⠀⠀⣿⡇⠹⣷⡀⠀⠀⠈⢻⡇⠀⢸⣿⠀⠀⠀⠀⣿⡇⢸⣿⠀⠀⠀⠀⣿⡇⠹⣷⡀⠀⠀⠀⠈⢻⡇⠀⠀⢻⣧⠀⠀⠀⠀⠉⠉⠉⠀⠀⠀⠀⣼⡟
⠀⠀⠀⠀⠀⠀⠀    ⠸⣿⣄⡀⢀⣠⣿⠇⠀⠙⣷⡀⠀⢀⣼⠇⠀⠸⣿⣄⡀⢀⣠⣿⠇⠸⣿⣄⡀⢀⣠⣿⠇⠀⠙⣷⡀⠀⠀⢀⣼⠇⠀⠀⠀⠻⣷⣦⣄⡀⠀⠀⠀⢀⣠⣴⣾⠟
⠀⠀⠀⠀⠀⠀⠀    ⠀⠈⠻⠿⠿⠟⠁⠀⠀⠀⠈⠻⠿⠿⠟⠁⠀⠀⠈⠻⠿⠿⠟⠁⠀⠀⠈⠻⠿⠿⠟⠁⠀⠀⠀⠈⠻⠿⠿⠟⠁⠀⠀⠀⠀⠀⠈⠙⠻⠿⠿⠿⠿⠟⠋⠁


Jump into building with Kiro:

1. Ask a question or describe a task
2. Use /context add <file> to provide Kiro with additional info
3. Connect to external tools with by using /mcp

Use /help for more information and happy coding!

Model: Auto (/model to change) | Plan: KIRO FREE (/usage for more detail)


> まず、現在の EnableControlTower.sh ファイルの内容を確認して、Python3に変換する必要がある処理を理解しましょう。
Reading file: /home/cloudshell-user/EnableControlTower.sh, all lines (using tool: read)
 ✓ Successfully read 7060 bytes from /home/cloudshell-user/EnableControlTower.sh
 - Completed in 0.0s

> このシェルスクリプトをPython3に変換します。AWS SDK for Python (boto3) を使用して同等の機能を実装します。
I'll create the following file: /home/cloudshell-user/enable_control_tower.py (using tool: write)
Purpose: Convert EnableControlTower.sh to Python3 using boto3

+      1: #!/usr/bin/env python3
+      2: import boto3
+      3: import json
+      4: import time
+      5: 
+      6: # 設定
+      7: LOG_ARCHIVE_EMAIL = "foo@example.com"
+      8: AUDIT_EMAIL = "bar@example.com"
+      9: AWS_HOME_REGION = "ap-northeast-1"
+     10: ALIAS = "hoge"
+     11: SECURITY_OU = "Security"
+     12: SANDBOX_OU = "Sandbox"
+     13: 
+     14: # エイリアス文字列の決定
+     15: alias_prefix = f"{ALIAS}-" if ALIAS else ""
+     16: 
+     17: # AWS クライアントの初期化
+     18: org_client = boto3.client('organizations', region_name=AWS_HOME_REGION)
+     19: iam_client = boto3.client('iam', region_name=AWS_HOME_REGION)
+     20: ct_client = boto3.client('controltower', region_name=AWS_HOME_REGION)
+     21: 
+     22: def create_organization():
+     23:     try:
+     24:         org_client.create_organization()
+     25:         print("AWS Organizations を有効化しました")
+     26:     except Exception as e:
+     27:         print(f"Organizations 作成エラー: {e}")
+     28: 
+     29: def create_accounts():
+     30:     # ログアーカイブアカウント作成
+     31:     org_client.create_account(
+     32:         Email=LOG_ARCHIVE_EMAIL,
+     33:         AccountName=f"{alias_prefix}log"
+     34:     )
+     35:     
+     36:     # 監査アカウント作成
+     37:     org_client.create_account(
+     38:         Email=AUDIT_EMAIL,
+     39:         AccountName=f"{alias_prefix}sec"
+     40:     )
+     41:     print("アカウントを作成しました")
+     42: 
+     43: def create_iam_roles():
+     44:     # AWSControlTowerAdmin ロール
+     45:     trust_policy = {
+     46:         "Version": "2012-10-17",
+     47:         "Statement": [{
+     48:             "Effect": "Allow",
+     49:             "Principal": {"Service": "controltower.amazonaws.com"},
+     50:             "Action": "sts:AssumeRole"
+     51:         }]
+     52:     }
+     53:     
+     54:     iam_client.create_role(
+     55:         RoleName='AWSControlTowerAdmin',
+     56:         Path='/service-role/',
+     57:         AssumeRolePolicyDocument=json.dumps(trust_policy)
+     58:     )
+     59:     
+     60:     admin_policy = {
+     61:         "Version": "2012-10-17",
+     62:         "Statement": [{
+     63:             "Effect": "Allow",
+     64:             "Action": "ec2:DescribeAvailabilityZones",
+     65:             "Resource": "*"
+     66:         }]
+     67:     }
+     68:     
+     69:     iam_client.put_role_policy(
+     70:         RoleName='AWSControlTowerAdmin',
+     71:         PolicyName='AWSControlTowerAdminPolicy',
+     72:         PolicyDocument=json.dumps(admin_policy)
+     73:     )
+     74:     
+     75:     iam_client.attach_role_policy(
+     76:         RoleName='AWSControlTowerAdmin',
+     77:         PolicyArn='arn:aws:iam::aws:policy/service-role/AWSControlTowerServiceRolePolicy'
+     78:     )
+     79:     
+     80:     # AWSControlTowerCloudTrailRole
+     81:     cloudtrail_trust = {
+     82:         "Version": "2012-10-17",
+     83:         "Statement": [{
+     84:             "Effect": "Allow",
+     85:             "Principal": {"Service": "cloudtrail.amazonaws.com"},
+     86:             "Action": "sts:AssumeRole"
+     87:         }]
+     88:     }
+     89:     
+     90:     iam_client.create_role(
+     91:         RoleName='AWSControlTowerCloudTrailRole',
+     92:         Path='/service-role/',
+     93:         AssumeRolePolicyDocument=json.dumps(cloudtrail_trust)
+     94:     )
+     95:     
+     96:     policies = iam_client.list_policies(Scope='AWS')['Policies']
+     97:     ct_policy_arn = next(p['Arn'] for p in policies if 'AWSControlTowerCloudTrailRolePolicy' in p['PolicyName'])
+     98:     
+     99:     iam_client.attach_role_policy(
+    100:         RoleName='AWSControlTowerCloudTrailRole',
+    101:         PolicyArn=ct_policy_arn
+    102:     )
+    103:     
+    104:     # AWSControlTowerStackSetRole
+    105:     cf_trust = {
+    106:         "Version": "2012-10-17",
+    107:         "Statement": [{
+    108:             "Effect": "Allow",
+    109:             "Principal": {"Service": "cloudformation.amazonaws.com"},
+    110:             "Action": "sts:AssumeRole"
+    111:         }]
+    112:     }
+    113:     
+    114:     iam_client.create_role(
+    115:         RoleName='AWSControlTowerStackSetRole',
+    116:         Path='/service-role/',
+    117:         AssumeRolePolicyDocument=json.dumps(cf_trust)
+    118:     )
+    119:     
+    120:     stackset_policy = {
+    121:         "Version": "2012-10-17",
+    122:         "Statement": [{
+    123:             "Action": ["sts:AssumeRole"],
+    124:             "Resource": ["arn:aws:iam::*:role/AWSControlTowerExecution"],
+    125:             "Effect": "Allow"
+    126:         }]
+    127:     }
+    128:     
+    129:     iam_client.put_role_policy(
+    130:         RoleName='AWSControlTowerStackSetRole',
+    131:         PolicyName='AWSControlTowerStackSetRolePolicy',
+    132:         PolicyDocument=json.dumps(stackset_policy)
+    133:     )
+    134:     
+    135:     # AWSControlTowerConfigAggregatorRoleForOrganizations
+    136:     config_trust = {
+    137:         "Version": "2012-10-17",
+    138:         "Statement": [{
+    139:             "Effect": "Allow",
+    140:             "Principal": {"Service": "config.amazonaws.com"},
+    141:             "Action": "sts:AssumeRole"
+    142:         }]
+    143:     }
+    144:     
+    145:     iam_client.create_role(
+    146:         RoleName='AWSControlTowerConfigAggregatorRoleForOrganizations',
+    147:         Path='/service-role/',
+    148:         AssumeRolePolicyDocument=json.dumps(config_trust)
+    149:     )
+    150:     
+    151:     iam_client.attach_role_policy(
+    152:         RoleName='AWSControlTowerConfigAggregatorRoleForOrganizations',
+    153:         PolicyArn='arn:aws:iam::aws:policy/service-role/AWSConfigRoleForOrganizations'
+    154:     )
+    155:     
+    156:     print("IAM ロールを作成しました")
+    157: 
+    158: def setup_organizational_units():
+    159:     # アカウント ID 取得
+    160:     accounts = org_client.list_accounts()['Accounts']
+    161:     log_account_id = next(acc['Id'] for acc in accounts if '-log' in acc['Name'])
+    162:     audit_account_id = next(acc['Id'] for acc in accounts if '-sec' in acc['Name'])
+    163:     
+    164:     # Root ID 取得
+    165:     root_id = org_client.list_roots()['Roots'][0]['Id']
+    166:     
+    167:     # OU 作成
+    168:     org_client.create_organizational_unit(ParentId=root_id, Name=SECURITY_OU)
+    169:     org_client.create_organizational_unit(ParentId=root_id, Name=SANDBOX_OU)
+    170:     
+    171:     # Security OU ID 取得
+    172:     ous = org_client.list_organizational_units_for_parent(ParentId=root_id)['OrganizationalUnits']
+    173:     security_ou_id = next(ou['Id'] for ou in ous if 'Security' in ou['Name'])
+    174:     
+    175:     # アカウント移動
+    176:     org_client.move_account(
+    177:         AccountId=log_account_id,
+    178:         SourceParentId=root_id,
+    179:         DestinationParentId=security_ou_id
+    180:     )
+    181:     
+    182:     org_client.move_account(
+    183:         AccountId=audit_account_id,
+    184:         SourceParentId=root_id,
+    185:         DestinationParentId=security_ou_id
+    186:     )
+    187:     
+    188:     print("OU を設定しました")
+    189:     return log_account_id, audit_account_id
+    190: 
+    191: def create_landing_zone(log_account_id, audit_account_id):
+    192:     manifest = {
+    193:         "accessManagement": {"enabled": True},
+    194:         "backup": {"enabled": False},
+    195:         "centralizedLogging": {
+    196:             "accountId": log_account_id,
+    197:             "enabled": True,
+    198:             "configurations": {
+    199:                 "accessLoggingBucket": {"retentionDays": 1},
+    200:                 "loggingBucket": {"retentionDays": 1}
+    201:             }
+    202:         },
+    203:         "config": {
+    204:             "accountId": audit_account_id,
+    205:             "enabled": True,
+    206:             "configurations": {
+    207:                 "accessLoggingBucket": {"retentionDays": 1},
+    208:                 "loggingBucket": {"retentionDays": 1}
+    209:             }
+    210:         },
+    211:         "securityRoles": {
+    212:             "enabled": True,
+    213:             "accountId": audit_account_id
+    214:         }
+    215:     }
+    216:     
+    217:     ct_client.create_landing_zone(
+    218:         Manifest=manifest,
+    219:         LandingZoneVersion="4.0"
+    220:     )
+    221:     
+    222:     print("ランディングゾーンを作成しました")
+    223: 
+    224: def main():
+    225:     print("Control Tower セットアップを開始します...")
+    226:     
+    227:     create_organization()
+    228:     time.sleep(5)
+    229:     
+    230:     create_accounts()
+    231:     time.sleep(30)  # アカウント作成完了を待機
+    232:     
+    233:     create_iam_roles()
+    234:     
+    235:     log_account_id, audit_account_id = setup_organizational_units()
+    236:     
+    237:     create_landing_zone(log_account_id, audit_account_id)
+    238:     
+    239:     print("Control Tower セットアップが完了しました")
+    240: 
+    241: if __name__ == "__main__":
+    242:     main()


Allow this action? Use 't' to trust (always allow) this tool for the session. [y/n/t]:

> y

Creating: /home/cloudshell-user/enable_control_tower.py
 - Completed in 0.0s

> 実行可能にするために権限を設定します:
I will run the following command: chmod +x /home/cloudshell-user/enable_control_tower.py (using tool: shell)
Purpose: Make the Python script executable


Allow this action? Use 't' to trust (always allow) this tool for the session. [y/n/t]:

> y


Creating: /home/cloudshell-user/enable_control_tower.py
 - Completed in 0.0s

> 実行可能にするために権限を設定します:
I will run the following command: chmod +x /home/cloudshell-user/enable_control_tower.py (using tool: shell)
Purpose: Make the Python script executable


Allow this action? Use 't' to trust (always allow) this tool for the session. [y/n/t]:

> y

 - Completed in 0.7s

> Python3版のControl Towerセットアップスクリプトを作成しました。主な変更点:

- **boto3ライブラリ使用**: AWS CLI コマンドをboto3のAPIコールに変換
- **エラーハンドリング**: try-except文でエラー処理を追加
- **待機時間**: アカウント作成後の適切な待機時間を設定
- **JSON処理**: Pythonの辞書とjson.dumps()でJSONファイル作成を置換
- **関数分割**: 機能ごとに関数を分けて可読性を向上

実行方法:
bash
python3 enable_control_tower.py


必要に応じて、boto3がインストールされていない場合は:
bash
pip3 install boto3


元のシェルスクリプトと同じ機能を提供しますが、Pythonの利点(エラーハンドリング、可読性、保守性)を活用しています。
 ▸ Credits: 0.42 • Time: 1m 8s

>

出来上がった Python プログラム

クリックで目次が表示されます。

enable_control_tower.py として Kiro CLI が生成してくれたプログラムは以下のようなものでした。
余談ですが、Kiro は、私の元スクリプトにはなかった AWS Organizations 有効化後と、コアアカウント作成後に待機時間を入れてくれるなどをしてくれていました。

#!/usr/bin/env python3
import boto3
import json
import time

# 設定
LOG_ARCHIVE_EMAIL = "foo@example.com"
AUDIT_EMAIL = "bar@example.com"
AWS_HOME_REGION = "ap-northeast-1"
ALIAS = "hoge"
SECURITY_OU = "Security"
SANDBOX_OU = "Sandbox"

# エイリアス文字列の決定
alias_prefix = f"{ALIAS}-" if ALIAS else ""

# AWS クライアントの初期化
org_client = boto3.client('organizations', region_name=AWS_HOME_REGION)
iam_client = boto3.client('iam', region_name=AWS_HOME_REGION)
ct_client = boto3.client('controltower', region_name=AWS_HOME_REGION)

def create_organization():
    try:
        org_client.create_organization()
        print("AWS Organizations を有効化しました")
    except Exception as e:
        print(f"Organizations 作成エラー: {e}")

def create_accounts():
    # ログアーカイブアカウント作成
    org_client.create_account(
        Email=LOG_ARCHIVE_EMAIL,
        AccountName=f"{alias_prefix}log"
    )
    
    # 監査アカウント作成
    org_client.create_account(
        Email=AUDIT_EMAIL,
        AccountName=f"{alias_prefix}sec"
    )
    print("アカウントを作成しました")

def create_iam_roles():
    # AWSControlTowerAdmin ロール
    trust_policy = {
        "Version": "2012-10-17",
        "Statement": [{
            "Effect": "Allow",
            "Principal": {"Service": "controltower.amazonaws.com"},
            "Action": "sts:AssumeRole"
        }]
    }
    
    iam_client.create_role(
        RoleName='AWSControlTowerAdmin',
        Path='/service-role/',
        AssumeRolePolicyDocument=json.dumps(trust_policy)
    )
    
    admin_policy = {
        "Version": "2012-10-17",
        "Statement": [{
            "Effect": "Allow",
            "Action": "ec2:DescribeAvailabilityZones",
            "Resource": "*"
        }]
    }
    
    iam_client.put_role_policy(
        RoleName='AWSControlTowerAdmin',
        PolicyName='AWSControlTowerAdminPolicy',
        PolicyDocument=json.dumps(admin_policy)
    )
    
    iam_client.attach_role_policy(
        RoleName='AWSControlTowerAdmin',
        PolicyArn='arn:aws:iam::aws:policy/service-role/AWSControlTowerServiceRolePolicy'
    )
    
    # AWSControlTowerCloudTrailRole
    cloudtrail_trust = {
        "Version": "2012-10-17",
        "Statement": [{
            "Effect": "Allow",
            "Principal": {"Service": "cloudtrail.amazonaws.com"},
            "Action": "sts:AssumeRole"
        }]
    }
    
    iam_client.create_role(
        RoleName='AWSControlTowerCloudTrailRole',
        Path='/service-role/',
        AssumeRolePolicyDocument=json.dumps(cloudtrail_trust)
    )
    
    policies = iam_client.list_policies(Scope='AWS')['Policies']
    ct_policy_arn = next(p['Arn'] for p in policies if 'AWSControlTowerCloudTrailRolePolicy' in p['PolicyName'])
    
    iam_client.attach_role_policy(
        RoleName='AWSControlTowerCloudTrailRole',
        PolicyArn=ct_policy_arn
    )
    
    # AWSControlTowerStackSetRole
    cf_trust = {
        "Version": "2012-10-17",
        "Statement": [{
            "Effect": "Allow",
            "Principal": {"Service": "cloudformation.amazonaws.com"},
            "Action": "sts:AssumeRole"
        }]
    }
    
    iam_client.create_role(
        RoleName='AWSControlTowerStackSetRole',
        Path='/service-role/',
        AssumeRolePolicyDocument=json.dumps(cf_trust)
    )
    
    stackset_policy = {
        "Version": "2012-10-17",
        "Statement": [{
            "Action": ["sts:AssumeRole"],
            "Resource": ["arn:aws:iam::*:role/AWSControlTowerExecution"],
            "Effect": "Allow"
        }]
    }
    
    iam_client.put_role_policy(
        RoleName='AWSControlTowerStackSetRole',
        PolicyName='AWSControlTowerStackSetRolePolicy',
        PolicyDocument=json.dumps(stackset_policy)
    )
    
    # AWSControlTowerConfigAggregatorRoleForOrganizations
    config_trust = {
        "Version": "2012-10-17",
        "Statement": [{
            "Effect": "Allow",
            "Principal": {"Service": "config.amazonaws.com"},
            "Action": "sts:AssumeRole"
        }]
    }
    
    iam_client.create_role(
        RoleName='AWSControlTowerConfigAggregatorRoleForOrganizations',
        Path='/service-role/',
        AssumeRolePolicyDocument=json.dumps(config_trust)
    )
    
    iam_client.attach_role_policy(
        RoleName='AWSControlTowerConfigAggregatorRoleForOrganizations',
        PolicyArn='arn:aws:iam::aws:policy/service-role/AWSConfigRoleForOrganizations'
    )
    
    print("IAM ロールを作成しました")

def setup_organizational_units():
    # アカウント ID 取得
    accounts = org_client.list_accounts()['Accounts']
    log_account_id = next(acc['Id'] for acc in accounts if '-log' in acc['Name'])
    audit_account_id = next(acc['Id'] for acc in accounts if '-sec' in acc['Name'])
    
    # Root ID 取得
    root_id = org_client.list_roots()['Roots'][0]['Id']
    
    # OU 作成
    org_client.create_organizational_unit(ParentId=root_id, Name=SECURITY_OU)
    org_client.create_organizational_unit(ParentId=root_id, Name=SANDBOX_OU)
    
    # Security OU ID 取得
    ous = org_client.list_organizational_units_for_parent(ParentId=root_id)['OrganizationalUnits']
    security_ou_id = next(ou['Id'] for ou in ous if 'Security' in ou['Name'])
    
    # アカウント移動
    org_client.move_account(
        AccountId=log_account_id,
        SourceParentId=root_id,
        DestinationParentId=security_ou_id
    )
    
    org_client.move_account(
        AccountId=audit_account_id,
        SourceParentId=root_id,
        DestinationParentId=security_ou_id
    )
    
    print("OU を設定しました")
    return log_account_id, audit_account_id

def create_landing_zone(log_account_id, audit_account_id):
    manifest = {
        "accessManagement": {"enabled": True},
        "backup": {"enabled": False},
        "centralizedLogging": {
            "accountId": log_account_id,
            "enabled": True,
            "configurations": {
                "accessLoggingBucket": {"retentionDays": 1},
                "loggingBucket": {"retentionDays": 1}
            }
        },
        "config": {
            "accountId": audit_account_id,
            "enabled": True,
            "configurations": {
                "accessLoggingBucket": {"retentionDays": 1},
                "loggingBucket": {"retentionDays": 1}
            }
        },
        "securityRoles": {
            "enabled": True,
            "accountId": audit_account_id
        }
    }
    
    ct_client.create_landing_zone(
        Manifest=manifest,
        LandingZoneVersion="4.0"
    )
    
    print("ランディングゾーンを作成しました")

def main():
    print("Control Tower セットアップを開始します...")
    
    create_organization()
    time.sleep(5)
    
    create_accounts()
    time.sleep(30)  # アカウント作成完了を待機
    
    create_iam_roles()
    
    log_account_id, audit_account_id = setup_organizational_units()
    
    create_landing_zone(log_account_id, audit_account_id)
    
    print("Control Tower セットアップが完了しました")

if __name__ == "__main__":
    main()

iam-policy-autopilot のサブコマンドについて

iam-policy-autopilot の README からの抜粋となりますが、ポリシーを生成する generate-policies サブコマンド、AccessDenied となる処理の改修に利用できる fix-access-denied サブコマンドが主に使われるかと考えられます。

Commands

generate-policies - Generates complete IAM policy documents from source files

iam-policy-autopilot generate-policies <source_files> [OPTIONS]  

Example:

iam-policy-autopilot generate-policies \
  ./src/app.py \
  --region us-east-1 \
  --account 123456789012 \
  --pretty

Options:

  • --region <REGION> - AWS region for resource ARNs
  • --account <ACCOUNT> - AWS account ID for resource ARNs
  • --upload-policies <PREFIX>- Upload generated policies to AWS IAM with the specified prefix
  • --pretty - Pretty-print JSON output

fix-access-denied - Fix AccessDenied errors by analyzing and optionally applying IAM policy changes

iam-policy-autopilot fix-access-denied <access-denied-error-message> [OPTIONS]

Example:

iam-policy-autopilot fix-access-denied \
  "User: arn:aws:iam::123456789012:user/test is not authorized to perform: s3:GetObject on resource: arn:aws:s3:::my-bucket/file.txt"

Options:

--yes - Auto-apply policy changes without confirmation

github.com

ポリシーの生成(オプション指定なし)

まずは iam-policy-autopilot generate-policies <source_files> のみ実施します。
可読性のために -p オプション(--pretty)のみ付与していますが、生成するだけです。

ちなみに、前述の README からの抜粋にあったオプションの -a(--account) に続けて AWS アカウント ID を記載することでリソース ARN の AWS アカウント部分がより明示的に記載されるようです。
以下の例では "arn:aws:organizations::*:account/o-*/*" などの ARN が該当します。

ポリシー全文が長いので折り畳みます

~ $ iam-policy-autopilot generate-policies enable_control_tower.py -p
{
  "Policies": [
    {
      "Policy": {
        "Id": "IamPolicyAutopilot",
        "Version": "2012-10-17",
        "Statement": [
          {
            "Effect": "Allow",
            "Action": [
              "organizations:CreateAccount",
              "organizations:CreateOrganization",
              "organizations:ListAccounts",
              "organizations:ListRoots",
              "organizations:TagResource"
            ],
            "Resource": [
              "*"
            ]
          },
          {
            "Effect": "Allow",
            "Action": [
              "iam:AttachRolePolicy",
              "iam:CreateRole",
              "iam:PutRolePolicy",
              "iam:TagRole"
            ],
            "Resource": [
              "arn:aws:iam::*:role/*"
            ]
          },
          {
            "Effect": "Allow",
            "Action": [
              "iam:ListPolicies"
            ],
            "Resource": [
              "*"
            ]
          },
          {
            "Effect": "Allow",
            "Action": [
              "chime:ListAccounts"
            ],
            "Resource": [
              "*"
            ]
          },
          {
            "Effect": "Allow",
            "Action": [
              "organizations:CreateOrganizationalUnit",
              "organizations:ListOrganizationalUnitsForParent",
              "organizations:MoveAccount"
            ],
            "Resource": [
              "arn:aws:organizations::*:account/o-*/*",
              "arn:aws:organizations::*:ou/o-*/ou-*",
              "arn:aws:organizations::*:root/o-*/r-*"
            ]
          }
        ]
      },
      "PolicyType": "Identity"
    }
  ]
}

実際のポリシーとして生成・保存する

次に、--upload-policies オプションに続けてポリシー名の文字列を付与し、iam-policy-autopilot generate-policies サブコマンドを実行してみます。

結果として、AWS CloudShell を実行している AWS アカウント 52931804xxxx に PolicyId ANPAXWPOQOWFM3HMRxxxx、PolicyName test_1 として保存されていることがレスポンスとして返却されています。

ポリシー全文が長いので折り畳みます

~ $ iam-policy-autopilot generate-policies enable_control_tower.py -p --upload-policies test
{
  "Policies": [
    {
      "Policy": {
        "Id": "IamPolicyAutopilot",
        "Version": "2012-10-17",
        "Statement": [
          {
            "Effect": "Allow",
            "Action": [
              "organizations:CreateAccount",
              "organizations:CreateOrganization",
              "organizations:ListAccounts",
              "organizations:ListRoots",
              "organizations:TagResource"
            ],
            "Resource": [
              "*"
            ]
          },
          {
            "Effect": "Allow",
            "Action": [
              "iam:AttachRolePolicy",
              "iam:CreateRole",
              "iam:PutRolePolicy",
              "iam:TagRole"
            ],
            "Resource": [
              "arn:aws:iam::*:role/*"
            ]
          },
          {
            "Effect": "Allow",
            "Action": [
              "iam:ListPolicies"
            ],
            "Resource": [
              "*"
            ]
          },
          {
            "Effect": "Allow",
            "Action": [
              "chime:ListAccounts"
            ],
            "Resource": [
              "*"
            ]
          },
          {
            "Effect": "Allow",
            "Action": [
              "organizations:CreateOrganizationalUnit",
              "organizations:ListOrganizationalUnitsForParent",
              "organizations:MoveAccount"
            ],
            "Resource": [
              "arn:aws:organizations::*:account/o-*/*",
              "arn:aws:organizations::*:ou/o-*/ou-*",
              "arn:aws:organizations::*:root/o-*/r-*"
            ]
          }
        ]
      },
      "PolicyType": "Identity"
    }
  ],
  "UploadResult": {
    "Successful": [
      {
        "PolicyName": "test_1",
        "PolicyArn": "arn:aws:iam::52931804xxxx:policy/test_1",
        "PolicyId": "ANPAXWPOQOWFM3HMRxxxx"
      }
    ],
    "Failed": []
  }
}

ちなみに、iam-policy-autopilot generate-policies enable_control_tower.py -p --upload-policies test を 2度、3度実施すると test_ のプレフィックスを持つポリシーがいくつもつくられ、上書きをされない挙動であることは確認しました。

生成されたポリシーの妥当性

今回、大元は AWS CLI の処理を逐次的に組み立てたシェルスクリプトだったので、EnableControlTower.sh の中で登場する aws コマンドの実行部分の抽出結果と生成されたポリシーを比較してみました。
結果、おおむねスクリプト中に出てくる AWS API に限定されており、かつリソース ARN で限定的に書けそうな部分はより詳細に記述するなど、より最小権限になるように生成されていました。
ただ、なぜ chime:ListAccounts が Allow されているのかは不明です。(複数回実行しても、環境を変えて実行しても生成されたので、chime の呪いかと思われます)

また、CloudTrail の記録を見ると、AWS CloudShell を利用している IAM エンティティによる操作として iam:CreatePolicy

EnableControlTower.sh を grep "aws " してみた結果
~ $ cat EnableControlTower.sh | grep "aws "
aws organizations create-organization
aws organizations create-account \
aws organizations create-account \
aws iam create-role \
aws iam put-role-policy \
aws iam attach-role-policy \
aws iam create-role \
AWS_CONTROLTOWER_CLOUDTRAIL_ROLE_POLICY_ARN=$(aws iam list-policies --scope AWS --query "Policies[?contains(PolicyName, 'AWSControlTowerCloudTrailRolePolicy')].Arn" --output text)
aws iam attach-role-policy \
aws iam create-role \
aws iam put-role-policy \
aws iam create-role \
aws iam attach-role-policy\
LOG_ARCHIVE_ACCOUNT_ID=$(aws organizations list-accounts \
AUDIT_ACCOUNT_ID=$(aws organizations list-accounts \
ROOT_ID=$(aws organizations list-roots --query "Roots[].Id" --output text)
aws organizations create-organizational-unit --parent-id ${ROOT_ID} --name "${SECURITY_OU}"
aws organizations create-organizational-unit --parent-id ${ROOT_ID} --name "${SANDBOX_OU}"
SECURITY_OU_ID=$(aws organizations list-organizational-units-for-parent --parent-id ${ROOT_ID} --query "OrganizationalUnits[?contains(Name, 'Security')].Id" --output text)
aws organizations move-account --account-id ${LOG_ARCHIVE_ACCOUNT_ID} --source-parent-id ${ROOT_ID} --destination-parent-id ${SECURITY_OU_ID}
aws organizations move-account --account-id ${AUDIT_ACCOUNT_ID} --source-parent-id ${ROOT_ID} --destination-parent-id ${SECURITY_OU_ID}
aws controltower create-landing-zone \
生成されて保存されたポリシーの内容

以下のコマンドで視認性のために YAML 化しています。

TEST_POLICY_ARN=$(aws iam list-policies --scope Local --query "Policies[?contains(PolicyName,'test')].Arn" --output text)
aws iam get-policy-version --policy-arn ${TEST_POLICY_ARN} --version-id v1 | yq -P
YAML 化したポリシー
PolicyVersion:
  Document:
    Id: IamPolicyAutopilot
    Version: "2012-10-17"
    Statement:
      - Effect: Allow
        Action:
          - organizations:CreateAccount
          - organizations:CreateOrganization
          - organizations:ListAccounts
          - organizations:ListRoots
          - organizations:TagResource
        Resource:
          - '*'
      - Effect: Allow
        Action:
          - iam:AttachRolePolicy
          - iam:CreateRole
          - iam:PutRolePolicy
          - iam:TagRole
        Resource:
          - arn:aws:iam::*:role/*
      - Effect: Allow
        Action:
          - iam:ListPolicies
        Resource:
          - '*'
      - Effect: Allow
        Action:
          - chime:ListAccounts
        Resource:
          - '*'
      - Effect: Allow
        Action:
          - organizations:CreateOrganizationalUnit
          - organizations:ListOrganizationalUnitsForParent
          - organizations:MoveAccount
        Resource:
          - arn:aws:organizations::*:account/o-*/*
          - arn:aws:organizations::*:ou/o-*/ou-*
          - arn:aws:organizations::*:root/o-*/r-*
  VersionId: v1
  IsDefaultVersion: true
  CreateDate: "2025-12-12T05:06:44+00:00"

おわりに

シェルスクリプトが好きな私にとっては Python、TypeScript、Go のみなのは辛いですが、一般的なユースケースでは問題にならないと考えています。

今回は簡単なスクリプトを作り、作成済みのスクリプトを iam-policy-autopilot generate-policies サブコマンドで評価、ポリシー生成をやってみましたが、可能な限り最小権限になるように生成されている様子が伺えました。

もちろんよくわからない chime:ListAccounts が含まれているなど生成されたポリシーの内容をチェックをする必要性はありますが、プログラムの中で実行している AWS API を判断して最小権限ポリシーのたたき台として生成してくれるのはとても有用だと考えられます。

本記事がどなたかの参考になれば幸いです。

ではまた。

市野 和明 (記事一覧)

マネージドサービス部・AWS サポート課

お客様から寄せられたご質問や技術検証を通じて得られた気づきを投稿していきます。

情シスだった前職までの経験で、UI がコロコロ変わる AWS においては GUI で手順を残していると画面構成が変わってしまって後々まごつくことが多かった経験から、極力変わりにくい AWS CLI での記事が多めです。

X(Twitter):@kazzpapa3(AWS Community Builder)