Field Notes


3 min read

Work with Ubuntu Cloud Images

Installing Ubuntu from an ISO means clicking through an installer every time. Ubuntu’s cloud images skip that. They’re pre-installed disk images, the same ones cloud providers boot, and they configure themselves on first start with cloud-init: hostname, users, SSH keys, packages.

On a Linux machine with KVM, a fresh VM from a cloud image takes about a minute. This note shows the whole path, on Ubuntu 24.04 as the host and the guest.

1. Check for hardware virtualisation

KVM needs a CPU with Intel VT-x or AMD-V:

egrep -c '(vmx|svm)' /proc/cpuinfo

0 means the CPU doesn’t support it, or it’s switched off in the BIOS or UEFI settings. Anything above 0 is fine.

2. Install KVM and libvirt

sudo apt update
sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients virtinst cloud-image-utils
Package What it’s for
qemu-kvm The emulator, using the CPU’s virtualisation
libvirt-daemon-system The daemon that manages VMs, disks and networks
libvirt-clients virsh, the command line for libvirt
virtinst virt-install, which creates VMs
cloud-image-utils cloud-localds, which builds the cloud-init seed

To manage VMs without sudo, add yourself to the libvirt group, then log out and back in:

sudo adduser $USER libvirt

Without sudo, virsh connects to a private per-user session by default, not to the system-wide libvirt where the VMs below live. Point it at the system connection, and add the line to your shell profile:

export LIBVIRT_DEFAULT_URI=qemu:///system

Check that libvirt answers:

virsh list --all

An empty table is the right answer. A “permission denied” error usually means the new group isn’t active yet. If the daemon isn’t running, start it with sudo systemctl enable --now libvirtd.

3. Download a cloud image

Cloud images aren’t ISO files. They’re disk images in qcow2 format, published at cloud-images.ubuntu.com. Keep one copy as a read-only base:

sudo mkdir -p /var/lib/libvirt/images/base
sudo wget -O /var/lib/libvirt/images/base/noble.img \
  https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img

The images live under /var/lib/libvirt/images so that libvirt’s QEMU user can read them. Images in your home directory often fail with permission errors.

4. Give each VM its own disk

Don’t boot the base image directly. Create a new disk that uses it as a backing file. The VM only stores its own changes, so a new VM costs almost no space and the base stays clean:

sudo qemu-img create -f qcow2 -F qcow2 \
  -b /var/lib/libvirt/images/base/noble.img \
  /var/lib/libvirt/images/vm1.qcow2 20G

20G is the size the VM sees. The file grows only as the VM writes data.

5. Write the cloud-init seed

cloud-init reads its configuration from a small disk attached to the VM. Two files go into it.

user-data sets up the machine. Put your own public SSH key in it:

#cloud-config
hostname: vm1
users:
  - name: dev
    sudo: ALL=(ALL) NOPASSWD:ALL
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-ed25519 AAAA... you@laptop
package_update: true
packages:
  - htop

meta-data identifies the instance:

instance-id: vm1
local-hostname: vm1

Build the seed disk from both files:

sudo cloud-localds /var/lib/libvirt/images/vm1-seed.iso user-data meta-data

6. Start the VM

virt-install \
  --name vm1 \
  --memory 2048 \
  --vcpus 2 \
  --disk path=/var/lib/libvirt/images/vm1.qcow2,format=qcow2 \
  --disk path=/var/lib/libvirt/images/vm1-seed.iso,device=cdrom \
  --os-variant ubuntu24.04 \
  --network network=default \
  --import \
  --graphics none \
  --noautoconsole

--import tells virt-install to boot the existing disk instead of running an installer.

7. Connect

After a few seconds, the VM has an address on libvirt’s default network:

virsh domifaddr vm1
ssh dev@<IP_ADDRESS>

If the address doesn’t show yet, wait a moment: cloud-init is still running on first boot.

Clean up

Remove the VM and its disks when you’re done. The base image stays for the next one:

virsh destroy vm1
virsh undefine vm1 --remove-all-storage

A new VM is steps 4 to 6 again, with a different name.

More in Field Notes

Share