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:
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:
- Let’s make these images bigger, just so that we can have them ready for future capacity needs (and practice):
After growing our images, note that the size on the disk hasn’t changed much:
- The images that we are going to use are going to be stored in
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:
The images are now ready. The next step is to start the cloud-init configuration.
- First, create a local metadata file and put the new virtual machine name in it.
- The file will be named
meta-dataand we are going to use
local-hostnameto set the name:
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-datathat 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 email@example.com 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-keygencommand. 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:
Keys are stored in the local
.sshdirectory, 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
sudogroup and have to have the right permissions defined in the
sudoersfile. 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
runcmddirective 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
sshdand we need to restart the service.
Now we are ready for our first deployment.
- We have three files in our directory: a hard disk that uses a base file with the cloud template, a
meta-datafile 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
.isofile 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
.isoimage 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:
In the end, we are going to get something like this:
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
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:
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
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:
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:
What do we need to do? We need to copy both
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:
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:
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
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:
One more thing is to check the deployment log. Note that there is no mention of configuring SELinux since we are running on Ubuntu:
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 (
Since all the steps are more or less the same, we get the same result – success:
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
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
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.
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:
Ubuntu comes with a much fresher version, 19.3:
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:
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:
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:
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:
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:
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:
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.