flowbased

Building a Fast Serverless API with Lambda Functions in Go

🗓️  

AWS has deprecated the Go runtime for its Lambda service. The good news is that there are serious performance benefits associated with using a custom runtime based on Amazon Linux 2, especially when it comes to cold starts.

I typically write small helper and API functions for personal projects in Node.js and Python. Given the recent improvements and my appreciation for Go’s elegance, I see little reason not to write my next functions in Go. AWS even provides a lib for that. So, here we Go (pun intended).

One of my pet projects is a nutrition app that works with OpenAI APIs to estimate the nutritional content of meals by text description and picture. I migrated the functions of that project to Go. I invoke them via Lambda function URLs in order to provide a simple API. Here is the boilerplate code I wrote for this. The example function receives a POST request and returns the “message” field from the POST body ({ “message”: “Hello World!” }) in its response. Replace this with your own logic.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

func request(ctx context.Context, event events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) {
	var request map[string]string
	err := json.Unmarshal([]byte(event.Body), &request)
	if err != nil {
		return events.LambdaFunctionURLResponse{}, fmt.Errorf("error parsing request body: %v", err)
	}
	message, exists := request["message"]
	if !exists {
		return events.LambdaFunctionURLResponse{}, fmt.Errorf("message not found in request")
	}

	return events.LambdaFunctionURLResponse{StatusCode: 200, Body: "echo: " + message}, nil
}

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

My project includes multiple Lambda functions. I have organized these using the following directory structure: each subdirectory contains the code for one Lambda function.

The custom runtime requires that the compiled binary be named “bootstrap” and placed inside a ZIP file. To execute tests, compile the binary for the Amazon Linux 2 platform, and package the ZIP file, I wrote a small Makefile:

# Variables
GOOS=linux
GOARCH=amd64
DIRS=nutritional-value-estimator nutritional-value-estimator-image
BINARY_NAME=bootstrap
ZIP_NAME=lambda-handler.zip

# Default target
all: test $(addsuffix /out/$(ZIP_NAME),$(DIRS))

# Rule for running tests
test:
	@echo "Running tests..."
	@go test -v ./...

# Rule for building Go binaries
%/out/$(BINARY_NAME): %/main.go
	mkdir -p $(dir $@)
	GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $@ $<

# Rule for zipping the binaries
%/out/$(ZIP_NAME): %/out/$(BINARY_NAME)
	cd $(dir $<) && zip $(ZIP_NAME) $(BINARY_NAME)

.PHONY: all test

The makefile produces a file “lambda-handler.zip” in the out directory of each subdirectory, ready to be uploaded to AWS Lambda, or deployed via IaC. This results in an ultra-fast Lambda function with a cold start time of 65ms and a subsequent runtime of just 1ms.

Next post → ← Previous post  

Comments