Handling AWS Cloudformation Events

In this post, I'm going to share how to handle Cloudformation events. Cloudformation is an AWS service that provides developers and system engineers the ability to define and manage infrastructure as code in the form of templates. When you launch a Cloudformation template, Cloudformation reads it and creates what is called a Stack. A Cloudformation Stack is a group of AWS services that provide some business value. For this post, let us imagine a scenario where there is an application that creates Cloudformation Stacks. Programmatically creating stacks can raise event failures: service names may hit constraints, missing or incorrect parameters are set, service limits reached. The application that launches these templates needs to be aware of the Stacks state and state changes so that it can respond to those events.

Cloudwatch Fail

AWS offers a service called Cloudwatch that provides the ability to run logic for service-based events in your infrastructure. For the most part, it lets us create rule-based event triggers for various services. Unfortunately, when you select the Cloudformation service you will notice that there are no events available to choose fro. That is because Cloudformation does not send events to Cloudwatch. This service will not help us with what we are trying to achieve.

Using NotificationARNs

NotificationARNs is a non-required property array in the cloudformation stack resource. You can assign one or more SNS topics to the Cloudformation by adding them to that property array. Details about that property can be found here. When the Cloudformation Stack changes state during the creation process, Cloudformation will send an event notification to those designated SNS topics. However, before we can start receiving event notifications, we need to create an SNS topic that will relay those events to services for processing.

SNS Topic

The AWS Simple Notification Service or SNS for short is a notification service that coordinates message delivery. Navigate to the AWS SNS service and create an SNS topic for handling Cloudformation event notifications.

Make sure to give it a good descriptive name.

This is important, store the ARN identifier in your notepad. The ARN identifier is going to be important for the stack launching script. It tells Cloudformation where to send cloudformation event notifications.

Lambda + SNS

Now that we have an SNS topic, we need something that will process those events. Launch a Lambda and designate it as a subscriber by giving it an SNS trigger. Navigate to AWS Lambda service and launch a hello world Lambda.
Click on triggers and then in the drop-down options select SNS.

You should be prompted to enter a topic name. Enter the name of the SNS topic you created in the previous step. If you don't see its name, double check and make sure you are in the same region as the SNS topic.

Lambda Code

The SNS service is going to send that Lambda a JSON object. Inside that object is a message property that contains information on our Cloudformation Stack in key=value format separated by line breaks.
Here is a copy of a JSON object.

{
    "Records": [
        {
            "EventSource": "aws:sns",
            "EventVersion": "1.0",
            "EventSubscriptionArn": "arn:aws:sns:us-east-1:171566796811:cfnotify:931eab44-00ee-4020-980f-060b4ea8c08a",
            "Sns": {
                "Type": "Notification",
                "MessageId": "52a1f22d-497a-5024-a4f5-a055a81ebffe",
                "TopicArn": "arn:aws:sns:us-east-1:171566796811:cfnotify",
                "Subject": "AWS CloudFormation Notification",
                "Message": "StackId='arn:aws:cloudformation:us-east-1:171566796811:stack/superstack1/e7867990-c20e-11e7-a212-50d5cad95262'\nTimestamp='2017-11-05T09:51:32.785Z'\nEventId='e78763f0-c20e-11e7-a212-50d5cad95262'\nLogicalResourceId='superstack1'\nNamespace='171566796811'\nPhysicalResourceId='arn:aws:cloudformation:us-east-1:171566796811:stack/superstack1/e7867990-c20e-11e7-a212-50d5cad95262'\nPrincipalId='171566796811'\nResourceProperties='null'\nResourceStatus='CREATE_IN_PROGRESS'\nResourceStatusReason='User Initiated'\nResourceType='AWS::CloudFormation::Stack'\nStackName='superstack1'\nClientRequestToken='a8757'\n",
                "Timestamp": "2017-11-05T09:51:32.956Z",
                "SignatureVersion": "1",
                "Signature": "TLXNR3kezTbidyUjls8pRqc/ahjtnKFr0y0GcZUFg6HxcpBA+5ebsB8n3OYEk4MJ4dBGcqFCSHPcyzpup65AlCsZbFGlTXVITImA+dbS5ORBvhLoyPK9EVAMedFq5Zcz7VuBkPjMTtlv657GZwd05prMRrfvJGr454lkOAgRIVNZ3NaGRgMEcVfTptog560Aj0vUx5MZ01vhh5qfs//oe/RORkwzLzucudu+/xAbOWxIxev9RsTK+Zy1HifwFkrSS7o2y0aZ7JwiiEUstUyU4zKTO4mzLEc5tZXjmA+YgioUH9uAduULCLo0bOWtHtz/tFwtoaPYvh+QRTn6mb1wbg==",
                "SigningCertUrl": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-433026a4050d206028891664da859041.pem",
                "UnsubscribeUrl": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:171566796811:cfnotify:931eab44-00ee-4020-980f-060b4ea8c08a",
                "MessageAttributes": {}
            }
        }
    ]
}

After the Lambda parses the message property and extracts those parameters, we can either send the information to our application in an HTTP request or store it in a database. Here is an example of a Lambda extracting Cloudformation parameters from the SNS message property and putting them into an object and outputting them to the log. This example does not have any external dependencies and should work fine if its copied and pasted to your Lambda.

'use strict';

console.log('Loading function');



