Just show me the code!
As always, if you don’t care about the post I have uploaded the source code on my Github.

An AWS event indicates a change in a service. The following are examples of AWS events:

  • AWS EC2 generates an event when the state of an instance changes from pending to running.
  • AWS EC2 Auto Scaling generates events when it launches or terminates instances.
  • AWS ECS generates events when a new image gets pushed into an ECR Repository.
  • AWS S3 generates events when a bucket gets created or deleted.

And in this post I want to show you how you can notify those AWS events to a Microsoft Teams channel using AWS EventBridge and AWS Lambda.

AWS EventBridge

Amazon EventBridge is a serverless event bus service.

How it works? To put it simply, when EventBridge receives an event, it applies a rule that routes the event to a specific target.

You can have multiple event buses on EventBridge, but events are associated with a bus. Rules are also tied to a single event bus, so they can only be applied to events on that particular bus.
EventBridge has always a default event bus which receives events from AWS services.

Here’s the full list of AWS services that generates events that the EventBridge default bus receives:

Events are represented as JSON objects and they all have a similar structure. Here’s an example of how event generated by ECR looks like:

{
    "version": "0",
    "id": "71c174a2-6a18-8c75-6109-6546a541b54d",
    "detail-type": "ECR Image Action",
    "source": "aws.ecr",
    "account": "935156053038",
    "time": "2022-10-10T13:11:49Z",
    "region": "eu-west-1",
    "resources": [],
    "detail": {
        "result": "SUCCESS",
        "repository-name": "demo",
        "image-digest": "sha256:93648920bded8c34263deb2063b52c99a847c2af5e25c8a979b17f8aa66a4564",
        "action-type": "PUSH",
        "image-tag": "1.1.0"
    }
}

The contents of the “detail” top-level field are different depending on which service generated the event and what the event is.
The combination of the “source” and “detail-type” fields serves to identify the fields and values found in the “detail” field.

Architecture Diagram

The setup to notify an AWS event to Microsoft Teams using AWS EventBridge and Lambda is really simple:

notify-aws-event-to-teams-diagram

  • An AWS EventBridge Rule matches a specific set of AWS events and sends them to a target.
  • The target is a Lambda function that sends the event to a Microsoft Teams Channel using an Incoming HTTP WebHook.

Setting up an AWS EventBridge Rule

First step is to decide which AWS events we want to listen to on our EventBridge Rule.

In this post I’ll be listening to ECR PUSH events, so every time a new container image or image tag gets pushed to an ECR repository a notification pops on my Teams channel.

The next code snippet shows how to create an EventBridge Rule that listens to ECR Push Events. For creating the infrastructure I’m using AWS CDK with .NET.

var bus = EventBus.FromEventBusName(this,
    "default-event-bus",
    "default");

var rule = new Rule(this, 
    "rule-notify-ecr-push-img", 
    new RuleProps
{
    EventBus = bus,
    Description = "Sent a teams notification via lambda when an ECR push event is generated.",
    RuleName = "rule-ecr-push-image-teams-notification",
    Enabled = true,
    EventPattern = new EventPattern
    {
        Source = new []{"aws.ecr"},
        DetailType = new []{"ECR Image Action"},
        Detail = new Dictionary<string, object>
        {
            {"action-type", new[]{"PUSH"} },
            {"result", new[]{"SUCCESS"} },
        }
    }
});

You can have multiple event buses on EventBridge but AWS events are only received on the default event bus, that’s why I’m retrieving the default bus with the EventBus.FromEventBusName command and using it later when creating the Rule.

The most important part when configuring a Rule is the EventPattern block.

  • Source: Identifies the service that sourced the event.
  • DetailType: The type of event.
  • Detail: A JSON object, whose content is at the discretion of the service originating the event.

The easiest way of knowing which values to use when declaring the DetailType or the Detail attributes is to use the AWS Portal. An EventPattern builder is availaible on the AWS Portal when you try to create an EventBridge Rule.

aws-eventbridge-eventpattern-builder-portal

If you’re using IaC to create those EventBridge Rules, the easiest way to know what values to use for the EventPattern attribute is using first the AWS Portal to create the EventPattern expression and then copy it into your IaC files.

Building the Lambda function Target

A target is a resource or endpoint that EventBridge sends an event to when the event matches the event pattern defined for a rule.

The Lambda function we have to build only needs to do one thing:

  • Get the event sent by the EventBridge Rule and send it to Teams using an HTTP WebHook.

For building the lambda instead of .NET I’ll be using Python. For this kind of functions, Python is a better and cheaper option.

import json
import os
import logging
import urllib.request
from urllib.error import URLError, HTTPError

logger = logging.getLogger()
logger.setLevel(logging.DEBUG)

def lambda_handler(event, context):   
    try:       
        data = json.dumps(event)
        msg = {
            'text': data
        }    
        response = urllib.request.urlopen(urllib.request.Request(os.environ['teams_webhook_uri'], json.dumps(msg).encode('utf-8')))
        response.read()
    except HTTPError as err:
        logger.error(f"Request failed: {err.code} {err.reason}")
    except URLError as err:
        logger.error(f"Server connection failed: {err.reason}")

