Introduction
In this post, we are going to user packer to build a docker image to multiple architectures.
Defining our sources
Given that we are going to be building for multiple architectures, we also need our sources to pull from all the different architecture.
In this example we will make use of alpine linux to build our images.
So let’s first define a common source for amd64 architecture.
1
2
3
4
5
source "docker" "amd64" {
image = "docker.io/amd64/alpine"
commit = true
platform = "linux/amd64"
}
Notice the platform, this is the equivalent of docker –platform argument
Also notice the image source is docker.io/amd64/alpine, this is the amd64 version of alpine linux, as the base repository for different architectures for alpine are actually based on their architecture.
You can see them here: https://github.com/docker-library/official-images#architectures-other-than-amd64
Now let’s define our source for other architectures
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
source "docker" "arm64" {
image = "docker.io/arm64v8/alpine"
commit = true
platform = "linux/arm64/v8"
}
source "docker" "arm32" {
image = "docker.io/arm32v7/alpine"
commit = true
platform = "linux/arm/v7"
}
source "docker" "i386" {
image = "docker.io/i386/alpine"
commit = true
platform = "linux/i386"
}
source "docker" "ppc64le" {
image = "docker.io/ppc64le/alpine"
commit = true
platform = "linux/ppc64le"
}
Ok, so that’s all the sources we need, now let’s define our builders.
Defining our builders
1
2
3
4
5
6
7
8
9
build {
sources = [
"source.docker.amd64",
"source.docker.arm64",
"source.docker.arm32",
"source.docker.i386",
"source.docker.ppc64le",
]
}
If we now just run packer build by running the command
1
packer build .
We just end up pull and committing to our local docker registry
So, now let’s imagine we just want to build for amd64 and arm64, we can do that by just specifying the sources we want to use.
1
packer build -only=source.docker.amd64,source.docker.arm64 .
But, for now we are not using any provisioners, so let’s add some.
Let’s keep this post simple and just use a shell provisioner to just use uname so we can check that the output actually matches the platform we are building for.
1
2
3
4
5
provisioner "shell" {
inline = [
"uname -a",
]
}
Now, if we run packer build again, we will see that it will see a different output for each architecture.
1
2
3
4
5
6
7
8
9
=> docker.amd64: Creating a temporary directory for sharing data...
==> docker.arm32: Creating a temporary directory for sharing data...
...
docker.amd64: Linux 4d28a12e536c 5.10.104-linuxkit #1 SMP PREEMPT Thu Mar 17 17:05:54 UTC 2022 x86_64 Linux
docker.arm32: Linux 5d1adcf285da 5.10.104-linuxkit #1 SMP PREEMPT Thu Mar 17 17:05:54 UTC 2022 armv7l Linux
docker.arm64: Linux f841cbd37ea4 5.10.104-linuxkit #1 SMP PREEMPT Thu Mar 17 17:05:54 UTC 2022 aarch64 Linux
docker.ppc64le: Linux 3e1453ad893c 5.10.104-linuxkit #1 SMP PREEMPT Thu Mar 17 17:05:54 UTC 2022 ppc64le Linux
docker.i386: Linux 53e80c0a41b8 5.10.104-linuxkit #1 SMP PREEMPT Thu Mar 17 17:05:54 UTC 2022 i686 Linux
...
We can now continue to add further provisioners to our build, as another example we could install curl to our containers, before committing them to an image.
Example
1
2
3
4
5
provisioner "shell" {
inline = [
"apk add --no-cache curl",
]
}
And the appropriate binary will be installed to the corresponding architecture (curl is not the best example) but since we are running inside the container before commiting it to an image, it will be installed to the correct architecture.
Post processing our images
We can now just add to the build a post processor to push our images to a registry.
1
2
3
4
post-processor "docker-tag" {
repository = "local/packerbuild"
tag = "latest"
}
And now we can just run packer build and it will build our images locally
If we change the tag to reference the source, we can tag our images with the architecture.
1
2
3
4
post-processor "docker-tag" {
repository = "local/packerbuild"
tag = ["${source.name}"]
}
We can make use of the reference for the source to other mechanisms to control the follow for different architectures.
For simplicity we will not be exploring those in this tutorial.
Inspecting commited images
Given we are committing our containers as images, let’s check the history of the images to see the layers we are getting from the provisioner shell commands we are running.
1
docker history --no-trunc -H local/packerbuild:amd64
Should give us the following output
1
2
3
4
MAGE CREATED CREATED BY SIZE COMMENT
sha256:f570fd07ba52f62446716bcb4456b630eea4f5ef7dc90b1c68e6a1e73366febe About a minute ago 2.26MB
sha256:042a816809aac8d0f7d7cacac7965782ee2ecac3f21bcf9f24b1de1a7387b769 13 days ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 13 days ago /bin/sh -c #(nop) ADD file:e4d600fc4c9c293efe360be7b30ee96579925d1b4634c94332e2ec73f7d8eca1 in / 7.05MB
We only have the command and a missing layer with the file which is the installation for curl binary.
Conclusion
In this tutorial we have seen how we can use packer to build multi-architecture images based on several build sources, and how we can use the source reference to control the flow of the build for different architectures.
As usual our code is available on github.
You can find it under the following link https://github.com/Ilhicas/packer-examples/tree/main/docker-multiarch