Microsoft Teams の Webhook URL 変更に伴い AWS Organizations 配下の EventBridge API 送信先を一括更新してみた

Microsoft Teams の Webhook URL 変更に伴い AWS Organizations 配下の EventBridge API 送信先を一括更新してみた

複数のAWSアカウントに存在するEventBridge API送信先のWebhook URLを一括更新したので、Resource ExplorerとStackSetsを活用した実装方法を紹介します。
2026.08.27

はじめに

複数の AWS アカウントで、Amazon EventBridge の API 送信先から Microsoft Teams へ通知していました。

今回、Teams 側の Webhook URL を再発行したため、各 AWS アカウントに存在する EventBridge API 送信先のエンドポイントを変更する必要がありました。

アカウントごとにログインして手動で変更することもできますが、対象アカウントが多い場合は手間がかかります。

そこで今回は、以下の流れで EventBridge API 送信先を一括更新しました。

  1. AWS Resource Explorer で組織内の API 送信先を検索する
  2. 更新対象の ARN を CSV ファイルにまとめる
  3. AWS CloudFormation StackSets でメンバーアカウントにクロスアカウントロールを展開する
  4. 管理アカウントの AWS CloudShell から Dry run を実行する
  5. Dry run の結果を確認後、Webhook URL を一括更新する

今回の検証では、管理アカウントと2つのメンバーアカウントに存在する、合計6つの API 送信先を更新できることを確認しました。

検証構成

検証には以下の3アカウントを使用しました。

アカウント ID は例示です。

管理アカウント:111111111111
メンバーアカウント1:222222222222
メンバーアカウント2:333333333333

各アカウントには、次の2種類の API 送信先が存在する想定です。

管理アカウント
├─ management-teams
└─ ccoe-security-alert-teams

メンバーアカウント1
├─ sandbox1-teams
└─ ccoe-security-alert-teams

メンバーアカウント2
├─ sandbox2-teams
└─ ccoe-security-alert-teams

API 送信先名に応じて、変更する Webhook URL を振り分けます。

API 送信先名 通知先
ccoe-security-alert-teams CCoE 用 Teams チャネル
その他の *-teams アカウント担当者用 Teams チャネル

検証リージョンは ap-northeast-1 です。

実際の Webhook URL には認証情報に相当する値が含まれるため、記事中では以下のプレースホルダーに置き換えています。

<CCOE_WEBHOOK_URL>
<ACCOUNT_WEBHOOK_URL>

全体の流れ

今回の処理の流れは以下です。

Resource ExplorerでAPI送信先を検索
↓
更新対象のARNをCSVファイルにまとめる
↓
StackSetsでメンバーアカウントにIAMロールを展開
↓
管理アカウントのCloudShellにCSVファイルをアップロード
↓
Dry runで変更対象を確認
↓
Webhook URLを更新
↓
更新後のWebhook URLを再取得して確認

管理アカウント内の API 送信先は、CloudShell の現在の認証情報で操作します。

メンバーアカウント内の API 送信先は、対象アカウントに作成した IAM ロールを AssumeRole で引き受けて操作します。

Resource Explorer で対象リソースを確認する

AWS Resource Explorer では、EventBridge API 送信先を以下のリソースタイプで検索できます。

events:api-destination

Resource Explorer の組織全体を検索できるビューから、対象の API 送信先を検索しました。

今回は、検索結果から以下の命名規則に該当する API 送信先を更新対象とします。

ccoe-security-alert-teams
*-teams

Resource Explorer が対応しているリソースタイプは、以下のドキュメントで確認できます。

https://docs.aws.amazon.com/resource-explorer/latest/userguide/supported-resource-types.html

対象となるアカウント ID、リージョン、API 送信先 ARN を別の方法で取得できる場合は、必ずしも Resource Explorer を使う必要はありません。

更新対象の CSV ファイルを作成する

Resource Explorer の検索結果を CSV ファイルとしてエクスポートし、更新対象の ARN のみに整理しました。

今回使用するファイル名は以下です。

teams-api-destination-targets.csv

CSV ファイルの内容は以下の形式です。

ARN
arn:aws:events:ap-northeast-1:111111111111:api-destination/management-teams/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa
arn:aws:events:ap-northeast-1:111111111111:api-destination/ccoe-security-alert-teams/bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb
arn:aws:events:ap-northeast-1:222222222222:api-destination/sandbox1-teams/cccccccc-cccc-4ccc-8ccc-cccccccccccc
arn:aws:events:ap-northeast-1:222222222222:api-destination/ccoe-security-alert-teams/dddddddd-dddd-4ddd-8ddd-dddddddddddd
arn:aws:events:ap-northeast-1:333333333333:api-destination/sandbox2-teams/eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee
arn:aws:events:ap-northeast-1:333333333333:api-destination/ccoe-security-alert-teams/ffffffff-ffff-4fff-8fff-ffffffffffff

