End-to-end QEMU foundation for prep. Builds a customized Ubuntu cloud image with Packer + Ansible, then boots it under QEMU with a cloud-init seed ISO for per-boot configuration.
Ubuntu cloud qcow2 (upstream)
│
▼
Packer (qemu builder, disk_image=true)
│ ┌─ boots upstream image with a transient
│ │ cloud-init seed (packer/cidata/) that
│ │ creates an SSH user
│ └─ Packer SSHes in and runs:
▼
Ansible playbook (ansible/playbook.yml)
│ pkgs, sysctl, MOTD, users, marker file
▼
ubuntu-custom.qcow2 ◄── the "golden image"
│
▼
QEMU launch with:
• a qcow2 overlay over the golden image
• a runtime cloud-init seed.iso (cloud-init/)
│
▼
sre-lab-vm (SSH on host :2222)
Two cloud-init configs, two different jobs:
packer/cidata/— used during the build only, to give Packer an SSH user.cloud-init/— used at runtime, every time the final image boots.
qemu-lab/
├── packer/
│ ├── ubuntu.pkr.hcl # Packer template (qemu builder, disk_image=true)
│ └── cidata/ # Cloud-init seed used during Packer build only
│ ├── user-data # creates the `packer` SSH user
│ └── meta-data
├── ansible/
│ └── playbook.yml # Provisions the golden image (pkgs, sysctl, users)
├── cloud-init/ # Runtime seed: reconfigures the VM on every boot
│ ├── user-data # ← put your SSH pubkey here
│ └── meta-data
└── scripts/
├── build.sh # Run Packer → packer/output/ubuntu-custom.qcow2
├── make-seed.sh # Build seed.iso from cloud-init/
├── run.sh # Create qcow2 overlay + launch QEMU
└── cleanup.sh # Remove generated artifacts
sudo apt install -y qemu-system-x86 qemu-utils cloud-image-utils ansible packer
# Confirm KVM:
ls -l /dev/kvm # you must be in the kvm group (or root)If you're running this lab inside another VM, enable nested virtualization
on the host or kvm-ok will fail.
# 1. Build the custom image (downloads upstream cloud image on first run)
bash scripts/build.sh
# 2. Edit cloud-init/user-data and paste your ssh pubkey
vim cloud-init/user-data
# 3. Build the runtime seed ISO
bash scripts/make-seed.sh
# 4. Boot
bash scripts/run.sh
# In another terminal, once cloud-init finishes:
ssh -p 2222 sre@localhostQuit the QEMU serial console with Ctrl-A then X.
run.sh creates a fresh run.qcow2 overlay every time it starts, so the
golden image is never modified. Any changes made inside the VM are discarded
when you re-run run.sh.
# Remove per-run ephemeral files (run.qcow2, seed.iso)
bash scripts/cleanup.sh
# Remove everything including the golden image and packer download cache
bash scripts/cleanup.sh --allAfter --all, the next build.sh will re-download the upstream cloud image.
disk_image=trueskips full ISO installation — Packer treats the source as the starting disk. Standard pattern for cloud images.- NoCloud datasource: cloud-init looks for a filesystem labeled
cidatacontaininguser-dataandmeta-data. A CD-ROM with that label is the simplest way to deliver it; that's what both seed ISOs are. - The runtime overlay (
qemu-img create -f qcow2 -F qcow2 -b GOLDEN) keeps the golden image immutable. Throw away the overlay to reset. virtioeverywhere (disk, net) is paravirtualization — the guest knows it's a VM and uses a thin driver, vs. emulating real hardware.-cpu hostexposes the physical CPU's feature set to the guest. Critical for confidential computing (SEV/TDX) where the guest needs to see those flags.usernetworking (SLIRP) is convenient but slow and NAT-only. For real workloads you switch to a tap device on a Linux bridge.