exports.handler = (event, context, callback) => {

    console.log(JSON.stringify(event))

    if (event.Records[0]['Sns'] && event.Records[0]['Sns'].Subject === "AWS CloudFormation Notification") { //// we need to make sure the event is coming from cloudformation

        //// The SNS message we get from cloudformation is line seperated key=value format
        var information = event.Records[0]['Sns'].Message.split("\n").map(function(line) {
            if (line.indexOf('=') === -1) {
                return false;
            }
            var [key, value] = line.split("=");
            return {
                [key]: value
            };

        });

        console.log(information['ResourceStatus']) ///CREATE_IN_PROGRESS
        console.log(information['StackName']) ///superstack1
        console.log(information['LogicalResourceId']) ///S3Bucket

        //// Add your pinging logic here. 
        //// This can be an http request or database call

    }

    callback(null, event);
};

Cloudformation Create Stack Code

Remember the SNS ARN identifier I asked you to store in a notepad earlier? This is when it becomes important. We need to update our AWS SDK createStack parameters in our code to include the NotificationARNs property. First things first, here is the Cloudformation template that my code is launching. It creates an S3 bucket ready for hosting static websites. I'm using this for demonstration purposes and it can be found on the AWS Cloudformation examples page.

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "AWS CloudFormation Sample Template S3_Website_Bucket_With_Retain_On_Delete: Sample template showing how to create a publicly accessible S3 bucket configured for website access with a deletion policy of retail on delete. **WARNING** This template creates an S3 bucket that will NOT be deleted when the stack is deleted. You will be billed for the AWS resources used if you create a stack from this template.",
    "Resources": {
        "S3Bucket": {
            "Type": "AWS::S3::Bucket",
            "Properties": {
                "AccessControl": "PublicRead",
                "WebsiteConfiguration": {
                    "IndexDocument": "index.html",
                    "ErrorDocument": "error.html"
                }
            },
            "DeletionPolicy": "Retain",
            "Metadata": {
                "AWS::CloudFormation::Designer": {
                    "id": "de85f280-7bb0-46d9-ae59-4f8378c74604"
                }
            }
        }
    },
    "Outputs": {
        "WebsiteURL": {
            "Value": {
                "Fn::GetAtt": [
                    "S3Bucket",
                    "WebsiteURL"
                ]
            },
            "Description": "URL for website hosted on S3"
        },
        "S3BucketSecureURL": {
            "Value": {
                "Fn::Join": [
                    "",
                    [
                        "https://",
                        {
                            "Fn::GetAtt": [
                                "S3Bucket",
                                "DomainName"
                            ]
                        }
                    ]
                ]
            },
            "Description": "Name of S3 bucket to hold website content"
        }
    }
};

Your Cloudformation template may be different and that is fine. We are not making any modifications to the template itself. We are only focusing on the code that launches it. Here is the code that launches my Cloudformation template.

/// initialize the cloudformation class
var cloudformation = new AWS.CloudFormation({  
    region: 'us-east-1',
    apiVersion: '2010-05-15',
    secretAccessKey: 'YOUR SECRET ACCESS KEY',
    accessKeyId: 'YOUR ACCESS KEY ID'
});


/// my cloudformation template
var stack = {  
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "AWS CloudFormation Sample Template S3_Website_Bucket_With_Retain_On_Delete: Sample template showing how to create a publicly accessible S3 bucket configured for website access with a deletion policy of retail on delete. **WARNING** This template creates an S3 bucket that will NOT be deleted when the stack is deleted. You will be billed for the AWS resources used if you create a stack from this template.",
    "Resources": {
        "S3Bucket": {
            "Type": "AWS::S3::Bucket",
            "Properties": {
                "AccessControl": "PublicRead",
                "WebsiteConfiguration": {
                    "IndexDocument": "index.html",
                    "ErrorDocument": "error.html"
                }
            },
            "DeletionPolicy": "Retain",
            "Metadata": {
                "AWS::CloudFormation::Designer": {
                    "id": "de85f280-7bb0-46d9-ae59-4f8378c74604"
                }
            }
        }
    },
    "Outputs": {
        "WebsiteURL": {
            "Value": {
                "Fn::GetAtt": [
                    "S3Bucket",
                    "WebsiteURL"
                ]
            },
            "Description": "URL for website hosted on S3"
        },
        "S3BucketSecureURL": {
            "Value": {
                "Fn::Join": [
                    "", [
                        "https://",
                        {
                            "Fn::GetAtt": [
                                "S3Bucket",
                                "DomainName"
                            ]
                        }
                    ]
                ]
            },
            "Description": "Name of S3 bucket to hold website content"
        }
    }
};

/// create stack parameters
var params = {  
    StackName: 'superstack1', //// stack name
    ClientRequestToken: `a${Math.floor(Math.random(10).toString()*10000)}`,
    OnFailure: 'DELETE',
    NotificationARNs: [
        'arn:aws:sns:us-east-1:171566796811:cfnotify' //// ARN Identifier
    ],
    Capabilities: ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"],
    TemplateBody: JSON.stringify(stack)
};

/// cloudformation createStack call
cloudformation.createStack(params, function(err, data) {  
    if (err) console.log(err, err.stack); // an error occurred
    else console.log(data); // successful response
});

Notice how I added the NotificationARNs property and included my SNS ARN identifier as its value. Now, if you navigate to your Lambda Cloudwatch logs you should see Cloudformation events logged by the Lambda.
You can modify the Lambda to run business logic for any of the Cloudformation events that are coming through. If you get lost along the way, feel free to shoot me a tweet!
@notmilojda

Happy Hacking!