The source code can be as simple as the above code snippet, just stringify the event and send it to Teams via HTTP WebHook.

Before start using the function, we need to create an Incoming WebHook on Teams.

teams-incoming-webhook

And store the WebHook Uri value someplace where the Lambda can retrieve it at runtime, probably the best place to put is AWS Secret Manager, but to keep this post as simple as possible I decided to store the value in a Lambda Environment Variable named teams_webhook_uri.

If we take a look at how the end result looks on Teams, we’ll see this:

teams-raw-event

Sending the AWS Event directly to Teams is probably not the best option because it is hard to really know what has happened, but we can improve the readability using Adaptative Cards for Microsoft Teams.

Adaptative Cards for Microsoft Teams

Adaptive Cards are a platform-agnostic method of displaying blocks of information without the complexity of customizing CSS or HTML to render them.

Adaptive Cards have a JSON format and when delivered to Microsoft Teams, the JSON is transformed into native UI that automatically adapts its looks.

I’m not going to deepen on how to build Adaptative Cards, if you want to know more about it, you can read it in the following links:

The next code snippet shows the previous lambda function, but now it uses an Adaptative Card.

import json
import os
import logging
import urllib.request
from urllib.error import URLError, HTTPError

logger = logging.getLogger()
logger.setLevel(logging.DEBUG)

def lambda_handler(event, context):   
    try:
        msg = {
                "type": "message",
                "attachments": [
                    {
                        "contentType": "application/vnd.microsoft.card.adaptive",
                        "content": {
                            "type": "AdaptiveCard",
                            "body": [
                            {
                                "type": "TextBlock",
                                "size": "High",
                                "weight": "Bolder",
                                "text": "New ECR image available"
                            },
                            {
                                "type": "FactSet",
                                "facts": [
                                    {
                                        "title": "Image Name:",
                                        "value": f"{event['detail']['repository-name']}"
                                    },
                                    {
                                        "title": "Version Tag:",
                                        "value": f"{event['detail']['image-tag']}"
                                    }
                                ]
                            }
                            ],
                            "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
                            "version": "1.0",
                            "msteams": {
                            "width": "Full"
                            }
                        }
                    }
                ]
            }
        
        response = urllib.request.urlopen(urllib.request.Request(os.environ['teams_webhook_uri'], json.dumps(msg).encode('utf-8')))
        response.read()
    except HTTPError as err:
        logger.info(err)
        logger.error(f"Request failed: {err.code} {err.reason}")
    except URLError as err:
        logger.error(f"Server connection failed: {err.reason}")

The Adaptative Card I have built for the Lambda function is quite simple, you can do more complex and interesting stuff if you want, but nonetheless if we take a look at the end result on Teams:

teams-adaptative-card

Now it has a much better readability.

How to try it out

If you want to take a look at the source code, you can go to my GitHub repository.

To execute it by yourself, this is what you need to know.

Repository content

The repository contains a CDK app that creates 2 EventBridge Rules and 2 Lambda functions:

  • The first rule notifies when a new container image or image tag gets pushed into an ECR repository. This rule triggers a Lambda that sends this event to Teams.
  • The second rule notifies when an S3 Bucket is created or deleted. This rule triggers another Lambda that sends this event to Teams.

How to deploy the CDK app

1 - Create an Incoming Webhook on one of your Microsoft Teams Channels.

teams-incoming-webhook

2 - Deploy the CDK app.

To deploy it, use the command:

  • cdk deploy --profile <profile> --parameters teamsWebHookUri=<incoming-teams-webhook-uri>

Or the command:

  • cdk deploy --parameters teamsWebHookUri=<incoming-teams-webhook-uri>

The CDK app uses the CDK_DEFAULT_ACCOUNT and CDK_DEFAULT_REGION environment variables to specify the account and the region where the infrastructure will be created.

If you hard-code the target account and region on your CDK app, the stack will always be deployed to that specific account and region. To make the stack deployable to a different target, but to determine the target at synthesis time, your stack can use two environment variables provided by the AWS CDK CLI: CDK_DEFAULT_ACCOUNT and CDK_DEFAULT_REGION. These variables are set based on the AWS profile specified using the --profile option, or the default AWS profile if you don’t specify one.

Here’s an example of how to deploy the app:

  • cdk deploy --parameters teamsWebHookUri=https://cponsn.webhook.office.com/webhookb2/845c5df3-e285-4e3b-8a57-35a5543a05da@532ddc14-1479-45c7-b836-efbccb2bf6aa/IncomingWebhook/45a42011bfe54a2091567af10968422 2/a1d89e88-1b21-4da6-a2b1-dfb848d8b956

How to test it

Push a new image into an ECR repository, and take a look at your Teams Channel. teams-adaptative-card

Create an S3 bucket, and take a look at your Teams Channel. s3-bucket-create-adaptative-card

Delete an S3 bucket, and take a look at your Teams Channel. s3-bucket-delete-adaptative-card