Back to Blog
DevOps19 March 2024Michael Hettwer5 min read

Provisioning servers with cloud-init

How we use cloud-init for the first boot: structured user data, the pitfalls worth knowing, and a clean handover to configuration management.

A new server is rarely an empty machine someone types into by hand these days. There is typically a moment when the server boots for the first time already knowing what it is supposed to become. That moment is the working field of cloud-init: the tool reads the provided data from the platform environment on first boot and sets the server up from it. Used well, provisioning drops to minutes and the results become comparable across servers.

The building blocks at a glance

cloud-init distinguishes essentially two sources of information. Instance metadata comes from the platform: hostname, network details, credentials. User data is the part we shape ourselves, handed over as a YAML document in cloud-config format or as a shell script. The YAML route is the usual one, because it declares instead of acts.

The most-used blocks cover the first hour of a server: hostname, users for accounts and authorized keys, packages for the base installation, write_files for configuration files, and runcmd for commands that run at the end of the sequence. With that, a new server is ready after its first boot with network, access, base tooling and the key files in place, before a human logs in for the first time.

The semantics that trip people up

The most important mechanism is also the most common source of confusion: cloud-init primarily runs on first boot. Anyone who later changes the user data and waits for a new effect waits in vain, because the machine finished its initial setup long ago and marked it complete. Later changes belong to configuration management, not to provisioning. The sensible split reads: cloud-init brings the machine to a defined starting state; everything after that is handled by a tool allowed to run repeatedly.

The second stumbling block is ordering inside the document. cloud-init processes its modules in a fixed phase sequence, and runcmd runs late, not immediately. Anyone who assumes an early block can consume the result of a later command builds a race condition. A simple rule helps: write ordering dependencies explicitly into the user data, never implicitly assume them.

Verify instead of believe

A provisioning promise without verification is a wish. cloud-init therefore ships verification tools: cloud-init status shows whether and how the run went, and the log explains where something failed. Even more valuable is the schema check: cloud-init schema validates the YAML document against the known format and reports unknown keys, typos and structural errors before the first server boots with it. We run that check right where the user data is stored, so a broken document never reaches a server in the first place.

Testing itself can be ordered too: a provisioned server is mirrored against a short checklist that queries the expected states. The list stays small but earns its keep, because it finds exactly the everyday errors: the forgotten second key, the firewall rule that only becomes active after the second boot, the service that starts but misses its configuration.

What belongs in provisioning

From our practice a small core has proven itself, feeding into the user data of every server:

  • Take hostname and network data from the platform; maintain nothing twice.
  • Create administrative accounts with authorized keys, and rule out password login from the start.
  • Set the base packages every server should share through the package list, not through later patching.
  • Deliver the minimal, mandatory configuration as files that must be identical everywhere: journal limits, backup agent wiring, monitoring wiring.
  • Leave the rest deliberately to configuration management.

What the handover looks like in practice

The handover point between provisioning and ongoing configuration becomes visible when it is kept as a document. In our setup that means: next to the user data of every server sits a short entry stating what the provisioning delivers and what gets taken over afterwards. The entry prevents the most common source of confusion: the question whether a certain behavior is intent of the initial setup or state of the ongoing management. The answer then sits in one place instead of in the memory of whoever last touched the server.

The second practical value shows in the failure case. When a server is provisioned again after a recovery, the same sequence runs as the first time, and the checklist verifies the same state. That makes recoveries more predictable, because the path back to a working state is not reconstructed from memory but consists of artifacts that are versioned anyway. The same small rule applies here: whatever was discovered as manual work moves into the user data if it belongs to the initial setup, and into management if it belongs to ongoing care.

A word on choosing the tools: cloud-init replaces no configuration management and does not want to. The boundary lies in the nature of the initial setup: what happens once at first boot cannot meaningfully be repeated or corrected by cloud-init. The practical consequence for teams facing the decision is therefore a clear assignment of duties: everything that should be identical on every server and never changes belongs in the user data; everything that changes with operation belongs in the recurring management. Teams that draw this line find, a few weeks in, fewer arguments about ownership than before, because ownership is readable from the artifact instead of from memory.

That way provisioning ends not in a half-state but in a defined handover point. If you want your server procurement on this footing, the CI/CD pipeline management page describes how we bring provisioning and ongoing configuration together, Linux administration the ongoing operations, and the contact page is the fastest way to reach us.

Sources

  • cloud-init documentation, https://docs.cloud-init.io/ (accessed 2026-11-03)
  • cloud-init project, source and README, https://github.com/canonical/cloud-init (accessed 2026-11-03)