Trading Fish The blog of Hector Castro

Scheduling Lambda Functions with AWS SAM

A few days ago, I spent some time learning how to use Amazon’s Serverless Application Model (SAM) to schedule the recurring execution of Lambda functions. To help better cement my understanding, I assembled an overview of all the SAM template components necessary to schedule the periodic execution of a Go-based Lambda function. I also made note of how I used the SAM CLI to package and deploy everything to AWS.

Serverless Application Model

Amazon’s Serverless Application Model is a specification for translating SAM templates into CloudFormation templates. Much like macro expansion, it works through a textual transformation of the input SAM template into a template the CloudFormation engine can make sense of.

There are several components that make up a SAM template, but in this example we only use four: Format Version, Description, Transform, and Resources.

  • Format Version equates to AWSTemplateFormatVersion in the template, which identifies its capabilities
  • Description is optional, but provides a way to give the template a high-level description
  • Transform can map to multiple things, but here it maps to the AWS::Serverless-2016-10-31 transform, which is a version of the SAM specification

As far as Resources go, this template defines two: TestFunction and TestRole.

AWSTemplateFormatVersion: '2010-09-09'
Description: A scheduled Amazon Lambda function.
Resources:
  TestFunction:
    Properties:
      CodeUri: .
      Events:
        Testy:
        Properties:
          Schedule: rate(1 hour)
        Type: Schedule
      Handler: main
      Role: !GetAtt TestRole.Arn
      Runtime: go1.x
    Type: AWS::Serverless::Function
  TestRole:
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Action:
          - sts:AssumeRole
        Effect: Allow
        Principal:
          Service:
          - lambda.amazonaws.com
        Version: '2012-10-17'
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
    Type: AWS::IAM::Role
Transform: AWS::Serverless-2016-10-31

TestRole is a resource of type AWS::IAM::Role, which is a top-level CloudFormation resource. It creates an Identity and Access Management (IAM) role containing the permissions necessary for our Lambda function to do its thing. In this case, it simply encapsulates a canned IAM policy, AWSLambdaBasicExecutionRole. This policy allows the Lambda function to use the following CloudWatch API calls to log function output to CloudWatch Logs.

  • logs:CreateLogGroup
  • logs:CreateLogStream
  • logs:PutLogEvents

The next resource, TestFunction, is of type AWS::Serverless::Function. This is not a top-level CloudFormation resource. Instead, it is a SAM resource that expands into multiple top-level CloudFormation resources. Based on our usage, it expands into three:

  • AWS::Lambda::Function
  • AWS::Lambda::Permission
  • AWS::Events::Rule

AWS::Lambda::Function is the top-level CloudFormation resource to define an Amazon Lambda function. Because we want to schedule the function’s periodic execution, we include an Events property on our AWS::Serverless::Function resource. This allows us to define the function execution schedule within the context of the function’s properties. Behind-the-scenes, the Events property expands into a AWS::Events::Rule resource with an invocation rate of once per hour.

Lastly, in order for the CloudWatch Events API to invoke our function, it needs permissions to do so. AWS::Lambda::Permission grants CloudWatch Events the permission to invoke our function.

Package and ship

The AWS SAM CLI builds on top of the SAM specification by providing a single tool to manage the packaging and deployment of serverless applications. Installation is a bit out-of-scope for this post, but once you’ve managed to install the sam tool, the application deployment process occurs in three phases.

package main

import (
    "context"
    "fmt"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
)

func HandleRequest(ctx context.Context, e events.CloudWatchEvent) (string, error) {
    return fmt.Sprintf("Hello, world."), nil
}

func main() {
    lambda.Start(HandleRequest)
}

First, compile your Go-based Lambda function into a Linux compatible binary.

$ GOOS=linux go build -o main main.go

Once the binary exists, use sam to upload the binary to S3 and reference it in a newly created packaged.yaml CloudFormation configuration.

$ sam package --s3-bucket test-global-config-us-east-1 \
              --template-file template.yaml \
              --output-template-file packaged.yaml
Uploading to 7001c68762c2fcda61de373e0a30563d  29187040 / 29187040.0  (100.00%)
Successfully packaged artifacts and wrote output template to file packaged.yaml.

Before using sam to deploy using the contents of packaged.yaml, run a quick diff to see what changed.

$ diff template.yaml packaged.yaml
<       CodeUri: .
---
>       CodeUri: s3://test-global-config-us-east-1/7001c68762c2fcda61de373e0a30563d

Lastly, use sam again to deploy the template through a CloudFormation stack named Test.

$ sam deploy --template-file packaged.yaml \
             --stack-name Test \
             --capabilities CAPABILITY_IAM
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - Test

Within an hour or so (it only takes a few minutes to deploy—the wait is for the function schedule to trigger), you should see something like the following in your function’s CloudWatch Logs log stream.

START RequestId: 5886a0f4-50a1-1cca-10b2-67f512fd83b1 Version: $LATEST
"Hello, world."
END RequestId: 5886a0f4-50a1-1cca-10b2-67f512fd83b1
REPORT RequestId: 5886a0f4-50a1-1cca-10b2-67f512fd83b1
Duration: 1.59 ms       Billed Duration: 100 ms Memory Size: 128 MB     Max Memory Used: 5 MB