Deploy a Go Microservice in AWS

by Bautista Bambozzi — 20 min

example

In this article, we'll showcase how to deploy a Go microservice to AWS Lambda. We'll also launch it on ARM instead of x86, to further leverage the cost-efficiency of ARM. Everything we'll do in this article will fall under the free tier. However, you're expected to double-check you're not accidentally creating other resources. You're also expected to have the following:

      B) Have the AWS CLI credentials installed and configured.
      C) Have the AWS CDK installed and configured.
      E) Have a zip utility or equivalent installed (It's pre-installed in Linux).

Bootstrapping the application

We'll start off with a simple one-liner to create our working directory, bootstrap the application and get our dependencies. After that, we'll get ready to rock-n-roll 🎸!


mkdir hello-lambda && cd hello-lambda && cdk init --language go && go get

The AWS CDK has now created the hello-lambda.go file with the following:


// hello-lambda/main.go
package main

import (
	"github.com/aws/aws-cdk-go/awscdk/v2"
	// "github.com/aws/aws-cdk-go/awscdk/v2/awssqs"
	"github.com/aws/constructs-go/constructs/v10"
	"github.com/aws/jsii-runtime-go"
)

type HelloLambdaStackProps struct {
	awscdk.StackProps
}

func NewHelloLambdaStack(scope constructs.Construct, id string, props *HelloLambdaStackProps) awscdk.Stack {
	var sprops awscdk.StackProps
	if props != nil {
		sprops = props.StackProps
	}
	stack := awscdk.NewStack(scope, &id, &sprops)

	// The code that defines your stack goes here

	// example resource
	// queue := awssqs.NewQueue(stack, jsii.String("HelloLambdaQueue"), &awssqs.QueueProps{
	// 	VisibilityTimeout: awscdk.Duration_Seconds(jsii.Number(300)),
	// })

	return stack
}

func main() {
	defer jsii.Close()

	app := awscdk.NewApp(nil)

	NewHelloLambdaStack(app, "HelloLambdaStack", &HelloLambdaStackProps{
		awscdk.StackProps{
			Env: env(),
		},
	})

	app.Synth(nil)
}

func env() *awscdk.Environment {
	// If unspecified, this stack will be "environment-agnostic".
	// Account/Region-dependent features and context lookups will not work, but a
	// single synthesized template can be deployed anywhere.
	//---------------------------------------------------------------------------
	return nil
    // Some comments by aws here
	 }
}

Creating our lambda function

Let us create a new directory where we'll create our lambda function.


mkdir lambda-function && cd lambda-function && go mod init lambda-function && touch main.go

We'll then proceed to our main.go located inside lambda-function and create a simple function that does the following:

      A) Unmarshalls JSON into a Go struct
      B) Examines the contents of a property inside the struct
      C) Returns an error if the property is empty / not found
      D) Returns the very same property is everything is alright
      E) Create the main() function that will execute our lambda.

We can do a simple implementation like so:


// hello-lambda/lambda-function/main.go
package main
import (
	"fmt"
	"github.com/aws/aws-lambda-go/lambda"
)

type Letter struct {
	Content string `json:"content"` // A)
}

func HandleRequest(letter Letter) (string, error) {
	if letter.Content == "" { // B)
		return "", fmt.Errorf("letter content should not be empty") // C)
	}
	return fmt.Sprintf("Received letter %+v\n", letter), nil // D)
}

func main() {
	lambda.Start(HandleRequest) // E)
}

Once we've set up our lambda function, we're ready to move on to compiling our main.go file to a native executable. We're going for gold here, so we'll compile for the ARM architecture. This will make our lambdas even more cost-effective than they already are. We need to set the build to target the Linux OS and the ARM architecture, respectively. We name the output 'bootstrap' and then proceed to zip it.


GOOS=linux GOARCH=arm64 go build -o bootstrap && zip lambda-function.zip bootstrap

You should have a lambda-function.zip in your current working directory (hello-lambda/lambda-function/lambda-function.zip). We will now go back to our previous directory, to hello-lambda/hello-lambda.go. We're going to set up the CDK to deploy our lambda function ✨! We'll want to do the following:

      A) Add a new lambda function to our Stack.
      B) Select the runtime (we'll use Amazon Linux 2023)
      C) Select the location of the asset
      D) Provide the handler (In the case of Go, we'll always go with main)
      E) Select the arm architecture
      F) Return our new stack with all our additions incorporated!



func NewHelloLambdaStack(scope constructs.Construct, id string, props *HelloLambdaStackProps) awscdk.Stack {
	var sprops awscdk.StackProps
	if props != nil {
		sprops = props.StackProps
	}
	stack := awscdk.NewStack(scope, &id, &sprops)

	// A)
	awslambda.NewFunction(stack, jsii.String("hello-lambda"), &awslambda.FunctionProps{
		Runtime: awslambda.Runtime_PROVIDED_AL2023(), // B)
		Code:    awslambda.AssetCode_FromAsset(jsii.String("lambda-function/lambda-function.zip"), nil), // C)
		Handler: jsii.String("main"), // D)
		Architecture: awslambda.Architecture_ARM_64(), // E)

	})

	// F)
	return stack
}

Deploying to AWS

Now, for the grand finale, we'll deploy our app to AWS. We can evaluate the changes that will be done and deployed by running

cdk diff

After we've confirmed everything is alright, we're allowed to smile as we watch it deploy with the elegant:

cdk deploy

After responding Y to the classic 'are you sure?' prompt, we'll go back to the AWS Lambda console to check that our lambda is working correctly. In the event JSON section, place the following JSON object:


{
  "content": "Hello, Lambda!"
}

Then, scroll upwards and click on the 'test' button to send the payload. And Voila 👌! You've created your own lambda function in the cloud 🌤️! You can now destroy your deployment by running

cdk destroy