Forge Your Ultimate K8s Playground: Custom Ubuntu Images with Pre-baked Kubeadm and Cilium CNI

Table of Contents
Tired of waiting for container images to download every time you launch a new Kubernetes cluster? Setting up environments repeatedly can be a drag, slowing down your development and testing cycles. What if you could create a “golden” Ubuntu image, perfectly tailored for Kubernetes and pre-loaded with all the essentials including Cilium CNI?
This guide will show you how to customize an official Ubuntu Cloud Image to include containerd
, kubeadm
, kubectl
, and even the necessary images for the Cilium CNI
. By pre-baking these components into your image, you can spin up kubeadm
-based Kubernetes nodes in a fraction of the usual time.
This tutorial is for you if you want a faster, more automated way to set up clusters for testing, development, or learning.
We’ll be using standard Linux tools like qemu-img
and chroot
to perform open-heart surgery on an Ubuntu image, transforming it into a lean, mean, Kubernetes-launching machine.
What You’ll Need
Before we begin, make sure you’re on a Linux system (preferably Ubuntu) with the following tools installed:
curl
for downloading files.qemu-utils
which provides theqemu-img
tool.- Basic command-line familiarity.
You should also have a machine with at least 2 CPUs and 2 GB of RAM to later run a Kubernetes node using the image we create.
Step 1: Obtain and Prepare the Base Image#
First, we need a base to build upon. We’ll grab the latest Ubuntu 24.04 (Noble Numbat) server cloud image. These images are lightweight and designed for virtualized environments.
The cloud image comes in qcow2
format, which is efficient for storage. However, for direct manipulation of the filesystem, the raw
format is easier to work with. We’ll convert it and also add 7GB of extra space to accommodate our Kubernetes tools and container images.
# Download the latest Ubuntu Noble cloud image
curl -LO https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
# Convert from qcow2 to raw format
qemu-img convert -p -f qcow2 -O raw noble-server-cloudimg-amd64.img noble-server-cloudimg-amd64.raw
# Add 7GB of space to the raw image file
qemu-img resize noble-server-cloudimg-amd64.raw +7G
Step 2: Mount the Image and Grow the Filesystem#
Now we have a raw disk image, but the filesystem within it is still the original size. We need to mount this image as a loop device to interact with its partitions.
A loop device makes a file appear as a block device (like a hard drive), allowing us to mount and modify its contents.
# Set up a loop device for our raw image
sudo losetup -Pf noble-server-cloudimg-amd64.raw
# Find the name of your new loop device (e.g., /dev/loop9)
sudo losetup --list | grep raw
With the device mapped (we’ll use /dev/loop4
as an example for the rest of the guide, but be sure to use the one from your output), we can tell the partition table and the filesystem to recognize and use the extra 7GB of space we added.
# Use parted to fix the GPT table after resizing
# Note: parted might warn that not all space is used. Type "Fix".
sudo parted /dev/loop4
# (parted) p <- prints the partition table
# (parted) Fix <- fixes the table
# (parted) quit <- quits parted
# Check the filesystem for errors before resizing
sudo e2fsck -f /dev/loop4p1
# Tell the partition to grow and fill the available space
sudo growpart /dev/loop4 1
# Resize the filesystem
sudo resize2fs /dev/loop4p1
# Check the filesystem to confirm the integrity after resize
sudo e2fsck -f /dev/loop4p1
Step 3: Enter the Chroot Environment#
This is where the real magic happens. We will use chroot
(change root) to enter the filesystem of our image as if it were the root directory of our current system. This powerful technique allows us to run commands like apt-get
and modify system files inside the image without booting it in a VM.
# Mount the image's primary partition
sudo mount /dev/loop4p1 /mnt/raw
# Enter the chroot environment
sudo chroot /mnt/raw/
# Inside the chroot, mount necessary virtual filesystems
mount -t proc proc /proc
mount -t devpts devpts /dev/pts
# Temporarily set up DNS for package installation
unlink /etc/resolv.conf
echo "nameserver 1.1.1.1" > /etc/resolv.conf
Step 4: Install Kubernetes Components#
Now that we are “inside” the image, we can install everything needed for a Kubernetes node.
Install Containerd
First, we’ll set up containerd
, a lightweight and robust container runtime. We’ll configure it to use SystemdCgroup
, which is the recommended cgroup driver for Kubernetes to ensure resource management stability.
# Update package lists
apt-get update
# Add Docker's GPG key and repository to install containerd
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install containerd
apt-get update
apt-get install -y containerd.io apt-transport-https
# Generate default config and set SystemdCgroup to true
containerd config default | tee /etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
Install Kubeadm, Kubelet, and Kubectl
Next, we add the official Kubernetes repository and install the holy trinity of Kubernetes tooling: kubeadm
, kubelet
, and kubectl
. We also use apt-mark hold
to prevent them from being accidentally upgraded.
# Enable IP forwarding, a requirement for Kubernetes networking
echo "net.ipv4.ip_forward = 1" >/etc/sysctl.d/10-k8s.conf
# Add the Kubernetes package repository
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.32/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.32/deb/ /' | tee /etc/apt/sources.list.d/kubernetes.list > /dev/null
# Install and hold the packages
apt-get update
apt-get install -y kubelet kubeadm kubectl
apt-mark hold kubelet kubeadm kubectl
# Enable the kubelet service to start on boot
systemctl enable kubelet
Step 5: The Secret Sauce - Pre-pulling Images#
This step is the key to our lightning-fast cluster deployments. We will pre-pull all the necessary container images for the Kubernetes control plane and the Cilium CNI. When you later run kubeadm init
, it will find the images already present on the disk and skip the download phase entirely.
# Start containerd in the background to pull images
containerd &
# Pre-pull all required kubeadm images
for image in $(kubeadm config images list); do
ctr image pull $image
done
# Install Helm to help us find Cilium's images
curl -LO https://get.helm.sh/helm-v3.18.2-linux-amd64.tar.gz
tar -xvf helm-v3.18.2-linux-amd64.tar.gz -C /usr/local/bin linux-amd64/helm --strip-components=1
rm helm-v3.18.2-linux-amd64.tar.gz
helm repo add cilium https://helm.cilium.io/
# Magically find and pull all Cilium images using Helm values
for image in $(helm show values cilium/cilium --version 1.17.4 | grep repository: -A 1 --no-group-separator | awk '
/repository:/ { repo=$0; sub(/^\s*repository:\s*"/, "", repo); sub(/"\s*$/, "", repo) }
/tag:/ { tag=$0; sub(/^\s*tag:\s*"/, "", tag); sub(/"\s*$/, "", tag); print repo ":" tag }'); do
ctr image pull $image
done
# Bring the containerd process to the foreground and stop it
fg
# Press Ctrl+C
Step 6: Finalize and Package the Image#
Our work inside the image is done. It’s time to clean up, exit the chroot
, and convert our modified raw
image back into the efficient qcow2
format, ready for use in any modern hypervisor like KVM or VirtualBox.
# --- Still inside chroot ---
# Restore the original resolv.conf symlink
cd /etc/
rm resolv.conf
ln -s ../run/systemd/resolve/stub-resolv.conf resolv.conf
# Unmount virtual filesystems and exit chroot
umount /dev/pts
umount /proc
exit
# --- Back on your host system ---
# Unmount the image partition and detach the loop device
sudo umount /mnt/raw
sudo losetup --detach /dev/loop4
# Convert the final raw image back to qcow2 format
qemu-img convert -f raw -O qcow2 noble-server-cloudimg-amd64.raw noble-server-cloudimg-amd64-kubeadm-1.31-cilium.img
Conclusion#
Congratulations! You have successfully forged a custom, reusable Ubuntu image that is primed for Kubernetes. By embedding containerd
, kubeadm
, and all the necessary container images directly, you’ve eliminated the most time-consuming steps of cluster creation.
You can now use this qcow2
file as the base for your virtual machines. When you boot a node from this image and run kubeadm init
or kubeadm join
, the process will be astonishingly fast.
Go forth and build your clusters with unprecedented speed!