Introduction
In a previous post, I showed how I installed KVM on a spare laptop and used Cockpit to remotely manage virtual machines. In this post, I will to through the steps I took to quickly provision VMs based on Ubuntu Cloud Image with cloud-init.
Ubuntu Cloud Images are official Ubuntu images meant to run on public clouds such as AWS. These images are usually used in conjunction with cloud-init, a tool created by Canonical that is used to customize cloud images when virtual machines are first run.
cloud-init
runs a set of user-defined configurations in the instance’s first boot and can be used, for example, to generate and setup SSH private keys. In this example, we are going to have something similar to what we get on AWS: an Ubuntu virtual machine we can connect remotely through SSH, in which we can run passwordless sudo commands.
Ubuntu Cloud Images have shell access disabled by default, although we can customize this aspect as well.
Requirement
Having installed KVM, following Installing Cockpit to Manage KVM VMs or a similar guide.
Guide
Bridge Networking
In my case, the host is a separate machine on my local network, so I've created a network bridge on it to allow the virtual machines to connect to the local network directly and become visible in this context. Let’s first install the necessary dependency:
$ sudo apt install bridge-utils
Next, let’s edit the /etc/network/interfaces
file that was originally like this:
# interfaces(5) file used by ifup(8) and ifdown(8)
# Include files from /etc/network/interfaces.d:
source-directory /etc/network/interfaces.d
Add the following lines after the existing content:
auto lo
iface lo inet loopback
auto br0
iface br0 inet dhcp
bridge_ports enp6s0
bridge_stp off
bridge_fd 0
bridge_maxwait 0
In my case, I opted for DHCP as I’ve reserved my host IP address in the router. Also, bridge_ports
need to be replaced accordingly (in my case, the ethernet interface on the host is enp6s0
).
Now, we just need to restart networking:
$ sudo /etc/init.d/networking restart
Just by restarting the networking, my ethernet adapter was still showing up with an IP address assigned. So, I restarted the host and only the bridge is visible now, as it is supposed to be.
Install Required Packages
We first need to install cloud-image-utils
. This is needed to provision the client OS in the first boot.
$ sudo apt install cloud-image-utils
Download the Ubuntu Cloud Image
Ubuntu provides their cloud images at https://cloud-images.ubuntu.com/. In my case, I chose to use Bionic 18.04 LTS. I’ve created a ~/cloud_images
folder to store such files:
~/cloud_images$ wget https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img
It's always a good idea to check if the image is not corrupt nor has been tampered with. I've added instructions on how to do so in the appendix.
Branch from the Downloaded Image
We can get the details of the image we just downloaded with the command below:
$ qemu-img info bionic-server-cloudimg-amd64.img
# Output
image: bionic-server-cloudimg-amd64.img
file format: qcow2
virtual size: 2.2 GiB (2361393152 bytes)
disk size: 343 MiB
cluster_size: 65536
Format specific information:
compat: 0.10
refcount bits: 16
As we can see, the downloaded image is way too small for any practical purpose, so let’s create a new 20GB image based on the downloaded one:
$ qemu-img create -f qcow2 -F qcow2 -b bionic-server-cloudimg-amd64.img vm_0001-bionic-server-cloudimg-amd64.qcow2 20G
$ qemu-img info vm_0001-bionic-server-cloudimg-amd64.qcow2
# Output
image: vm_0001-bionic-server-cloudimg-amd64.qcow2
file format: qcow2
virtual size: 20 GiB (21474836480 bytes)
disk size: 196 KiB
cluster_size: 65536
backing file: bionic-server-cloudimg-amd64.img
Format specific information:
compat: 1.1
lazy refcounts: false
refcount bits: 16
corrupt: false
Note: we could have also made a copy of the downloaded image and resized with
qemu-img resize vm_0001-bionic-server-cloudimg-amd64.qcow2 20G
Create the Provisioning Configuration
Now, we are going to create the cloud-init
configuration to be used in the guest provisioning. Let’s first create a user-data
file containing the content below. The meta-data
file may be used in the future but, for now, let’s keep it empty.
$ mkdir cloud-init
$ cd cloud-init
$ touch user-data
$ touch meta-data
user-data
#cloud-config
hostname: vm_0001
fqdn: vm_0001.localdomain
manage_etc_hosts: true
ssh_pwauth: false
disable_root: true
users:
- name: ubuntu
home: /home/ubuntu
shell: /bin/bash
groups: sudo
sudo: ALL=(ALL) NOPASSWD:ALL
ssh-authorized-keys:
- <SSH public key 1>
- <SSH public key 2>
The <SSH public key n>
needs to be replaced accordingly. This configuration defines the guest's host name, disables root SSH access as well as password authentication, configures passwordless sudo
and the authorized keys.
For networking, we create a file called network-config
. In this example, I will use the dynamic IP configuration below:
version: 2
ethernets:
enp1s0:
dhcp4: true
I had wrongly named the interface
ens1
at first, but the guest VM was not able to have an IP address assigned when started. In these failed VM starts, network information was displayed during the boot and I noticed the interface was being identified asenp1s0
. So, I’ve changed the interface name in the network-config file and the guests started getting IP addresses from the DHCP server.
Let’s now create a disk image containing the provisioning configuration, what will be attached to the guest on initialization. This is where the cloud-image-utils
package is used.
$ cloud-localds -v --network-config=network-config cloud-init-provisioning.qcow2 user-data meta-data
One of the tutorials in the References uses
genisoimage
instead ofcloud-localds
.
Creating a New Virtual Machine
Now, we can create the new virtual machine with the following command:
$ virt-install \
--name vm_0001 \
--virt-type kvm \
--vcpus 2 \
--memory 2048 \
--disk path=cloud_images/vm_0001-bionic-server-cloudimg-amd64.qcow2,device=disk \
--disk path=cloud-init/cloud-init-provisioning.qcow2,device=cdrom \
--os-type Linux \
--os-variant ubuntu18.04 \
--graphics none \
--network bridge=br0 \
--import
The machine will start its initialization process until it reaches the login prompt. According to our configuration, we are supposed to access the guest through SSH and no user is allowed shell access to it. We can go back to our shell with the Ctrl + ]
key combination. As mentioned in my previous post, I'm using Cockpit and the new virtual machine shows up in interface right after its creation.
In my case, my KVM host is not my main desktop, so the guest is on a separate machine, but accessing it from my desktop should not be a problem since the guest is using bridge networking. Since I set the SSH public key from my desktop in the cloud-init provisioning configuration, I can access the newly created guest with:
$ ssh ubuntu@<ip address of the guest>
We can discover the IP address of the guest during its boot process when network information is displayed. But, because I've configured the guest to use DHCP in this example, as far as I know, the only way to check the IP address assigned to the guest afterwards is to check the list of DHCP clients on my router’s admin screen. We can fix the guest's IP address in the router as well, but this is not exactly practical, so I intend to stick to using static IP addresses instead. The article below mentions the same limitation:
Lastly, we can check some output generated by cloud-init in the following files:
/run/cloud-init/result.json
/var/log/cloud-init.log
/var/log/cloud-init-output.log
Clean Up
cloud-init is setup to run everytime the machine starts and it can be left this way if we want to enforce those settings. If that’s not the case, we can disable cloud-init.
# this file disables cloud-init execution
$ sudo touch /etc/cloud/cloud-init.disabled
# alternatively, we could remove the cloud-init package
Conclusion
Before start playing with KVM, I was not familiar with cloud-init, so it was interesting to learn a little bit about how it can help in the provisioning step.
I still haven't done so, but it should be quite easy to automate the process to create a new VM with a single script. So, that's what I intend to do next.
Appendix A - Verifying the Downloaded Image
To make sure the image is not corrupted nor has been tampered with, let’s execute the steps below. We use gpg
to verify the image’s authenticity and sha256
to verify its integrity. The necessary tools are installed by default on Pop_OS! I’m using. For other cases, you can check the link below:
https://ubuntu.com/tutorials/how-to-verify-ubuntu#2-necessary-software
But first, also download both the SHA256SUMS
and SHA256SUMS.gpg
files from the Ubuntu repository to the same folder as the image itself. Then:
# check if we need to download the public key used to authenticate the checksum file:
$ gpg --keyid-format long --verify SHA256SUMS.gpg SHA256SUMS
For me, it returned something like:
gpg: Signature made <date and time>...
gpg: using RSA key <key ID>
gpg: Can't check signature: No public key
From this message, we know the ID of the key we need to request to the Ubuntu key server:
$ gpg --keyid-format long --keyserver hkp://keyserver.ubuntu.com --recv-keys <key ID>
This command should report that the key was imported, what means that it was retrieved and added to the keyring. Now, we can inspect the key fingerprints:
$ gpg --keyid-format long --list-keys --with-fingerprint 0x<key ID>
It should display the key used to sign Ubuntu Cloud Images checksums. We can now verify the checksum file using the signature:
$ gpg --keyid-format long --verify SHA256SUMS.gpg SHA256SUMS
This returned the following line in the output:
gpg: Good signature from "UEC Image Automatic Signing Key <cdimage@ubuntu.com>" [unknown]
A Good signature means the checksum file was indeed created by Ubuntu. So, now we can check that the image’s sha256 checksum matches the downloaded checksum:
$ sha256sum -c SHA256SUMS 2>&1 | grep OK
An output like the following indicates the ISO file matches the checksum and should be used without problems.
bionic-server-cloudimg-amd64.img: OK
References
- https://ubuntu.com/tutorials/how-to-verify-ubuntu#1-overview
- https://fabianlee.org/2020/02/23/kvm-testing-cloud-init-locally-using-kvm-for-an-ubuntu-cloud-image/
- https://stafwag.github.io/blog/blog/2019/03/03/howto-use-centos-cloud-images-with-cloud-init/
- https://medium.com/@art.vasilyev/use-ubuntu-cloud-image-with-kvm-1f28c19f82f8
- https://medium.com/@yping88/use-ubuntu-server-20-04-cloud-image-to-create-a-kvm-virtual-machine-with-fixed-network-properties-62ecae025f6c
- https://help.ubuntu.com/community/KVM/Networking#Creating_a_network_bridge_on_the_host
Additional Information
The URL below provides a very friendly overview of cloud-init:
https://www.digitalocean.com/community/tutorials/an-introduction-to-cloud-config-scripting