全メンバーアカウントの全リージョンでAWS Configの記録対象を全てに設定し、IAMロールをサービスリンクロールに変更する方法

全メンバーアカウントの全リージョンでAWS Configの記録対象を全てに設定し、IAMロールをサービスリンクロールに変更する方法

Clock Icon2025.03.28

はじめに

AWS Security Hubのコントロール「Config.1」には、以下の3つのチェック項目があります。

  1. AWS Config(レコーダー)が有効化されているか
  2. 有効化されているすべてのSecurity Hubコントロールに対応するすべてのリソースタイプがConfigレコーダーで記録できているか
  3. AWS Configのサービスリンクロール(AWSServiceRoleForConfig)が設定されているか

https://docs.aws.amazon.com/ja_jp/securityhub/latest/userguide/config-controls.html

以前、2点目と3点目のチェックに失敗していると仮定し、1アカウントの全リージョンでAWS Configの記録対象を「すべて」に設定し、IAMロールをサービスリンクロール(AWSServiceRoleForConfig)に一括変更する方法をブログにまとめました。

https://dev.classmethod.jp/articles/update-aws-config-recorder-and-service-role-in-all-regions/

今回は、全メンバーアカウントの全リージョンを対象に一括変更する方法をまとめました。

前提条件

  • 以下の記事を参考に、管理アカウントのLambdaから各メンバーアカウントのリソースを操作するために必要なクロスアカウント用IAMロールを各メンバーアカウントに作成されていること。(IAMロール名は、CrossAccountAdminRole)

https://dev.classmethod.jp/articles/aws-cloudformation-stacksets-iam-role-deployment/

  • 管理アカウントのLambdaで変更されるアカウント対象は、メンバーアカウントのみです。管理アカウントを変更したい場合、以下の記事を参照に、個別に対応ください

https://dev.classmethod.jp/articles/update-aws-config-recorder-and-service-role-in-all-regions/

  • IAMロールを変更するにあたり、事前に確認すべき点がいくつかあります。以下の記事の「前提条件」の欄をご確認ください。

https://dev.classmethod.jp/articles/update-aws-config-recorder-and-service-role-in-all-regions/#%25E5%2589%258D%25E6%258F%2590%25E6%259D%25A1%25E4%25BB%25B6

  • 管理アカウントで有効化している全リージョンを対象に、各メンバーアカウントのConfig設定を変更します。特定のメンバーアカウントのみ有効化しているリージョンがある場合、設定変更対象外となります。

IAMポリシー作成

Lambda用のIAMポリシーを作成します。

ポリシーは以下の通りです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "organizations:ListAccountsForParent",
                "organizations:ListChildren",
                "ec2:DescribeRegions"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::*:role/CrossAccountAdminRole"
        }
    ]
}

Lambda作成

