Ubuntu immutable QEMU images
The qemu-ubuntu-2404-immutable target builds an Ubuntu 24.04 Cluster API
image with a read-only root filesystem and a separate persistent data
partition. Packer and Ansible provision the image while the root filesystem is
still writable. The final immutable runtime step creates the persistent data
partition, bind mounts the writable runtime paths, configures tmpfs scratch
paths, and writes / as read-only in /etc/fstab before Goss runs.
Build the target from images/capi:
PACKER_VAR_FILES="packer/config/kubernetes.json packer/config/cni.json packer/config/containerd.json" \
PACKER_FLAGS="\
--var 'format=qcow2' \
--var 'kubernetes_semver=v1.36.1' \
--var 'kubernetes_series=v1.36' \
--var 'kubernetes_deb_version=1.36.1-1.1' \
--var 'immutable_read_only_root=true'" \
make build-qemu-ubuntu-2404-immutable
Use --var 'format=raw' when the consuming provider expects a raw disk image.
Immutable configuration
The target enables the data partition and read-only root by default. These variables can be overridden through Packer variables when a provider needs a different contract:
| Variable | Default |
|---|---|
immutable_data_partition | true |
immutable_data_partition_fstype | ext4 |
immutable_data_partition_label | CAPI-DATA |
immutable_data_partition_mount | /.capi-data |
immutable_data_partition_mount_options | defaults,x-systemd.device-timeout=30s |
immutable_root_partition_size | 12884901888 |
immutable_read_only_root | true |
immutable_persistent_paths | /etc,/home,/root,/mnt,/media,/opt,/srv,/usr/local,/var/backups,/var/cache,/var/crash,/var/lib,/var/local,/var/log,/var/mail,/var/opt,/var/spool |
immutable_tmpfs_paths | /tmp,/var/tmp |
The persistent path list covers first-boot configuration under /etc, user home
directories, SSH host keys, /mnt, /media, /opt, /usr/local, /srv, and
the common mutable /var subtrees used by package state, cloud-init, kubelet,
containerd, CNI, systemd, dbus, NetworkManager, control-plane etcd data, caches,
crash dumps, spools, and logs. The data partition is mounted outside /var so /var/lib
can be persistent as a whole. Extend immutable_persistent_paths when a
provider writes additional bootstrap files after the root filesystem is
remounted read-only.
The data partition is required for the booted node contract. Do not add
nofail to the data or bind mount options unless the consuming image target has
a separate recovery path for missing writable runtime storage. The immutable
runtime step also writes systemd drop-ins so cloud-init, containerd, kubelet,
and SSH wait for the data, persistent, and tmpfs mounts.
The default QEMU disk is 20 GiB and immutable_root_partition_size reserves
12 GiB for the read-only root. The data partition receives the remaining disk
space, which must be large enough for /var/lib/containerd, kubelet state,
logs, and bootstrap data. Increase disk_size when the workload or provider
needs more writable runtime capacity.
Validation
Run the focused local checks after changing this target:
make test-qemu-immutable
make validate-qemu-ubuntu-2404-immutable
These checks validate the autoinstall renderer, immutable runtime helper, Packer target, Packer variable wiring, and Goss contract. They do not replace a provider-backed Cluster API boot test for the exact artifact format and network path that will consume the image.
At minimum, provider validation should boot the produced image through Cluster
API, wait for the infrastructure provider machine and CAPI Machine readiness
contract, and verify the guest contract over SSH:
findmnt -no OPTIONS / | tr ',' '\n' | grep -qx ro
test -w /.capi-data
for path in \
/etc \
/home \
/root \
/mnt \
/media \
/opt \
/srv \
/usr/local \
/var/backups \
/var/cache \
/var/crash \
/var/lib \
/var/local \
/var/log \
/var/mail \
/var/opt \
/var/spool; do
test -w "${path}"
done
Write sentinels under at least /etc/kubernetes, /home, and
/var/lib/kubelet, reboot the guest, then verify the sentinels are still
present and / is still mounted read-only. The same boot test should also
verify that cloud-init finished, /etc/hostname contains the provider-assigned
hostname, and /etc/machine-id is non-empty after first boot.