Multiarch Support In LitmusChaos
Tags: litmuschaos, kubernetes, docker, devops
Published: 2020-12-07 09:10:19

In this blog, I will be discussing how to build a multi-arch image for any desired docker architecture to run litmus chore components. For those who are new to litmus and wanted to explore more about [chaos engineering](https://dev.to/umamukkara/chaos-engineering-for-cloud-native-systems-2fjn), I would recommend to check out [litmus blog](https://dev.to/umamukkara/litmuschaos-in-cncf-sandbox-3j57). Now coming back to the topic we will be using docker buildx for building the images in the following sections.

## **Pre-requisites:**

- Docker(version 19.03)

## **Understanding Multiarch Builds**
Docker introduced the principle of multi-arch builds to support the "Build once, deploy anywhere" concept which helps to use ARM targets and reduce your bills such as _AWS A1_ and _Raspberry Pis_ instances. But how do we produce them? and How do they work? 
A multi-arch Docker image supports a different architecture behind the same imagetag. Let's compare the manifest of the multi-arch image with the image having a single arch. 

Enable docker CLI if not already enabled using the following steps:
1. `export DOCKER_CLI_EXPERIMENTAL=enabled`
2. Add `"experimental": "enabled",` to `~/.docker/config.json` (default location) at the beginning of the file and not at the end.

***Image with single arch manifest***

```bash
$ sudo docker manifest inspect --verbose litmuschaos/go-runner:1.9.0
{
	"Ref": "docker.io/litmuschaos/go-runner:1.9.0",
	"Descriptor": {
		"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
		"digest": "sha256:bc14ce592d7d600eb14c0b6dbfb0f6e8211f7795b5bc9def057cba6b3c994185",
		"size": 2002,
		"platform": {
			"architecture": "amd64",
			"os": "linux"
		}
	},
	"SchemaV2Manifest": {
		"schemaVersion": 2,
		"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
		"config": {
			"mediaType": "application/vnd.docker.container.image.v1+json",
			"size": 5502,
			"digest": "sha256:79fc16e2bbab6885589bd491c5ebf68c693239b8dbf13d45223b84e1a3aaa25b"
		},
		"layers": [ ... ]
	}
}
```

***Multi arch image manifest***

```bash
$ sudo docker manifest inspect --verbose litmuschaos/go-runner:1.10.0
[
	{
		"Ref": "docker.io/litmuschaos/go-runner:1.10.0@sha256:89717f4c87c7fc93c63d2d74c54c5e4457dd99dd37b1bbda3bbbabfc0f2eacb4",
		"Descriptor": {
			"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
			"digest": "sha256:89717f4c87c7fc93c63d2d74c54c5e4457dd99dd37b1bbda3bbbabfc0f2eacb4",
			"size": 2207,
			"platform": {
				"architecture": "arm64",
				"os": "linux"
			}
		},
		"SchemaV2Manifest": {
			"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
			"schemaVersion": 2,
			"config": {
				"mediaType": "application/vnd.docker.container.image.v1+json",
				"digest": "sha256:959a3aa87f6ae3add03a5a568d218e99e9394c785d924e5d6398d7cb4be0f472",
				"size": 5123
			},
			"layers": [ ... ]
		}
	},
	{
		"Ref": "docker.io/litmuschaos/go-runner:1.10.0@sha256:04cec97644444d564e256e87184e0daa73f3f335d187c65a1723f0f8dc61ad22",
		"Descriptor": {
			"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
			"digest": "sha256:04cec97644444d564e256e87184e0daa73f3f335d187c65a1723f0f8dc61ad22",
			"size": 2210,
			"platform": {
				"architecture": "amd64",
				"os": "linux"
			}
		},
		"SchemaV2Manifest": {
			"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
			"schemaVersion": 2,
			"config": {
				"mediaType": "application/vnd.docker.container.image.v1+json",
				"digest": "sha256:679ad1c72c9be392b47b3fc6c455c383e28b8fcdbcf923dd5fb9db2cfb5c3cc2",
				"size": 5125
			},
			"layers": [ ... ]
		}
	}
]
```
We can observe that the manifest for the multi-arch image is a simple list of manifests prepared for each platform while an image with a single arch will have only one platform in its manifest.

## **Build Multi-arch image for Litmus Components**

For building a multi-arch image for litmus core components just follow the following simple steps:
- Setup buildx
- Build go binary for different architectures.
- Prepare Dockerfile for Multiarch.
- Build and push the multi-arch image.

### **Build using Buildx instance**

_Introduction:_

Docker Buildx is a CLI plugin that extends the docker command with the full support of the features provided by [Moby BuildKit](https://github.com/moby/buildkit) builder toolkit. It has new features like creating scoped builder instances and building against multiple nodes concurrently.

_Setup:_
 
The Docker Buildx is included in Docker 19.03 to install it run the following commands:

```bash
$ export DOCKER_BUILDKIT=1
$ docker build --platform=local -o . git://github.com/docker/buildx
$ mkdir -p ~/.docker/cli-plugins
$ mv buildx ~/.docker/cli-plugins/docker-buildx
```
_Confirm the installation:_
```bash
$ sudo docker buildx version
```
Output
```bash
github.com/docker/buildx v0.4.2-16-g1595352 159535261de8416d2a83d600ccdd7dbfa6d71303
```

for any issue in installation refer [installation guide](https://github.com/docker/buildx/blob/master/README.md).

Now the buildx is setup in your system. Buildx allows you to create new instances of isolated builders. You can use this to get a scoped environment for your CI builds that does not change the state of the shared daemon, or for isolating builds for different projects. You can create a new instance for a set of remote nodes, forming a build farm, and quickly switch between them.

### **Setup Builder instance**

**1. Using the QEMU emulation support in the kernel**
```bash
sudo apt-get install qemu-user-static -y
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes i
```
**2. Command to create new builder instances.**

`docker buildx create --name multibuilder`

Output
```
multibuilder
```

**3. To show the builder instance created**

`docker buildx ls`

Output
```
NAME/NODE       DRIVER/ENDPOINT             STATUS  PLATFORMS
multibuilder *  docker-container                    
  multibuilder0 unix:///var/run/docker.sock running linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
default         docker                              
  default       default                     running linux/amd64, linux/386
```

**4. Inspect the builder**

`docker buildx inspect multibuilder`

```
Name:   multibuilder
Driver: docker-container

Nodes:
Name:      multibuilder0
Endpoint:  unix:///var/run/docker.sock
Status:    running
Platforms: linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
```
Here the platforms will show the platform available to build images. 

**5. If the Status is not `running` run the following command**
`docker buildx inspect multibuilder --bootstrap`
 
This will activate the Status of the builder instance.
Output
```
               
[+] Building 6.5s (1/1) FINISHED                                                                                                                                                       
 => [internal] booting buildkit                                                                                                                                                   6.5s
 => => pulling image moby/buildkit:buildx-stable-1                                                                                                                                5.0s
 => => creating container buildx_buildkit_multibuilder0                                                                                                                           1.5s
Name:   multibuilder
Driver: docker-container

Nodes:
Name:      multibuilder0
Endpoint:  unix:///var/run/docker.sock
Status:    running
Platforms: linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/386, linux/arm/v7, linux/arm/v6, linux/s390x
```

**6. Use the multibuilder instance**

`docker buildx use multibuilder`

### **Build Go binary**

We know that all the litmus core components (chaos experiment, operator, and runner) are written in Golang and we are building the go binaries and using them in the image. 

**Note:**
_You can build the go binaries and copy them in Dockerfile or we can build the binary in the Dockerfile itself and use them._ 

In this example, I'll be following the first way that is building binary and copying them in the Dockerfile.
The main **key point** here is to build different binaries with Go ENVs set to desired values like `GOARCH=amd64` &`GOOS=linux` for which we have to build the image. 


```bash
git clone https://github.com/litmuschaos/chaos-operator.git
cd chaos-operator
vi build/go-multiarch-build.sh 
```
The following script will help to do that:

```bash
#!/usr/bin/env bash

package=$1
if [[ -z "$package" ]]; then
  echo "usage: $0 <package-name>"
  exit 1
fi

package_split=(${package//\// })
package_name=${package_split[-1]}

# add the arch for which we want to build the image
platforms=("linux/amd64" "linux/arm64")

for platform in "${platforms[@]}"
do
    platform_split=(${platform//\// })
    GOOS=${platform_split[0]}
    GOARCH=${platform_split[1]}
    output_name=build/_output/bin/chaos-operator$GOARCH

    # The script executes for the argument passed (in package variable)
    # here the arg will be "github.com/litmuschaos/chaos-operator/cmd/manager" for creating binary
    env GOOS=$GOOS GOARCH=$GOARCH go build -o $output_name $package

    if [ $? -ne 0 ]; then
        echo 'An error has occurred! Aborting the script execution...'
        exit 1
    fi
done
```
**Command**
- `./build/go-multiarch-build.sh github.com/litmuschaos/chaos-operator/cmd/manage`

In the above script, we are creating the binary of `github.com/litmuschaos/chaos-operator/cmd/manage` with `GOARCH` & `GOOS` set in the platforms. This will create binaries as follow:

```bash
$ ls build/_output/bin/
chaos-operatoramd64*  chaos-operatorarm64*
```

### **Prepare Dockerfile for Multiarch**

Prepare a Dockerfile that should work for all the different architectures. Make sure that:
**1. The base image should be multi-arch:** Use the base image which is having multiarch support.

```dockerfile
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.3
```
Which supports both ARM64 and AMD64 arch.

![Screenshot from 2020-11-23 20-58-06](https://dev-to-uploads.s3.amazonaws.com/i/ljq3df35jgwturydaorg.png)


**2. Use TARGETARCH variable for using binary in Dockerfile:** All binaries used should support multi-arch builds and are built accordingly. The Docker predefines a set of ARG variables according to the `--platform` we pass in buildx. This is only available when using the BuildKit backend. Here the `TARGETARCH` is the architecture component of `TARGETPLATFORM`. Check out more platform env [here](https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope).

**Example**
```dockerfile
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.3

ARG TARGETARCH

ENV OPERATOR=/usr/local/bin/chaos-operator \
    USER_UID=1001 \
    USER_NAME=chaos-operator

# install operator binary
COPY build/_output/bin/chaos-operator${TARGETARCH} ${OPERATOR}

COPY build/bin /usr/local/bin
RUN  /usr/local/bin/user_setup

ENTRYPOINT ["/usr/local/bin/entrypoint"]

USER ${USER_UID}
```
This will copy the binaries created in the previous step while building the image.

### **Build and push the multi-arch image**

BuildKit is designed to work well for building for multiple platforms and not only for the architecture and operating system that the user invoking the build happens to run. When you invoke a build, you can set the `--platform` flag to specify the target platform for the build output, (for example, linux/amd64, linux/arm64, darwin/amd64).

A sample command to build and push the multiarch image with the Dockerfile created in the previous stage. 

```bash
sudo docker buildx build --file build/Dockerfile --progress plane --platform linux/arm64,linux/amd64 --tag $(ORG_NAME)/$(IMAGE_NAME):$(IMAGE_TAG) . --push
```
The above command will build a multiarch image and push it to Dockerhub. The flags used are:
- `--file`: Path of the Dockerfile (Default is ‘PATH/Dockerfile’)
- `--progress`: Use to control verbosity. Set type of progress output (auto, plain, tty). Use plain to show container output.
- `--platform`: Set target platform for build.
- `--tag`: Set tag of the image in the ‘name:tag’ format.
- `--push`: Used to push the image to the registry.

for more flags checkout buildx [man page](https://docs.docker.com/engine/reference/commandline/buildx_build/).

**Multiarch image in Dockerhub:**

We have successfully built and pushed the multiarch images on Dockerhub. The images will look like: 

![Screenshot from 2020-11-24 01-52-28](https://dev-to-uploads.s3.amazonaws.com/i/iw6zc5cnl4jxm5gjjmyu.png)
 

## **Troubleshooting**

**1. standard_init_linux.go:211: exec user process caused "exec format error"**:

There could be multiple reasons for this. Try to check out the base image you're using is supporting all the platforms for which you're building the images. 

Make sure while building and copying the binary in Dockerfile that the arch and os should be same. Like you're building the binary with `linux/amd64` and you're trying to run it on linux/arm64. 

**2. rpc error: code = Unknown desc = failed to load LLB: runtime execution on platform linux/arm/v7 not supported**: 

This means the platform you're looking for is not available. You can verify the available platforms using `docker buildx inspect <builder-name>`.Now you can run the following commands to resolve the above error.

```bash

sudo apt-get install qemu-user-static -y
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes i
docker buildx rm multibuilder
docker buildx create --name multibuilder
docker buildx ls
docker buildx inspect multibuilder
docker buildx inspect multibuilder --bootstrap
docker buildx use multibuilder
docker ps -a
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t <ORG_NAME>/<IMAGE_NAME>:<IMAGE_TAG> --push .
```

## **Conclusion**

In this blog, we learn how to build and push an image that can be used at different docker architecture with the same image tag on it. This can be done using a powerful docker buildx feature which helps to build image with a single command. We can create images that can run on any desired platform using buildx and can be used to reduce your bills using ARM targets _Raspberry Pis_ instance. So have ever tried building multiarch images? How useful it could be? Please leave your thoughts in the comment box.

:rocket: Special Thanks :raised_hands:

Thanks to [**Michael Fornaro**](https://www.linkedin.com/in/michael-fornaro-5b756179/) for helping in suggesting, reviewing & adopting the multiarch support for Litmus Chore Components.


---

Are you an SRE or a Kubernetes enthusiast? Does Chaos Engineering excite you?
Join Our Community On Slack For Detailed Discussion, Feedback & Regular Updates On Chaos Engineering For Kubernetes: *https://kubernetes.slack.com/messages/CNXNB0ZTN* 
(#litmus channel on the Kubernetes workspace)
Check out the Litmus Chaos GitHub repo and do share your feedback: *https://github.com/litmuschaos/litmus*
Submit a pull request if you identify any necessary changes.
{% github litmuschaos/litmus %}