cloud-init for VM Templates
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-configYAML 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¶
- Start from the distro cloud image (already has cloud-init).
- Boot it once with minimal user-data, install your baseline (agents, monitoring, base packages), harden.
- 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 - Shut down,
virt-sparsify/ compress, mark as a template. - 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) withuser-data+meta-data. Checkcloud-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 theinstance-idinmeta-data, orcloud-init cleanto 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 userdatato confirm it parsed. #cloud-configignored, 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-idand host keys in the template. - Network not applied — the distro uses netplan/NM and the
network-configschema version is wrong for it, or the interface name doesn't match (enp1s0vsens3vseth0— depends on the machine type/NIC). runcmdfailed silently — checkcloud-init-output.log;runcmdruns late (cloud-final) and a failing command doesn't stop the rest.
Related tools and reading¶
- On-site: YAML ↔ JSON converter, Linux commands reference.
- Related posts: KVM and libvirt: your first VM, Structuring an Ansible playbook you can maintain.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Virtualization
virtio Drivers Explained
Emulating a real NIC or disk controller is slow. virtio is a paravirtual interface the guest talks to directly — here's what each virtio device does and how to enable it.
October 24, 2026GPU Passthrough with VFIO
Passing a physical GPU to a VM needs IOMMU, a clean IOMMU group, vfio-pci binding, and often a vBIOS. Here's the path and the notorious failure modes.
October 24, 2026libvirt Networking: NAT vs Bridged vs Others
The default libvirt network NATs VMs behind the host — fine for a laptop, wrong for a server. Here's each mode and how to put VMs directly on your LAN.
October 24, 2026