Deployment of Go services to AWS Lambda made easy with `serverless-go-plugin`

| 3 min read

Introduction

Recently, I started writing a few toy projects with Go and I've realised that Serverless Framework does not offer great support for Go-based services out of the box. I started looking around and found serverless-go-plugin which greatly simplified the setup of my serverless Go services. In today's post I wanted to share how you can take advantage of this plugin to quickly build and deploy Go-based applications to AWS Lambda with Serverless Framework

Prerequisites

We will be using Serverless Framework to develop and deploy our application. We will also need to have go installed. In my case it's go1.21.3, but any modern version of Go should be fine.

Setting up our service

We will setup our service manually as I could not find a good Go template anywhere. Let's start with creating a directory and initializing our Go module:

mkdir serverless-go-service
cd serverless-go-service

go mod init example.com/serverless-go-service

In the next step, let's create our Serverless Framework configuration file, serverless.yml in the root of the directory:

service: serverless-go-service

frameworkVersion: '^3'

provider:
  name: aws
  architecture: arm64
  runtime: provided.al2
  region: us-east-1

plugins:
  - serverless-go-plugin

package:
  individually: true

functions:
  hello:
    handler: bootstrap
    events:
      - httpApi: '*'
    package:
      patterns:
        - '!**/**'
        - './bin/hello/bootstrap'

Okay, that's a lot of boilerplate setup for a single function, so let's go over some of the parts in more details. When it comes to Go Lambda functions, for a long time we had an option of using a dedicated go1.x runtime, or custom runtimes such as provided.al2. With the deprecation of go1.x scheduled for December 31, 2023, it leaves us the provided family of runtimes as our only option. In our case, we will be using provided.al2, which is the latest available runtime, based on Amazon Linux 2023. When using provided runtimes, there is a requirement to name the executable for your function bootstrap. Serverless Framework doesn't have any built-in utilities for handling Go code, so we're simply taking advantage of package configuration where we're specifying individual packaging and we're pointing to a specific binary that should be included in our function package. But how to get that binary? Right now we only have a filepath, let's add some code!

Let's create the following file under the path functions/hello/main.go:

package main

import (
	"context"

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

func handler(ctx context.Context) (string, error) {
	return "hello there!", nil
}

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

It's a very simple function that will just return hello there! string when called. Let's also make sure to add our dependency by running the following command in the root of the project:

go get github.com/aws/aws-lambda-go

Now that we have our code ready, we also need to compile it to the expected bootstrap file. We can do it, for example, with the following command from the root of the project.

GOOS=linux go build -ldflags="-s -w" -o ./bin/hello/bootstrap ./functions/hello

After that, we're finally ready to deploy our project. But what if we could make it much more convenient?

Making things easier with serverless-go-plugin

The approach presented above works okay, but it requires separate compliation step and extra package configuration for each of our functions, so clearly the developer experience is not perfect in such setup. Fortunately, we can avoid all of that by taking advantage of serverless-go-plugin. It is a plugin created by Maciej Winnicki, that automates away all these steps, seamlessly integrating with both serverless deploy and serverless deploy function commands. In order to take advantage of the plugin, we need to install it first. The easiest way to do so will be to install it locally by running the following commands:

npm init -y

npm i --save-dev serverless-go-plugin

In the process, we also initialized package.json file as we didn't need it previously.

Next, let's modify our serverless.yml configuration to take advantage of the plugin:

service: serverless-go-service

frameworkVersion: '^3'

provider:
  name: aws
  architecture: arm64
  deploymentMethod: direct
  runtime: provided.al2
  region: us-east-1

plugins:
  - serverless-go-plugin

functions:
  hello:
    handler: ./functions/hello
    events:
      - httpApi: '*'

custom:
  go:
    supportedRuntimes: ["provided.al2"]
    buildProvidedRuntimeAsBootstrap: true

Let's try to dissect the changes a little bit. The main config for the plugin is placed under custom.go, where we specify runtimes that we want to recognize. In our case it's provided.al2. Additionally, we need to specify buildProvidedRuntimeAsBootstrap: true. This is because the plugin was initially created with go1.x runtime in mind and using provided runtimes was an alternative. In the future it might change and this configuration might be no longer needed, but for now we need to keep it. Additionally, now we can simply configure our handler to point to our code, instead of reconfiguring package.patterns for each of the functions. Now, during sls deploy, serverless-go-plugin will take care of compliation and packaging of all our functions.

Note: The serverless-go-plugin is not perfect and at the moment it does not support the newest provided.al2023 runtime, that's why the example is using provided.al2 instead.

Summary

Thanks to serverless-go-plugin, we were able to simplify the configuration and packaging process for our Go-based services. If you'd like to try it out yourself, you can find the full example from this blog post here. Thanks for reading!