Coder Thoughts Piotr Mionskowski's thoughts on software, technology, architecture, design patterns and programming in general.

Piotr Mionskowski

How to deploy Lambda function with CloudFormation?

11 March 2018 aws, cloudformation, lambda, and cloudform

Serverless deployments are popular these days. With a minimal cost you can have your own code wait and respond to various events. AWS Lambda, Azure Functions are just 2 examples of serverless offering from the biggest cloud providers. For a long time I had thought about them only in the context of ad-hoc setups not suitable for a long term development. This was until I found out that you can, with a little effort, version and deploy the serverless API just as a traditional back-end. In this post I am going to show how to deploy AWS Lambda functions with the help of the tool Adam created at Bright Inventions called cloudform.

Lambda function

Step 1: Define a template

Let’s install the cloudform npm i --save-dev cloudform and define a minimal template:

import cloudform, { Lambda, IAM, Fn } from 'cloudform';

const LambdaExecutionRole = 'LambdaExecutionRole';

cloudform({
    Resources: {
        HelloWorld: new Lambda.Function({
            Code: {
                ZipFile: "exports.wrtiteToConsole = function (event, context, callback){ console.log('Hello'); callback(null); }"
            },
            Handler: "index.wrtiteToConsole",
            Role: Fn.GetAtt(LambdaExecutionRole, "Arn"),
            Runtime: "nodejs6.10"
        }),
        [LambdaExecutionRole]: new IAM.Role({
            AssumeRolePolicyDocument: {
                Statement: [{
                    Effect: "Allow",
                    Principal: { Service: ["lambda.amazonaws.com"] },
                    Action: ["sts:AssumeRole"]
                }]
            },
            Path: "/",
            ManagedPolicyArns: ["arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]
        })
    }
});

There are only 2 resources defined in the template. HelloWorld is the AWS Lambda function definition. We have set the Runtime property to use nodejs6.10 hence the Code.ZipFile contains a JavaScript code. Despite the name ZipFile should contain application source code in plain text. Obviously the ZipFile may only be used for the most simple functions. The S3Bucket and S3Key properties can be used to deploy a more involved function implementation. The Handler defines which function from Code the Lambda runtime should invoke. In case of ZipFile the first part is always index.

The Role is a required setting for every AWS Lambda function. It defines what entitlements the code invoked by the Lambda runtime has. In the above template we define a policy which can be assumed by lambda.amazonaws.com and references a pre-defined AWS managed policy AWSLambdaBasicExecutionRole. The AWSLambdaBasicExecutionRole makes it possible for the Lambda function logs to be pushed to Cloud Watch.

Step 2: Create the AWS Lambda function

Deploying our Lambda function using CloudFormation requires a single command:

aws cloudformation create-stack \
    --capabilities CAPABILITY_IAM \
    --stack-name lambda-example \
    --template-body file://<(node_modules/.bin/cloudform aws-template.ts)

The --capabilities CAPABILITY_IAM is required whenever the CloudFormation has to define Roles, Policies or related resources. The --template-body file://<(node_modules/.bin/cloudform aws-template.ts) instructs CloudFormation to use a template defined in a file. The <(...) is bash and zsh way to pass an output of a command to other program as if the output was a file. After waiting a bit for the invocation to complete we will see the following in the AWS Console:

AWS Lambda Screen

It is possible to use the AWS Console editor to test and change the function. However, if we are to treat the serverless approach seriously we should not forget about the standard practices like versioning of our source code.

Step 3: Update and version AWS Lambda function

Since we have defined the AWS Lambda function using a cloudform template we can version it as any other code. The whole serverless infrastructure we use and configure is treated as a source code allowing for an easy replication of deployment environments, audit trail and change management. Let’s see how we can use cloudform to add another function that will be called by the first one.

import cloudform, { Lambda, IAM, Fn } from 'cloudform';
import { FunctionProperties } from 'cloudform/types/lambda/function';
import { readFileSync } from 'fs';

const
    LambdaExecutionRole = 'LambdaExecutionRole',
    Alice = 'Alice',
    Bob = 'Bob';

function lambdaFunction(
    functionCode: string,
    options?: Partial<FunctionProperties>) {
    return new Lambda.Function({
        Code: { ZipFile: functionCode },
        Handler: "index.main",
        Role: Fn.GetAtt(LambdaExecutionRole, "Arn"),
        Runtime: "nodejs6.10",
        ...options
    });
}

export default cloudform({
    Resources: {
        [Alice]: lambdaFunction(readFileSync('Alice.js', 'utf-8'), {
            Environment: {
                Variables: {
                    BobFunction: Fn.GetAtt(Bob, "Arn")
                }
            }
        }),
        [Bob]: lambdaFunction(readFileSync('Bob.js', 'utf-8')),
        ... // LambdaExecutionRole see below
    }
})

As you can see above, we’ve extracted a function lambdaFunction to simplify Lambda function declaration. Both Alice and Bob function’s bodies are defined in separate files. Interestingly Alice function, during the invocation, will have access to BobFunction environment variable pointing to Bob function ARN.

Our LambdaExecutionRole lacks a permission to invoke another Lambda function. Let’s fix that:

    [LambdaExecutionRole]: new IAM.Role({
        AssumeRolePolicyDocument: {
            Statement: [{
                Effect: "Allow",
                Principal: { Service: ["lambda.amazonaws.com"] },
                Action: ["sts:AssumeRole"]
            }]
        },
        Path: "/",
        ManagedPolicyArns: ["arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"],
        Policies: [{
            PolicyName: "AllowCallingOtherLambda",
            PolicyDocument: {
                Version: "2012-10-17",
                Statement: [{
                    Sid: "InvokeLambdaPermission",
                    Effect: "Allow",
                    Action: ["lambda:InvokeFunction"],
                    Resource: "*"
                }]
            }
        }]
    })

The Alice and Bob sources export main functions invoked by AWS Lambda runtime:

// Alice.ts
import aws from 'aws-sdk';

export function main(event: any, context: any, callback: Function) {
    new aws.Lambda().invoke({
        FunctionName: process.env.BobFunction!,
        Payload: JSON.stringify({message: "Hi!. I'm Alice."})
    }, (error: Error, data: any) => {
        const response = JSON.parse(data.Payload);
        console.log('FromBob', error || response)
        callback(error);
    });
}

// Bob.ts
export function main(event: any, context: any, callback: any) {
    console.log('FromAlice', event)
    callback(null, {message: "Hi! I'm Bob. Nice to meet you!"});
}

Since we now use TypeScript for Alice and Bob source we need to install the compiler npm i --save-dev typescript @types/node aws-sdk. Unfortunately currently in cloudform it is not possible to use custom tsconfig.json for the compilation. Fortunately we can invoke the compiler manually and use its outputs as any other Node.js source code. With a tsconfig.json:

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es6",
    "strict": true, 
    "strictPropertyInitialization": false,
    "esModuleInterop": true
  },
  "exclude": ["node_modules"],
  "compileOnSave": true
}

