Go and Lambda

Golang and AWS Lambda are two of the primary tools I use everyday for work, so when AWS announced that lambda would natively support Go earlier this year I was ecstatic. In this tutorial, I'll walk through how easy it is to deploy a fully functional API using these technologies and others by way of the serverless framework.

Disclaimer

This tutorial is designed to be deployed to AWS, and while the majority of resources should fall under the free tier, please be aware that this could be costing you actual money.

Some Background

So why is official support for Golang in AWS Lambda so great?

Well, firstly, building a Golang application is dead simple, and this makes deployment dead simple. The output of go build is a statically linked binary, and this is literally the only thing you need to deploy an application to lambda.

Second, Golang enjoys one of the lower cold start times of the currently supported languages, and is likely to improve on that mark in the future.

Also, sometimes you just get tired of javascript.

Setup

Before we get started, we need to prepare our environment. My machine is setup with the following:

Once that's squared away, we'll need to install the serverless cli.

npm install -g serverless

You'll also need the aws cli configured with a set of access credentials to an AWS account you control.

Initial Project

Now that we have our tools installed, we can begin setting up a new project. Enter serverless.

$ cd somewhere/in/GOPATH
$ serverless create -t aws-go-dep -p go-lambda-test

Once complete, serverless will scaffold a new project using the aws-go-dep template. This comes with all sorts of goodies:

  • dep has been initialized with the aws-lambda-go package.
  • Two very simple lambda functions.
  • A Makefile for building.
  • A serverless.yml configuration file.

In fact, even in this simple form, we are able to deploy the two functions to lambda and test them out. I like to add a new build target to the Makefile to facilitate this. Here's what that looks like:

build:
	dep ensure
	env GOOS=linux go build -ldflags="-s -w" -o bin/hello hello/main.go
	env GOOS=linux go build -ldflags="-s -w" -o bin/world world/main.go

deploy: build
	serverless deploy

From here, we can deploy to AWS and test each function.

$ make deploy
$ serverless invoke -f hello

If everything goes as expected, you should see the following output:

{
    "message": "Go Serverless v1.0! Your function executed successfully!"
}

Taking a look at the provided hello/main.go, it should pretty intuitive to see what's happening here.

package main

import (
	"github.com/aws/aws-lambda-go/lambda"
)

type Response struct {
	Message string `json:"message"`
}

func Handler() (Response, error) {
	return Response{
		Message: "Go Serverless v1.0! Your function executed successfully!",
	}, nil
}

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

Our Handler func is being executed on every lambda function invocation, always returning a static response.

Adding an API

Now that we're starting to get a feel for deploying and executing lambda functions with Golang, let's begin to introduce an API. First, we'll need to install some dependencies and remove the aws-lambda-go dependency.

  • gin - http framework
  • gateway - lambda wrapper for http.Handler interface

To achieve this, we can run:

$ dep ensure -add github.com/gin-gonic/gin github.com/apex/gateway

You'll likely receive a warning from dep at this point indicating that the installed dependencies are not imported by your project, but we'll address that soon enough. Let's start by creating a new directory for our handler and removing the demo functions.

$ rm -r hello world
$ mkdir cmd
$ touch cmd/handler.go

Once complete, open up cmd/handler.go and enter the following:

package main

import (
	"log"
	"net/http"
	"os"

	"github.com/apex/gateway"
	"github.com/gin-gonic/gin"
)

func foo(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{"success": true, "message": "hello lambda API!"})
}

func main() {
	addr := ":3000"
	mode := os.Getenv("GIN_MODE")
	g := gin.New()
	g.GET("/foo", foo)

	if mode == "release" {
		log.Fatal(gateway.ListenAndServe(addr, g))
	} else {
		log.Fatal(http.ListenAndServe(addr, g))
	}
}

Basically, what we're doing here boils down to this:

  • Instantiating an instance of *gin.Engine.
  • Telling that instance to run our foo function when it receives a GET at /foo.
  • The most interesting part is how we "start the engine". If the environment variable GIN_MODE is set to "release", we wrap our handler with the gateway package, which basically abstracts the API gateway syntax and allows us to focus on the server itself. Cool, right?
  • If GIN_MODE does not equal "release", we start an ordinary http server.

Let's go ahead and start a local server to test with first.

$ go run cmd/handler.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /foo                      --> main.foo (1 handlers)

And from a separate terminal window, we can test our simple API using curl.

$ curl localhost:3000/foo
{"message":"hello lambda API!","success":true}

Great! Now that we're confirmed to be working locally, let's update our Makefile and serverless.yml to know about our new function.

First, the Makefile:

build:
	dep ensure
	env GOOS=linux go build -ldflags="-s -w" -o bin/handler cmd/handler.go

deploy: build
	serverless deploy

Then, serverless.yml:

service: blog-test

provider:
  name: aws
  runtime: go1.x

package:
 exclude:
   - ./**
 include:
   - ./bin/**

functions:
  handler:
    handler: bin/handler
    events:
      - http:
          path: /foo
          method: get
    environment:
      GIN_MODE: release

A few things to note on these changes. In the Makefile, we remove the old calls to go build and introduce a new one for our handler. In serverless.yml, we're again removing references to our previous functions and adding our handler.

The more interesting part is the addition of events. This block tells serverless what events need to take place in order to invoke our Lambda function. In this case, we're saying that we want our function to be invoked in response to API gateway receiving a GET request to /foo.

Now, we can deploy.

$ make deploy
dep ensure
env GOOS=linux go build -ldflags="-s -w" -o bin/handler cmd/handler.go
serverless deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (8.07 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
.......................................
Serverless: Stack update finished...
Service Information
service: blog-test
stage: dev
region: us-east-1
stack: blog-test-dev
api keys:
  None
endpoints:
  GET - https://ko86rwumf6.execute-api.us-east-1.amazonaws.com/dev/foo # here is your endpoint
functions:
  handler: blog-test-dev-handler

Be sure to take special care to note the endpoints section of the output. This is the public URL of your API gateway endpoint and should end in /dev/foo. Let's test that endpoint with curl (yours will be different).

$ curl https://ko86rwumf6.execute-api.us-east-1.amazonaws.com/dev/foo
{"message":"hello lambda API!","success":true}

Awesome! We've just deployed our first API written in Golang to Lambda, fronted by API gateway. From here, the world is your oyster.

Finished

In my next post, we'll look at how to extend our serverless configuration to add a DynamoDB table and integrate that into our API.