I tried bulk updating EventBridge API destinations under AWS Organizations due to a Microsoft Teams Webhook URL change

I tried bulk updating EventBridge API destinations under AWS Organizations due to a Microsoft Teams Webhook URL change

I bulk-updated Webhook URLs for EventBridge API destinations that exist across multiple AWS accounts, so I will introduce an implementation method that leverages Resource Explorer and StackSets.
2026.08.27

This page has been translated by machine translation. View original

Introduction

I was sending notifications to Microsoft Teams from Amazon EventBridge API destinations across multiple AWS accounts.

This time, since the Webhook URL on the Teams side was reissued, I needed to change the endpoints of the EventBridge API destinations existing in each AWS account.

It is possible to log in to each account and make changes manually, but this is time-consuming when there are many target accounts.

So this time, I updated the EventBridge API destinations in bulk using the following flow.

  1. Search for API destinations within the organization using AWS Resource Explorer
  2. Compile the target ARNs into a CSV file
  3. Deploy cross-account roles to member accounts using AWS CloudFormation StackSets
  4. Run a Dry run from AWS CloudShell in the management account
  5. After confirming the Dry run results, update the Webhook URLs in bulk

In this verification, I confirmed that a total of six API destinations existing in the management account and two member accounts could be updated.

Verification Configuration

The following three accounts were used for verification.

The account IDs are examples.

Management account:111111111111
Member account 1:222222222222
Member account 2:333333333333

Each account is assumed to have the following two types of API destinations.

Management account
├─ management-teams
└─ ccoe-security-alert-teams

Member account 1
├─ sandbox1-teams
└─ ccoe-security-alert-teams

Member account 2
├─ sandbox2-teams
└─ ccoe-security-alert-teams

The Webhook URL to be changed is assigned according to the API destination name.

API destination name Notification target
ccoe-security-alert-teams Teams channel for CCoE
Other *-teams Teams channel for account owners

The verification region is ap-northeast-1.

Since the actual Webhook URL contains values equivalent to authentication information, it is replaced with the following placeholders in this article.

<CCOE_WEBHOOK_URL>
<ACCOUNT_WEBHOOK_URL>

Overall Flow

The processing flow this time is as follows.

Search for API destinations with Resource Explorer

Compile target ARNs into a CSV file

Deploy IAM roles to member accounts with StackSets

Upload CSV file to CloudShell in the management account

Confirm targets with Dry run

Update Webhook URLs

Re-retrieve and confirm updated Webhook URLs

API destinations within the management account are operated using the current credentials of CloudShell.

API destinations within member accounts are operated by assuming the IAM role created in the target account using AssumeRole.

Check Target Resources with Resource Explorer

In AWS Resource Explorer, you can search for EventBridge API destinations using the following resource type.

events:api-destination

I searched for target API destinations from the view that can search the entire organization in Resource Explorer.

This time, I will target API destinations matching the following naming conventions from the search results for updating.

ccoe-security-alert-teams
*-teams

The resource types supported by Resource Explorer can be confirmed in the following documentation.

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

If you can obtain the target account ID, region, and API destination ARN by other means, you do not necessarily need to use Resource Explorer.

Create the Target CSV File

I exported the Resource Explorer search results as a CSV file and organized it to include only the target ARNs.

The file name used this time is as follows.

teams-api-destination-targets.csv

The contents of the CSV file are in the following format.

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

The script retrieves the following information from the ARN.

  • AWS account ID
  • Region
  • API destination name

Therefore, there is no need to prepare these as separate columns.

When editing the CSV file with spreadsheet software, be careful that the leading 0 of the AWS account ID is not removed.

Deploy Cross-Account Roles

To operate API destinations in member accounts from the management account, create cross-account roles in each member account.

This time, I used service-managed permissions for CloudFormation StackSets.

Using service-managed permissions, you can deploy stack instances based on the same CloudFormation template to accounts managed by AWS Organizations.