以下の設定でLambda関数を作成します。

  • ランタイム:Python 3.13
  • IAMポリシー
    • AWSLambdaBasicExecutionRole
    • 先ほど作成したIAMポリシーを適用
  • メモリ:2048MB
  • タイムアウト:10分
    アカウント数によってメモリやタイムアウトを変更ください。
    OU_IDは、対象のOUのIDを指定ください。(例:ou-xxxx-xxxx
import boto3
import logging
from boto3.session import Session
from concurrent.futures import ThreadPoolExecutor, as_completed
from botocore.exceptions import ClientError
import threading

OU_ID = 'xxxx'
ROLE_NAME = 'CrossAccountAdminRole'
TOKYO_REGION = 'ap-northeast-1'
MAX_WORKERS_REGIONS = 5
MAX_WORKERS_ACCOUNTS = 10

logger = logging.getLogger()
logger.setLevel(logging.INFO)
logging.getLogger('boto3').setLevel(logging.WARNING)
logging.getLogger('botocore').setLevel(logging.WARNING)

account_results = {}
account_results_lock = threading.Lock()

def get_all_account_ids(org_client, ou_id):
    accounts = []  # アクティブなアカウントIDを格納するリスト
    queue = [ou_id]  # 探索対象のOUを管理するキュー(最初は指定されたOU IDを追加)
    while queue:
        current_ou = queue.pop(0)  # キューから1つのOUを取り出して処理
        try:
            # 現在のOUに属するアカウントを取得
            response = org_client.list_accounts_for_parent(ParentId=current_ou)
            for account in response['Accounts']:
                if account['Status'] == 'ACTIVE':
                    accounts.append(account['Id'])
            child_ous = org_client.list_children(ParentId=current_ou, ChildType='ORGANIZATIONAL_UNIT')
            # 現在のOUに属する子OUをキューに追加
            queue.extend([child['Id'] for child in child_ous['Children']])
        except Exception as e:
            logger.error(f"Error retrieving accounts for OU {current_ou}: {e}")
    logger.info(f"All active accounts retrieved: {', '.join(accounts)}")
    return accounts

def execute_in_regions(session, account_id, regions):
    # ThreadPoolExecutorを使用して、複数のリージョンで、処理(update_config_recorder)を並列に実行
    with ThreadPoolExecutor(max_workers=MAX_WORKERS_REGIONS) as executor:
        futures = []
        for region in regions:
            futures.append(
                executor.submit(update_config_recorder, session.client('config', region_name=region), account_id, region)
            )
        # 全てのタスクが完了するまで待機
        for future in as_completed(futures):
            try:
                future.result()
            except Exception as e:
                logger.error(f"Error processing account {account_id}: {e}")

def process_account(account_id, regions, role_name):
    session = sts_assume_role(account_id, role_name, 'us-east-1')
    # セッションの作成に失敗した場合は処理を中断
    if not session:
        logger.error(f"Failed to assume role for account {account_id}")
        return

    check_and_create_service_role(session.client('iam'), account_id)
    execute_in_regions(session, account_id, regions)

def sts_assume_role(account_id, role_name, region):
    role_arn = f"arn:aws:iam::{account_id}:role/{role_name}"
    try:
        response = boto3.client('sts').assume_role(
            RoleArn=role_arn,
            RoleSessionName="ConfigRecorderUpdate"
        )
        return Session(
            aws_access_key_id=response['Credentials']['AccessKeyId'],
            aws_secret_access_key=response['Credentials']['SecretAccessKey'],
            aws_session_token=response['Credentials']['SessionToken'],
            region_name=region
        )
    except Exception as e:
        logger.error(f"Error assuming role for account {account_id}: {e}")
        return None

def check_and_create_service_role(iam_client, account_id):
    try:
        iam_client.get_role(RoleName='AWSServiceRoleForConfig')
    except iam_client.exceptions.NoSuchEntityException:
        try:
            iam_client.create_service_linked_role(AWSServiceName='config.amazonaws.com')
            logger.info(f"Created AWSServiceRoleForConfig in account {account_id}")
        except Exception as e:
            logger.error(f"Error creating service linked role in account {account_id}: {e}")
    except Exception as e:
        logger.error(f"Error checking service linked role in account {account_id}: {e}")

def update_config_recorder(config_client, account_id, region):
    role_arn = f"arn:aws:iam::{account_id}:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig"
    # IAMロールをカスタムロールに戻したい場合
    # role_arn = f"arn:aws:iam::{account_id}:role/ロール名"
    recording_group = {
        'allSupported': True,
        'includeGlobalResourceTypes': region == TOKYO_REGION
    }
    try:
        config_client.put_configuration_recorder(
            ConfigurationRecorder={
                'name': 'default',
                'roleARN': role_arn,
                'recordingGroup': recording_group
            }
        )
        # スレッドセーフに結果を記録
        # (複数のスレッドが同時に `account_results` にアクセスする可能性があるため、ロックを使用してデータの競合や不整合を防ぎます。)
        with account_results_lock:
            account_results.setdefault(account_id, {'successful_regions': set(), 'failed_regions': set()})
            account_results[account_id]['successful_regions'].add(region)
    except Exception as e:
        logger.error(f"Error updating config recorder in account {account_id} region {region}: {e}")
        # スレッドセーフに失敗した結果を記録
        with account_results_lock:
            account_results.setdefault(account_id, {'successful_regions': set(), 'failed_regions': set()})
            account_results[account_id]['failed_regions'].add(region)

def generate_execution_summary():
    summary_lines = ["=== Execution Summary ==="]
    total_success = total_failure = 0
    # スレッドセーフに結果を記録
    with account_results_lock:
        for account_id, result in sorted(account_results.items()):
            success_count = len(result['successful_regions'])
            failure_count = len(result['failed_regions'])
            total_success += success_count
            total_failure += failure_count
            # アカウントごとの結果をサマリーに追加
            summary_lines.extend([
                f"\nAccount: {account_id}",
                f"Successful regions ({success_count}): {', '.join(sorted(result['successful_regions']))}"
            ])
            if failure_count > 0:
                summary_lines.append(f"Failed regions ({failure_count}): {', '.join(sorted(result['failed_regions']))}")

    total_updates = total_success + total_failure
    success_rate = (total_success / total_updates * 100) if total_updates > 0 else 0
    summary_lines.extend([
        "\n=== Overall Statistics ===",
        f"Total successful updates: {total_success}",
        f"Total failed updates: {total_failure}",
        f"Success rate: {success_rate:.2f}%"
    ])
    return "\n".join(summary_lines), total_success, total_failure, success_rate

def lambda_handler(event, context):
    org_client = boto3.client('organizations')
    member_accounts = get_all_account_ids(org_client, OU_ID)
    regions = [region['RegionName'] for region in boto3.client('ec2').describe_regions()['Regions']]
    # ThreadPoolExecutorを使用して、各メンバーアカウントのConfig Recorderを並列で更新
    with ThreadPoolExecutor(max_workers=MAX_WORKERS_ACCOUNTS) as executor:
        futures = [
            executor.submit(process_account, account_id, regions, ROLE_NAME)
            for account_id in member_accounts
        ]
        for future in as_completed(futures):
            future.result()

    summary, total_success, total_failure, success_rate = generate_execution_summary()
    logger.info(summary)
    return {
        'statusCode': 200,
        'body': 'Config Recorder update completed',
        'summary': {
            'total_success': total_success,
            'total_failure': total_failure,
            'success_rate': f"{success_rate:.2f}%"
        }
    }

処理の流れは以下の通りです。

  1. メンバーアカウントの取得

    • AWS OrganizationsのAPIを使用して、指定したOU(組織単位)に属するすべてのアクティブなメンバーアカウントのIDを取得します。
  2. 対象リージョンの取得

    • ec2:DescribeRegions APIを使用して、管理アカウントにおいて、利用可能なすべてのリージョンを取得します。
  3. 各アカウントに対する処理の実行

    • sts:AssumeRole を使用して、各メンバーアカウントのIAMロール(CrossAccountAdminRole)を引き受け、操作権限を取得します。
    • 取得したセッションを使用して、各リージョンで以下の処理を実行します。
    • サービスリンクロールの確認・作成
      • AWSServiceRoleForConfig が存在しない場合は作成します。
    • AWS Configの設定変更
      • put_configuration_recorder APIを使用して、記録対象を「すべて」に設定し、IAMロールを AWSServiceRoleForConfig に変更します。
  4. 並列処理の活用

    • ThreadPoolExecutor を使用して、複数のアカウントやリージョンの処理を並列で実行し、処理時間を短縮します。
  5. 実行結果の集計とログ出力

    • 各アカウント・リージョンごとの成功・失敗状況を記録し、最終的な成功率を計算してログに出力します。

試してみる

Lambdaを実行します。テストイベントに渡す値はありませんので、空にします。

cm-hirai-screenshot 2025-03-17 8.38.15

出力結果例
Response:
{
  "statusCode": 200,
  "body": "Config Recorder update completed",
  "summary": {
    "total_success": 51,
    "total_failure": 0,
    "success_rate": "100.00%"
  }
}

Function Logs:
START RequestId: e08e6d08-33af-448f-b101-b73d61c05c85 Version: $LATEST
[INFO]	2025-03-12T06:23:24.445Z	e08e6d08-33af-448f-b101-b73d61c05c85	=== Execution Summary ===

Account: 111111111111
Successful regions (17): ap-northeast-1, ap-northeast-2, ap-northeast-3, ap-south-1, ap-southeast-1, ap-southeast-2, ca-central-1, eu-central-1, eu-north-1, eu-west-1, eu-west-2, eu-west-3, sa-east-1, us-east-1, us-east-2, us-west-1, us-west-2

Account: 222222222222
Successful regions (17): ap-northeast-1, ap-northeast-2, ap-northeast-3, ap-south-1, ap-southeast-1, ap-southeast-2, ca-central-1, eu-central-1, eu-north-1, eu-west-1, eu-west-2, eu-west-3, sa-east-1, us-east-1, us-east-2, us-west-1, us-west-2

Account: 333333333333
Successful regions (17): ap-northeast-1, ap-northeast-2, ap-northeast-3, ap-south-1, ap-southeast-1, ap-southeast-2, ca-central-1, eu-central-1, eu-north-1, eu-west-1, eu-west-2, eu-west-3, sa-east-1, us-east-1, us-east-2, us-west-1, us-west-2

=== Overall Statistics ===
Total successful updates: 51
Total failed updates: 0
Success rate: 100.00%

これで、設定したOU ID配下の全アカウントのConfigレコーダー設定が以下の3点を満たしていることになります。

  1. サービスリンクロールであるAWSServiceRoleForConfigをConfigレコーダーのIAMロールとして指定する
  2. 東京リージョンはグローバルリソース (IAM リソースなど) を含め全てのリソースタイプを記録
  3. 東京リージョン以外のリージョンは、グローバルリソースを除く全てのリソースタイプを記録

設定が正しいか確認する

管理アカウントのConfigアグリゲータから、AWS Security Hubのコントロール[Config.1]が成功するように、各アカウントのConfigレコーダー設定が上記の3点を満たしているかを確認します。

設定方法は、以下の記事をご参照ください。

https://dev.classmethod.jp/articles/aws-config-aggregator-check-multi-account/

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.