chrismitchellonline

Create a Lambda Function with CloudFormation

2020-10-30

In this article we will take a look at creating and deploying a new Lambda function using CloudFormation and discuss why using CloudFormation to manage AWS resouces is beneficial.

Starting with CloudFormation

Technically speaking creating a new AWS Lambda function can be trivial when using the web console. But as our functions grow in complexity and interact with other AWS services, our Lambda functions evolve into full blown applications instead of just functions. The ability to manage these applications in a consistent way becomes apparent. By using CloudFormation up front to create and deploy our initial Lambda function, we can manage our entire application infrasturcture in a consistent way using configuation files. This pattern is referred to as the Infrastructure as Code design pattern.

Prerequisites

In this article we will be using an AWS account with elevated permissions to manage IAM roles, create new Lambda functions, manage Cloudformation stacks, etc. Additionally, AWS CLI tools are needed to deploy resources to AWS. Lastly I’m writing this on a Mac using VS Code but commands should be similar across platform/IDE.

New Lambda Function Code

The Lambda function we will create will be a simple hello world NodeJS function. You will need to save this file to a local file index.js:

exports.handler = async (event) => {     
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};

To make our code available to CloudFormation we need to upload a zip file to S3. I have a dedicated bucket for deploying code, aptly named cm-lambda-code-deploy. From the command line I can zip up my hello world function code and deploy to S3:

zip hello-world.zip *
aws s3 cp hello-world.zip s3://cm-lambda-code-deploy/

Next we can create the CloudFormation template to describe our Lambda function.

CloudFormation Template - Lambda

CloudFormation stacks require a template describing each resource in the stack. I’ll be using JSON format for all of my CloudFormation templates. I’ve created CFTemplate.json for the initial template as follows:

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Resources": {
  }
}

Under the resources section is were you add each of your AWS resources defined in our stack. For this article I’ve only included a subset of options for a Lambda function resource. For all options visit the AWS::Lambda::Function section on the official CloudFormation documentation site.

Update CFTemplate.json as follows, adding the HelloWorld resource object:

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Resources": {
    "HelloWorld": {
      "Type": "AWS::Lambda::Function",
      "Properties": {
        "FunctionName": "hello-world",
        "Handler": "index.handler",
        "Role": {
          "Fn::GetAtt": [
            "HelloWorldLambdaRole",
            "Arn"
          ]
        },       
        "Code": {
          "S3Bucket": "cm-lambda-code-deploy",
          "S3Key": "hello-world.zip"
        },
        "Runtime": "nodejs12.x"    
      }
    }
  }
}

Important properties of the HelloWorld resource:

  • FunctionName: Not required, but helpful in creating named resources
  • Handler: Required, The name of the method within your code that Lambda calls to execute your function, with the file name.
  • Role: Required, the role that will be running the Lambda function. More on this resource syntax below.
  • Code: Required, where the source code of the Lambda function lives.
  • Runtime: Required, the runtime of the Lambda function.

Next we will add a role resource to run our Lambda function.

CloudFormation Template - IAM Role

Again we could use the web console or other means to create this role for us, but because we are defining our application in a CloudFormation stack, we should define our role here. My role resource looks like this:

"HelloWorldLambdaRole": {
  "Type": "AWS::IAM::Role",
  "Properties": {
    "RoleName": "HelloWorldLambdaRole",
    "AssumeRolePolicyDocument": {
      "Version": "2012-10-17",
      "Statement": [{
        "Effect": "Allow",
        "Principal": {
          "Service": [ "lambda.amazonaws.com" ]
        },
        "Action": [ "sts:AssumeRole" ]
      }]
    },
    "Path": "/",
    "Policies": [{
      "PolicyName": "AWSLambdaBasicExecutionRole",
      "PolicyDocument": {
        "Version": "2012-10-17",
        "Statement": [{
          "Effect": "Allow",
          "Action": [
            "logs:CreateLogGroup",
            "logs:CreateLogStream",
            "logs:PutLogEvents"
          ],
          "Resource": "*"
        }]
      }
    }]
  }
}

