Vivian Voss

What cloud-init Installs to Read One YAML File

lean software freebsd cloud-init nuageinit

Every virtual machine in every cloud begins its life by reading a note like this one:

#cloud-config
hostname: lean01
timezone: Europe/Berlin
users:
  - name: vivian
    shell: /bin/sh
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... vivian@desk
packages:
  - tmux
  - rsync

A name, a timezone, one user with one key and two packages. An administrator could do all of it by hand in a couple of minutes, and nobody does, because it is precisely the sort of work a machine ought to manage for itself when it first wakes up. What the machine has to carry in order to read those eleven lines depends rather heavily on the system it runs.

What Debian brings to read it

On Debian and Ubuntu the reader is cloud-init, and the official cloud images of both ship it. To see what it brings along, I took this morning's package index of Debian 13 for amd64 and resolved the dependencies of cloud-init 25.1.4 against a base of every required and important package plus the OpenSSH server. The difference came to 46 packages and 61.2 MiB of installed size. Twenty-six of them are Python libraries, python3-requests, python3-jinja2, python3-cryptography and python3-jsonschema among them, and seven more carry Python 3.13 itself with its standard library, an interpreter that a minimal Debian server would otherwise never install. Together they come to 33.4 MiB, more than half of the whole.

What sits under the reader Debian 13, cloud-init 25.1.4 FreeBSD 15.1, nuageinit cloud-init 69,102 lines of Python · 3.1 MiB 26 Python libraries requests, jinja2, cryptography … · 11.7 MiB Python 3.13 and its standard library 7 packages · 21.7 MiB 12 further packages locales, OpenSSL, netcat-openbsd … · 24.8 MiB base system required, important, OpenSSH server added: 46 packages, 61.2 MiB nuageinit 1,685 lines of Lua, three rc scripts base system flua and lyaml already inside added: nothing Debian 13 package index, trixie main amd64, 14 October 2026 · FreeBSD releng/15.1

cloud-init 25.1.4 itself runs to 69,102 lines of Python, with 31 datasources, each of which knows where one kind of cloud keeps the note, and 59 modules that know what to do with it once found. It runs at every boot, the first and every one after, because it has to establish whether the machine is still the instance it was yesterday.

Among the 46 sits netcat-openbsd, which looks rather lost in a list of Python libraries. It is there because, until version 24.3 in 2024, cloud-init started as four separate Python processes on every boot, one for each stage, and each of them loaded the interpreter and its libraries from scratch. The project merged the four into a single process that sits on a Unix socket until systemd tells it to begin the next stage. Its notes list the price frankly, a "new dependency on openbsd's netcat implementation", in exchange for "a faster boot time", and so a Linux server now borrows a tool from OpenBSD to let its Python wait more cheaply.

Why it is built that way

cloud-init came by its size honestly. Each large cloud invented its own way of handing a new machine its configuration, with its own address and its own dialect, and cloud-init, which Canonical began for Ubuntu on Amazon's cloud, learned them one after another. Its note, meanwhile, became the common language of the trade. An image builder can write #cloud-config once and ship the same image to every provider cloud-init knows, provided the image carries cloud-init and everything it depends on for as long as the machine lives.

The note itself is plain YAML, which a small C library can parse without any help from Python.

What FreeBSD reads it with

Since FreeBSD 14.1, released in June 2024, the base system has carried nuageinit, nuage being French for cloud. It reads the same note, and its manual page states that it "is believed to conform to the Cloud Init specification", which is about as close to a solemn vow as manual pages come.

It is written in Lua and run by flua, the interpreter FreeBSD keeps in its base system for its own tools, 301,192 bytes on the machine that serves this blog. The YAML goes through lyaml, a binding to libyaml that sits in the base system beside it. One rc script reads the note early, once the local filesystems are mounted and before the network comes up; the packages and the users wait for a second script, which runs once the network is there, and any shell script the note carries runs at the very end. All of them carry the firstboot keyword, so rc(8) runs them only while the sentinel file /firstboot exists, and that file is removed at the end of the first start. From the second boot onwards, nuageinit costs the machine nothing whatsoever. In the 15.1 sources the whole affair comes to 1,685 lines, of which the program itself has 809 (I counted twice, a week apart, because the first figure looked like a typing error).

