Bringing Up Clouds
Core Technology
VM instances in the cloud are different beasts, even if they start off as a single image. Discover how they get their configuration in this month's Core Technologies.
First as a buzzword and then as a commodity, the cloud lives the typical life of an IT industry phenomenon. This means that running something (but usually Linux) in a cloud is a thing you now do more often than not. From a user perspective, it's simple: You click a button on the cloud provider's dashboard and get your virtual machine (VM) running within a minute.
This is drastically different from what you do on your desktop. Here, you insert the DVD or plug in a USB pen drive and spawn the installer. Be it an old-school, text-based or a slick GUI installer, it typically asks you some questions (Figure 1). Which locale do you want to use? What's your computer's hostname? What's your time zone? How do you want your user account named? Which password do you want to use? You may not even notice these questions, because installation takes a quarter of an hour or more, and you spend most of this time sipping coffee or chatting with friends. Yet these questions are essential for the system's operation. Without a password, you won't be able to log in. Or, even worse, everyone will be able to.
A VM in a cloud starts in seconds because it's not really installed. Clouds host prebuilt hard disk images that are effectively cloned when you need a new VM. Although it's faster, it also means there is no "configure" stage, when you can adjust settings to your liking. Cloud-based VMs need some other mechanism to make these changes on the fly so that many instances can start off of a single image. One of these mechanisms is cloud-init
[1].
cloud-init in a Nutshell
Initially an Ubuntu project, cloud-init becomes "the standard for customising cloud instances," as the homepage says (Figure 2). The initial idea was to make Ubuntu easier to consume in Amazon Elastic Compute Cloud (EC2) instances. Today, cloud-init integrates with many popular Linux varieties (and even FreeBSD) and runs across different clouds, from Microsoft Azure to your company's private OpenStack deployment. Needless to say, cloud-init is free (GPLv3), and you can get it through your package manager, as well as in Python sources.
As an end user, you don't think about cloud-init too much: It "just works." However, imagine that you want to build your own custom OS image for the cloud. Following the "least surprise" principle for your end user, you'd want to integrate cloud-init and make sure it is able to find the relevant settings and apply them.
You might think of cloud-init as an init [2] process replacement because of the name, but that's not true. Linux already has many init implementations, and it makes little sense to write another one just because your system now runs in the cloud. Instead, cloud-init is designed to interoperate with an existing init, which calls it at well-known points during system startup.
Like it or not, in most Linux flavors today init is equal to systemd, so I'll look at cloud-init from this perspective. Otherwise, the stages are almost the same, but implementation varies per init, as you might expect.
Up and Running
It all starts with the generator (see the "What on Earth Is a Generator?" box), which decides whether cloud-init needs to run at all. To disable cloud-init, you either create an /etc/cloud/cloud-init.disabled
file or pass cloud-init=disabled
as a kernel command-line option. The KERNEL_CMDLINE="cloud-init=disabled"
environment variable takes precedence over the latter, which is useful if you run cloud-init in a container rather than a VM. So, you can temporarily disable cloud-init to run an image outside the cloud in your local virt-manager, for instance. This step is the only systemd-specific one, because no other init system has a notion of a generator.
What on Earth Is a Generator?
A generator is a systemd concept. It's a small binary whose name ends with -generator
. Generators run very early in the system startup and were designed to convert non-systemd configuration files into native units (hence the name). They should write unit files or create symbolic links, introducing new dependencies between them. This makes generators a viable means to customize the systemd boot process.
There are different places where a generator can reside in your system, each with its own priority (consult the systemd.generator(7)
man page [5] for details). For instance, on my Ubuntu 16.04 LTS system, cloud-init-generator
resides in /lib/systemd/system-generators/
. It's a small shell script spanning a bit more than a hundred lines of code. This script performs the checks I discussed in the main text, and if cloud-init is not disabled and the data source is found, it creates a symlink to cloud-init.target
in the appropriate multi-user-target.wants
directory.
If cloud-init is not disabled, the generator injects cloud-init.target
into the multi-user.target
. This target uses WantedBy
to pull several units (.service
files) that ultimately call /usr/bin/cloud-init
, passing it different command-line options. Note that this executable is not a daemon: It does what was requested and then exits, and you won't find it running in your cloud instance. The wonderful cloud-init documentation [3] covers the boot process in a greater detail, but here is a quick summary for your convenience.
First, there is cloud-init-local.service
, which runs as soon as the root filesystem becomes writable and before the network is configured. It translates to /usr/bin/cloud-init init --local
, and the main idea is to render the required network settings on the first boot.
Second, cloud-init.service
already has access to the network; that is, it can use non-local data sources (I'll revisit this later). This stage runs /usr/bin/cloud-init init
to finalize the initialization process. It can configure SSH or provision CA certificates, for instance.
Third, cloud-init modules come into play with /usr/bin/cloud-init modules
. This happens in two stages (or substages): the "config" stage (cloud-config.service
) and the "final" stage (cloud-final.service
). The config stage modules may install packages or configure NTP and the time zone. At the final stage, you upgrade packages and run configuration management agents, such as Salt Minions [4]. This is a proper place for your custom setup. Think of it as an rc.local
equivalent in a traditional init.
Modules Galore
The problem with computers is that they do what you say, not what you mean. Before cloud-init can apply any configuration, it must be told exactly what you want to apply. Internally, cloud-init stores all settings in a large dict()
, and while it's not a Perl hash, there is more than one way to fill it.
For starters, cloud-init comes with a bare minimum of defaults. They enable data sources for the most popular cloud providers and do some other tweaks. The main configuration file resides in /etc/cloud/cloud.cfg
by default, and it's an appropriate place for settings shared between all instances. For instance, you can configure your company's package repository or add an internal CA certificate here. Additional config files may come from /run/cloud/cloud.cfg
(this is known as a "run-time config") or in fact any location you supply via a CLOUD_CFG
environment variable. Again, the latter is mostly useful in containers.
Many data sources would allow you to supply configuration bits via some type of data: metadata, user data, or vendor data. This is the way to go for all instance-specific settings, but I will leave the details until the next section.
This may sound weird, but you can also configure cloud-init via the kernel command line! The format is:
cc: config bits end cc
Cloud-init translates a \n
into a newline, so you can pass it a multiline text. Imagine you have spawned an instance and realized you've chosen the wrong SSH key. The warning shown in Figure 3 may look difficult, but if you can get to the VM's GRUB prompt, things are easy to fix. Just append
cc: ssh_import_id: gh:johndoe end_cc
to the kernel command line, and it will make cloud-init import your (actually, John Doe's) public key from GitHub. That's exactly what the ssh_import_id
module is for.
/etc/cloud/cloud.cfg
is actually a YAML file. This holds true for most other configuration means, such as the kernel command line above. Moreover, cloud-init supports putting configuration snippets in /etc/cloud/cloud.d
to make it easier to extend configuration from the scripts. Those snippets must also be valid YAML, and they are merged together in lexical order as if they were forming a single file [6].
Now, I'll take a quick look at what's typically in /etc/cloud/cloud.cfg
, which depends on the distribution and who built the image, so I opted for a generic CentOS 7 OpenStack image [7]. The config file is about 60 lines long, and you can find a heavily trimmed version in Listing 1.
Listing 1
/etc/cloud/cloud.cfg (Abbreviated)
First, you see some settings for the cloud-init modules. They prescribe creating a default user, disabling root, and SSHing password authentication, as recommended for networking setups. Three modules really take care of this: users-groups
, ssh
, and set-passwords
, respectively. The first two come through cloud_init_modules
, which lists modules to run at the init stage. The last one is from cloud_config_modules
, so it runs at the config
stage.
If you have a background in YAML, you may think options such as disable_root
should be nested under -
ssh
, and you are probably right. Remember, however, that the same configuration bits may come through the kernel command line or other means. They have a different structure, and putting these settings at the top level makes handling module code more uniform.
Next is the system_info
section. You may think of it as a read-only piece of information regarding the system itself and the distribution. You specify the latter via the distro
key, which is important because different distributions provide different tools for common tasks, like applying network configuration. This is not to mention that modules such as yum_add_repo
are naturally per distribution. The aforementioned ssh_import_id
is also a good example. It runs on Ubuntu and Debian only, perhaps because the ssh-import-id
command itself [8] is Ubuntu-specific.
The default_user
key is another typical resident under system_config
. It specifies the default user parameters that end up in /etc/passwd
. The code in Listing 1 shows that you will be able to log in as centos, using the SSH key that you provided, thanks to the ssh
module.
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters
Support Our Work
Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.
News
-
Halcyon Creates Anti-Ransomware Protection for Linux
As more Linux systems are targeted by ransomware, Halcyon is stepping up its protection.
-
Valve and Arch Linux Announce Collaboration
Valve and Arch have come together for two projects that will have a serious impact on the Linux distribution.
-
Hacker Successfully Runs Linux on a CPU from the Early ‘70s
From the office of "Look what I can do," Dmitry Grinberg was able to get Linux running on a processor that was created in 1971.
-
OSI and LPI Form Strategic Alliance
With a goal of strengthening Linux and open source communities, this new alliance aims to nurture the growth of more highly skilled professionals.
-
Fedora 41 Beta Available with Some Interesting Additions
If you're a Fedora fan, you'll be excited to hear the beta version of the latest release is now available for testing and includes plenty of updates.
-
AlmaLinux Unveils New Hardware Certification Process
The AlmaLinux Hardware Certification Program run by the Certification Special Interest Group (SIG) aims to ensure seamless compatibility between AlmaLinux and a wide range of hardware configurations.
-
Wind River Introduces eLxr Pro Linux Solution
eLxr Pro offers an end-to-end Linux solution backed by expert commercial support.
-
Juno Tab 3 Launches with Ubuntu 24.04
Anyone looking for a full-blown Linux tablet need look no further. Juno has released the Tab 3.
-
New KDE Slimbook Plasma Available for Preorder
Powered by an AMD Ryzen CPU, the latest KDE Slimbook laptop is powerful enough for local AI tasks.
-
Rhino Linux Announces Latest "Quick Update"
If you prefer your Linux distribution to be of the rolling type, Rhino Linux delivers a beautiful and reliable experience.