AWS infrastructure changes notifications with Slack
CloudTrail is a powerful service that helps audit the actions of users and roles in your AWS account. It can be integrated with other services to improve the way you react to infrastructure changes. This is especially useful for large projects.
In this article, we’ll describe both a basic and an improved way to track infrastructure changes in your AWS account and send notifications to Slack.
Basic Implementation
For the basic setup, you’ll need four components:
- CloudTrail trail
- EventBridge rule
- SNS topic
- Amazon Q (formerly Chatbot)
Step 1: Configure Amazon Q (Chatbot)
Set up Chatbot with your Slack Workspace ID and the channel where you want notifications delivered. Configure client. In this example we use Slack. Choose it from drop-down menu and click “Configure client”, you’ll be redirected to Slack’s authorization page to request permission for Amazon Q to access your Slack workspace.
Define configuration name and use Channel ID from your Slack (can be easily found in Slack messages links). Use Channel role that can be created here or use existing with appropriate permissions.
Don’t forget to add the Amazon Q app to your Slack channel integrations so it can post messages.
Step 2: Create SNS topic
Create Standard topic, other configs are optional.
When topic created, add new subscription for it. Use HTTPS protocol and paste https://global.sns-api.chatbot.amazonaws.com endpoint field.
Step 3: Create a CloudTrail trail
Start by creating a trail:
Select options you need here, no requirements at this point:
To reduce costs, you can configure it to log only management events, which will be sufficient for auditing infrastructure changes:
Step 4: Create an EventBridge Rule
Next, set up an EventBridge rule. No matter to choose default or custom bus, it will work in both:
Define an event pattern that matches the API calls you want to track across the services of interest. Since CloudTrail logs API calls, you need to explicitly list the actions you want:
Example event pattern:
{
"detail": {
"eventName": [
"AttachGroupPolicy", "CreateGroup", "DeleteGroup", "DetachGroupPolicy", "ModifyUserGroup",
"CreatePolicy", "CreatePolicyVersion", "DeletePolicy", "DeletePolicyVersion", "AttachRolePolicy",
"CreateRole", "DeleteRole", "DeleteRolePolicy", "DetachRolePolicy", "ModifyClusterIamRoles",
"UpdateRole", "UpdateAssumeRolePolicy", "AttachUserPolicy", "CreateUser", "DeleteUser",
"DetachUserPolicy", "ModifyUser", "AttachInternetGateway", "DeleteInternetGateway",
"DetachInternetGateway", "CreateNetworkAcl", "CreateNetworkAclEntry", "DeleteNetworkAcl",
"DeleteNetworkAclEntry", "ReplaceNetworkAclAssociation", "ReplaceNetworkAclEntry", "ConsoleLogin"
],
"eventSource": ["iam.amazonaws.com", "ec2.amazonaws.com", "signin.amazonaws.com"]
},
"source": ["aws.iam", "aws.ec2", "aws.signin"]
}
EventBridge cannot target Chatbot directly. Instead, configure the rule to publish events to an SNS topic. Choose topic created in Step 2 from drop-down menu:
Voilà! After setup, you’ll receive event details in Slack along with a link to the event. You can also customize the message structure — see the documentation .
Improved Implementation
We found two areas for improvement:
- Message structure – The default Slack messages contain redundant details and lack some useful ones.
- Multi-account management – Large organizations often have many AWS accounts, making it hard to manage multiple CloudTrail trails.
Step 1: Add application to Slack
Create Slack app and generate incoming webhook. Follow official doc to achieve it. This webhook will be used in next step.
Step 2: Replace Chatbot with a Lambda
At the time of implementation, many needed parameters for message customization were not yet available in Chatbot. So, instead of using Amazon Q, we switched to a Python Lambda function that parses and formats events before posting to Slack. Lambda function overview you can find in this article.
With Lambda, you can fully customize the message to be posted in Slack based on the specific event that occurred.
Event JSON contains only account ID, if you want to use account alias, it can be retrieved with API call only from account itself or Organization Management account. Workaround described in Lambda Function Overview
Step 3: Use an Organizational Trail
With AWS Organizations, you can create a single Organizational Trail in the management account. This ensures all member accounts automatically inherit a similar trail.
If you migrate from individual trails to an organizational trail, don’t forget to delete old trails. Otherwise, costs may increase if you’re logging data, network, or other events. Events won’t be duplicated, but billing may double.
Lambda Function Overview
The Lambda function handles three main tasks:
- Read environment variables (account map, safe IPs, Slack webhook).
- Parse CloudTrail events into human-readable summaries.
- Send formatted messages to Slack.
Example: Parsing Events
def parse_event(event):
"""Parse details of event"""
# Map account IDs to names (not available in member accounts)
raw_accounts = os.environ['ACCOUNTS']
accounts = {k: v for k, v in (item.split('=') for item in raw_accounts.split(','))}
result = ""
event_name = "UNKNOWN EVENT"
event_detail = event.get('detail')
role = event_detail.get('userIdentity', {}).get('sessionContext', {}).get('sessionIssuer', {}).get('arn', '')
source_ip = event_detail.get('sourceIPAddress', '')
gp_ips = os.environ['GP_IPS']
gp_ips_list = gp_ips.split(", ")
account_id = event_detail.get('userIdentity').get('accountId')
account_name = accounts.get(account_id, account_id)
Example: Handling Console Login Events
if event_detail:
event_name = event_detail.get('eventName')
try:
match event_name:
case "ConsoleLogin" if login_type == "AssumedRole" and login_status == "Success" and source_ip not in gp_ips_list:
result = (
f"🔓 *Console login attempt from IP:* {event_detail.get('sourceIPAddress')}\n\n"
f"*Account:* {account_name}\n"
f"*Status:* ✅ {login_status}\n"
f"*Role:*\n{event_detail.get('userIdentity').get('arn')}\n"
f"*User Agent:* {event_detail.get('userAgent')}\n"
f"*Event ID:* {event_detail.get('eventID')}"
)
This logic can be extended for all event types defined in your EventBridge rule.
Sending Messages to Slack
def send_slack_message(payload, webhook):
headers = {'Content-Type': 'application/json'}
return requests.post(webhook, data=json.dumps(payload), headers=headers)
Lambda Handler
def lambda_handler(event, context):
try:
slack_msg = parse_event(event)
send_slack_message({"text": slack_msg}, os.environ['SLACK_WEBHOOK_URL'])
Deployment Notes:
- You don’t need to deploy the Lambda in every account.
- Deploy it once and configure EventBridge rules in other accounts to invoke it.
- To enable cross-account invocation, create an IAM role in each account with the right permissions. This role can then assume the Lambda-execution role in the central account.
Conclusion
With this setup:
- Basic implementation gives you fast, out-of-the-box notifications via CloudTrail + EventBridge + SNS + Chatbot.
- Improved implementation provides cleaner messages and better multi-account support using Lambda and Organizational Trails.
Either way, you’ll receive actionable Slack messages whenever key infrastructure changes occur in your AWS accounts.
