You are looking to use serverless.com for Golang lambda deployments or want to switch to arm64 images? This article will guide you through the process and point out some limitations and things to consider.
TL;DR;
Using ARM64 runtimes with serverless.com for Golang lambdas on AWS can provide significant performance and cost advantages. While there may be some limitations and considerations to keep in mind, such as the possible deprecation of the go1.x runtime and the need to provide bootstrap files for the provided.al2 runtime, the benefits of switching to ARM64 are worth considering. The code for this guide is available in this GitHub repository: https://github.com/matthiasbruns/golang-sls-provided.al2-arm64.
Why should you switch to ARM64?
Performance and cost
ARM64 runtimes are faster and cheaper than x86_64 runtimes on AWS lambda. ARM processors use less power and generate less heat, which lowers the cost of running them. Additionally, ARM64 runtimes are optimized for high-performance computing and can lead to faster execution times for applications. With recent advancements in ARM64 technology, it's becoming an increasingly popular choice for serverless computing.
Advantages of using arm64 architecture
Lambda functions that use arm64 architecture (AWS Graviton2 processor) can achieve significantly better price and performance than the equivalent function running on x86_64 architecture. Consider using arm64 for compute-intensive applications such as high-performance computing, video encoding, and simulation workloads.
The Graviton2 CPU uses the Neoverse N1 core and supports Armv8.2 (including CRC and crypto extensions) plus several other architectural extensions.
Graviton2 reduces memory read time by providing a larger L2 cache per vCPU, which improves the latency performance of web and mobile backends, microservices, and data processing systems. Graviton2 also provides improved encryption performance and supports instruction sets that improve the latency of CPU-based machine learning inference.
For more information about AWS Graviton2, see AWS Graviton Processor.
💡 Taken directly from AWS https://docs.aws.amazon.com/lambda/latest/dg/foundation-arch.html#foundation-arch-adv
Possible deprecation of go1.x runtime
AWS will drop the go1.x runtime support for lambda end of June according to a migration blog post on their blog. They offer an official way to migrate to the new provided.al2 runtime using SAM templates. But what, if you use serverless.com?
How to migrate to ARM64?
To simulate a migration, we will build a sample application with two lambda endpoints using the old go1.x runtime. After that, I will guide you through the migration process.
Bootstrapping serverless.com with Golang using x86
You can find the bootstrapped project here: https://github.com/matthiasbruns/golang-sls-provided.al2-arm64/tree/chapter/bootstrapping.
There is no real complexity in the project itself, since this does not impact the sls setup.
Project Layout
The project layout looks like this
serverless: files created by serverless after running
sls deploy
build: binaries built for serverless to deploy to AWS lambda
cmd: holds the main.go files which will be compiles into binaries
go.mod: go mod file which holds dependencies
Makefile: builds the binaries - you can also use Taskfile as an alternative
serverless.yml: sls configuration file
The Code
The code is pretty straightforward and assumes that the lambda will be triggered by an AWS API Gateway2 call.
// cmd/lambda/hello/main.go
package main
import (
"context"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
func main() {
// we are simulating a lambda behind an ApiGatewayV2
lambda.Start(handler)
}
func handler(ctx context.Context, request events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
return events.APIGatewayV2HTTPResponse{
StatusCode: 200,
Body: "Hello",
}, nil
}
This code starts a lambda handler, which always returns http-code 200 with “Hello” in the body. The other main file in the world
package simply returns "World" instead of "Hello"
// cmd/lambda/world/main.go
package main
import (
"context"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
func main() {
// we are simulating a lambda behind an ApiGatewayV2
lambda.Start(handler)
}
func handler(ctx context.Context, request events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
return events.APIGatewayV2HTTPResponse{
StatusCode: 200,
Body: "World",
}, nil
}
The Makefile for building
The make file is pretty simple. It builds x86 binaries from both hello
and world
code branches and stores the output into the build/lambda
folder. You can run the make file by calling make build
.
build:
echo "Building lambda binaries"
env GOOS=linux GOARCH=amd64 go build -o build/lambda/hello cmd/lambda/hello/main.go
env GOOS=linux GOARCH=amd64 go build -o build/lambda/world cmd/lambda/world/main.go
💡 For the sake of this example, I omitted build flags that speed up lambda cold starts.
The serverless configuration file
# serverless.yml
service: golang-go1x-x86
frameworkVersion: "3"
configValidationMode: error
provider:
name: aws
runtime: go1.x
architecture: x86_64
stage: ${opt:stage, 'dev'}
region: ${opt:region, 'eu-central-1'}
httpApi:
cors: true
name: ${self:service}-${self:provider.stage}
functions:
hello:
name: hello
handler: build/lambda/hello
events:
- httpApi:
path: /x86/hello
method: get
world:
name: world
handler: build/lambda/world
events:
- httpApi:
path: /x86/world
method: get
The serverless file contains the following elements that are important for this example:
service: name of the service, which will be used to name the Cloudformation stack
provider: where sls deployed the stack to - we use AWS in this example. It also defines shared definitions like runtime and architecture, which will be used by the lambda functions.
functions: definition of the lambdas and their triggers. In this case, we use ApiGatewayV2 triggers, indicated by
httpApi
. We also define which binaries to use by settings thehandler
to the binary path relative to theserverless.yml
file.
Deploying the stack
To deploy the stack, you need an AWS account as well as CLI credentials in your path. I assume you have this setup already.
You only have to run sls deploy
and serverless.com will take care of everything you need.
After successfully deploying your stack, you can call the ApiGateway endpoints. They will be written to your console by sls. Since they are GET endpoints, you can simply open them with your browser.
Let’s also check how the lambda looks on AWS:
Migrating to Arm64 with bootstrap files
After setting up the x86 project, we can begin to migrate to ARM64. To do this, we need to provide a so called bootstrap
files for the provided.al2
runtime. According to AWS docs, we cannot name this file differently and it needs to be in the root of the ZIP-file we upload to be used in the lambda. You can check the code here: https://github.com/matthiasbruns/golang-sls-provided.al2-arm64/tree/chapter/migrate
Using a custom runtime
To use a custom runtime, set your function's runtime to
provided.al2
. The runtime can be included in your function's deployment package, or in a layer.Example function.zip
. ├── bootstrap ├── function.sh
If there's a file named
bootstrap
in your deployment package, Lambda runs that file. If not, Lambda looks for a runtime in the function's layers. If the bootstrap file isn't found or isn't executable, your function returns an error upon invocation.💡 Taken from AWS docs https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html
So let’s do it!
Updating Makefile and serverless.yml to run on ARM64
To migrate from x86 to arm64, we only need to change the build files. No code changes are required.
The Makefile for building arm64 Golang binaries
Let’s update the binary compilation first. You need to change GOARCH
from amd64 to arm64 and rename the output files to bootstrap
.
We also need to add a new task called zip
, which zips each binary in a separate archive, since we need to upload each binary on it’s own through serverless to prevent naming conflicts in the .zip files.
build:
echo "Building lambda binaries"
env GOOS=linux GOARCH=arm64 go build -o build/lambda/hello/bootstrap cmd/lambda/hello/main.go
env GOOS=linux GOARCH=arm64 go build -o build/lambda/world/bootstrap cmd/lambda/world/main.go
zip:
zip -j build/lambda/hello.zip build/lambda/hello/bootstrap
zip -j build/lambda/world.zip build/lambda/world/bootstrap
The serverless configuration file for provided.al2
Now for the serverless.yml
, we have to change two things:
# serverless.yml
service: golang-provided-al2-arm64
frameworkVersion: "3"
configValidationMode: error
provider:
name: aws
runtime: provided.al2 # <- change from go1.x to provided.al2
architecture: arm64 # <- change from x86_64 to arm64
stage: ${opt:stage, 'dev'}
region: ${opt:region, 'eu-central-1'}
httpApi:
cors: true
name: ${self:service}-${self:provider.stage}
package:
individually: true # <- package each function individually, to prevent file name conflicts
functions:
hello:
name: hello-arm64
handler: bootstrap # <- the handler name must be bootstrap and in the root of the zip
package:
artifact: build/lambda/hello.zip # override the default artifact handling to use the built zip
events:
- httpApi:
path: /arm64/hello
method: get
world:
name: world-arm64
handler: bootstrap # <- the handler name must be bootstrap and in the root of the zip
package:
artifact: build/lambda/world.zip # override the default artifact handling to use the built zip
events:
- httpApi:
path: /arm64/world
method: get
Let’s go through the changes.
First, we change runtime
and architecture
to the new target settings..
We also need to change how sls packages the .zip files. By default, it takes the whole project and puts it into one zip and uploads this to AWS, including all binaries built with Makefile. Since we need to provide a boostrap
file in the root of the zip, we cannot use this packaging method anymore. Each function is not packaged individually, which also leads to smaller .zip archived and faster cold-starts.
The functions also to change a bit. In a nutshell, we change the handler
to point to the new bootstrap
file in the root of the .zip. Since we take over packaging, we have to point what .zip sls should upload to AWS. This is done in the functions package
definition.
If we would not have done the changes, the deployment would have worked, but the lambdas would have crashed with some error like this:
Error: Couldn't find valid bootstrap(s): [/var/task/bootstrap /opt/bootstrap]
Deploy and test
💡 Recompile and zip the binaries with
rm -rf build && make build && make zip
, otherwise you will deploy x86 binaries to your arm64 lambdas!
After applying those two changes, we can redeploy the stack with sls deploy
.
Let’s try out our new arm64 runtimes!
Looking good. Let’s also check the deployed lambdas on AWS
As you can see, we are now running on Linux 2 with the bootstrap handler and arm64.
Conclusion
In conclusion, switching to ARM64 can provide significant performance and cost advantages for running Golang lambdas on AWS. While there may be some limitations and considerations to keep in mind, such as the possible deprecation of the go1.x runtime and the need to provide bootstrap files for the provided.al2 runtime, the benefits of switching to ARM64 are worth considering. With the help of serverless.com, migrating to ARM64 can be a relatively straightforward process.
You can find the whole code in this GitHub repository: https://github.com/matthiasbruns/golang-sls-provided.al2-arm64