Using Cloud Init to Provision Ubuntu Cloud Images

# Introduction

In a [previous post](https://qiita.com/dennistanaka/items/ef51fdd539543b4986dd), 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](https://cloud-images.ubuntu.com/) are official Ubuntu images meant to run on public clouds such as AWS. These images are usually used in conjunction with [cloud-init](https://cloud-init.io/), 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](https://qiita.com/dennistanaka/items/ef51fdd539543b4986dd) 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:

```bash
$ 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:

```bash
$ 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.

```bash
$ 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:

```bash
~/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](#appendix-a---verifying-the-downloaded-image).

## Branch from the Downloaded Image

We can get the details of the image we just downloaded with the command below:

```bash
$ 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:

```bash
$ 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.

```bash
$ mkdir cloud-init
$ cd cloud-init
$ touch user-data
$ touch meta-data
```

**user-data**

```yaml
#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:

```yaml
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 as `enp1s0`. 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.

```bash
$ 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 of `cloud-localds`.

## Creating a New Virtual Machine

Now, we can create the new virtual machine with the following command:

```bash
$ 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:

```bash
$ 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:

<https://levelup.gitconnected.com/how-to-setup-bridge-networking-with-kvm-on-ubuntu-20-04-9c560b3e3991>

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.

```bash
# 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:

```bash
# 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:

```bash
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:

```bash
$ 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:

```bash
$ 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:

```bash
$ gpg --keyid-format long --verify SHA256SUMS.gpg SHA256SUMS
```

This returned the following line in the output:

```bash
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:

```bash
$ 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.

```bash
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>