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.
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 aGET
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 thegateway
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.