Server

cloud-init for VM Templates

By Prabath Thalangama· October 24, 2026· 4 min read
#cloud-init#kvm#automation

Introduction

cloud-init is the de facto standard for first-boot VM configuration. Distro "cloud images" ship with it installed and enabled. You provide a small configuration; on first boot cloud-init reads it and sets up the machine, then disables itself.

The datasources

cloud-init detects where to read config from. For cloud it's the metadata service; for local KVM/libvirt it's usually NoCloud — a tiny ISO (or vfat disk) with two files:

  • meta-data — instance id, hostname.
  • user-data — the main config (a #cloud-config YAML or a script).
  • network-config — optional, network setup (v1 or v2/netplan schema).

user-data example

#cloud-config
hostname: web01
fqdn: web01.example.com
manage_etc_hosts: true

users:
  - name: deploy
    groups: [sudo]
    sudo: "ALL=(ALL) NOPASSWD:ALL"
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-ed25519 AAAA... [email protected]

package_update: true
package_upgrade: true
packages:
  - qemu-guest-agent
  - chrony

write_files:
  - path: /etc/myapp/config.yaml
    content: |
      environment: production
    permissions: "0644"

runcmd:
  - systemctl enable --now qemu-guest-agent
  - [ systemctl, restart, chrony ]

power_state:
  mode: reboot
  condition: true

network-config example (v2)

version: 2
ethernets:
  enp1s0:
    dhcp4: false
    addresses: [10.0.1.20/24]
    gateway4: 10.0.1.1
    nameservers:
      addresses: [10.0.1.1]

Building the NoCloud seed

# meta-data
cat > meta-data <<EOF
instance-id: web01-$(date +%s)
local-hostname: web01
EOF

# make the seed ISO
cloud-localds seed.iso user-data meta-data network-config
# (cloud-localds is from cloud-image-utils; or use genisoimage with -volid cidata)

Attach seed.iso as a CD-ROM to the VM alongside a copy of the cloud image:

qemu-img create -f qcow2 -F qcow2 -b jammy-server-cloudimg-amd64.img web01.qcow2 20G

virt-install --name web01 --memory 2048 --vcpus 2 \
  --disk web01.qcow2,device=disk,bus=virtio \
  --disk seed.iso,device=cdrom \
  --os-variant ubuntu22.04 --import --network bridge=br0 --noautoconsole

libvirt/virt-install and oVirt/OpenStack can also inject cloud-init config directly (--cloud-init, the oVirt "Run Once → Initial Run" tab) without you building the ISO by hand.

Building a golden template

  1. Start from the distro cloud image (already has cloud-init).
  2. Boot it once with minimal user-data, install your baseline (agents, monitoring, base packages), harden.
  3. Clean cloud-init state so it runs fresh on clones:
    cloud-init clean --logs --seed
    truncate -s 0 /etc/machine-id     # regenerate per clone
    rm -f /etc/ssh/ssh_host_*         # regenerate host keys per clone
    
  4. Shut down, virt-sparsify / compress, mark as a template.
  5. Each new VM gets its own seed → unique hostname, keys, IP.

Verification and troubleshooting

cloud-init status --long           # done / running / error
cloud-init analyze show            # what ran, timings
journalctl -u cloud-init -u cloud-init-local -u cloud-config -u cloud-final
cat /var/log/cloud-init.log /var/log/cloud-init-output.log
cloud-init query userdata          # what config did it actually see?
  • cloud-init did nothing — no datasource found. NoCloud needs the seed disk labelled cidata (volume id) with user-data + meta-data. Check cloud-init query platform / the log for "no instance data".
  • Ran once, ignores new config on reboot — by design; cloud-init keys off instance-id. Change the instance-id in meta-data, or cloud-init clean to force a re-run.
  • SSH key not added / can't log in — YAML indentation in ssh_authorized_keys, wrong user, or the image's default user differs (ubuntu, cloud-user, ec2-user, debian). cloud-init query userdata to confirm it parsed.
  • #cloud-config ignored, treated as a script — the very first line must be exactly #cloud-config (no leading space, no BOM).
  • Cloned VMs share a hostname / SSH host key / DHCP lease — you didn't clean machine-id and host keys in the template.
  • Network not applied — the distro uses netplan/NM and the network-config schema version is wrong for it, or the interface name doesn't match (enp1s0 vs ens3 vs eth0 — depends on the machine type/NIC).
  • runcmd failed silently — check cloud-init-output.log; runcmd runs late (cloud-final) and a failing command doesn't stop the rest.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.