全メンバーアカウントの全リージョンでAWS Configの記録対象を全てに設定し、IAMロールをサービスリンクロールに変更する方法
はじめに
AWS Security Hubのコントロール「Config.1」には、以下の3つのチェック項目があります。
- AWS Config(レコーダー)が有効化されているか
- 有効化されているすべてのSecurity Hubコントロールに対応するすべてのリソースタイプがConfigレコーダーで記録できているか
- AWS Configのサービスリンクロール(AWSServiceRoleForConfig)が設定されているか
以前、2点目と3点目のチェックに失敗していると仮定し、1アカウントの全リージョンでAWS Configの記録対象を「すべて」に設定し、IAMロールをサービスリンクロール(AWSServiceRoleForConfig)に一括変更する方法をブログにまとめました。
今回は、全メンバーアカウントの全リージョンを対象に一括変更する方法をまとめました。
前提条件
- 以下の記事を参考に、管理アカウントのLambdaから各メンバーアカウントのリソースを操作するために必要なクロスアカウント用IAMロールを各メンバーアカウントに作成されていること。(IAMロール名は、CrossAccountAdminRole)
- 管理アカウントのLambdaで変更されるアカウント対象は、メンバーアカウントのみです。管理アカウントを変更したい場合、以下の記事を参照に、個別に対応ください
- IAMロールを変更するにあたり、事前に確認すべき点がいくつかあります。以下の記事の「前提条件」の欄をご確認ください。
- 管理アカウントで有効化している全リージョンを対象に、各メンバーアカウントの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}%"
}
}
処理の流れは以下の通りです。
-
メンバーアカウントの取得
- AWS OrganizationsのAPIを使用して、指定したOU(組織単位)に属するすべてのアクティブなメンバーアカウントのIDを取得します。
-
対象リージョンの取得
ec2:DescribeRegions
APIを使用して、管理アカウントにおいて、利用可能なすべてのリージョンを取得します。
-
各アカウントに対する処理の実行
sts:AssumeRole
を使用して、各メンバーアカウントのIAMロール(CrossAccountAdminRole
)を引き受け、操作権限を取得します。- 取得したセッションを使用して、各リージョンで以下の処理を実行します。
- サービスリンクロールの確認・作成
AWSServiceRoleForConfig
が存在しない場合は作成します。
- AWS Configの設定変更
put_configuration_recorder
APIを使用して、記録対象を「すべて」に設定し、IAMロールをAWSServiceRoleForConfig
に変更します。
-
並列処理の活用
ThreadPoolExecutor
を使用して、複数のアカウントやリージョンの処理を並列で実行し、処理時間を短縮します。
-
実行結果の集計とログ出力
- 各アカウント・リージョンごとの成功・失敗状況を記録し、最終的な成功率を計算してログに出力します。
試してみる
Lambdaを実行します。テストイベントに渡す値はありませんので、空にします。
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点を満たしていることになります。
- サービスリンクロールであるAWSServiceRoleForConfigをConfigレコーダーのIAMロールとして指定する
- 東京リージョンはグローバルリソース (IAM リソースなど) を含め全てのリソースタイプを記録
- 東京リージョン以外のリージョンは、グローバルリソースを除く全てのリソースタイプを記録
設定が正しいか確認する
管理アカウントのConfigアグリゲータから、AWS Security Hubのコントロール[Config.1]が成功するように、各アカウントのConfigレコーダー設定が上記の3点を満たしているかを確認します。
設定方法は、以下の記事をご参照ください。