Provisioning KVM Virtual Machines with Cloud Init
Table of Contents
- KVM Provisioning with cloud-init
- Introduction
- Requirement
- Guide
- Branch from the Downloaded Image
- Create the Provisioning Configuration
- Bridge Networking
- Creating a New Virtual Machine
- Clean Up
- References
KVM Provisioning with cloud-init
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 Managing KVM Virtual Machines with Cockpit or a similar guide.
Guide
Install Required Packages
We first needd 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
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 to the same folder. 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
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
#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>
The <SSH public key>
needs to be replaced accordingly.
For networking, we create a file called network-config
. This is an example for a fixed IP configuration:
version: 2
ethernets:
enp1s0:
dhcp4: false
addresses:
- 192.168.122.100/24
gateway4: 192.168.122.1
nameservers:
addresses:
- 192.168.122.1
- 8.8.8.8
In my case, I’ve used the dynamic IP configuration below:
version: 2
ethernets:
enp1s0:
dhcp4: true
I had named the interface
ens1
, 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 thenetwork-config
file and the guests started getting IP addresses from the DHCP server.
Let’s now create a disk image containing the provisioning configuration. 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 uses
genisoimage
instead ofcloud-localds
Bridge Networking
Before creating a new virtual machine, we are going to create a network bridge on the host first. This will allow our 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 network, 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.
Creating a New Virtual Machine
Now, we can create the new virtual machine:
$ 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.
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, 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 clienst on my router’s admin screen. So, in this case, it’s probably a better idea to set a static IP address for the guest or, maybe, fix the guest’s IP address in the router. 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
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