KVM: Testing cloud-init locally using KVM for an Ubuntu cloud image

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

man virt-install

man qemu-img

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