we can use the following commands to compile and deploy Lambda functions:

./node_modules/.bin/tsc

aws cloudformation update-stack \
    --capabilities CAPABILITY_IAM \
    --stack-name lambda-example \
    --template-body file://<(node -e "console.log((require('./aws-template').default))")

After the stack update completes we now should have 2 Lambda functions available. When we invoke the Alice function, we’ll see that the 2 AWS Lambda functions communicate:

START RequestId: 6a87c764-251e-11e8-b921-f9ca7649c7d7 Version: $LATEST
2018-03-11T11:22:00.615Z	6a87c764-251e-11e8-b921-f9ca7649c7d7	Alice says: Hi!. I'm Alice.
2018-03-11T11:22:01.676Z	6a87c764-251e-11e8-b921-f9ca7649c7d7	Bob says: Hi! I'm Bob. Nice to meet you!
END RequestId: 6a87c764-251e-11e8-b921-f9ca7649c7d7
REPORT RequestId: 6a87c764-251e-11e8-b921-f9ca7649c7d7	Duration: 1110.68 ms	Billed Duration: 1200 ms 	Memory Size: 128 MB	Max Memory Used: 34 MB	

Serverless infrastructure offers endless possibilities. With cloudform we can take AWS Lambda development, change management and deployment to the next level.