Development, Packaging and Deployment

With Go, Lambda and SAM

The Twelve-Factor App documents best practices for building software-as-a-service, most of which apply to our Go FaaS app. Two of the factors cover how to develop and deploy an app:

We can easily implement these these factors for our Go FaaS app with help of the AWS SAM Local development environment.

Build with go cross-compiler

Go is a compiled language, so a build phase is implicit in every development workflow.

A killer feature Go brings to the table is a cross compiler. Mac, Windows and any other operating system with the Go tool chain has the capability to build Linux binaries with a single command.

A killer feature Lambda brings is that it’s input is a simple .zip file, a format with ubiquitous support.

So our build and packaging process is two commands that run on virtually any computer:

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

Compare this to the tools and services required to build Docker images, EC2 AMIs or Heroku slugs…

Develop with SAM Local

But a new problem arises… How do run assemble these function packages into an API on our development box? Enter AWS SAM Local, a tool for local development and testing of serverless applications. It leverages Docker and the lambci/lambda images to run the Linux binary in the main.zip packages.

invoke

The simplest example is the aws-sam-local local invoke command:

$ echo '{}' | aws-sam-local local invoke WorkerFunction
2018/02/24 15:27:01 Successfully parsed template.yml
2018/02/24 15:27:01 Connected to Docker 1.35
2018/02/24 15:27:01 Fetching lambci/lambda:go1.x image for go1.x runtime...
2018/02/24 15:27:03 Reading invoke payload from stdin
2018/02/24 15:27:03 Invoking handler (go1.x)
2018/02/24 15:27:03 Decompressing main.zip
2018/02/24 15:27:03 Mounting /var/task:ro inside runtime container
START RequestId: 358be5fc-928c-1a9a-b655-c84bb2958a5e Version: $LATEST
2018/02/24 23:27:04 Worker Event: {SourceIP: TimeEnd:0001-01-01 00:00:00 +0000 UTC TimeStart:0001-01-01 00:00:00 +0000 UTC}
END RequestId: 358be5fc-928c-1a9a-b655-c84bb2958a5e
REPORT RequestId: 358be5fc-928c-1a9a-b655-c84bb2958a5e  Duration: 524.34 ms  Billed Duration: 600 ms  Memory Size: 128 MB	Max Memory Used: 14 MB

This offers a fairly faithful representation of the Lambda production environment.

start-api

We can also run assemble our HTTP functions with aws-sam-local local start-api:

$ aws-sam-local local start-api
2018/02/24 15:33:15 Connected to Docker 1.35
2018/02/24 15:33:15 Fetching lambci/lambda:go1.x image for go1.x runtime...

Mounting handler (go1.x) at http://127.0.0.1:3000/users [POST]
Mounting handler (go1.x) at http://127.0.0.1:3000/users/{id} [PUT]
Mounting handler (go1.x) at http://127.0.0.1:3000/users/{id} [DELETE]
Mounting handler (go1.x) at http://127.0.0.1:3000/ [GET]
Mounting handler (go1.x) at http://127.0.0.1:3000/users/{id} [GET]

## run `curl http://localhost:3000`

2018/02/24 15:41:57 Invoking handler (go1.x)
2018/02/24 15:41:57 Decompressing handlers/dashboard/main.zip
START RequestId: b913c432-3ed8-1e30-5c6d-e4582e59cb02 Version: $LATEST
END RequestId: b913c432-3ed8-1e30-5c6d-e4582e59cb02
REPORT RequestId: b913c432-3ed8-1e30-5c6d-e4582e59cb02  Duration: 2.12 ms  Billed Duration: 100 ms  Memory Size: 128 MB  Max Memory Used: 8 MB

This boots a local API Gateway that takes an HTTP request, invokes a function with the request event, and returns an HTTP response from the response event.

With a couple of first-party commands we can develop our app with a strong amount of dev/prod parity.

Compare this to the difference between developing an Rails app with rails server and deploying it to Elastic Beanstalk.

make and watchexec

The final trick is to rebuild handler packages on every code change.

Thanks to the fact that the local API server unzips a handler package on every request, and Go effectively caches builds, we have a simple solution: rebuild all handlers in parallel on every change.

We can usemake, the ubiquitous tool for generating binaries from source files with watchexec, a program that watches for file changes and re-runs a command.

$ watchexec -f '*.go' 'make -j handlers'

From Makefile

Again we find a simple solution with great dev/prod parity.

CloudFormation package and deploy

Our template.yml AWS config file is a CloudFormation template of the SAM dialect. So we can lean on the aws CLI to release and run our app.

The release step is accomplished with the aws cloudformation package command. This uploads our main.zip files to S3 and writes a new CloudFormation template with the S3 URLs. The run step is executed with the aws cloudformation deploy command. This uses the CloudFormation API to update our resources – such as updating Lambda functions to the new release.

$ aws cloudformation package --output-template-file out.yml --s3-bucket $(BUCKET) --template-file template.yml
$ aws cloudformation deploy --capabilities CAPABILITY_NAMED_IAM --template-file out.yml --stack-name gofaas

From Makefile

There’s a lot of functionality baked into these commands like uploading package as efficiently as possible, creating new resources in dependency order, and safely rolling back updates on a failure. But it’s all managed by CloudFormation.

The end result is glorious: a single config file that declares our entire app infrastructure, and a single command to deploy our app that generally takes less than a minute.

Summary

An app with Go and SAM offers:

  • Fast cross-compiled builds
  • Single command release and run steps
  • A development environment with strong production parity

We don’t need to worry about:

  • Dockerfile or docker-compose.yml files
  • Code syncing
  • Complex package formats
  • Build services

Go and SAM makes it significantly easier to build and release applications with strong “dev/prod” parity.