chrismitchellonline

Create Cloudwatch Rule with Cloudformation

2020-01-08

In this article we will look at creating a Cloudwatch rule in Amazon Web Services with Cloudformation that will invoke a Lambda function on a timer. We’ll also include custom JSON input to pass to our Lambda function.

Prerequisits: You’ll need an AWS account with a Lambda function that we can call from our Cloudwatch rule.

Cloudwatch Rules

With AWS Cloudwatch we can use rules to invoke our Lambda function. We can have our Cloudwatch rule invoke our Lambda function in two ways, Event Patterns or a Scheduled Events. Event Patterns occur when other AWS services like CodeCommit or EC2 emmit events that Cloudwatch can listen too, Scheduled events are run on a schedule, similar to running CRON on a linux server. In this article we are going to create a Scheduled Event to regularly invoke our Lambda function.

Cloudformation

There are a few different options we can use to create our Cloudwatch rule including the AWS web console, the AWS CLI tools, or Cloudformation. The AWS web console or the CLI are quick and easy ways to get Cloudwatch rules setup, but if you are building systems that use integrated AWS services, Cloudwatch can help keep these services organized and in a central location. In this article we will use Cloudformation to create our Cloudwatch rule.

Cloudformation Template

We will use a JSON Cloudformation template to create our Cloudwatch rule. First, we will start with a basic JSON object that represents our Cloudwatch rule:

    "AWSTemplateFormatVersion": "2010-09-09",
    "Resources": {
        "cloudwatchRuleInvokeLambdaEveryHour": {
            "Type": "AWS::Events::Rule",
            "Properties": {
            }
        }
    },
    "Description": "Cloudformation template to create a Cloudwatch rule"    

First we have the name of our template declaration with a AWSTemplateFormatVersion and a Resources object. Then we have our first resource object, in this case cloudwatchRuleInvokeLambda. Then we have a a Type:“AWS::Events::Rule” which is our resource type identifier in this case, AWS Events Rule. These resource type identifiers can be named as you see fit, but will need to be unique. They will always have the format:

service-provider::service-name::data-type-name

Next we will need to populate our properties for the rule.

Cloudwatch Rule Properties

Here are the properties of the Cloudwatch rule:

"Properties": {
    "Description": "Invoke Lambda",
    "State": "ENABLED",
    "ScheduleExpression": "rate(1 hour)",          
    "Targets": []
}

We set the following properties, Description, ScheduleExpression, State, and Targets. Description shows in the description field when viewing your Cloudwatch rule, and State is used to enable and disable to rule. ScheduleExpression, in this case of a schedule rule is used to tell how often to invoke the Lamda function. And finally Targets will be an array of objects that describe the Lambda function to invoke.

Our updated Cloudwatch template now looks like this:

    "AWSTemplateFormatVersion": "2010-09-09",
    "Resources": {
        "cloudwatchRuleInvokeLambdaEveryHour": {
            "Type": "AWS::Events::Rule",
            "Properties": {
                "Description": "Invoke Lambda",
                "State": "ENABLED",
                "ScheduleExpression": "rate(1 hour)",          
                "Targets": []
            }
        }
    },
    "Description": "Cloudformation template to create a Cloudwatch rule"

Now we have to set up our rule target, our Lambda function.

Cloudwatch Rule Targets

Here is our Target objects:

"Targets": [{
        "Arn": "arn:aws:lambda:us-west-2:XXX:function:lambda-demo:ACC",
        "Input": "{\"operation\":\"call-lambda\"}",
        "Id": "cloudwatchRuleInvokeLambdaACC"
    },
    {
        "Arn": "arn:aws:lambda:us-west-2:XXX:function:lambda-demo:PRD",
        "Input": "{\"operation\":\"call-lambda\"}",
        "Id": "cloudwatchRuleInvokeLambdaPRD"
    }
]

Each target has an ARN, Input, and Id. The ARN is the ARN of the Lambda function you want to invoke, followed by the Qualifier to call, with the syntax :ACC. You can read up on how to use Lambda Qualifiers here. Next is the Input to send to the Lambda function. This will be available in the Event object in your function. Last is the Id, this helps you keep track of the targets your rule calls.

Multiple Targets

Notice that the Targets object accepts an array. In our example, we call two different targets using the same rule. In this example, we are calling the same Lambda function, but using Qualifiers to specify calling the ACC qualifier:

    "Arn": "arn:aws:lambda:us-west-2:XXX:function:lambda-demo:ACC"

Or the PRD function qualifier:

"Arn": "arn:aws:lambda:us-west-2:XXX:function:lambda-demo:PRD"

This target array can also be used to call other Lambda functions, SQS operations, or other targets that listen to Cloudwatch rules.

