Examples on how to use a cloud-config script with cloud-init – KVM

First, you need to download the cloud images and resize them in order to make sure that the disk size after everything is installed is large enough to accommodate all the files you plan to put in the machine you created. In these examples, we are going to use two images, one for CentOS, and another for Ubuntu Server. We can see that the CentOS image we are using is 8 GB in size, and we will enlarge it to 10 GB. Note that the actual size on the disk is not going to be 10 GB; we are just allowing the image to grow to this size.

We are going to do the same with the Ubuntu image, after we get it from the internet. Ubuntu also publishes cloud versions of their distribution daily, for all supported versions. The main difference is that Ubuntu creates images that are designed to be 2.2 GB when full. We downloaded an image from https://cloud.centos.org; let’s now get some information about it:

 Figure 9.3 – Cloud-init image sizes

Figure 9.3 – Cloud-init image sizes

Note that the actual size on the disk is different – qemu-img gives us 679 MB and 2.2 GB versus roughly 330 MB and 680 MB of actual disk usage:

 Figure 9.4 – Image size via qemu-img differs from the real virtual image size

Figure 9.4 – Image size via qemu-img differs from the real virtual image size

We can now do a couple of everyday administration tasks on these images – grow them, move them to the correct directory for KVM, use them as a base image, and then customize them via cloud-init:

  1. Let’s make these images bigger, just so that we can have them ready for future capacity needs (and practice):
    Figure 9.5 – Growing the Ubuntu and CentOS maximum image size to 10 GB via qemu-img

    Figure 9.5 – Growing the Ubuntu and CentOS maximum image size to 10 GB via qemu-img

    After growing our images, note that the size on the disk hasn’t changed much:

     Figure 9.6 – The real disk usage has changed only slightly

    Figure 9.6 – The real disk usage has changed only slightly

    The next step is to prepare our environment for the cloud-image procedure so that we can enable cloud-init to do its magic.

  2. The images that we are going to use are going to be stored in /var/lib/libvirt/images:
     Figure 9.7 – Moving images to the KVM default system directory

    Figure 9.7 – Moving images to the KVM default system directory

    We are going to create our first cloud-enabled deployment in the simplest way possible, by only repartitioning the disk and creating a single user with a single SSH key. The key belongs to the root of the host machine, so we can directly log in to the deployed machine after cloud-init is done.

    Also, we are going to use our images as base images by running the following command:

    Figure 9.8 – Creating an image disk for deployment

    Figure 9.8 – Creating an image disk for deployment

    The images are now ready. The next step is to start the cloud-init configuration.

  3. First, create a local metadata file and put the new virtual machine name in it.
  4. The file will be named meta-data and we are going to use local-hostname to set the name:

     Figure 9.9 – Simple meta-data file with only one option

    Figure 9.9 – Simple meta-data file with only one option

    This file is all it takes to name the machine the way we want and is written in a normal YAML notation. We do not need anything else, so this file essentially becomes a one-liner. Then we need an SSH key pair and we need to get it into the configuration. We need to create a file called user-data that will look like this:

    #cloud-config
    users:
      - name: cloud
        ssh-authorized-keys:
          - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCZh6 6Gf1lNuMeenGywifUSW1T16uKW0IXnucNwoIynhymSm1fkTCqyxLk ImWbyd/tDFkbgTlei3qa245Xwt//5ny2fGitcSa7jWvkKvTLiPvxLP 0CvcvGR4aiV/2TuxA1em3JweqpNppyuapH7u9q0SdxaG2gh3uViYl /+8uuzJLJJbxb/a8EK+szpdZq7bpLOvigOTgMan+LGNlsZc6lqE VDlj40tG3YNtk5lxfKBLxwLpFq7JPfAv8DTMcdYqqqc5PhRnnKLak SUQ6OW0nv4fpa0MKuha1nrO72Zyur7FRf9XFvD+Uc7ABNpeyUTZVI j2dr5hjjFTPfZWUC96FEh root@localhost.localdomain
        sudo: ['ALL=(ALL) NOPASSWD:ALL']
        groups: users
        shell: /bin/bash
    runcmd:
      - echo "AllowUsers cloud" >> /etc/ssh/sshd_config
      - restart ssh

    Note that the file must follow the way YAML defines everything including the variables. Pay attention to the spaces and newlines, as the biggest problems with deployment come from misplaced newlines in the configuration.

    There is a lot to parse here. We are creating a user that uses the username cloud. This user will not be able to log in using a password since we are not creating one, but we will enable login using SSH keys associated with the local root account, which we will create by using the ssh-keygen command. This is just an example SSH key, and SSH key that you’re going to use might be different. So, as root, go through the following procedure:

    Figure 9.10 – SSH keygen procedure done, SSH keys are present and accounted for

    Figure 9.10 – SSH keygen procedure done, SSH keys are present and accounted for

    Keys are stored in the local .ssh directory, so we just need to copy them. When we are doing cloud deployments, we usually use this method of authentication, but cloud-init enables us to define any method of user authentication. It all depends on what we are trying to do and whether there are security policies in place that enforce one authentication method over another.

    In the cloud environments, we will rarely define users that are able to log in with a password, but for example, if we are deploying bare-metal machines for workstations, we will probably create users that use normal passwords. When we create a configuration file like this, it is standard practice to use hashes of passwords instead of literal cleartext passwords. The directive you are looking for is probably passwd: followed by a string containing the hash of a password.

    Next, we configured sudo. Our user needs to have root permissions since there are no other users defined for this machine. This means they need to be a member of the sudo group and have to have the right permissions defined in the sudoers file. Since this is a common setting, we only need to declare the variables, and cloud-init is going to put the settings in the right files. We will also define a user shell.

    In this file, we can also define all the other users’ settings available on Linux, a feature that is intended to help deploy user computers. If you need any of those features, check the documentation available here: https://cloudinit.readthedocs.io/en/latest/topics/modules.html#users-and-groups. All the extended user information fields are supported.

    The last thing we are doing is using the runcmd directive to define what will happen after the installation finishes, in the last stage. In order to permit the user to log in, we need to put them on the list of allowed users in the sshd and we need to restart the service.

    Now we are ready for our first deployment.

  5. We have three files in our directory: a hard disk that uses a base file with the cloud template, a meta-data file that contains just minimal information that is essential for our deployment, and user-data, which contains our definitions for our user. We didn’t even try to install or copy anything; this install is as minimal as it gets, but in a normal environment this is a regular starting point, as a lot of deployments are intended only to bring our machine online, and then do the rest of the installation by using other tools. Let’s move to the next step.

    We need a way to connect the files we just created, the configuration, with the virtual machine. Usually, this is done in a couple of ways. The simplest way is usually to generate a .iso file that contains the files. Then we just mount the file as a virtual CD-ROM when we create the machine. On boot, cloud-init will look for the files automatically.

    Another way is to host the files somewhere on the network and grab them when we need them. It is also possible to combine these two strategies. We will discuss this a little bit later, but let’s finish our deployment first. The local .iso image is the way we are going to go on this deployment. There is a tool called genisoimage (provided by the package with the same name) that is extremely useful for this (the following command is a one-line command):

    genisoimage -output deploy-1-cidata.iso -volid cidata -joliet -rock user-data meta-data

    What we are doing here is creating an emulated CD-ROM image that will follow the ISO9660/Joliet standard with Rock Ridge extensions. If you have no idea what we just said, ignore all this and think about it this way – we are creating a file that will hold our metadata and user data and present itself as a CD-ROM:

 Figure 9.11 – Creating an ISO image

