DEV Community

Cover image for Customize Go Builds on AWS SAM with Dockerfiles and Makefiles
Daniel Rayo
Daniel Rayo

Posted on

Customize Go Builds on AWS SAM with Dockerfiles and Makefiles

This post is the second part on my series Building APPS with AWS SAM and Go feel free to check the first part. In last chapter we discover that AWS don't provide much info on how to structure a scalable Go project without REPEATING A LOT OF CODE.

Now I will show you ways in which we can control our build steps with Dockerfiles and Makefiles.

The code showed in this lecture will be in this repo:
https://github.com/DanielRasho/AWS-SAM-go
Check the different git branches for each use case 😉.

Lets get started 🏃💨!

The problem

After came up with the new project structure, I decided to use Nix ❄️ for managing all my project dependencies (languajes, tools, libraries). The way Nix works, is by creating a new temporary shell with all the dependencies you request it.

I noted that that whenever i tried to run executables that where built within a Nix shell, they throw a weird error:

libc.so.6 not found in /nix/23fj39chsggb09s.libc
Enter fullscreen mode Exit fullscreen mode

And the lambda will stop execution. It took my an ENTIRE DAY to figure out the cause: When compiling, Go sometimes link C libraries libraries dynamically to the executable, by specifying the route on the system to where the library is. This where the linked libraries to the executables built with Nix:

$ ldd bootstrap 
        linux-vdso.so.1 (0x00007ffff7fc4000)
        libresolv.so.2 => /nix/store/65h17wjrrlsj2rj540igylrx7fqcd6vq-glibc-2.40-36/lib/libresolv.so.2 (0x00007ffff7fac000)
        libpthread.so.0 => /nix/store/65h17wjrrlsj2rj540igylrx7fqcd6vq-glibc-2.40-36/lib/libpthread.so.0 (0x00007ffff7fa7000)
        libc.so.6 => /nix/store/65h17wjrrlsj2rj540igylrx7fqcd6vq-glibc-2.40-36/lib/libc.so.6 (0x00007ffff7c00000)
        /nix/store/65h17wjrrlsj2rj540igylrx7fqcd6vq-glibc-2.40-36/lib/ld-linux-x86-64.so.2 => /nix/store/65h17wjrrlsj2rj540igylrx7fqcd6vq-glibc-2.40-36/lib64/ld-linux-x86-64.so.2 (0x00007ffff7fc6000)
Enter fullscreen mode Exit fullscreen mode

Nix has a not-standard way to store dependencies, and since the lambdas run on isolated Docker containers, when they tried to execute the code, and try to find the linked libraries on those paths THEY COULDN'T FIND THEM, since those files lived only on my local Nix installation.

I had to find a way to tell AWS SAM how to compile my code , to control this linked libraries.

Ways to deliver our Go project to AWS

So first a bit of context. We can deliver our code to AWS in 2 presentations:

Zip files 📁

We compile the code locally on our machine, and sent the raw executable to AWS on .zip. Amazon will simply copy the executable on the docker containers. This option provides the fastest Cold Starts.

Docker Images 🐋

Instead of giving AWS the executable. We give them the instructions to compile it WITHIN the docker container in which will be run. This option ensures full compatibility at the costs of slower Cold Starts

The solutions

At the end I went for Dockerfiles, since I wanted to still be using Nix. But I will show you both methods

Zip files 📁

To specify the build steps for a Zip file we will need a structure like this one (notice the Makefile at the root):

.
├── cmd/
│   ├── function1/
│   │   └── function1.go  # contains main()
│   └── function2/
│       └── function2.go  # contains main()
├── internal/
│   └── SHAREDFUNC.go
├── Makefile
├── go.mod
├── go.sum
├── samconfig.toml
└── template.yaml
Enter fullscreen mode Exit fullscreen mode

Then within the Makefile we provide a command for each function following the pattern: build-<Function_Name> (this is enforced by AWS SAM) followed by our built instructions, AWS has this article in how to set this build commands

.PHONY: build

build:
    sam build

build-HelloWorldFunction:
    GOARCH=amd64 GOOS=linux go build -tags lambda.norpc -o bootstrap ./cmd/function1/main.go
    cp ./bootstrap $(ARTIFACTS_DIR)

build-ByeWorldFunction:
    GOARCH=amd64 GOOS=linux go build -tags lambda.norpc -o bootstrap ./cmd/function2/main.go
    cp ./bootstrap $(ARTIFACTS_DIR)

Enter fullscreen mode Exit fullscreen mode

$(ARTIFACTS_DIR) is an env variable provided by SAM automatically

Finally we just have to notify SAM of this new process:

  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Metadata:
      BuildMethod: makefile
    Properties:
      CodeUri: ./
      Handler: bootstrap
      Runtime: provided.al2023
      Architectures:
        - x86_64
      Events:
        CatchAll:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: GET
Enter fullscreen mode Exit fullscreen mode

We use BuildMethod: makefile, also SAM will look for the Makefile only where CodeUri specifies.

Docker Images 🐋

The process is similar for building docker images, we create a Dockerfile and .dockerignore at the root dir:

.
├── cmd/
│   ├── function1/
│   │   └── function1.go  # contains main()
│   └── function2/
│       └── function2.go  # contains main()
├── internal/
│   └── SHAREDFUNC.go
├── Dockerfile
├── .dockerignore
├── go.mod
├── go.sum
├── samconfig.toml
└── template.yaml
Enter fullscreen mode Exit fullscreen mode

Within the Dockerfile we specify the building steps. I used the ARG statement to tell Docker where to look for then entry point of my lambda at build time.

FROM public.ecr.aws/docker/library/golang:1.19 as build-image
ARG ENTRY_POINT  # !IMPORTANT
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -tags lambda.norpc -o lambda-handler ${ENTRY_POINT}
FROM public.ecr.aws/lambda/provided:al2023
COPY --from=build-image /src/lambda-handler .
ENTRYPOINT ./lambda-handler
Enter fullscreen mode Exit fullscreen mode

And finally, made some teaws on template.yaml:

  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Metadata:
      DockerTag: provided.al2023-v1
      DockerContext: ./  # Where to look for the dockerfile and code.
      Dockerfile: Dockerfile
      DockerBuildArgs: 
        ENTRY_POINT: "./cmd/function1/main.go" # Location of ENTRY POINT
    Properties:
      PackageType: Image
      Architectures:
        - x86_64
      Events:
        CatchAll:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: GET
Enter fullscreen mode Exit fullscreen mode

Pay attention to the Metadata, and PackageType:Image sections. Remember the ARG ENTRY_POINT we defined on Dockerfile we are passing it on DockerBuildArgs, this way we just need on Dockerfile for all lambdas

Wrap up

There's not wrap up, Its already late at night, I should go to sleep 😴 😴 😴, and you should do it too.

Top comments (0)