4. Buildah - Granularity & Control
4. Buildah - Granularity & Control ๊ด๋ จ
The goal of this lab is to introduce you to Buildah and the flexibility it provides when you need to build container images your way. There are a lot of different use cases that just "feel natural" when building container images, but you often, you can't quite wire together and elegant solutions with the client server model of existing container engines. In comes Buildah. To get started, lets introduce some basic decisions you need to think through when building a new container image.
Image vs. Scratch: Do you want to start with an existing container image as the source for your new container image, or would you prefer to build completely from scratch? Source images are the most common route, but it can be nice to build from scratch if you have small, statically linked binaries.
Inside vs. Outside: Do you want to execute the commands to build the next container image layer inside the container, or would you prefer to use the tools on the host to build the image? This is completely new concept with Buildah, but with existing container engines, you always build from within the container. Building outside the container image can be useful when you want to build a smaller container image, or an image that will always be ran read only, and never built upon. Things like Java would normally be built in the container because they typically need a JVM running, but installing RPMs might happen from outside because you don't want the RPM database in the container.
External vs. Internal Data: Do you have everything you need to build the image from within the image? Or, do you need to access cached data outside of the build process? For example, It might be convenient to mount a large cached RPM cache inside the container during build, but you would never want to carry that around in the production image. The use cases for build time mounts range from SSH keys to Java build artifacts - for more ideas, see this GitHub issue.
Alright, let's walk through some common scenarios with Buildah.
Prep Work
Just like Podman, Buildah can execute in rootless mode, but since you have tools on the container host interacting files in the container image, you need to make Buildah think it's running as root. Buildah comes with a cool sub-command called unshare which does just this. It puts our shell into a user namespace just like when you have a root shell in a container. The difference is, this shell has access to tools installed on the container host, instead of in the container image. Before we complete the rest of this lab, execute the "buildah unshare
" command. Think of this as making yourself root, without actually making yourself root:
buildah unshare
# WARN[0000] Reading allowed ID mappings: reading subuid mappings for user "root" and subgid mappings for group "root": no subuid ranges found for user "root" in /etc/subuid
# WARN[0000] Found no UID ranges set aside for user "root" in /etc/subuid.
# WARN[0000] Found no GID ranges set aside for user "root" in /etc/subgid.
# ERRO[0000] no command specified
Now, look at who your shell thinks you are:
whoami
# root
It's looks like you are root, but you really aren't, but let's prove it:
touch /etc/shadow
The touch
command fails because you're not actually root. Really, the touch command executed as an arbitrary user ID in your /etc/subuid
range. Let that sink in. Linux containers are mind bending. OK, let's do something useful.
Basic Build
First declare what image you want to start with as a source. In this case, we will start with Red Hat Universal Base Image:
buildah from ubi8
# Resolved "ubi8" as an alias (/etc/containers/registries.conf.d/001-rhel-shortnames.conf)
# Trying to pull registry.access.redhat.com/ubi8:latest...
# Getting image source signatures
# Checking if image destination supports signatures
# Copying blob 6c53be4efe39 done
# Copying config e8e5725e8a done
# Writing manifest to image destination
# Storing signatures
# ubi8-working-container
This will create a "reference" to what Buildah calls a "working container" - think of them as a starting point to attach mounts and commands. Check it out here:
buildah containers
# CONTAINER ID BUILDER IMAGE ID IMAGE NAME CONTAINER NAME
# b44d8d9ffbf5 * e8e5725e8af3 registry.access.redhat.com/ub... ubi8-working-container
Now, we can mount the image source. In effect, this will trigger the graph driver to do its magic, pull the image layers together, add a working copy-on-write layer, and mount it so that we can access it just like any directory on the system:
buildah mount ubi8-working-container
# /var/lib/containers/storage/overlay/75e1913298de0bbeeae41a0ff5934f54029718d0779c5cf99a0cd9cbc5db9aaf/merged
Now, lets add a single file to the new container image layer. The Buildah mount command can be ran again to get access to the right directory:
echo "hello world" > $(buildah mount ubi8-working-container)/etc/hello.conf
Lets analyze what we just did. It's super simple, but kind of mind bending if you come from using other container engines. First, list the directory in the copy-on-write layer:
ls -alh $(buildah mount ubi8-working-container)/etc/
You should see hello.conf
right there. Now, cat the file:
cat $(buildah mount ubi8-working-container)/etc/hello.conf
# hello world
You should see the text you expect. Now, lets commit this copy-on-write layer as a new image layer:
buildah commit ubi8-working-container ubi8-hello
# Getting image source signatures
# Copying blob b51194abfc91 skipped: already exists
# Copying blob ade74f4361f4 done
# Copying config a76d813d62 done
# Writing manifest to image destination
# Storing signatures
# a76d813d62b333c22d5a47fd1a3ce5016424a2b5e7f698e92e42a2bb5ca9d613
Now, we can see the new image layer in our local cache. We can view it with either Podman or Buildah (or CRI-O for that matter, they all use the same image store):
buildah images
podman images
# REPOSITORY TAG IMAGE ID CREATED SIZE
# localhost/ubi8-hello latest a76d813d62b3 27 seconds ago 215 MB
# registry.access.redhat.com/ubi8 latest e8e5725e8af3 6 weeks ago 215 MB
When we are done, we can clean up our environment quite nicely. The following command will delete references to "working containers" and completely remove their mounts:
buildah delete -a
# b44d8d9ffbf58ce9555e1facc3a890aa69b9f551497eb33f180265ecc96871a6
But, we still have the new image layer just how we want it. This could be pushed to a registry server to be shared with others if we like:
buildah images
podman images
# REPOSITORY TAG IMAGE ID CREATED SIZE
# localhost/ubi8-hello latest a76d813d62b3 27 seconds ago 215 MB
# registry.access.redhat.com/ubi8 latest e8e5725e8af3 6 weeks ago 215 MB
Using Tools Outside The Container
Create a new working container, mount the image, and get a working copy-on-write layer:
WORKING_MOUNT=$(buildah mount $(buildah from scratch))
echo $WORKING_MOUNT
# /var/lib/containers/storage/overlay/5d10a3ffb6e3c3350b7d16f8bb670103ef549cdf68ca780464ffb2b9cd98c781/merged
Verify that there is nothing in the directory:
ls -alh $WORKING_MOUNT
# total 0
# dr-xr-xr-x. 1 root root 6 Aug 8 02:53 .
# drwx------. 6 root root 69 Aug 8 02:53 ..
Now, lets install some basic tools:
yum install --installroot $WORKING_MOUNT bash coreutils --releasever 8 --setopt install_weak_deps=false -y
yum clean all -y --installroot $WORKING_MOUNT --releasever 8
Verify that some files have been added:
ls -alh $WORKING_MOUNT
# total 4.0K
# dr-xr-xr-x. 1 root root 224 Aug 8 02:56 .
# drwx------. 6 root root 69 Aug 8 02:53 ..
# lrwxrwxrwx. 1 root root 7 Jun 21 2021 bin -> usr/bin
# dr-xr-xr-x. 2 root root 6 Jun 21 2021 boot
# drwxr-xr-x. 2 root root 18 Aug 8 02:56 dev
# drwxr-xr-x. 25 root root 4.0K Aug 8 02:57 etc
# drwxr-xr-x. 2 root root 6 Jun 21 2021 home
# lrwxrwxrwx. 1 root root 7 Jun 21 2021 lib -> usr/lib
# lrwxrwxrwx. 1 root root 9 Jun 21 2021 lib64 -> usr/lib64
# drwxr-xr-x. 2 root root 6 Jun 21 2021 media
# drwxr-xr-x. 2 root root 6 Jun 21 2021 mnt
# drwxr-xr-x. 2 root root 6 Jun 21 2021 opt
# dr-xr-xr-x. 2 root root 6 Aug 8 02:56 proc
# dr-xr-x---. 2 root root 6 Jun 21 2021 root
# drwxr-xr-x. 3 root root 21 Aug 8 02:56 run
# lrwxrwxrwx. 1 root root 8 Jun 21 2021 sbin -> usr/sbin
# drwxr-xr-x. 2 root root 6 Jun 21 2021 srv
# dr-xr-xr-x. 2 root root 6 Aug 8 02:56 sys
# drwxrwxrwt. 2 root root 6 Jun 21 2021 tmp
# drwxr-xr-x. 12 root root 144 Aug 8 02:56 usr
# drwxr-xr-x. 18 root root 233 Aug 8 02:56 var
Now, commit the copy-on-write layer as a new container image layer:
buildah commit working-container minimal
# Getting image source signatures
# Copying blob adb0e8b27e71 done
# Copying config c90b4f8954 done
# Writing manifest to image destination
# Storing signatures
# c90b4f8954fb2d3b8a7badd43f08dadb68dfad3d85de5a9f62b536c203896e0a
Now, test the new image layer, by creating a container:
podman run -it minimal bash
exit
Clean things up for our next experiment:
buildah delete -a
# d2aaed4e880576eb15f5905179d10d3c3d69b1adff25a2b9d80a5369b9abf3b3
We have just created a container image layer from scratch without ever installing RPM or YUM. This same pattern can be used to solve countless problems. Makefiles often have the option of specifying the output directory, etc. This can be used to build a C program without ever installing the C toolchain in a container image layer. This is best for production security where we don't want the build tools laying around in the container.
External Build Time Mounts
As a final example, lets use a build time mount to show how we can pull data in. This will represent some sort of cached data that we are using outside of the container. This could be a repository of Ansible Playbooks, or even Database test data:
mkdir ~/data
dd if=/dev/zero of=~/data/test.bin bs=1MB count=100
# 100+0 records in
# 100+0 records out
# 100000000 bytes (100 MB, 95 MiB) copied, 0.0360907 s, 2.8 GB/s
ls -alh ~/data/test.bin
# ls -alh ~/data/test.bin
Now, lets fire up a working container:
buildah from ubi8
# ubi8-working-container
buildah mount ubi8-working-container
# /var/lib/containers/storage/overlay/3aeee0506cba19f116a97765b778e09ee3829bc61ad4e1ed45ad29c58b04347f/merged
To consume the data within the container, we use the buildah-run subcommand. Notice that it takes the -v
option just like "run" in Podman. We also use the Z
option to relabel the data for SELinux. The dd
command simply represents consuming some smaller portion of the data during the build process:
buildah run -v ~/data:/data:Z ubi8-working-container dd if=/data/test.bin of=/etc/small-test.bin bs=100 count=2
# 2+0 records in
# 2+0 records out
# 200 bytes copied, 6.57e-05 s, 3.0 MB/s
Commit the new image layer and clean things up:
buildah commit ubi8-working-container ubi8-data
# Getting image source signatures
# Copying blob b51194abfc91 skipped: already exists
# Copying blob 495a8d09a9e1 done
# Copying config 9e2f87e78f done
# Writing manifest to image destination
# Storing signatures
# 9e2f87e78ff4cb488861bf182381ed81fef059d984141125c3bd84775ecf51eb
buildah delete -a
# 757fe0f753ff90b359cc28ed4eae3a0b99c60cdb3e1e194350a661564988fcf3
Test it and note that we only kept the pieces of the data that we wanted. This is just an example, but imagine using this with a Makefile cache, or Ansible playbooks, or even a copy of production database data which needs to be used to test the image build or do a schema upgrade, which must be accessed during the image build process. There are tons of places where you need to access data, only at build time, but don't want it during production deployment:
podman run -it ubi8-data ls -alh /etc/small-test.bin
# -rw-r--r--. 1 root root 200 Aug 8 03:01 /etc/small-test.bin
Cleanup
Exit the user namespace:
exit
Conclusion
Now, you have a pretty good understanding of the cases where Buildah really shines. You can start from scratch, or use an existing image, use tools installed on the container host (not in the container image), and move data around as needed. This is a very flexible tool that should fit quite nicely in your tool belt. Buildah lets you script builds with any language you want, and build tiny images with only the bare minimum of utilities needed inside the image.
Now, lets move on to sharing containers with Skopeo...