DEV Community

Cover image for Building an API with AWS SAM and Go
Daniel Rayo
Daniel Rayo

Posted on

Building an API with AWS SAM and Go

AWS SAM is a great way to deploy web apps through infrastructure as code (IAC). I recently tried to use it for a project in my job, but I stumble with a harsh reality...

Go is the Ugly Duckling of AWS ๐Ÿฆ†

The chapter dedicated to Go in AWS SAM documentation, is really short and vague and suggest to REPEAT OUR SOURCE CODE A LOT! Having one go.mod, go.sum and utility functions for each lambda ๐Ÿ‘บ!

I wrote this post for you, someone whoโ€™s probably just as lost as I was ๐Ÿ˜ตโ€๐Ÿ’ซ. Letโ€™s figure this out together!

This will be a 2 parts series:

  1. File Structure to NOT REPEAT CODE (This post)
  2. Custimize builds with Makefiles and Dockerfiles

Some Context on Go Runtimes

At the current time, Go runtime for lambdas is not supported as-is. This means that there is not specific option on AWS lambdas to specify that your code is written in Go. Instead, AWS provides 2 generic runtimes ๐Ÿง:

  • al2 (Amazon Linux 2)
  • al2023 (Amazon Linux 2023)

Which refers to the OS in which the lambda will be run. Is recommended to use al2023 since is newer and compatible with AWS Graviton processors which deliver better performance for a better price.

Anyway, this runtimes asks us to provide an executable (usually named bootstrap), which will be executed on every lambda function. So instead of deliver code to the lambdas, we deliver an executable that we compiled with Go previously. Simple right?

This also removes the necessity of using lambda layers, that languages like JS need, since all common dependencies will already be packed in the compiled executable ๐Ÿ˜ผ.

The problem

So, how we build that executable? AWS suggest us that every lambda should be stored on a folder along with its go.mod and go.sum, the template they provides us looks like this:

.
โ”œโ”€โ”€ hello-world/
โ”‚   โ”œโ”€โ”€ go.mod
โ”‚   โ”œโ”€โ”€ go.sum
โ”‚   โ””โ”€โ”€ main.go
โ”œโ”€โ”€ events/
โ”‚   โ””โ”€โ”€ ...
โ”œโ”€โ”€ samconfig.toml
โ””โ”€โ”€ template.yaml
Enter fullscreen mode Exit fullscreen mode

And this is the function definition on template.yaml

  HelloWorldFunction:
    Type: AWS::Serverless::Function 
    Metadata:
      BuildMethod: go1.x
    Properties:
      CodeUri: hello-world/
      Handler: bootstrap
      Runtime: provided.al2023
      Architectures:
        - x86_64
      Events:
        CatchAll:
          Type: Api
          Properties:
            Path: /hello
            Method: GET
Enter fullscreen mode Exit fullscreen mode

If we look the Lambda definition, we learn that

  1. BuildMethod: go1.x We use AWS built-it Go builder, to build the executable for us
  2. CodeUri: hello-world/ The lambda code will exclusively live in this directory.
  3. Handler: bootstrap The name of the executable will be bootstrap
  4. Runtime: provided.al2023 this will be the runtime.

Do you see the problem? At the moment we need a second lambda, we have to create a A NEW DIRECTORY WITH ITS OWN go.mod, go.sum and dependencies, ยฟWhat if we want to share a utility function between to lambdas? TO BAAD ๐Ÿ˜ฟ !you will have to copy the same file in the new lambda folder. Leaving with a file structure that looks like this:

.
โ”œโ”€โ”€ function1/
โ”‚   โ”œโ”€โ”€ go.mod
โ”‚   โ”œโ”€โ”€ go.sum
โ”‚   โ”œโ”€โ”€ main.go
โ”‚   โ””โ”€โ”€ SHAREDFUNC.go
โ”œโ”€โ”€ function2/
โ”‚   โ”œโ”€โ”€ go.mod
โ”‚   โ”œโ”€โ”€ go.sum
โ”‚   โ”œโ”€โ”€ main.go
โ”‚   โ””โ”€โ”€ SHAREDFUNC.go
โ”œโ”€โ”€ events/
โ”‚   โ””โ”€โ”€ ...
โ”œโ”€โ”€ samconfig.toml
โ””โ”€โ”€ template.yaml
Enter fullscreen mode Exit fullscreen mode

This is gross ๐Ÿคฎ! There is a lot of repeated code! And it will become worst the more lambdas we add. There must be a better way!

The Solution

Since I wanted to share go.mod, go.sum and utility code through all lambdas, I came up with this structure:

.
โ”œโ”€โ”€ cmd/
โ”‚   โ”œโ”€โ”€ function1/
โ”‚   โ”‚   โ””โ”€โ”€ function1.go  # contains main()
โ”‚   โ””โ”€โ”€ function2/
โ”‚       โ””โ”€โ”€ function2.go  # contains main()
โ”œโ”€โ”€ internal/
โ”‚   โ””โ”€โ”€ SHAREDFUNC.go
โ”œโ”€โ”€ events/
โ”‚   โ””โ”€โ”€ ...
โ”œโ”€โ”€ go.mod
โ”œโ”€โ”€ go.sum
โ”œโ”€โ”€ samconfig.toml
โ””โ”€โ”€ template.yaml
Enter fullscreen mode Exit fullscreen mode
  1. I factored out all the common code on an internal/ folder,
  2. Place the go.mod and go.sum files in the root dir
  3. Move all lambda entry points to /cmd (in Go exist this convention that whenever a project produces more than one executable, the entry points are placed within a cmd dir)

Now I just had to notify AWS SAM of this new structure ๐Ÿ˜ธ! And I found the solution just by tweaking around the values of CodeUri and Handler.

The Secret ๐Ÿ”’

Seems that if you

  • Move the go.mod and go.sum to the root folder
  • Set CodeUri, to wherever folder your function entry point is.

SAM will detect it automatically and build with root dependencies and internal/ code ๐ŸŽ‰ ๐ŸŽ‰ ๐ŸŽ‰

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: go1.x
    Properties:
      CodeUri: ./cmd/function1/
      Handler: bootstrap
      Runtime: provided.al2023
      ... # More properties ...
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: go1.x
    Properties:
      CodeUri: ./cmd/function2
      Handler: bootstrap
      Runtime: provided.al2023
      ... # More properties ...

Enter fullscreen mode Exit fullscreen mode

Can it be better?

Yeeees โœจ, we will discuss more ways to customize your Go compilation in the next post!

Top comments (0)