Now that we have our targets setup, here is our full template:


{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Resources": {
        "cloudwatchRuleInvokeLambdaEveryHour": {
            "Type": "AWS::Events::Rule",
            "Properties": {
                "Description": "Invoke Lambda",
                "State": "ENABLED",
                "ScheduleExpression": "rate(1 hour)",          
                "Targets": [{
                        "Arn": "arn:aws:lambda:us-west-2:XXX:function:lambda-demo:ACC",
                        "Input": "{\"operation\":\"call-lambda\"}",
                        "Id": "cloudwatchRuleInvokeLambdaACC"
                    },
                    {
                        "Arn": "arn:aws:lambda:us-west-2:XXX:function:lambda-demo:PRD",
                        "Input": "{\"operation\":\"call-lambda\"}",
                        "Id": "cloudwatchRuleInvokeLambdaPRD"
                    }
                ]
            }
        }
    },
    "Description": "Cloudformation template to create a Cloudwatch rule"

Now we have to setup permissions for our Cloudwatch rule to invoke our Lambda function

Permissions

Using a new Cloudformation object and the type AWS::Lambda::Permission, we can set the permissions:

"cloudwatchRulePermissions": {
    "Type": "AWS::Lambda::Permission",
    "Properties": {
        "FunctionName": "lambda-demo",
        "Action": "lambda:InvokeFunction",
        "Principal": "events.amazonaws.com",
        "SourceArn": { "Fn::GetAtt": ["cloudwatchRuleInvokeLambdaEveryHour", "Arn"] }
    }
}

The interesting part about the permissions object is the SourceArn property, with Fn::GetAtt. You can use this property in Cloudwatch to get information about other objects in your Cloudwatch stack. In this case, we are getting the ARN of the cloudwatchRuleInvokeLambdaEveryHour object.

Here is the final Cloudformation template:

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Resources": {
        "cloudwatchRuleInvokeLambdaEveryHour": {
            "Type": "AWS::Events::Rule",
            "Properties": {
                "Description": "Invoke Lambda",
                "State": "ENABLED",
                "ScheduleExpression": "rate(1 hour)",          
                "Targets": [{
                        "Arn": "arn:aws:lambda:us-west-2:XXX:function:lambda-demo:ACC",
                        "Input": "{\"operation\":\"call-lambda\"}",
                        "Id": "cloudwatchRuleInvokeLambdaACC"
                    },
                    {
                        "Arn": "arn:aws:lambda:us-west-2:XXX:function:lambda-demo:PRD",
                        "Input": "{\"operation\":\"call-lambda\"}",
                        "Id": "cloudwatchRuleInvokeLambdaPRD"
                    }
                ]
            }
        },
        "cloudwatchRulePermissions": {
            "Type": "AWS::Lambda::Permission",
            "Properties": {
                "FunctionName": "lambda-demo",
                "Action": "lambda:InvokeFunction",
                "Principal": "events.amazonaws.com",
                "SourceArn": { "Fn::GetAtt": ["cloudwatchRuleInvokeLambdaEveryHour", "Arn"] }
            }
        }       
    },
    "Description": "Cloudformation template to create a Cloudwatch rule"
}

Validating and Running Template

We will use the AWS CLI to first test our template to ensure the JSON is valid:

aws cloudformation validate-template --template-body file://template.json

If you do not recieve an error back from AWS your template is valid. My response looks like this:

{
    "Description": "Cloudformation template to create a Cloudwatch rule", 
    "Parameters": []
}

Now we can create our stack. I am going to call mine CloudwatchRule:

aws cloudformation create-stack --stack-name CloudwatchRule --template-body file://template.json

If everything goes ok you will get a response from AWS with your new StackId:

{
    "StackId": "arn:aws:cloudformation:us-east-1:xxx:stack/CloudwatchRule/cd69e230-32fb-11ea-b4a5-0ac97ebd7097"
}

Notes and Gotchas

Creating Stacks: When you create your Cloudformation stack, if any of the resources fail to create, the stack will be stuck in a ROLLBACK_COMPLETE state. Check the Events page in the web console to view logs to see what failed. At this point you cannot update the template for your stack, you must delete the stack and re-create.

Target input: Sending data to your Lambda function is important. Be sure to use the Input property of your target object. This property requires a string, but the target typically accepts JSON, so you will need to stringify your JSON. From our template that looks like this:

"Input": "{\"operation\":\"call-lambda\"}",

Conclusion

After following this article you should now have a valid Cloudformation template to create a Cloudwatch rule that invokes a Lambda function ever hour. You can expand this template to add other resources to expand your Cloudformation templates. If you have any questions about using Cloudformation or this article, reach out in the comments below or send me an email chris@chrismitchellonline.com.

comments powered by Disqus