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
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)
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
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)
$(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
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
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
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
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)