When the reader runs cloud-init, Debian 13 at every boot cloud-init-local cloud-init-network cloud-config cloud-final until 24.3: four Python processes, each loading the interpreter from scratch since 24.3: one Python process on a Unix socket, told by systemd when to begin, netcat-openbsd as messenger nuageinit, FreeBSD 15.1 at first boot only nuageinit before the network nuageinit_post_net packages and users nuageinit_user_data_script any script in the note rc removes /firstboot all three carry KEYWORD: firstboot second boot onwards: skipped, nothing loaded cloud-init 25.1.4 systemd units and breaking changes 24.3 · rc.d scripts on FreeBSD 15.1-RELEASE-p3

Everything in the note above is covered, and most of what a first boot asks for besides: groups, passwords, SSH host keys, sudo and doas rules, files to write, interfaces with their addresses and gateways, package updates and upgrades, and commands to run at the end. There is nothing to install. The official FreeBSD 15.0 images built for this purpose, called BASIC-CLOUDINIT, switch nuageinit on in rc.conf, and their build script adds exactly one package to the image: firstboot-freebsd-update, so that the machine fetches its security fixes on its first boot.

Measured on one machine

For a comparison on the same hardware, I installed the cloud-init port, py312-cloud-init 25.2, into my measurement jail, a FreeBSD 15.0 userland on an Intel Core i5-13500. pkg brought 36 packages for it. Then I started each program twenty times.

cloud-init --version loads itself and prints a version number, nothing more. Twenty runs took 4.13 seconds, 206 milliseconds apiece. nuageinit without arguments loads its library together with the YAML and UCL parsers, notices that nobody has given it any work and says so; twenty runs took 0.10 seconds, five milliseconds apiece.

One start, same machine cloud-init --version 206 ms nuageinit, no arguments 5 ms a factor of about forty, before either has read a line of YAML FreeBSD 15.0 jail, Intel Core i5-13500, 14 October 2026 · twenty starts each, /usr/bin/time -p

That is a factor of about forty before either program has read a line of YAML. Until 24.3, the Python side paid it four times on every boot.

Who profits, who pays

The image builder profits, and so do the cloud providers: one agent and one note serve every platform, and nobody has to write a reader of their own. The project that defines the note, maintained at Canonical, decides what goes into it.

The bill goes to every machine that boots the image, which is to say to its operator. A minimal Debian server carries 61.2 MiB, more than half of it Python, for a program it needs for a minute at first boot and for a short check at every boot thereafter. Its operator follows the release notes and security notices of 46 more packages and keeps one more language runtime current on a machine that was meant to run something else entirely.

FreeBSD paid for the reader once, inside its base system, in a language it already carried for its boot loader and its own tools, and released it with everything else from one tree. The machine needs nothing it did not already have, which is the argument of my book, Integrated by Design, applied to the first minute of a machine's life.

The limit

nuageinit finds the note in two places: on a disk labelled cidata or config-2, and through OpenStack's metadata service. cloud-init knows 31 sources, Amazon's and Azure's among them. On those clouds the cloud-init port runs on FreeBSD as it runs elsewhere, at version 25.2, one pkg install away.

It reads the common keys and passes over the rest. cloud-init's 59 modules include Chef, Puppet, LXD and disk partitioning, whereas nuageinit covers what a first boot usually asks for. A note built around the rarer modules goes to cloud-init, and the ports tree supplies it.

The measurement compares program starts. A whole first boot also waits for the network and the package mirror, which take as long on either system; the factor of forty is the reader's own share.

The note at the top still has eleven lines, and on FreeBSD 15.1 the program that reads them was on the disk before the machine knew its own name. FreeBSD took cloud-init's note and left the luggage.

To read a note of eleven lines, cloud-init brings 46 packages and 61.2 MiB to a Debian 13 server, more than half of it Python, and runs at every boot. FreeBSD reads the same note with nuageinit from the base system, 1,685 lines of Lua that run at first boot only and start about forty times faster on the same machine.