The ability to quickly stand up a guest OS with cloud-init is most often associated with deployment of virtual machines in an IaaS like EC2 or Azure.
But cloud-init is not just for remote cloud providers, and using cloud-init for local images that can be quickly deployed in KVM works great for local development and testing.
This article will step through testing a guest Ubuntu bionic cloud image on KVM.
Prerequisites
As a prerequisite for this article, you must install KVM and libvirt as described here.
Also install additional packages needed to manage cloud-images:
sudo apt-get install -y cloud-image-utils
Ubuntu Cloud Image
We will use the Ubuntu bionic image called “bionic-server-cloudimg-amd64.img“. Download this 329Mb file to your “~/Downloads” directory.
Create a snapshot so that we can branch from this disk image without affecting the parent. We will also use this opportunity to increase the root filesystem from 2G to 10G.
# original image is 2G, create snapshot and make it 10G qemu-img create -b ~/Downloads/bionic-server-cloudimg-amd64.img -f qcow2 -F qcow2 snapshot-bionic-server-cloudimg.qcow2 10G # show snapshot info qemu-img info snapshot-bionic-server-cloudimg.qcow2
Create ssh keypair
In order to use ssh public/private key login later, we need to generate a keypair. cloud-init will embed the public side of the key into the running OS.
ssh-keygen -t rsa -b 4096 -f id_rsa -C test1 -N "" -q
This creates files named “id_rsa” and “id_rsa.pub”.
Create cloud-init configuration
Create a file named “cloud_init.cfg” with the below content. This content is also made available in my github local-kvm-cloudimage repository.
#cloud-config hostname: test1 fqdn: test1.example.com manage_etc_hosts: true users: - name: ubuntu sudo: ALL=(ALL) NOPASSWD:ALL groups: users, admin home: /home/ubuntu shell: /bin/bash lock_passwd: false ssh-authorized-keys: - <sshPUBKEY> # only cert auth via ssh (console access can still login) ssh_pwauth: false disable_root: false chpasswd: list: | ubuntu:linux expire: False package_update: true packages: - qemu-guest-agent # written to /var/log/cloud-init-output.log final_message: "The system is finally up, after $UPTIME seconds"
Then replace the “<sshPUBKEY>” placeholder in the file above, with the content of “id_rsa.pub”.
Create network configuration
Create a file named “network_config_static.cfg” to define the networking parameters. We will use a simple static configuration on the default KVM bridge subnet.
version: 2 ethernets: ens3: dhcp4: false # default libvirt network addresses: [ 192.168.122.158/24 ] gateway4: 192.168.122.1 nameservers: addresses: [ 192.168.122.1,8.8.8.8 ] search: [ example.com ]
Insert metadata into seed image
Now we generate a seed disk that has the cloud-config metadata.
# insert network and cloud config into seed image cloud-localds -v --network-config=network_config_static.cfg test1-seed.img cloud_init.cfg # show seed disk just generated $ qemu-img info test1-seed.img image: test1-seed.qcow2 file format: raw virtual size: 368K (376832 bytes) disk size: 368K
Start VM
Now we use virtlib to create the guest VM with the cloud image and seed disk that has the cloud-init metadata.
virt-install --name test1 \ --virt-type kvm --memory 2048 --vcpus 2 \ --boot hd,menu=on \ --disk path=test1-seed.img,device=cdrom \ --disk path=snapshot-bionic-server-cloudimg.qcow2,device=disk \ --graphics vnc \ --os-type Linux --os-variant ubuntu18.04 \ --network network:default \ --console pty,target_type=serial
After the machine has booted up and you have given cloud-init a couple of minutes to configure networking, you should be able to login to this guest OS from either virt-viewer or ssh.
ssh ubuntu@192.168.122.158 -i id_rsa # final cloud-init status cat /run/cloud-init/result.json # cloud logs vi /var/log/cloud-init.log vi /var/log/cloud-init-output.log
Disable cloud-init system
Once setup with the proper hostname, network config, packages, etc., you can either leave cloud-config enabled to enforce these settings, or you can disable cloud-init so that you can manage them yourself (or another tool).
From within OS:
# flag that signals that cloud-init should not run sudo touch /etc/cloud/cloud-init.disabled # optional, remove cloud-init completely sudo apt-get purge cloud-init # shutdown VM so CDROM seed can be ejected sudo shutdown -h now
From host OS using virsh to control libvirt eject.
# get name of target path targetDrive=$(virsh domblklist test1 | grep test1-seed.img | awk {' print $1 '}) # force ejection of CD virsh change-media test1 --path $targetDrive --eject --force
cloud-init will no longer be invoked when the guest VM is powered back on.
More advanced example
For a more advanced example see my local-kvm-cloudimage/ubuntu-bionic repository. This has full shell scripts and support for additional data disks formatted as xfs.
REFERENCES
theurbanpenguin, cloud-images and kvm
stafwag, centos and cloud-init on kvm
blog.strandboge.com, cloud images, qemu, cloud-init
serverfault, multipole qemu port forwards
smoser asciicinema, boot ubuntu with networking config through NoCloud
debian, cloud-localds man page
bugs.launchpad.net, cloud-image known bug “error: no such device: root”
cloudinit docs, removing cloud-init using /etc/cloud/cloud-init.disabled
poftut.com, qemu command examples
github george-hawkins, qemu-system command syntax for arm
spyff.github.io, qemu shared folder between guest and host
mymediasystem.net, cloud-init and vmware VGA driver settings
cmdref.net, qemu-image to convert between sparse and non-sparse
NOTS
check os-variant list
sudo apt install libosinfo-bin osinfo-query os
Replacement of cert placeholder with sed
# get content of public key pubkey=$(cat id_rsa.pub) # replace placeholder with public key # using percent sign for sed separator char # this requires that cloud_init.cfg not include this char sed "s% - <sshPUBKEY>% - $pubkey%" cloud_init.cfg