Figure 9.11 – Creating an ISO image

In the end, we are going to get something like this:

Figure 9.12 – ISO is created and we are ready to start a cloud-init deployment

Figure 9.12 – ISO is created and we are ready to start a cloud-init deployment

Please note that images are taken post deployment, so the size of disk can vary wildly based on your configuration. This was all that was needed in the form of preparations. All that’s left is to spin up our virtual machine.

Now, let’s start with our deployments.

The first deployment

We are going to deploy our virtual machine by using a command line:

virt-install --connect qemu:///system --virt-type kvm --name deploy-1 --ram 2048 --vcpus=1 --os-type linux --os-variant generic --disk path=/var/lib/libvirt/images/deploy-1/centos1.qcow2,format=qcow2 --disk /var/lib/libvirt/images/deploy-1/deploy-1-cidata.iso,device=cdrom --import --network network=default --noautoconsole

Although it may look complicated, if you came to this part of the book after reading its previous chapters, there should be nothing you haven’t seen yet. We are using KVM, creating a name for our domain (virtual machine), we are going to give it 1 CPU and 2 GB of RAM. We are also telling KVM we are installing a generic Linux system. We already created our hard disk, so we are mounting it as our primary drive, and we are also mounting our .iso file to serve as a CD-ROM. Lastly, we will connect our virtual machine to the default network:

Figure 9.13 – Deploying and testing a cloud-init customized virtual machine

The deployment will probably take a minute or two. As soon as the machine boots, it will get the IP address and we can SSH to it using our predefined key. The only thing that was not automated is accepting the fingerprint of the newly booted machine automatically.

Now, the time has come to see what happened when we booted the machine. Cloud-init generated a log at /var/log named cloud-init.log. The file will be fairly large, and the first thing you will notice is that the log is set to provide debug information, so almost everything will be logged:

Figure 9.14 – The cloud-init.log file, used to check what cloud-init did to the operating system

Figure 9.14 – The cloud-init.log file, used to check what cloud-init did to the operating system

