[小ネタ]Organizations組織内のCloudTrailの重複証跡を一括で無効化するスクリプト
お疲れさまです。とーちです。
CloudTrailでは2つめの証跡に対して課金が発生することをご存知でしょうか?特にControlTower環境では意図せず2つ目の証跡を設定してしまっているケースは多いのではないかと思います。以下の記事でも詳しく解説されていますが、知らないうちに余分なコストが発生している可能性があります。
今回は、ControlTowerやOrganizations環境を想定し、各アカウントで証跡が2つ設定されていないかどうかを確認し、設定されている場合は指定した証跡名の記録を停止するというスクリプトを作ってみましたのでその内容を紹介します。
想定ケース
今回のスクリプトが対応する想定ケースは以下の通りです。
- OrganizationsあるいはControlTowerを使用しており、組織内の全アカウント、全リージョンで2つ目の証跡が設定されていないかをチェックしたい
- CloudTrail証跡を停止する条件としては、以下とする
- 記録を実施しているCloudTrail証跡が指定されたリージョンに2つ以上存在していること(証跡が1つしかないならそれは必要な証跡と見なす)、かつ
- 「組織の証跡」ではないこと
スクリプト
それでは早速ですが、スクリプトです。このスクリプトは管理アカウントにAdministratorAccess権限を持つIAMでログインしCloudShellで実行することを想定しています。
#!/bin/bash
# 設定パラメータ
PRIMARY_ROLE_NAME="AWSControlTowerExecution" # 最初に試みるロール名
FALLBACK_ROLE_NAME="OrganizationAccountAccessRole" # フォールバックのロール名
MIN_ACTIVE_TRAILS=2 # 最低限必要な有効な証跡の数
EXCLUDE_ACCOUNTS=() # 除外するアカウントID(例: "111122223333" "444455556666")
DRY_RUN=true # trueの場合、実際に無効化は行わない
# 色付きの出力用の関数
print_info() {
echo -e "\033[1;34m[INFO] $1\033[0m"
}
print_success() {
echo -e "\033[1;32m[SUCCESS] $1\033[0m"
}
print_error() {
echo -e "\033[1;31m[ERROR] $1\033[0m"
}
print_warning() {
echo -e "\033[1;33m[WARNING] $1\033[0m"
}
# CloudShellのデフォルト認証情報を保存
ORIGINAL_AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID
ORIGINAL_AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY
ORIGINAL_AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN
# デフォルト認証情報を復元する関数
restore_default_credentials() {
export AWS_ACCESS_KEY_ID=$ORIGINAL_AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY=$ORIGINAL_AWS_SECRET_ACCESS_KEY
export AWS_SESSION_TOKEN=$ORIGINAL_AWS_SESSION_TOKEN
}
# 前提条件チェック - CloudShellの認証情報を確認
if ! aws sts get-caller-identity &> /dev/null; then
print_error "AWSにアクセスできません。CloudShellの認証情報を確認してください。"
exit 1
fi
# 管理アカウントのIDを取得
print_info "管理アカウントのIDを取得しています..."
MANAGEMENT_ACCOUNT_ID=$(aws sts get-caller-identity \
--query 'Account' \
--output text)
print_info "管理アカウントID: $MANAGEMENT_ACCOUNT_ID"
# Organizations管理アカウントからすべてのアカウントIDを取得
print_info "AWS Organizationsからアカウントリストを取得しています..."
ACCOUNT_IDS=$(aws organizations list-accounts \
--query 'Accounts[?Status==`ACTIVE`].Id' \
--output text)
if [ -z "$ACCOUNT_IDS" ]; then
print_error "アカウントリストの取得に失敗しました。Organizations管理アカウントの権限を確認してください。"
exit 1
fi
# アカウント数をカウント
account_count=$(echo "$ACCOUNT_IDS" | wc -w)
print_info "$account_count 個のアクティブなアカウントが見つかりました"
print_info "全リージョンで不要なCloudTrail証跡の無効化処理を開始します..."
print_info "条件: 有効な証跡が $MIN_ACTIVE_TRAILS つ以上存在し、組織の証跡でない場合のみ無効化します。"
read -p "続行しますか?(y/n): " confirm
if [[ $confirm != [yY] ]]; then
print_info "処理を中止しました"
exit 0
fi
# 結果サマリー用の変数
successful_accounts=()
failed_accounts=()
skipped_accounts=()
excluded_accounts=()
# 指定されたリージョンでCloudTrail証跡を処理する関数
process_region() {
local account_id=$1
local region=$2
print_info "アカウント $account_id のリージョン $region を処理中..."
# 全ての証跡を取得
local all_trails=$(aws cloudtrail describe-trails \
--region $region \
--query 'trailList[]' \
--output json 2>/dev/null)
if [ -z "$all_trails" ] || [ "$all_trails" == "[]" ]; then
print_warning "アカウント $account_id の $region リージョンに証跡が見つかりませんでした"
return 0
fi
# 有効な証跡の数をカウント
local active_trail_count=0
local non_org_trails=0
local trails_to_process=()
# 各証跡の情報を取得
while read -r trail_info; do
if [ -z "$trail_info" ]; then
continue
fi
local trail_name=$(echo "$trail_info" | jq -r '.Name')
local trail_arn=$(echo "$trail_info" | jq -r '.TrailARN')
local is_org_trail=$(echo "$trail_info" | jq -r '.IsOrganizationTrail')
local is_multi_region=$(echo "$trail_info" | jq -r '.IsMultiRegionTrail')
local home_region=$(echo "$trail_info" | jq -r '.HomeRegion')
# 証跡が有効かどうかを確認
local logging_status=$(aws cloudtrail get-trail-status \
--name "$trail_arn" \
--region $region \
--query 'IsLogging' \
--output text 2>/dev/null)
if [ "$logging_status" == "True" ]; then
((active_trail_count++))
# 組織の証跡でないかを確認
if [ "$is_org_trail" == "false" ]; then
((non_org_trails++))
# マルチリージョン証跡の場合、ホームリージョンでのみ処理
if [ "$is_multi_region" == "true" ] && [ "$region" != "$home_region" ]; then
print_info "アカウント $account_id のマルチリージョン証跡 $trail_name はホームリージョン $home_region で処理します"
skipped_accounts+=("$account_id:$region:$trail_name (マルチリージョン証跡、ホームリージョン $home_region で処理)")
continue
fi
# 処理対象の証跡として追加
trails_to_process+=("$trail_arn|$trail_name")
fi
fi
done < <(echo "$all_trails" | jq -c '.[]' 2>/dev/null)
print_info "アカウント $account_id のリージョン $region には $active_trail_count 個の有効な証跡があります"
print_info "そのうち組織の証跡でないものは $non_org_trails 個です"
# 有効な証跡が最低数以上あり、かつ組織の証跡でないものが存在する場合
if [ $active_trail_count -ge $MIN_ACTIVE_TRAILS ] && [ ${#trails_to_process[@]} -gt 0 ]; then
for trail_info in "${trails_to_process[@]}"; do
IFS='|' read -r trail_arn trail_name <<< "$trail_info"
print_info "アカウント $account_id のリージョン $region で証跡 $trail_name を無効化しています..."
if [ "$DRY_RUN" == "true" ]; then
print_warning "DRY_RUNモードでは実際には無効化しません"
else
aws cloudtrail stop-logging \
--name "$trail_arn" \
--region $region
fi
if [ $? -eq 0 ]; then
print_success "アカウント $account_id のリージョン $region の証跡 $trail_name のログ記録を停止しました"
successful_accounts+=("$account_id:$region:$trail_name (無効化成功)")
else
print_error "アカウント $account_id のリージョン $region の証跡 $trail_name のログ記録停止に失敗しました"
failed_accounts+=("$account_id:$region:$trail_name (無効化失敗)")
fi
done
else
if [ $active_trail_count -lt $MIN_ACTIVE_TRAILS ]; then
print_info "アカウント $account_id のリージョン $region の有効な証跡数が $MIN_ACTIVE_TRAILS 未満のため、処理をスキップします"
skipped_accounts+=("$account_id:$region (有効な証跡が $active_trail_count 個しかない)")
elif [ $non_org_trails -eq 0 ]; then
print_info "アカウント $account_id のリージョン $region には組織の証跡でないものがないため、処理をスキップします"
skipped_accounts+=("$account_id:$region (組織の証跡のみ)")
fi
fi
return 0
}
# 指定されたアカウントでCloudTrail証跡を処理する関数
process_account() {
local account_id=$1
local use_default_credentials=$2 # デフォルト認証情報を使用するかどうか
print_info "アカウント $account_id の処理を開始..."
# デフォルト認証情報を復元
restore_default_credentials
if [ "$use_default_credentials" == "true" ]; then
print_info "管理アカウントのため、CloudShellのデフォルト認証情報を使用します"
else
# 一時的な認証情報を取得(まずはプライマリロールを試す)
local role_name=$PRIMARY_ROLE_NAME
local role_arn="arn:aws:iam::${account_id}:role/${role_name}"
local session_name="cloudtrail-disable-session-$(date +%s)"
print_info "ロール $role_name の引き受けを試みています..."
local credentials=$(aws sts assume-role \
--role-arn "$role_arn" \
--role-session-name "$session_name" \
--query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' \
--output text 2>/dev/null)
# プライマリロールが失敗した場合、フォールバックロールを試す
if [ $? -ne 0 ]; then
print_warning "ロール $role_name の引き受けに失敗しました。フォールバックロール $FALLBACK_ROLE_NAME を試します..."
role_name=$FALLBACK_ROLE_NAME
role_arn="arn:aws:iam::${account_id}:role/${role_name}"
credentials=$(aws sts assume-role \
--role-arn "$role_arn" \
--role-session-name "$session_name" \
--query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' \
--output text 2>/dev/null)
if [ $? -ne 0 ]; then
print_error "アカウント $account_id のどのロールも引き受けできませんでした"
failed_accounts+=("$account_id (ロール引き受け失敗)")
return 1
fi
fi
# 認証情報を環境変数に設定
if [ -n "$credentials" ]; then
read -r AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN <<< "$credentials"
export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
print_success "ロール $role_name の引き受けに成功しました"
else
print_error "アカウント $account_id の認証情報の取得に失敗しました"
failed_accounts+=("$account_id (認証情報取得失敗)")
return 1
fi
fi
# 利用可能なリージョンを取得
print_info "利用可能なリージョンを取得中..."
local regions=$(aws ec2 describe-regions \
--query 'Regions[].RegionName' \
--output text 2>/dev/null)
if [ -z "$regions" ]; then
print_warning "リージョン情報の取得に失敗しました。"
exit 1
fi
# 各リージョンで処理
for region in $regions; do
process_region "$account_id" "$region"
done
return 0
}
# 各アカウントに対して処理を実行
for account_id in $ACCOUNT_IDS; do
# 除外アカウントのチェック
if [[ " ${EXCLUDE_ACCOUNTS[@]} " =~ " ${account_id} " ]]; then
print_info "アカウント $account_id は除外リストに含まれているためスキップします"
excluded_accounts+=("$account_id (除外リスト)")
continue
fi
# 管理アカウントかどうかをチェック
if [ "$account_id" == "$MANAGEMENT_ACCOUNT_ID" ]; then
# 管理アカウントの場合はCloudShellのデフォルト認証情報を使用
process_account "$account_id" "true"
else
# メンバーアカウントの場合はロールを引き受ける
process_account "$account_id" "false"
fi
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
done
# スクリプト終了時にデフォルト認証情報を復元
restore_default_credentials
# 結果サマリーの表示
echo ""
print_info "===== 処理結果サマリー ====="
echo "処理成功アカウント: ${#successful_accounts[@]}"
for acc in "${successful_accounts[@]}"; do
echo " - $acc"
done
echo ""
echo "処理スキップアカウント: ${#skipped_accounts[@]}"
for acc in "${skipped_accounts[@]}"; do
echo " - $acc"
done
echo ""
echo "除外アカウント: ${#excluded_accounts[@]}"
for acc in "${excluded_accounts[@]}"; do
echo " - $acc"
done
echo ""
echo "処理失敗アカウント: ${#failed_accounts[@]}"
for acc in "${failed_accounts[@]}"; do
echo " - $acc"
done
print_info "全アカウントの処理が完了しました"
処理の全体の流れ
処理の流れとしては以下です。
- Organizations組織内の各アカウントIDを取得(
aws organizations list-accounts
) - 各アカウントIDを引数として
process_account
関数を実行 - process_account関数
- 他アカウントのIAMロール(AWSControlTowerExecutionなど)へのスイッチを実施
- 対象アカウントで有効なリージョンの一覧を出力、各リージョンごとに
process_region
関数を実行
- process_region関数
- そのリージョンに設定されているCloudTrail証跡の一覧を取得し、各証跡に対して以下を実施
aws cloudtrail get-trail-status
で証跡のステータスが有効
であることを確認- 「組織の証跡」ではないことを確認
- マルチリージョン証跡の場合、現在の処理対象リージョンがホームリージョンと一致していることを確認(一致してない場合はスキップして次の証跡を確認)
- 有効な証跡が最低数以上あり、かつ「組織の証跡」でないものが存在する場合、
aws cloudtrail stop-logging
でログ記録を停止する
- そのリージョンに設定されているCloudTrail証跡の一覧を取得し、各証跡に対して以下を実施
- 最後にサマリを出す形になっています
ポイントとなる処理について
スクリプトの中でいくつかポイントとなる処理があるので説明します。
ドライラン フラグについて
DRY_RUN=true # trueの場合、実際に無効化は行わない
スクリプトの頭のほうでDRY_RUNという変数を設定しており、これがtrueの場合は実際には設定が変更されないようにしています。変更する場合はこちらのフラグをtrue以外にしてください。
アカウント認証の処理
if [ "$use_default_credentials" == "true" ]; then
print_info "管理アカウントのため、CloudShellのデフォルト認証情報を使
```bash
if [ "$use_default_credentials" == "true" ]; then
print_info "管理アカウントのため、CloudShellのデフォルト認証情報を使用します"
# CloudShellのデフォルト認証情報を使用するため、特に何もしない
return 0
else
# 一時的な認証情報を取得(まずはプライマリロールを試す)
local role_name=$PRIMARY_ROLE_NAME
local role_arn="arn:aws:iam::${account_id}:role/${role_name}"
local session_name="cloudtrail-disable-session-$(date +%s)"
print_info "ロール $role_name の引き受けを試みています..."
local credentials=$(aws sts assume-role \
--role-arn "$role_arn" \
--role-session-name "$session_name" \
--query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' \
--output text 2>/dev/null)
# プライマリロールが失敗した場合、フォールバックロールを試す
if [ $? -ne 0 ]; then
print_warning "ロール $role_name の引き受けに失敗しました。フォールバックロール $FALLBACK_ROLE_NAME を試します..."
role_name=$FALLBACK_ROLE_NAME
role_arn="arn:aws:iam::${account_id}:role/${role_name}"
credentials=$(aws sts assume-role \
--role-arn "$role_arn" \
--role-session-name "$session_name" \
--query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' \
--output text 2>/dev/null)
if [ $? -ne 0 ]; then
print_error "アカウント $account_id のどのロールも引き受けできませんでした"
return 1
fi
fi
メンバーアカウントへの処理はOrganizationsやControlTowerを有効化した際にデフォルトで作成されるIAMロールを使ってAssume Roleしますが、管理アカウントの場合は元々のIAM権限を使用したいです。そのため、上記のような処理にしています。管理アカウント以外なら、ControlTowerであれば、AWSControlTowerExecutionが存在しているはずですし、ControlTowerに属さないOrganizations配下のアカウントであれば、OrganizationAccountAccessRoleが存在するはずなので、それを使用するようにしています。
aws sts assume-role
で取得した一時アクセスキー等の認証情報は、export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
で環境変数にセットすることで使用しています。
証跡の取得と処理対象かどうかの判断
# 全ての証跡を取得
local all_trails=$(aws cloudtrail describe-trails \
--region $region \
--query 'trailList[]' \
--output json 2>/dev/null)
<中略>
while read -r trail_info; do
if [ -z "$trail_info" ]; then
continue
fi
local trail_name=$(echo "$trail_info" | jq -r '.Name')
local trail_arn=$(echo "$trail_info" | jq -r '.TrailARN')
local is_org_trail=$(echo "$trail_info" | jq -r '.IsOrganizationTrail')
local is_multi_region=$(echo "$trail_info" | jq -r '.IsMultiRegionTrail')
local home_region=$(echo "$trail_info" | jq -r '.HomeRegion')
# 証跡が有効かどうかを確認
local logging_status=$(aws cloudtrail get-trail-status \
--name "$trail_arn" \
--region $region \
--query 'IsLogging' \
--output text 2>/dev/null)
if [ "$logging_status" == "True" ]; then
((active_trail_count++))
# 組織の証跡でないかを確認
if [ "$is_org_trail" == "false" ]; then
((non_org_trails++))
# マルチリージョン証跡の場合、ホームリージョンでのみ処理
if [ "$is_multi_region" == "true" ] && [ "$region" != "$home_region" ]; then
print_info "アカウント $account_id のマルチリージョン証跡 $trail_name はホームリージョン $home_region で処理します"
skipped_accounts+=("$account_id:$region:$trail_name (マルチリージョン証跡、ホームリージョン $home_region で処理)")
continue
fi
# 処理対象の証跡として追加
trails_to_process+=("$trail_arn|$trail_name")
fi
fi
done < <(echo "$all_trails" | jq -c '.[]' 2>/dev/null)
まず aws cloudtrail describe-trails
で指定されたAWSアカウント内かつリージョン内にある証跡を以下のようにリスト形式で出力しています。(読みやすく整形しています)
[
{
"Name": "Test",
"S3BucketName": "test-cloudtrail-XXXX",
"IncludeGlobalServiceEvents": true,
"IsMultiRegionTrail": true,
"HomeRegion": "ap-northeast-1",
"TrailARN": "arn:aws:cloudtrail:ap-northeast-1:XXXX:trail/Test",
"LogFileValidationEnabled": true,
"HasCustomEventSelectors": true,
"HasInsightSelectors": false,
"IsOrganizationTrail": false
},
{
"Name": "aws-controltower-BaselineCloudTrail",
"S3BucketName": "aws-controltower-logs-XXXX-ap-northeast-1",
"S3KeyPrefix": "o-XXXXXXXX",
"SnsTopicName": "arn:aws:sns:ap-northeast-1:XXXX:aws-controltower-AllConfigNotifications",
"SnsTopicARN": "arn:aws:sns:ap-northeast-1:XXXX:aws-controltower-AllConfigNotifications",
"IncludeGlobalServiceEvents": true,
"IsMultiRegionTrail": true,
"HomeRegion": "ap-northeast-1",
"TrailARN": "arn:aws:cloudtrail:ap-northeast-1:XXXX:trail/aws-controltower-BaselineCloudTrail",
"LogFileValidationEnabled": true,
"CloudWatchLogsLogGroupArn": "arn:aws:logs:ap-northeast-1:XXXX:log-group:aws-controltower/CloudTrailLogs:*",
"CloudWatchLogsRoleArn": "arn:aws:iam::XXXX:role/service-role/AWSControlTowerCloudTrailRole",
"HasCustomEventSelectors": false,
"HasInsightSelectors": false,
"IsOrganizationTrail": true
}
]
このリストをWhile文に入力として与えています。jq -c '.[]'
で上記のような配列になっている一つ一つの証跡を1行ごとに分割しています。'.[]'
で要素の展開、 -c
オプションが各要素を1行にまとめるためのものです。
done < <(echo "$all_trails" | jq -c '.[]' 2>/dev/null)
証跡ごとに、while文が実行されます。その中では、aws cloudtrail get-trail-status
で証跡のステータスを確認します。True
の場合はログ記録が有効になっているということです。
# 証跡が有効かどうかを確認
local logging_status=$(aws cloudtrail get-trail-status \
--name "$trail_arn" \
--region $region \
--query 'IsLogging' \
--output text 2>/dev/null)
if [ "$logging_status" == "True" ]; then
続いて、IsOrganizationTrail
項目で 組織の証跡ではない
ことを確認しています。今回はControlTower環境でのCloudTrail証跡の重複検出が目的なので、組織の証跡はそのままにし、それ以外に有効になっている証跡を抽出しようとしています。
local is_org_trail=$(echo "$trail_info" | jq -r '.IsOrganizationTrail')
<中略>
# 組織の証跡でないかを確認
if [ "$is_org_trail" == "false" ]; then
また、マルチリージョン証跡かどうかも IsMultiRegionTrail
項目で見ています。マルチリージョン証跡は、全てのリージョンで存在している形で出てきますが、実際の設定は「ホームリージョン」と呼ばれる1つのリージョンにのみ存在します。このため、証跡を停止するような操作は、ホームリージョンで行う必要があります。
local is_multi_region=$(echo "$trail_info" | jq -r '.IsMultiRegionTrail')
local home_region=$(echo "$trail_info" | jq -r '.HomeRegion')
<中略>
# マルチリージョン証跡の場合、ホームリージョンでのみ処理
if [ "$is_multi_region" == "true" ] && [ "$region" != "$home_region" ]; then
print_info "アカウント $account_id のマルチリージョン証跡 $trail_name はホームリージョン $home_region で処理します"
skipped_accounts+=("$account_id:$region:$trail_name (マルチリージョン証跡、ホームリージョン $home_region で処理)")
continue
fi
最終的に条件に合致する証跡があれば、 trails_to_process
変数にその情報を格納し、後のステップで、ログ記録有効な証跡が2つ以上ある場合に、trails_to_process
に格納されたCloudTrail証跡を aws cloudtrail stop-logging
で無効にしているというわけです。 なお、aws cloudtrail stop-logging
は記録を無効にするだけでCloudTrail証跡自体を削除するものではありませんので、最悪、意図しない証跡が処理対象になってしまった場合でもマネージメントコンソール等から簡単に有効化できます。
動かしてみる
このスクリプトを動かしてみると以下のような出力がされます。
[INFO] 利用可能なリージョンを取得中...
[INFO] アカウント **** のリージョン ap-south-1 を処理中...
[INFO] アカウント **** のリージョン ap-south-1 には 1 個の有効な証跡があります
[INFO] そのうち組織の証跡でないものは 0 個です
[INFO] アカウント **** のリージョン ap-south-1 の有効な証跡数が 2 未満のため、処理をスキップします
[INFO] アカウント **** のリージョン eu-north-1 を処理中...
[INFO] アカウント **** のリージョン eu-north-1 には 1 個の有効な証跡があります
[INFO] そのうち組織の証跡でないものは 0 個です
<中略>
[INFO] アカウント **** のリージョン ap-northeast-1 を処理中...
[INFO] アカウント **** のリージョン ap-northeast-1 には 2 個の有効な証跡があります
[INFO] そのうち組織の証跡でないものは 1 個です
[INFO] アカウント **** のリージョン ap-northeast-1 で証跡 Members を無効化しています...
[SUCCESS] アカウント **** のリージョン ap-northeast-1 の証跡 Members のログ記録を停止しました
実際にCloudTrailを見てみるとちゃんと停止されていました。
まとめ
今回はCloudTrailの重複証跡を検出して無効化するスクリプトを紹介しました。
CloudTrailの2つ目の証跡から発生する余分なコストを削減したい場合は、ぜひ活用してみてください。
以上、とーちでした。