スクリプトでは ARN から以下の情報を取得します。

  • AWS アカウント ID
  • リージョン
  • API 送信先名

そのため、これらを別の列として用意する必要はありません。

CSV ファイルを表計算ソフトで編集する場合は、AWS アカウント ID の先頭の 0 が削除されないよう注意してください。

クロスアカウントロールを展開する

管理アカウントからメンバーアカウントの API 送信先を操作するため、各メンバーアカウントにクロスアカウントロールを作成します。

今回は、CloudFormation StackSets のサービスマネージドアクセス許可を利用しました。

サービスマネージドアクセス許可を使用すると、AWS Organizations で管理されているアカウントへ、同じ CloudFormation テンプレートを基にスタックインスタンスを展開できます。

StackSets を使って組織内のアカウントへ IAM ロールを展開する手順は、以下の記事で紹介しています。

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

CloudFormation テンプレート

今回使用したテンプレートは以下です。

ManagementAccountId は、自身の管理アカウント ID に置き換えてください。

AWSTemplateFormatVersion: '2010-09-09'
Description: Create IAM role for updating EventBridge API destinations

Parameters:
  ManagementAccountId:
    Type: String
    Default: '111111111111'
    AllowedPattern: '^\d{12}$'

Resources:
  CrossAccountRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: CrossAccountEventBridgeApiDestinationUpdateRole
      Path: /

      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: AllowAssumeRoleFromManagementAccount
            Effect: Allow
            Principal:
              AWS: !Sub arn:${AWS::Partition}:iam::${ManagementAccountId}:root
            Action:
              - sts:AssumeRole

      Policies:
        - PolicyName: UpdateTeamsApiDestinations
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Sid: DescribeTeamsApiDestinations
                Effect: Allow
                Action:
                  - events:DescribeApiDestination
                Resource:
                  - !Sub arn:${AWS::Partition}:events:ap-northeast-1:${AWS::AccountId}:api-destination/*-teams

              - Sid: UpdateTeamsApiDestinations
                Effect: Allow
                Action:
                  - events:UpdateApiDestination
                Resource: '*'
                Condition:
                  StringEquals:
                    aws:RequestedRegion: ap-northeast-1

Outputs:
  CrossAccountRoleArn:
    Description: ARN of the cross-account role
    Value: !GetAtt CrossAccountRole.Arn

ロールに付与する権限は以下です。

  • events:DescribeApiDestination
  • events:UpdateApiDestination

events:UpdateApiDestination は、aws:RequestedRegion 条件により ap-northeast-1 での操作に限定しています。

また、信頼ポリシーでは、管理アカウントからのみロールを引き受けられるようにしています。

管理アカウント自身には StackSets からスタックインスタンスがデプロイされないため、管理アカウント内の API 送信先は CloudShell の実行権限で更新します。

CSV ファイルを CloudShell へアップロードする

管理アカウントで ap-northeast-1 の AWS CloudShell を開き、以下の CSV ファイルをアップロードします。

teams-api-destination-targets.csv

アップロード後、ファイルが存在することを確認します。

ls -l teams-api-destination-targets.csv

ヘッダーを除いた対象件数も確認します。

tail -n +2 teams-api-destination-targets.csv | grep -c '^arn:'

今回の検証用 CSV では、以下が出力されます。

6

CloudShell から一括更新する

今回はスクリプトファイルを作成せず、以下のコードを CloudShell へ直接貼り付けました。

環境に合わせて変更する項目は以下です。

変数 内容
CSV 更新対象を記載した CSV ファイル
MANAGEMENT_ACCOUNT 管理アカウント ID
ROLE_NAME メンバーアカウントで引き受ける IAM ロール名
CCOE_URL CCoE 用 Teams チャネルの Webhook URL
ACCOUNT_URL アカウント担当者用 Teams チャネルの Webhook URL
APPLY false で Dry run、true で更新
LOG="update-result-$(date +%Y%m%d-%H%M%S).log"

(
set -euo pipefail
export AWS_PAGER=""

CSV="teams-api-destination-targets.csv"
MANAGEMENT_ACCOUNT="111111111111"
ROLE_NAME="CrossAccountEventBridgeApiDestinationUpdateRole"

# false:Dry run、true:実際に更新
APPLY=false

CCOE_URL='<CCOE_WEBHOOK_URL>'
ACCOUNT_URL='<ACCOUNT_WEBHOOK_URL>'

[[ -f "$CSV" ]] || {
  echo "ERROR: $CSV が見つかりません。"
  exit 1
}

CURRENT_ACCOUNT="$(
  aws sts get-caller-identity \
    --query Account \
    --output text
)"

[[ "$CURRENT_ACCOUNT" == "$MANAGEMENT_ACCOUNT" ]] || {
  echo "ERROR: 管理アカウント $MANAGEMENT_ACCOUNT で実行してください。"
  exit 1
}

echo "開始日時 : $(date '+%Y-%m-%d %H:%M:%S')"
echo "実行モード: $([[ "$APPLY" == true ]] && echo APPLY || echo DRY_RUN)"
echo

while IFS= read -r ARN || [[ -n "$ARN" ]]; do
  ARN="${ARN%$'\r'}"
  [[ -z "$ARN" ]] && continue

  REGION="$(cut -d: -f4 <<< "$ARN")"
  ACCOUNT_ID="$(cut -d: -f5 <<< "$ARN")"
  NAME="$(cut -d/ -f2 <<< "$ARN")"

  if [[ "$NAME" == "ccoe-security-alert-teams" ]]; then
    URL="$CCOE_URL"
    TYPE="CCOE"
  elif [[ "$NAME" == *-teams ]]; then
    URL="$ACCOUNT_URL"
    TYPE="ACCOUNT"
  else
    echo "SKIP    $ACCOUNT_ID $NAME(対象外)"
    continue
  fi

  if [[ "$ACCOUNT_ID" == "$MANAGEMENT_ACCOUNT" ]]; then
    run_aws() {
      aws "$@"
    }
  else
    read -r AK SK ST <<< "$(
      aws sts assume-role \
        --role-arn "arn:aws:iam::$ACCOUNT_ID:role/$ROLE_NAME" \
        --role-session-name UpdateTeamsWebhook \
        --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' \
        --output text
    )"

    run_aws() {
      AWS_ACCESS_KEY_ID="$AK" \
      AWS_SECRET_ACCESS_KEY="$SK" \
      AWS_SESSION_TOKEN="$ST" \
      aws "$@"
    }
  fi

  CURRENT_URL="$(
    run_aws events describe-api-destination \
      --region "$REGION" \
      --name "$NAME" \
      --query InvocationEndpoint \
      --output text \
      --no-cli-pager
  )"

  if [[ "$CURRENT_URL" == "$URL" ]]; then
    echo "SKIP    $ACCOUNT_ID $NAME [$TYPE](更新済み)"
  elif [[ "$APPLY" != true ]]; then
    echo "CHANGE  $ACCOUNT_ID $NAME [$TYPE](Dry run)"
  else
    run_aws events update-api-destination \
      --region "$REGION" \
      --name "$NAME" \
      --invocation-endpoint "$URL" \
      --no-cli-pager \
      >/dev/null

    UPDATED_URL="$(
      run_aws events describe-api-destination \
        --region "$REGION" \
        --name "$NAME" \
        --query InvocationEndpoint \
        --output text \
        --no-cli-pager
    )"

    if [[ "$UPDATED_URL" == "$URL" ]]; then
      echo "UPDATED $ACCOUNT_ID $NAME [$TYPE]"
    else
      echo "ERROR   $ACCOUNT_ID $NAME(更新後の確認に失敗)"
      exit 1
    fi
  fi

done < <(tail -n +2 "$CSV")

echo

if [[ "$APPLY" == true ]]; then
  echo "更新処理が完了しました。"
else
  echo "Dry runのため変更していません。"
fi

echo "終了日時 : $(date '+%Y-%m-%d %H:%M:%S')"

) 2>&1 | tee "$LOG"

echo
echo "ログファイル: $LOG"

AWS Security Token Service(AWS STS)の AssumeRole では、一時的なアクセスキー ID、シークレットアクセスキー、セッショントークンを取得できます。スクリプトでは、この一時認証情報を使ってメンバーアカウントの EventBridge API を呼び出します。

https://docs.aws.amazon.com/cli/latest/reference/sts/assume-role.html

update-api-destination では、--name で API 送信先名を指定し、--invocation-endpoint で新しい Webhook URL を指定します。

https://docs.aws.amazon.com/cli/latest/reference/events/update-api-destination.html

スクリプトの処理内容

ARN から必要な情報を取得する

CSV ファイルに記載された ARN から、リージョン、アカウント ID、API 送信先名を取得します。

REGION="$(cut -d: -f4 <<< "$ARN")"
ACCOUNT_ID="$(cut -d: -f5 <<< "$ARN")"
NAME="$(cut -d/ -f2 <<< "$ARN")"

たとえば、以下の ARN を読み込んだ場合を考えます。

arn:aws:events:ap-northeast-1:222222222222:api-destination/sandbox1-teams/<resource-id>

取得結果は以下です。

REGION=ap-northeast-1
ACCOUNT_ID=222222222222
NAME=sandbox1-teams

API 送信先名から Webhook URL を決める

API 送信先名が ccoe-security-alert-teams の場合は、CCoE 用の Webhook URL を使用します。

それ以外の *-teams は、アカウント担当者用の Webhook URL を使用します。

if [[ "$NAME" == "ccoe-security-alert-teams" ]]; then
  URL="$CCOE_URL"
  TYPE="CCOE"
elif [[ "$NAME" == *-teams ]]; then
  URL="$ACCOUNT_URL"
  TYPE="ACCOUNT"
fi

固有名の判定を先に行っているため、ccoe-security-alert-teams が汎用的な *-teams の分岐で処理されることを防いでいます。

管理アカウントとメンバーアカウントで処理を分ける

管理アカウント自身の API 送信先は、CloudShell の現在の認証情報で操作します。

if [[ "$ACCOUNT_ID" == "$MANAGEMENT_ACCOUNT" ]]; then
  run_aws() {
    aws "$@"
  }
fi

メンバーアカウントの場合は、対象アカウントのクロスアカウントロールを引き受けます。

aws sts assume-role \
  --role-arn "arn:aws:iam::$ACCOUNT_ID:role/$ROLE_NAME" \
  --role-session-name UpdateTeamsWebhook

これにより、同じ CSV ファイル内に管理アカウントとメンバーアカウントの ARN が混在していても処理できます。

Dry run を実行する

最初は、以下の設定で実行します。

APPLY=false

Dry run では、現在の Webhook URL と変更先 URL を比較しますが、update-api-destination は実行しません。

実行結果は以下です。

開始日時 : 2026-08-18 02:35:10
実行モード: DRY_RUN

CHANGE  111111111111 management-teams [ACCOUNT](Dry run)
CHANGE  111111111111 ccoe-security-alert-teams [CCOE](Dry run)
CHANGE  222222222222 sandbox1-teams [ACCOUNT](Dry run)
CHANGE  222222222222 ccoe-security-alert-teams [CCOE](Dry run)
CHANGE  333333333333 sandbox2-teams [ACCOUNT](Dry run)
CHANGE  333333333333 ccoe-security-alert-teams [CCOE](Dry run)

Dry runのため変更していません。
終了日時 : 2026-08-18 02:35:16

6つの API 送信先が CHANGE となりました。

この結果から、以下を確認できました。

  • 管理アカウント内の API 送信先を参照できる
  • メンバーアカウントのクロスアカウントロールを引き受けられる
  • メンバーアカウント内の API 送信先を参照できる
  • API 送信先名に応じた Webhook URL の振り分けができる

Webhook URL を更新する

Dry run の結果を確認後、以下へ変更して再実行します。

APPLY=true

実行結果は以下です。

開始日時 : 2026-08-18 02:54:52
実行モード: APPLY

UPDATED 111111111111 management-teams [ACCOUNT]
UPDATED 111111111111 ccoe-security-alert-teams [CCOE]
UPDATED 222222222222 sandbox1-teams [ACCOUNT]
UPDATED 222222222222 ccoe-security-alert-teams [CCOE]
UPDATED 333333333333 sandbox2-teams [ACCOUNT]
UPDATED 333333333333 ccoe-security-alert-teams [CCOE]

更新処理が完了しました。
終了日時 : 2026-08-18 02:55:03

スクリプトでは、更新後に describe-api-destination を実行しています。

取得した InvocationEndpoint が指定した Webhook URL と一致した場合に、UPDATED を出力します。

今回の検証では、管理アカウントと2つのメンバーアカウントに存在する、合計6つの API 送信先を更新できました。

実行結果は以下の形式のログファイルにも保存されます。

update-result-YYYYMMDD-HHMMSS.log

作成されたログファイルは、以下のコマンドで確認できます。

ls -lt update-result-*.log

まとめ

Resource Explorer で確認した API 送信先の ARN を CSV ファイルにまとめ、管理アカウントの CloudShell から、複数アカウントの EventBridge API 送信先を一括更新できました。

管理アカウント自身は CloudShell の実行権限、メンバーアカウントは StackSets で展開したクロスアカウントロールを利用しています。また、Dry run で対象を確認してから、API 送信先名に応じた Webhook URL を設定しました。


そのマルチアカウント運用、気合いで支えていませんか

Organizations や Control Tower で土台は作れても、アカウントもポリシーも増えるほど、運用は「詳しい一人」に寄りかかっていく。属人化が限界を迎える前に、組織として回す仕組み=CCoEへ。5,600社の支援から得た立ち上げの型を、無料資料にまとめました。

CCoE総合支援

組織で回す仕組みの資料をもらう

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事