Another thing is how much actually happens below the surface completely automatically. Since this is CentOS, cloud-init has to deal with the SELinux security contexts in real time, so a lot of the information is simply that. There are also a lot of probes and tests going on. Cloud-init has to establish what the running environment is and what type of cloud it is running under. If something happens during the boot process and it in any way involves cloud-init, this is the first place to look.

Let’s now deploy our second virtual machine by using a second (Ubuntu) image. This is where cloud-init really shines – it works with various Linux (and *BSD) distributions, whatever they might be. We can put that to the test now.

The second deployment

The next obvious step is to create another virtual machine, but to prove a point, we are going to use Ubuntu Server (Bionic) as our image:

 Figure 9.15 – Preparing our environment for another cloud-init-based virtual machine deployment

Figure 9.15 – Preparing our environment for another cloud-init-based virtual machine deployment

What do we need to do? We need to copy both meta-data and user-data to the new folder. We need to edit the metadata file since it has the hostname inside it, and we want our new machine to have a different hostname. As for user-data, it is going to be completely the same as on our first virtual machine. Then we need to create a new disk and resize it:

 Figure 9.16 – Growing our virtual machine image for deployment purposes

Figure 9.16 – Growing our virtual machine image for deployment purposes

We are creating a virtual machine from our downloaded image, and just allowing for more space as the image is run. The last step is to start the machine:

Figure 9.17 – Deploying our second virtual machine with cloud-init

Figure 9.17 – Deploying our second virtual machine with cloud-init

The command line is almost exactly the same, only the names change:

virt-install --connect qemu:///system --virt-type kvm --name deploy-2 --ram 2048 --vcpus=1 --os-type linux --os-variant generic --disk path=/var/lib/libvirt/images/deploy-2/bionic.qcow2,format=qcow2 --disk /var/lib/libvirt/images/deploy-2/deploy-2-cidata.iso,device=cdrom --import --network network=default –noautoconsole

Now let’s check the IP addresses:

Figure 9.18 – Check the virtual machine IP addresses

Figure 9.18 – Check the virtual machine IP addresses

We can see both of the machines are up and running. Now for the big test – can we connect? Let’s use the SSH command to try:

 Figure 9.19 – Using SSH to verify whether we can connect to our virtual machine

Figure 9.19 – Using SSH to verify whether we can connect to our virtual machine

As we can see, the connection to our virtual machine works without any problems.

One more thing is to check the deployment log. Note that there is no mention of configuring SELinux since we are running on Ubuntu:

 Figure 9.20 – The Ubuntu cloud-init log file has no mention of SELinux

Figure 9.20 – The Ubuntu cloud-init log file has no mention of SELinux

Just for fun, let’s do another deployment with a twist – let’s use a module to deploy a software package.

The third deployment

Let’s deploy another image. In this instance, we are creating another CentOS 7 but this time we are installing (not starting) httpd in order to show how this type of configuration works. Once again, the steps are simple enough: create a directory, copy the metadata and user data files, modify the files, create the .iso file, create the disk, and run the machine.

This time we are adding another section (packages) to the configuration, so that we can tell cloud-init that we need a package to be installed (httpd):

 Figure 9.21 – Cloud-init configuration file for the third virtual machine deployment

Figure 9.21 – Cloud-init configuration file for the third virtual machine deployment

Since all the steps are more or less the same, we get the same result – success:

 Figure 9.22 – Repeating the deployment process for the third virtual machine

Figure 9.22 – Repeating the deployment process for the third virtual machine

We should wait for a while so that the VM gets deployed. After that, let’s log in and check whether the image deployed correctly. We asked for httpd to be installed during the deployment. Was it?

 Figure 9.23 – Checking whether httpd is installed but not started

Figure 9.23 – Checking whether httpd is installed but not started

We can see that everything was done as expected. We haven’t asked for the service to start, so it is installed with the default settings and is disabled and stopped by default.

After the installation

The intended use of cloud-init is to configure machines and create an environment that will enable further configuration or straight deployment into production. But to enable that, cloud-init has a lot of options that we haven’t even mentioned yet. Since we have an instance running, we can go through the most important and the most useful things you can find in the newly booted virtual machine.

The first thing to check is the /run/cloud-init folder:

 Figure 9.24 – /run/cloud-init folder contents

Figure 9.24 – /run/cloud-init folder contents

Everything that is created at runtime is written here, and available for users. Our demo machine was run under the local KVM hypervisor so cloud-init is not detecting a cloud, and consequently is unable to provide more data about the cloud, but we can see some interesting details. The first one is two files named enabled and network-config-ready. Both of them are empty but very important. The fact that they exist signifies that cloud-init is enabled, and that network has been configured and is working. If the files are not there, something went wrong and we need to go back and debug. More about debugging can be found at https://cloudinit.readthedocs.io/en/latest/topics/debugging.html.

