Doug.Instance

Pipelines for AWS Lambda - Part 2: The Code

Nov 5, 2021

TL/DR;

One of the great things about AWS Lambda is that you can write your code and deploy without worrying about the hosting environment (kind of). So let's talk about what that code should look like so you really don’t have to worry.

Background

As I mentioned in my previous post, the AWS Serverless Application Model (SAM), has made (some) things better about developing serverless functions in AWS Lambda. We are going to create a fairly basic Hello World API. The code itself is relatively simple but Lambda only works when deployed with all of the correct resources and permissions linked correctly. Using SAM, we will deploy the Lambda function and an API gateway. The resources and permissions for this initial implementation are pretty simple, but there are still mistakes that can be made so I’ll walk through the troubleshooting steps.

Creating the Lambda Code Before we talk about deployment, we need to have some code to deploy. To make sure we capture all of the things we need for our function to work, we are just going to scaffold a new project using sam init. There is a large collection of starter templates maintained by AWS and SAM uses this repository to scaffold new projects. Below shows the selections I used to generate a "hello world" project in Node.js:

$ sam init
Which template source would you like to use?
    1 - AWS Quick Start Templates
    2 - Custom Template Location
Choice: 1
What package type would you like to use?
    1 - Zip (artifact is a zip uploaded to S3)	
    2 - Image (artifact is an image uploaded to an ECR image repository)
Package type: 1

Which runtime would you like to use?
    1 - nodejs14.x
    2 - python3.9
    3 - ruby2.7
    4 - go1.x
    5 - java11
    6 - dotnetcore3.1
    7 - nodejs12.x
    8 - nodejs10.x
    9 - python3.8
    10 - python3.7
    11 - python3.6
    12 - python2.7
    13 - ruby2.5
    14 - java8.al2
    15 - java8
    16 - dotnetcore2.1
Runtime: 1

Project name [sam-app]: sam-test-node

Cloning from https://github.com/aws/aws-sam-cli-app-templates

AWS quick start application templates:
    1 - Hello World Example
    2 - Step Functions Sample App (Stock Trader)
    3 - Quick Start: From Scratch
    4 - Quick Start: Scheduled Events
    5 - Quick Start: S3
    6 - Quick Start: SNS
    7 - Quick Start: SQS
    8 - Quick Start: Web Backend
Template selection: 1

    -----------------------
    Generating application:
    -----------------------
    Name: sam-test-node
    Runtime: nodejs14.x
    Dependency Manager: npm
    Application Template: hello-world
    Output Directory: .
    
    Next steps can be found in the README file at ./sam-test-node/README.md

You can view the template code in GitHub to se what is created. Let’s walk through each file.

Function Code

In the hello-world folder, you will find app.js. This file contains all of the code required for the function. There is some commented out code that requires axios for making a simple HTTP call, but the active code does not have any dependencies so if you simply upload this code into a new Lambda function and test it via the AWS console, you will get a simple output message looking like this:

{ "message": "hello world" }

The full code for the function is below:

// const axios = require('axios')
// const url = 'http://checkip.amazonaws.com/';
let response;
 
/**
 *
 * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
 * @param {Object} event - API Gateway Lambda Proxy Input Format
 *
 * Context doc: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html 
 * @param {Object} context
 *
 * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
 * @returns {Object} object - API Gateway Lambda Proxy Output Format
 * 
 */
exports.lambdaHandler = async (event, context) => {
    try {
        // const ret = await axios(url);
        response = {
            'statusCode': 200,
            'body': JSON.stringify({
                message: 'hello world',
                // location: ret.data.trim()
            })
        }
    } catch (err) {
        console.log(err);
        return err;
    }
 
    return response
};

There isn't much code here but what is here is very important. First off, the lambdaHandler function is exposed as a static function meaning you do not need to create an instance of a class to invoke the function. This is important because this is how Lambda expects to invoke the handler so when you specify the handler in the Lambda configuration, it must point to a static function.

Also notice that the handler function is marked async. If you do not specify an async function, a third parameter named callback will be passed to your handler and you will need to invoke this callback as shown in the AWS documentation.

Note: The event parameter varies based on the type of invocation and documentation is not as thorough as you would think. If you write the parameter out with console.log(event), you can see the contents in the CloudWatch log for the Lambda.

Note that the error handler in this code returns the any error caught by the Lambda handler. This allows Lambda to log the invocation as an error. If your Lambda returns a valid response with an error statusCode value (ex: 500), it will still be logged as a successful invocation since the Lambda itself did not fail.

SAM Template

The next file generated by sam init is the template.yaml file which is also placed in the root folder. This template is similar to CloudFormation and in fact can contain most CloudFormation syntax. However, SAM provides simplified syntax and linkage for creating serverless applications. Let's take a look at the file generated when I ran sam init.

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-test-node
 
  Sample SAM Template for sam-test-node
   
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3
 
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello-world/
      Handler: app.lambdaHandler
      Runtime: nodejs14.x
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get
 
Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

Note: We don't specify a name for our Lambda function or API Gateway. When we deploy using SAM, we provide a stack name that is used for the CloudFormation stack but also carried to other resources for consistent naming. This allows us to identify resources created for testing purposes based off of the branch they were created from.

The first meaningful section of the template is the Globals configuration. This allows you to specify – you guessed it – global information that applies to all resources. In this example, the timeout is set to 3 seconds for all Lambda functions. This just happens to be the default, but you can set any default values here you want to apply for all functions. Since we only have one function in this template, we could have just as easily placed this Timeout key in the Properties section of the Lambda function configuration, but it is placed in Globals as an example.

The second important section is the Resources section. Even though there is only one resource specified, SAM will actually create 2 resources: the Lambda function and the API Gateway. The deployment process will also create a third resource: a Lambda Application which will provide information on all of the resources, deployments, and Lambda invocations all in one place.

The first key under Resources is HelloWorldFunction. This is a logical ID that can be used to reference the function in other parts of the template. The Type key specifies that this is a Lambda function, and the Properties key contains all of the configuration for the function (see the AWS documentation for more options for configuring a function). The CodeUri key is optional and defines the base path for your code and, as I mentioned before, the Handler key in the points to the static function in your Lambda code. If you define multiple Lambda Functions in one template and all of your code is in a folder such as src or bin, you can define the CodeUri in the Globals section and have it apply to all of your functions. Otherwise, you can simply include the path in the Handler key like hello-world/app.lambdaHandler and remove the CodeUri key. The Runtime key allows you to specify a specific version of your runtime. I’m using Node.js version 14 in this series of posts, but you can find the list of supported runtimes in the AWS documentation.

The Events section under the Lambda function is where the most significant SAM magic happens. Once again we provide a logical key (HelloWorld) and then give it a Type value (Api) and then we can configure the resource with Properties. In this example we set the Path of the API to /hello and the Method to get. Under the hood, there is a lot more going on here. SAM does all of this based on just these 2 entries:

  • Creates an API Gateway
  • Creates a Route with path /hello and HTTP method GET
  • Creates an integration between the /hello route and the Lambda function
  • Creates a $default deployment stage for the API Gateway

Summary

In this post, we created code using sam init. The two most important files created by this code are the lambda function code and the SAM template (template.yaml). The code generated by SAM is obviously just a placeholder and will require significant editing. The SAM template is very important to how your function is deployed, but we are sticking with the basic Lambda "Hello World" example with a REST API.