This role will allow our Lambda function to execute and publish logs to CloudWatch. If our Lambda function needed additional permissions, writing to S3 for example, additional permissions can be added to the policies array.

Since we’ve created this role as part of our CloudFormation template, we can access it in other resource objects of our template. Our HelloWorld Lambda resource accesses this role definition using the CloudFormation function Fn::GetAtt. From the Lambda resource our role definition looks like this:

"Role": {
    "Fn::GetAtt": [
    "HelloWorldLambdaRole",
    "Arn"
    ]
}

Finalized CloudFormation Template

Putting it all together we have the following CloudFormation template:

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Resources": {
      "HelloWorld": {
        "Type": "AWS::Lambda::Function",
        "Properties": {
          "FunctionName": "",
          "Handler": "lambda/index.handler",
          "Role": {
            "Fn::GetAtt": [
              "HelloWorldLambdaRole",
              "Arn"
            ]
          },       
          "Code": {
            "S3Bucket": "cm-lambda-code-deploy",
            "S3Key": "hello-world.zip"
          },
          "Runtime": "nodejs12.x"  
        }
      },
      "HelloWorldLambdaRole": {
        "Type": "AWS::IAM::Role",
        "Properties": {
          "RoleName": "HelloWorldLambdaRole",
          "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [{
              "Effect": "Allow",
              "Principal": {
                "Service": [ "lambda.amazonaws.com" ]
              },
              "Action": [ "sts:AssumeRole" ]
            }]
          },
          "Path": "/",
          "Policies": [{
            "PolicyName": "AWSLambdaBasicExecutionRole",
            "PolicyDocument": {
              "Version": "2012-10-17",
              "Statement": [{
                "Effect": "Allow",
                "Action": [
                  "logs:CreateLogGroup",
                  "logs:CreateLogStream",
                  "logs:PutLogEvents"
                ],
                "Resource": "*"
              }]
            }
          }]
        }
      }
    }
  }

And we are ready to deploy our stack.

Deploying our Function

Using AWS CLI CloudFormation command we can push our stack to AWS to create our resources:

aws cloudformation create-stack --stack-name HelloWorld --template-body file://CFTemplate.json --capabilities CAPABILITY_NAMED_IAM

The CLI will report any errors, otherwise the Lambda function is now ready to use.

Updating our Function Parameters

Now that our Lambda function definition lives in CloudFormation to modify our function we can update our template and call the update-stack CLI command. For example, I updated the timeout parameter in the Lambda resource to be 300 seconds instead of the default 3 seconds:

Timeout:300

And then run the update command:

aws cloudformation update-stack --stack-name HelloWorld --template-body file://CFTemplate.json --capabilities CAPABILITY_NAMED_IAM

Inspecting the Lambda function I can see the updated parameter as soon as the CloudFormation stack update completes.

Note: This does not update function code source. Read on for more information.

Updating Function Code

Because CloudFormation doesn’t manage source code, only the infrastructure associated with the Lambda function, we have to manage our source code via S3. A quick way to update the source code with our current stack is to zip up changes to the source code and upload as a new S3 key. For example, on the CLI:

zip hello-world-v1.zip index.js 
aws s3 cp hello-world-v1.zip s3://cm-lambda-code-deploy/

Then update CFTemplate.json to reflect this change:

"Code": {
    "S3Bucket": "cm-lambda-code-deploy",
    "S3Key": "hello-world-v1.zip"
},

And run our update command again will result in promoting our latest code changes from S3 zip file to Lambda.

While this method works there are other options to integrate continous integration and continous deployment methods (CI/CD) techniques using other AWS services like Code Build and Code Pipeline. Check back at a later time for more articles on those services.

Conclusion

We should now have a CloudFormation stack that created our Lambda function and can easily update our infrastructure. By using a fairly simple configuration file and deployment command, we can update our stack quickly and effeciently with only a configuration change and by running one CLI command.

Hopefully this article has outlined the usefulness of using CloudFormation to manage your Lambda functions. Have you used similar techniques in your deployments? I would be interested in hearing about your work with CloudFormation and Lambda. Drop a line in the comments section below and lets discuss.

comments powered by Disqus
Social Media