Jean-Nicolas Moal
Posted on October 20, 2021
Photo by Ian Taylor on Unsplash
Introduction
In the Linux world, container technology has evolved in a way that we can consider them as a standard way to package and run an application.
Because Docker is the most known tools to play with containers, Dockerfile is the most common way to build them.
But today I want to share with you my preferred tool: Buildah.
It's a powerful tool for building container image in a simple but yet efficient way.
In this article, we'll see how to build a basic image, how to optimize build time using cache, and how to install OS packages without having the OS installed within the container.
Prerequisites
To follow this article, you'll need:
And a Fedora based machine.
Building an image
First, we'll create a basic container image and install Ansible in it.
Create a file called build-unoptimized.sh
with the following content:
#!/usr/bin/env bash
set -o errexit
# Creates a new container instance, and stores its name in CONTAINER_ID.
CONTAINER_ID=$(buildah from docker.io/fedora:34)
# Runs commands within the created container.
buildah run "${CONTAINER_ID}" /bin/bash -c "dnf install -y ansible-$ANSIBLE_VERSION"
# Save the container into an image.
buildah commit "${CONTAINER_ID}" "${IMAGE_NAME}:${IMAGE_TAG}"
Before running this script, let's add a Taskfile.yaml
to easily configure and build the image:
version: "3"
env:
IMAGE_TAG:
sh: git log -n 1 --format=%h
ANSIBLE_VERSION: "2.9.25"
tasks:
build-unoptimized:
env:
IMAGE_NAME: "buildah-demo-unoptimized"
desc: "Build the container image without any optimization"
cmds:
- time -p ./build-unoptimized.sh
Now, if you run task build-unoptimized
, Buildah will create a container image based on docker.io/fedora:34
.
It will install ansible
using dnf
.
On my computer, running this task took 123 seconds and the image size is 614 MB.
We could optimize the size of the image by cleaning up all the DNF cache, but there is a better alternative.
Optimizing by caching
We can kill two birds with one stone with caching, this will help on building time and on the image size.
Let's create a file called build-with-cache.sh
with the following content:
#!/usr/bin/env bash
set -o errexit
DNF_CACHE_FOLDER=${HOME}/.dnfcache/
RUN_OPTION="-v ${DNF_CACHE_FOLDER}:/var/cache/dnf:Z"
mkdir -p "${DNF_CACHE_FOLDER}"
# Creates a new container instance, and stores its name in CONTAINER_ID.
CONTAINER_ID=$(buildah from docker.io/fedora:34)
# Runs commands within the created container.
# The keepcache=true in the dnf.conf file tells dnf not to delete successfully installed packages.
buildah run ${RUN_OPTION} "${CONTAINER_ID}" /bin/bash -c "echo 'keepcache=True' >> /etc/dnf/dnf.conf && dnf install -y --nodocs ansible-$ANSIBLE_VERSION"
# Save the container into an image.
buildah commit "${CONTAINER_ID}" "${IMAGE_NAME}:${IMAGE_TAG}"
This new script does the exact same installation as the previous one, but this time it shares a specific local folder with the container,
so that all files downloaded with dnf are kept locally for next runs.
Let's add the corresponding task within the Taskfile.yaml
file:
build-with-cache:
env:
IMAGE_NAME: "buildah-demo-with-cache"
desc: "Build the container image using caching"
cmds:
- time -p ./build-with-cache.sh
In order to test this new script, we need to run it twice.
The first time to create the cache, and the second one to see the improvements.
On my computer this build took 35 seconds, and the image size is 365 MB.
That's a huge improvement, we reduced the time by almost 4, and the size by almost 2.
The size is reduced because with this new script the cache is outside the container, so it doesn't end within the container.
We could call it a day, but, in my opinion, containers shall only contains what is necessary to run.
So I don't want all the extra pre-installed packages that comes from the base image.
Let's try to install Ansible from a scratch container.
Installing OS package, without the OS
The idea is simple, we'll tell DNF to install all the package in a specific location.
If the package properly declares all its dependencies, then this should work fine.
But if it doesn't, then we'll need to find the missing packages and install them too.
Let's create a file called build-without-os.sh
with the following content:
#!/usr/bin/env bash
set -o errexit
set -x
CONTAINER_ID=$(buildah from scratch)
MOUNT_POINT=$(buildah mount "${CONTAINER_ID}")
DNF_CACHE_FOLDER=${HOME}/.dnfcache
mkdir -p "${DNF_CACHE_FOLDER}"
dnf --installroot "${MOUNT_POINT}" --releasever 34 --nodocs --setopt install_weak_deps=false --setopt cachedir="${DNF_CACHE_FOLDER}" install -y ansible-$ANSIBLE_VERSION
# Save the container into an image.
buildah commit "${CONTAINER_ID}" "${IMAGE_NAME}:${IMAGE_TAG}"
In this script, I added the --installroot
option, which tells dnf
to install package in a specific location.
I also configured DNF with the keepcache=True
on my workstation.
With this new script, we do not use command inside the container, but command available from the host.
This way, no need to have an OS installed or any other package manager inside the container.
Let's add the corresponding task in the Taskfile.yaml
file:
build-without-os:
env:
IMAGE_NAME: "buildah-demo-without-os"
desc: "Build the container image from scratch"
cmds:
- time -p buildah unshare ./build-without-os.sh
Running task build-without-os
took 115 seconds on my computer, and the image size is 284 MB.
We've lost the build time improvement, but won on the size.
Since we started from scratch, dnf
needs to install more packages.
IMHO, it's an acceptable tradeoff since this new image only contains what's necessary.
But from now we only built images, but never tested it, and I can't say it's working if I don't test it.
Testing the image
I won't use a container registry just to test the image, so we're going to export the image and reload it from a container runtime.
Note that buildah builds OCI images, which might not work as-is for docker
.
Don't worry, there is an option --format
that you can use to build docker
compliant images.
In my case, I'm going to use podman
to test the image, hereunder a generic task to test images:
test:
vars:
IMAGE_NAME: '{{ default "" .IMAGE_NAME }}'
env:
IMAGE_ID:
sh: buildah images --filter=reference={{ .IMAGE_NAME }} --format='{{ printf "{{ .ID }}" }}'
desc: "Test"
cmds:
- buildah push $IMAGE_ID oci-archive:${PWD}/{{ .IMAGE_NAME }}.tar.gz
- podman import ./{{ .IMAGE_NAME }}.tar.gz
- podman run -ti --rm localhost/{{ .IMAGE_NAME }}:$IMAGE_TAG /bin/ansible --version
And now the task to test the image built from scratch:
test-without-os:
vars:
IMAGE_NAME: "buildah-demo-without-os"
desc: "Load the image in podman and run a test on it"
cmds:
- task: test
vars: { IMAGE_NAME: "{{ .IMAGE_NAME }}" }
If you run task test-without-os
ansible version shall be printed.
This is it for now, thank you for reading this post.
Feel free to share it, and see you later for more tech.
Have a nice day!
Posted on October 20, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 20, 2021