Simplify Golang Multi Architecture Container Builds
Tags: go, docker, drone, containerapps
Published: 2022-10-26 01:47:10

With arm64 based laptops getting very popular it has become a need for the container developers to build a multi architecture images e.g. build amd64 on arm64 machine. 

In my case I use Apple M1, as part of my day job I build,test  and deploy container applications; by default my macbook produces _linux/arm64_ images. If I need to deploy the same image to any cloud services e.g. Google Cloud Run or Google Kubernetes Engine , then I need to have the _linux/amd64_ image as well. 

For such use cases the we usually write multiple Dockerfile for each architecture, a manifest file etc., there nothing wrong with that approach but then it will soon become too hard to maintain.

<img  width="100%" height="100%" src="https://media.giphy.com/media/IHnROpQICe4kE/giphy.gif"/>

So what we need is a tool chain that can,

- Have same process to build image locally as well as for cloud usage. I found [GoReleaser](https://goreleaser.com) to be apt for this requirement.

- Is declarative, typically mapping to standard steps that will be part of building a golang application i.e. test, build and push image to container registry. [Drone](https://drone.io) help to setup a build pipeline in declarative way and has plugins for all major tools/platforms including one for GoReleaser.

- Build multi architecture images. [Buildx](https://docs.docker.com/build/) helps us to build multi architecture image using the standard docker build semantics. Adding to our tools there is a Drone plugin Drone Buildx Plugin that allow us to just plug it into our pipeline.

The end to end demo of this blog post is available on my [GitHub Repo](https://github.com/kameshsampath/go-hello-world.git). 

Clone it one to your laptop for quick reference,

```shell
git clone https://github.com/kameshsampath/go-hello-world.git \
&& cd "$(basename "$_" .git)"
export DEMO_HOME="$PWD"
```

For the rest of the post we will refer to this cloned folder as `$DEMO_HOME`.

- For this demo we will be using a short lived container registry called [ttl.sh](https://ttl.sh) for pushing and pulling the demo app image. 

- Download and [Drone CLI](https://docs.drone.io/cli/install/) to your path.

> **NOTE**: You are welcome to use your own registry. Please check the [Drone Buildx Plugin](https://drone-plugin-index.geekdocs.de/plugins/drone-docker-buildx/) documentation on how to configure the extra parameters like `username` and `password`.

Let us setup some environment variables that we will be using as part of the demo
```shell
# a unique uid as image identifier, it needs to be in the lowercase
export IMAGE_NAME=$(uuidgen | tr '[:upper:]' '[:lower:]')
# short lived image for 10 mins
export IMAGE_TAG=10m
```
And for convenience we will save these values on to `.env` file which we will use to pass environment variables to Drone Pipelines and Docker runs.

```shell
envsubst < "$DEMO_HOME/.env.example" | tee "$DEMO_HOME/.env"
```

> **NOTE**: The example above uses [envsubst](https://medium.com/r?url=https%3A%2F%2Fwww.man7.org%2Flinux%2Fman-pages%2Fman1%2Fenvsubst.1.html) to update the file. If you don't have envsubst installed you can manually update the `.env` file.

Let us examine the Drone pipeline `.drone.yml` that we have,

{% embed https://gist.github.com/kameshsampath/ac832564c0fe31bc69c7e9c6d97273de %}

It is very simple Drone Pipeline  that has three steps ,

- **test** that runs the golang tests
- **build** that uses the GoReleaser to build the golang application
- **push** that pushes the application to the container registry that we had configured earlier. 

The push steps also specifies the platforms `linux/arm64` and `linux/amd64` that instructs the _docker buildx_ build to perform multi architecture build that will produce a container image that is compatible with the respective platforms.

The push step uses the Dockerfile above to perform the build. 

{% embed https://gist.github.com/kameshsampath/df7c37599345c28522bae6d8225a5300 %}

There are few important things to note,

- The comment `# syntax=docker/dockerfile:1.4` instructs the docker build to use buildx Dockerfile semantics from docker/dockerfile:1.4 . Though it does not have big significance in our case but we ensure the we use buildx to perform the builds. Buildx is default from Docker v18.x and above.

- The `ARG TARGETARCH` allows the docker build to know the architecture thats is being build for e.g. amd64 or arm64 etc.,

- The base image `gcr.io/distroless/base` allows to build a tiny image with just the binary of our application in it :).

That's pretty much need to build our multi architecture golang application, run the following drone command to start that pipeline that will build push the image to the container registry.

```shell
drone exec --trusted --env-file=.env
```

Once the build is done run the following command to start the built container locally,

```shell
docker-compose up
```

Doing a simple `curl` to <http://localhost:8080> that should say "Hello World".

Lastly, a small caveat when using GoReleaser. GoReleaser, when cross compiling the code, creates distribution folders like linux_arm64 and `linux_amd64_v1` under the **dist** folder, where the built binaries for the platforms  (`linux/arm64` and `linux/amd64`) are saved.

The folder names follows a pattern like `<platform>_<arch>_<version>` with `version` being optional. For `amd64` GoReleaser creates a binary folder like `linux_amd64_v1` that made it difficult to pick the binary by using the arg `TARGETARCH` from within the Dockerfile; as `TARGETARCH` just returns only the arch name without version. In our case just `amd64`.


As a workaround we need to write GoReleaser build post hook script as shown below that will rename the folder `linux_amd64_v1` to `linux_amd64` which then allows us to pick the right binary based on the architecture using the `TARGETARCH`,

{% embed https://gist.github.com/kameshsampath/4ae6457d520b90e65174fb8a4178cfb2 %}


```shell
COPY server_linux_${TARGETARCH}/server /bin/server
```

> **NOTE** Special thanks to my son [Rithul Kamesh](https://twitter.com/rithulkamesh) who volunteered to write that little script for me 😊.

To summarize what we did,

- Use [Buildx](https://docs.docker.com/build/) to build multi architecture container images
- Leveraged [Drone](https://drone.io) Pipelines to define a declarative builds 
- Used [GoReleaser](https://goreleaser.com) to have standard configuration to build golang applications 
- Finally leveraged GoReleaser, Buildx via Drone plugins to perform reproducible multi architecture builds

I have also written [Drone CI Docker Extension](https://hub.docker.com/extensions/drone/drone-ci-docker-extension) that allows you run this demo or any other Drone pipelines right from your Docker for Desktop. If you have minute please do try it out and let me know your valuable feedback/improvements.