The procedure for deploying IAM roles to accounts within the organization using StackSets is introduced in the following article.

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

CloudFormation Template

The template used this time is as follows.

Please replace ManagementAccountId with your own management account 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

The permissions granted to the role are as follows.

  • events:DescribeApiDestination
  • events:UpdateApiDestination

events:UpdateApiDestination is limited to operations in ap-northeast-1 by the aws:RequestedRegion condition.

Also, the trust policy ensures that only the management account can assume the role.

Since no stack instances are deployed to the management account itself from StackSets, API destinations within the management account are updated using the execution permissions of CloudShell.

Upload the CSV File to CloudShell

Open AWS CloudShell for ap-northeast-1 in the management account and upload the following CSV file.

teams-api-destination-targets.csv

After uploading, confirm that the file exists.

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

Also confirm the number of targets excluding the header.

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

For the verification CSV used this time, the following is output.

6

Bulk Update from CloudShell

This time, no script file was created; instead, the following code was pasted directly into CloudShell.

The items to change according to your environment are as follows.

Variable Content
CSV CSV file listing the targets to update
MANAGEMENT_ACCOUNT Management account ID
ROLE_NAME IAM role name to assume in member accounts
CCOE_URL Webhook URL for the CCoE Teams channel
ACCOUNT_URL Webhook URL for the account owner Teams channel
APPLY false for Dry run, true to update
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"

With AWS Security Token Service (AWS STS) AssumeRole, you can obtain temporary access key IDs, secret access keys, and session tokens. In the script, these temporary credentials are used to call the EventBridge API of member accounts.

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

In update-api-destination, the API destination name is specified with --name, and the new Webhook URL is specified with --invocation-endpoint.

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

Script Processing Details

Retrieve Required Information from the ARN

Retrieve the region, account ID, and API destination name from the ARN recorded in the CSV file.

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

For example, consider the case where the following ARN is read.

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

The retrieval results are as follows.

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

Determine the Webhook URL from the API Destination Name

If the API destination name is ccoe-security-alert-teams, the Webhook URL for CCoE is used.

For other *-teams, the Webhook URL for account owners is used.

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

Since the specific name is evaluated first, ccoe-security-alert-teams is prevented from being processed in the general *-teams branch.

Separate Processing for Management Account and Member Accounts

API destinations of the management account itself are operated using the current credentials of CloudShell.

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

For member accounts, the cross-account role of the target account is assumed.

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

This allows processing even when ARNs from both the management account and member accounts are mixed within the same CSV file.

Run Dry Run

Initially, run with the following setting.

APPLY=false

In Dry run, the current Webhook URL and the target URL are compared, but update-api-destination is not executed.

The execution results are as follows.

開始日時 : 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

Six API destinations showed CHANGE.

From these results, the following was confirmed.

  • API destinations within the management account can be referenced
  • The cross-account role of member accounts can be assumed
  • API destinations within member accounts can be referenced
  • Webhook URLs can be assigned according to API destination names

Update Webhook URLs

After confirming the Dry run results, change to the following and re-run.

APPLY=true

The execution results are as follows.

開始日時 : 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

In the script, describe-api-destination is executed after the update.

UPDATED is output when the retrieved InvocationEndpoint matches the specified Webhook URL.

In this verification, a total of six API destinations existing in the management account and two member accounts were successfully updated.

The execution results are also saved in a log file in the following format.

update-result-YYYYMMDD-HHMMSS.log

The created log file can be confirmed with the following command.

ls -lt update-result-*.log

Summary

By compiling the ARNs of API destinations confirmed with Resource Explorer into a CSV file, I was able to update EventBridge API destinations across multiple accounts in bulk from CloudShell in the management account.

The management account itself uses the execution permissions of CloudShell, while member accounts use the cross-account role deployed by StackSets. Additionally, after confirming the targets with Dry run, the Webhook URL corresponding to each API destination name was set.


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

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

CCoE総合支援

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

Share this article

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