I tried bulk updating EventBridge API destinations under AWS Organizations due to a Microsoft Teams Webhook URL change
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.
- Search for API destinations within the organization using AWS Resource Explorer
- Compile the target ARNs into a CSV file
- Deploy cross-account roles to member accounts using AWS CloudFormation StackSets
- Run a Dry run from AWS CloudShell in the management account
- 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.
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.
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:DescribeApiDestinationevents: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.
In update-api-destination, the API destination name is specified with --name, and the new Webhook URL is specified with --invocation-endpoint.
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.

