Emailing AWS Cloudwatch Charts with Lambda

Published on 29 August 2020

Introduction

Cloudwatch is a fairly essential tool for monitoring your AWS resources. There are plenty of other tools out there that can assist with monitoring, but you will probably need at least a basic understanding of Cloudwatch to get the most out of those. More so, you may want ensure you are getting what you can from Cloudwatch first.

In the office, a recent request was for regular delivery of cloudwatch data, showing information from the past seven days. The easiest way to do this (once), was to do a screengrab, and email that, but that is something that should be automated. No one wants to repeat that kind of thing. I considered a few different ways inclduing screenscraping from a browser, but that means a device running that has access to a console, and was just not ideal, so I looked at the options from boto3 and using Lambda to run it.

The basic steps are to create a Lambda function that will:

  • Grab the image of a chart using supplied JSON data
  • Convert the data into the correct format.
  • Embed it into some HTML code
  • Use this HTML code in your email, sent using AWS SES.

Creating the Lambda function

Create a basic Lambda function using Python 3.x, and then create a IAM role. The IAM role will need permissions to log to Cloudwatch, read from Cloudwatch, and also send using SES. The put together the code to go in your handler. First you need to import your modules, and configure your connection to Cloudwatch and to SES.

    import boto3, base64

    cloudwatch = boto3.client('cloudwatch')
    ses = boto3.client('ses')

    def lambda_handler(event, context):

Getting the Charts

Boto3 includes get_metric_widget_image, which will return an image of a chart, based on the configuration that you provide in JSON format. Originally, I was going to build the JSON code piecemeal, but you can actually select this directly from a chart that you can build in Cloudwatch.

Go to Cloudwatch, build your chart, go to the "Source" tab, and copy the JSON after selecting the "image API" button.

Chart

You can place this in a here string in your Lambda function.

    json_var = """
    {
        "view": "timeSeries",
        "stacked": false,
        "metrics": [
            [ "AWS/EC2", "CPUUtilization", "InstanceId", "i-02a55abee393add9f" ]
        ],
        "width": 1110,
        "height": 217,
        "start": "-PT3H",
        "end": "P0D"
    }
    """

This is the JSON that we will need to pass to get_metric_widget_image.

metric1 = cloudwatch.get_metric_widget_image(
    MetricWidget=json_var
)

Preparing the Image to go in our Mail Message

Excellent! Now we have a the image contained within "metric1", so what do we do with it? We need to get the image contents themselves, convert it to base64, and then decode it back to a string:

image = metric1['MetricWidgetImage']
img64String = base64.b64encode(image).decode('utf-8')

Finally, we need to then place it inside a string that is also contains the HTML code that will end up inside our email later on. We do that using a string and the python "format" command, which will replace the curly braces with what is in img64String.

image1Code = '<img src="data:image/png;base64,{}" alt="img" />'.format(img64String)

So that is the hard part done. Without knowing a lot about HTML emails or converting strings to base64, that was the hard part. The rest of it is a bit easier.

Build the Email

So now we just place the imagecode we built above, into another here string.

    html = """
    <center><h1>AWS Cloudwatch Charts</h1></center>
    <p><b>Instance CPU</b></p>
    {}
    <p><br><br><br><br></p>
    <p>Generated by the EmailCWCharts lambda function.</p>
    """.format(image1Code)

If you are using multiple charts, you can use multiple instances of curly braces in your here string, and seperate the elements with a comma - .format(image1Code, image2Code, image3Code). Once we have used for the format function to put the images and text into our "html", we will use our html here string in the send_email command.

The last piece is to send the email using SES (Simple Email Service). You need to have verified the email address first in SES, so if not done, verify that first.

response = ses.send_email(
        Destination={
            'BccAddresses': [
            ],
            'CcAddresses': [
            ],
            'ToAddresses': [
                'destination@example.com',
            ],
        },
        Message={
            'Body': {
                'Html': {
                    'Charset': 'UTF-8',
                    'Data': html
                },
                'Text': {
                    'Charset': 'UTF-8',
                    'Data': 'This is the message body in text format.',
                },
            },
            'Subject': {
                'Charset': 'UTF-8',
                'Data': 'AWS Charts - MyAccount',
            },
        },
        Source='source@example.com'
    )    
print(response)

Troubleshooting

As always with a Lambda function, start with your Cloudwatch log group. After that, try sending the most basic email possible, and ensure that you have that piece working. You can do that by changing

'Data': html

to something like

'Data': "This is a really basic email". 

That will ensure that SES is configured correctly. Then just run the ses.send_email and see what you get in your inbox. After that, any other issues are probably to do with encoding. This is a good link with examples of what the code for the image should look like. If you see a "b'" at the beginning of your image text, it is still base64 and needs to be decoded back to a string.

Failing that, check your IAM permissions.

comments powered by Disqus