Updating Ubuntu cloud images

One approach to updating (and making PCI DSS-compliant…) Ubuntu cloud images would be to start a stock instance with an unmodified image, customise this VM, and then either snapshot or save and convert the resulting filesystem. The two drawbacks of this methodology are that the resulting image isn’t necessarily pristine – the commands run to migrate its state and and temporary files will still be present – and the image will be much larger than the original compressed/deduplicated source. This latter aspect is important when there is a need to spin-up a large number of VMs quickly, and the smaller the source image the faster this can occur.

The other alternative, then, is to mount the cloud image locally using the tools provided by qemu and then make changes in-place within the mounted filesystem, or having chroot()d into it.

Handily, the Ubuntu cloud images available from cloud-images.ubuntu.com are already in QEMU’s qcow2 (QEMU Copy-On-Write version 2) format. We can therefore mount the image as follows:

wget https://cloud-images.ubuntu.com/releases/trusty/14.04.2/ubuntu-14.04-server-cloudimg-amd64-disk1.img
sudo modprobe nbd max_part=1
sudo qemu-nbd --connect=/dev/nbd0 ubuntu-14.04-server-cloudimg-amd64-disk1.img
sudo mkdir -p /mnt/nbd
sudo mount /dev/nbd0p1 /mnt/nbd/ -o noatime
pushd /mnt/nbd
sudo mount -t proc proc proc
sudo mount -t sysfs sysfs sys
sudo mount -t devtmpfs devtmpfs dev
sudo mkdir -p dev/pts dev/shm
sudo mount -t devpts devpts dev/pts
sudo mount -t tmpfs shmtmpfs dev/shm
sudo mount -t tmpfs runtmpfs run
sudo touch run/utmp
sudo mkdir run/lock run/resolvconf
sudo chown root:uucp run/lock && sudo chmod 775 run/lock
sudo ln -s /dev/shm /run/shm
sudo mv etc/resolv.conf etc/resolv.conf.baseimage-backup
sudo cp /etc/resolv.conf etc/ # Or reconstruct a generic one, if running resolvconf :(
sudo chroot /mnt/nbd /bin/bash # You may see the following line if the host distro isn't Ubuntu...
groups: cannot find name for group ID 11

As root, within the chroot() gaol:

rm etc/mtab
rmdir media
ln -s /proc/self/mounts etc/mtab

The following statements are necessary to allow successful package installation without upstart/systemd running from the same system image…

dpkg-divert --local --rename --add /sbin/initctl
Adding 'local diversion of /sbin/initctl to /sbin/initctl.distrib'
dpkg-divert --local --rename --add /usr/sbin/invoke-rc.d
Adding 'local diversion of /usr/sbin/invoke-rc.d to /usr/sbin/invoke-rc.d.distrib'
ln -s /bin/true /sbin/initctl
ln -s /bin/true /usr/sbin/invoke-rc.d

Finally, install and updates with:

apt-get update
LC_ALL=C apt-get -qy install language-pack-en language-pack-en-base libpam-cracklib
apt-get -qy dist-upgrade
apt-get -y remove --purge linux-headers-3.13.0-46 linux-headers-3.13.0-46-generic linux-image-3.13.0-46-generic && rm -rf /usr/src/linux-headers-3.13.0-46*
apt-get -qy autoremove --purge
apt-get -qy autoclean

… and then the diversions can be removed, the gaol can be left, the image fully unmounted (cd && mount | grep nbd | cut -d' ' -f 3 | tac | sudo umount), and the Network Block Device disconnected (nbd-client -d /dev/nbd0) – at which point the resulting image can be deduplicated and compressed by running:

qemu-img convert -c -O qcow2 -o compat=0.10 ubuntu-14.04-server-cloudimg-amd64-disk1.img ubuntu-14.04-server-cloudimg-amd64-disk1.qcow2