The results.json file holds this particular instance metadata. status.json is more concentrated on what happened when the whole process was running, and it provides info on possible errors, the time it took to configure different parts of the system, and whether everything was done.

Both those files are intended to help with the configuration and orchestration, and, while some things inside these files are important only to cloud-init, the ability to detect and interact with different cloud environments is something that other orchestration tools can use. Files are just a part of it.

Another big part of this scheme is the command-line utility called cloud-init. To get information from it, we first need to log in to the machine that we created. We are going to show the differences between machines that were created by the same file, and at the same time demonstrate similarities and differences between distributions.

Before we start talking about this, be aware that cloud-init, as with all Linux software, comes in different versions. CentOS 7 images use an old version, 0.7.9:

Figure 9.25 – CentOS cloud-init version – quite old

Figure 9.25 – CentOS cloud-init version – quite old

Ubuntu comes with a much fresher version, 19.3:

Figure 9.26 – Ubuntu cloud-init version – up to date

Figure 9.26 – Ubuntu cloud-init version – up to date

Before you freak out, this is not as bad as it seems. Cloud-init decided to switch its versioning system a couple of years ago, so after 0.7.9 came 17.1. There were many changes and most of them are directly connected to the cloud-init command and configuration files. This means that the deployment will work, but a lot of things after we deploy will not. Probably the most visible difference is when we run cloud-init --help. For Ubuntu, this is what it looks like:

Figure 2.27 – Cloud-init features on Ubuntu

Figure 2.27 – Cloud-init features on Ubuntu

Realistically, a lot of things are missing for CentOS, some of them completely:

 Figure 9.28 – Cloud-init features on CentOS

Figure 9.28 – Cloud-init features on CentOS

Since our example has a total of three running instances – one Ubuntu and two CentOS virtual machines – let’s try to manually upgrade to the latest stable version of cloud-init available on CentOS. We can use our regular yum update command to achieve that, and the result will be as follows:

 Figure 9.29 – After a bit of yum update, an up-to-date list of cloud-init features

Figure 9.29 – After a bit of yum update, an up-to-date list of cloud-init features

As we can see, this will make things a lot easier to work with.

We are not going to go into too much detail about the cloud-init CLI tool, since there is simply too much information available for a book like this, and as we can see, new features are being added quickly. You can freely check additional options by browsing at https://cloudinit.readthedocs.io/en/latest/topics/cli.html. In fact, they are being added so quickly that there is a devel option that holds new features while they are in active development. Once they are finished, they become commands of their own.

There are two commands that you need to know about, both of which give an enormous amount of information about the boot process and the state of the booted system. The first one is cloud-init analyze. It has two extremely useful subcommands: blame and show.

The aptly named blame is actually a tool that returns how much time was spent on things that happened during different procedures cloud-init did during boot. For example, we can see that configuring grub and working with the filesystem was the slowest operation on Ubuntu:

 Figure 9.30 – Checking time consumption for cloud-init procedures

Figure 9.30 – Checking time consumption for cloud-init procedures

The third virtual machine that we deployed uses CentOS image and we added httpd to it. By extension, it was by far the slowest thing that happened during the cloud-init process:

 Figure 9.31 – Checking time consumption – it took quite a bit of time for  cloud-init to deploy the necessary httpd packages

Figure 9.31 – Checking time consumption – it took quite a bit of time for cloud-init to deploy the necessary httpd packages

A tool like this makes it easier to optimize deployments. In our particular case, almost none of this makes sense, since we deployed simple machines with almost no changes to the default configuration, but being able to understand why the deployment is slow is a useful, if not essential, thing.

Another useful thing is being able to see how much time it took to actually boot the virtual machine:

 Figure 9.32 – Checking the boot time

Figure 9.32 – Checking the boot time

We are going to end this part with a query – cloud-init query enables you to request information from the service, and get it in a useable structured format that you can then parse:

Figure 9.33 – Querying cloud-init for information

Figure 9.33 – Querying cloud-init for information

After working with it for even a few hours, cloud-init becomes one of those indispensable tools for a system administrator. Of course, its very essence means it will be much more suited to those of us who have to work in the cloud environment, because the thing it does best is the quick and painless deployment of machines from scripts. But even if you are not working with cloud technologies, the ability to quickly create instances that you can use for testing, and then to remove them without any pain, is something that every administrator needs.

Related Articles

How to add swap space on Ubuntu 21.04 Operating System

How to add swap space on Ubuntu 21.04 Operating System

The swap space is a unique space on the disk that is used by the system when Physical RAM is full. When a Linux machine runout the RAM it use swap space to move inactive pages from RAM. Swap space can be created into Linux system in two ways, one we can create a...

read more

Lorem ipsum dolor sit amet consectetur

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *

two × 1 =