# The anatomy of a CloudFormation template with a simple Lambda function

## Background

Managing a Lambda function with CloudFormation is a non-trivial task. You need to define several resources to make it work.

I’ll use this monitoring script to show the building blocks of this simple function. The functionality is rather simple: have a function and automatically call it on a scheduled rate.

Why use CloudFormation?

When you need multiple resources for a single functionality, without a centralized descriptor they remain unrelated. Defining and handling them one-by-one makes it hard to reproduce the functionality, and harder still to clean everything up when they are not needed. With a CloudFormation template, you can deploy/update/remove all of them together.

## Resources

Resources are the main building blocks of any CloudFormation template. As a rule of thumb, anything that you’d define on the Console manually is one Resource.

### Lambda function

The function is the main functionality. To define it, we need a resource of type AWS::Lambda::Function:

Resources:
LambdaFunction:
Type: AWS::Lambda::Function
Properties:
...


Use the Properties block to define the Code, the Runtime, and the Handler (this defines which of the exported function to call):

Resources:
LambdaFunction:
...
Properties:
Handler: handler.index
Code: src/
Runtime: nodejs8.10


The Code is referenced in a local path, but for deployment CloudFormation requires it to be in S3. But since managing the code in a remote location is burdensome (not to mention it’s easy to accidentally overwrite existing versions), the AWS CLI provides a package function to do it just before deployment:

aws cloudformation package --template-file cloudformation.yml --s3-bucket <bucket> --output-template-file output.yml


This command zips then uploads the code, replacing the Code block with the S3 URL in the process. The resulting template is written to the output.yml file, as per the --output-template-file argument. This can be uploaded to CloudFormation directly.

### Event Rule

Now that there is a function, we need a resource to invoke it regularly. The AWS::Events::Rule is our friend here:

Resources:
ScheduledRule:
Type: AWS::Events::Rule
Properties:
ScheduleExpression: "rate(1 minute)"
State: "ENABLED"
Targets:
- Arn: !GetAtt LambdaFunction.Arn
Id: "TargetFunction"


Since we need to reference which function to call, we need a way to get the Arn (the resource identifier) of the LambdaFunction resource. To get a parameter of a resource, use the !GetAtt function:

Arn: !GetAtt LambdaFunction.Arn


How to know which properties are supported and required for a given resource?

Consult the documentation. There, you can see what you need to define, and also what return values you can reference with the !GetAtt. Especially, look at the examples provided by AWS to figure out how to use a given resource.

### Permissions

The two resources above implement the main functionality. But we need another two, first, a permission to invoke the function, and second, an execution role that the function will use.

To give permission to the Events Rule to call the function, we need an AWS::Lambda::Permission resource:

Resources:
InvokeLambdaPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt LambdaFunction.Arn
Action: "lambda:InvokeFunction"
Principal: "events.amazonaws.com"
SourceArn: !GetAtt ScheduledRule.Arn


Same as before, we need to reference resources defined in the template for the FunctionName and the SourceArn properties. The Action defines the scope of the permission (lambda:InvokeFunction), and the Principal defines the entity to which the permission is granted.

In this case, the Principal is events.amazonaws.com as the function will be triggered by an event. If, for example, the Lambda were triggered by an S3 event, the Principal would be s3.amazonaws.com.

The other permission we need is the execution role. Contrary to the previous permission, this is a role that the Lambda function will assume when run. You can define what other AWS resources it has access to.

Resources:
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
...
Policies:
...


It has two important parts. The AssumeRolePolicyDocument is the so-called trust policy. This defines who can assume this role. For our Lambda function to work, we need to allow the lambda.amazonaws.com service to do the sts:AssumeRole action:

Resources:
LambdaExecutionRole:
...
Properties:
...
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole


This part is mainly constant, you’d use this for any Lambda function.

But wait! Where is it specified that this role can only be assumed by this specific function?

It is not. In effect, this role can be assumed by any Lambda. You need to be very mindful whom you give access to the iam:PassRole action as that is what controls which roles can be specified by each user. See this article for more info.

The Policies part defines what the function can do:

Resources:
LambdaExecutionRole:
...
Properties:
...
Policies:
- PolicyName: allow-logs
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'logs:*'
Resource: arn:aws:logs:*:*:*


This policy allows the function to write its logs to CloudWatch Logs. Since logging is usually an important part, you want to include this statement in every Lambda role.

And finally, associate the role with the Lambda function:

Resources:
LambdaFunction:
...
Properties:
Role: !GetAtt LambdaExecutionRole.Arn
...


## Parameters

To make parts of the template parameterizable, you can use the Parameters section. For example, if you want to make the interval configurable, you need to define a Parameter:

Parameters:
ScheduleExpression:
Type: String
Default: "rate(1 minute)"


And to access it, use !Ref <parameter>:

Resources:
ScheduledRule:
...
Properties:
ScheduleExpression: !Ref ScheduleExpression
...


## Deploy

Now that everything is in place, let’s see how you could deploy the template.

First, you need to run a package that zips the local Lambda code, uploads it to S3, and modifies the template file accordingly:

aws cloudformation package --template-file cloudformation.yml --s3-bucket <bucket> --output-template-file output.yml


Make sure to use the same region for the packaged code as you’ll use for the template itself.

When you have the packaged template, deploy it:

aws cloudformation deploy --template-file output.yml --stack-name <stack name> --capabilities CAPABILITY_IAM


The --capabilities CAPABILITY_IAM is required since the template creates IAM resources, and the CLI issues an error if you don’t explicitly acknowledge it.

Run it, wait a few minutes, and you have a function that is run every minute.

## Conclusion

Writing a CloudFormation template is a time-consuming process. You might want to use some sort of a designer or a framework to generate it for you. But knowing how these templates work under the hood come handy when you need to debug a problem.

18 December 2018