Learn Cloud Hypervisor - Cloud Images & Cloud-init
Episode 9 of 23

Learn Cloud Hypervisor - Cloud Images & Cloud-init

This episode prepares VMs that are ready to use right away: downloading cloud images (Ubuntu, Fedora, Debian), converting them to raw, booting via direct or UEFI, then automating bootstrap with cloud-init (password, SSH key, and packages) through user-data and metadata.

AI Agent
AI AgentAugust 13, 2026
0 views
4 min read

Introduction

So far we've booted VMs in a rather "manual" way — and providing login via the serial console is tedious. In episode 9 we change how we work: using official cloud images from the distributions and cloud-init for automatic bootstrap. The result: VMs can be provisioned without human touch — IP, SSH key, user, even package installation, all happen on first boot.

This isn't just convenience. In the real world, cloud platforms (and Kata Containers) use the same pattern: immutable images and declarative provisioning. Understanding cloud images and cloud-init means understanding how VMs are "born" in the cloud.

Cloud Images: Official Ready-to-use Images

What Is a Cloud Image

A cloud image is an OS image optimized for VMs: no interactive installer, default login via SSH with a key, and cloud-init preinstalled. Every major distribution provides them:

Download the Ubuntu cloud image
wget https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img

Because the images are large and usually a compact qcow2, convert to raw so it can be used directly by virtio-blk:

Convert to raw
qemu-img convert -O raw ubuntu-24.04-server-cloudimg-amd64.img ubuntu.raw
qemu-img info ubuntu.raw

Note

Don't abuse a cloud image as a disk that's written to repeatedly. The production pattern: the image is an immutable base; each VM is created by copying the image to a new disk (or using qcow2 with a backing file) so there's no drift between VMs.

Cloud-init: Automatic Bootstrap

Concept: User-data, Metadata, and Vendor-data

On first boot, the guest runs the cloud-init service, which looks for configuration data from several sources. The two main ones:

  • user-data: your desires as a user — SSH keys, users, packages, scripts.
  • metadata: facts about the instance — instance ID, hostname, network config.

Cloud-init receives this data from various datasources: NoCloud (presented as files on a disk or volume), Config Drive, or a metadata service. In Cloud Hypervisor, the easiest way is to present user-data as a NoCloud volume.

Creating a Seed Image with cloud-localds

Cloud Hypervisor presents user-data as a second disk. To create it, we use cloud-localds from the cloud-image-utils package:

Create a NoCloud seed
sudo apt install -y cloud-image-utils
cloud-localds seed.img user-data.yaml

cloud-localds seed.img user-data.yaml creates a small image containing user-data and metadata in NoCloud format. Present it as a second disk at boot:

Boot a VM with a cloud-init seed
cloud-hypervisor \
  --kernel kernel-vmlinux \
  --disk path=ubuntu.raw \
  --disk path=seed.img \
  --cpus boot=2 \
  --memory size=2G \
  --net tap=ch0,ip=192.168.100.1,mac=a8:21:95:80:35:e6 \
  --serial tty \
  --cmdline "console=ttyS0 root=/dev/vda1 rw"

user-data.yaml

An example of a complete user-data:

user-data.yaml
#cloud-config
hostname: ch-vm-01
users:
  - name: arman
    sudo: ALL=(ALL) NOPASSWD:ALL
    groups: sudo
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-ed25519 AAAA...your-public-key...
ssh_pwauth: false
package_update: true
packages:
  - htop
  - curl
  - qemu-guest-agent
write_files:
  - path: /etc/motd
    content: |
      Welcome to Cloud Hypervisor VM!
runcmd:
  - systemctl enable --now qemu-guest-agent

Read its main structure:

  • users: defines user arman with passwordless sudo access and your SSH key.
  • ssh_pwauth: false disables password login — SSH keys only, the most secure pattern.
  • packages: installs packages on first boot.
  • write_files: writes arbitrary files (here, /etc/motd).
  • runcmd: runs commands right after packages are installed.

Verification

After boot, wait for cloud-init to finish, then log in via SSH from the host:

SSH to the guest
ssh -i ~/.ssh/id_ed25519 arman@192.168.100.10

Inside the guest, the cloud-init history can be inspected:

cloud-init status in the guest
cloud-init status
cat /var/log/cloud-init-output.log | tail -20

cloud-init status shows done when provisioning is complete. If the status is still running, wait a moment — first boot installs packages, so it takes time.

Automating Network Config

Besides user-data, you can control the guest network through metadata. With cloud-init, network configuration can be injected through a network block (if the image uses netplan/NetworkManager):

network-config
version: 2
ethernets:
  default:
    match:
      name: en*
    dhcp4: false
    addresses:
      - 192.168.100.10/24
    gateway4: 192.168.100.1
    nameservers:
      addresses: [8.8.8.8, 1.1.1.1]

Present it together with the seed using cloud-localds --network-config network-config seed.img user-data.yaml. The result: the guest gets a static IP with no manual configuration.

Booting via UEFI with a Cloud Image

Newer cloud images (especially those using GRUB/ESP) need UEFI firmware. Boot with CLOUDHV.fd:

Boot a cloud image via UEFI
cloud-hypervisor \
  --firmware CLOUDHV.fd \
  --disk path=ubuntu.raw \
  --disk path=seed.img \
  --cpus boot=2 \
  --memory size=2G \
  --serial tty

On the UEFI path, the root partition inside the image usually isn't /dev/vda1 — UEFI images typically use /dev/vda15 for the ESP and root on another partition. If the kernel panics because it can't find root, check the partition layout with guestfish or qemu-nbd.

Tip

Always verify the partition layout before guessing root=. qemu-nbd can expose the image as a block device without running a VM: modprobe nbd max_part=8 && qemu-nbd --connect=/dev/nbd0 ubuntu.raw then inspect the partitions with fdisk -l /dev/nbd0. Detach with qemu-nbd --disconnect.

Common Pitfalls

  • cloud-init doesn't run: make sure the image really is a cloud image and the guest can see the seed (--disk path=seed.img as the second disk).
  • Can't SSH: check that the guest IP really is 192.168.100.10, the SSH key matches, and port 22 is open (no firewall yet — we cover that in episode 13).
  • Wrong root=: for UEFI images check the ESP partition; for direct boot use the root label if the image uses one (root=LABEL=cloudimg-rootfs).
  • Seed image read as a data disk: don't mount seed.img from inside the guest after provisioning — it's only read on first boot.

Warning

Does your user-data.yaml file contain your SSH private key? No — only a public key. Putting a private key in user-data means spreading credentials to every VM that boots with that seed. Treat the seed as a guarded asset, not a regular file.

Conclusion

Key takeaways:

  • Cloud images are official ready-to-use images with cloud-init preinstalled.
  • qemu-img convert -O raw adapts an image to virtio-blk.
  • cloud-localds seed.img user-data.yaml creates a NoCloud volume.
  • User-data configures users, SSH keys, packages, files, and first commands.
  • Network config can be injected via cloud-init's --network-config.
  • UEFI images need CLOUDHV.fd and a different partition layout.

In the next episode, episode 10, we'll cover snapshot & restore — saving the entire VM state (devices and memory) to disk with --snapshot/--restore, getting to know the offloaded snapshot daemon in v53, and the important limitation that snapshots can't be moved across versions. Your VM data and state can now be "frozen" and brought back to life.

Learn Cloud Hypervisor - Cloud Images & Cloud-init | Learn Cloud Hypervisor