Independent marketplace for cloud provider accounts across 22 providers

DevOps & Automation

cloud-init for Server Provisioning

cloud-init turns a generic disk image into your server before you ever log in. This guide covers the cloud-config format, the six modules most bootstraps need, a hardened example, where each provider hides the field, and how to debug a silent run.

A cloud-config document being applied to a new Linux instance during first boot

The Minutes Before Anyone Logs In

Between the moment a cloud instance is created and the moment you first connect, something has to turn a generic disk image into a machine that belongs to you. It has to set the hostname, install your public key, expand the root filesystem to the volume size you paid for and bring up the network.

On nearly every Linux cloud image, the thing doing that work is cloud-init. It runs as a set of systemd units early in boot, reads instructions supplied by the platform, and applies them in a defined order. Most people never notice it, which is a sign it works.

The interesting part is that you can supply your own instructions. Anything you would otherwise type in the first ten minutes of a server's life can be declared in advance, which closes the window where a machine sits on the public internet in its default state.

The Input Format, and Its Two Fussy Rules

Instructions arrive through a field the provider calls user data, custom data or a startup script depending on the platform. The most useful format is cloud-config, a YAML document with one mandatory feature: the very first line must be exactly #cloud-config.

That line looks like a comment and is not. Omit it, or let an editor insert a byte order mark ahead of it, and cloud-init classifies the payload as something else and quietly skips your configuration. This accounts for a large share of the "it did nothing" reports.

The second rule is that YAML indentation is significant and unforgiving. Validate the document before pasting it, ideally with cloud-init schema --config-file user-data.yaml on any machine that has the package installed.

A shell script starting with a shebang line is also accepted and is fine for something trivial. Beyond a handful of commands, cloud-config is easier to read and its modules handle ordering and idempotence for you.

The Handful of Modules That Cover Most Needs

cloud-config is organised into modules, each keyed by a top level name. You rarely need more than six.

KeyWhat it does
usersCreates accounts, sets sudo rules, installs authorized keys
package_update and packagesRefreshes the index, then installs a named list
write_filesDrops configuration files with a chosen owner and mode
runcmdRuns commands late in boot, after the modules above
ssh_pwauthTurns password authentication on or off for the SSH daemon
power_stateReboots or shuts down once processing finishes

Ordering matters and is fixed by cloud-init, not by the order keys appear in your file. Files are written before runcmd executes, which is why the usual pattern is to place a unit file or an application config with write_files and then enable it with a single command afterwards.

A Bootstrap That Produces a Usable Server

A practical first boot configuration creates an unprivileged account with your key, removes password login entirely, patches the image and installs what the workload needs.

users: - name: deploy sudo: "ALL=(ALL) NOPASSWD:ALL" shell: /bin/bash ssh_authorized_keys: - ssh-ed25519 AAAAC3Nz...

ssh_pwauth: false and disable_root: true close the two doors that automated scanners knock on first. Then package_update: true with package_upgrade: true brings the image forward from its build date, because published images are always missing fixes released since they were made.

Finish with the specifics. A short runcmd list can enable a firewall, start a service and write a marker file you can check for later.

runcmd: - ufw allow OpenSSH - ufw --force enable - systemctl enable --now nginx

The result is a machine that reaches a hardened state before its first human connection. The reasoning behind each of those choices is unpacked in securing a new cloud server, and key handling specifically in SSH keys for cloud servers.

Where the Field Hides on Each Platform

The mechanism is shared; the label is not. On AWS the field is called user data and can be supplied at launch or edited while the instance is stopped, as described in the EC2 instance metadata documentation. Azure exposes both custom data and user data on Linux virtual machines. Google Compute Engine accepts a user-data metadata key on images that ship cloud-init.

Developer clouds keep it simpler. DigitalOcean, Vultr, Linode and Hetzner all present a single user data box in the creation form and an equivalent field in their APIs and command line tools, which means the same document works across all of them with no edits.

That portability is the quiet advantage. A bootstrap written once tends to survive a change of provider, unlike a provisioning script tied to one vendor's tooling. If you are testing that claim, the Vultr account listings describe what such an account provides, and the Hetzner versus Vultr comparison looks at how the two present the same field.

When Nothing Appears to Have Happened

Three commands answer almost every question. cloud-init status --long reports whether processing finished and whether it recorded errors. cloud-init query userdata prints what the instance actually received, which immediately settles whether the payload arrived intact.

For detail, read /var/log/cloud-init-output.log. It contains the standard output of every module and command, including the package manager, and it is where a failing runcmd entry confesses.

Remember that cloud-config is applied once, on first boot. Rebooting does not reapply it. To test a change, destroy the instance and create a new one, which is cheap and also proves the configuration works from a clean state rather than from a machine you have already poked at.

If a command needs the network, be aware it may run before DNS is fully settled on some images. Adding a short retry loop around anything that fetches from the internet removes a frustrating class of intermittent failure.

Knowing When to Hand Over

cloud-init is a bootstrap tool, not a configuration management system. It has no concept of ongoing state, no way to correct drift, and no reporting across a fleet. Once a document grows past roughly a screen of YAML, you are writing a fragile installer.

The healthier split is to let cloud-init do the minimum, install your key, harden SSH, install an agent, and then let a dedicated tool own everything after that. Ansible for cloud server configuration covers that handover, and infrastructure as code with Terraform shows how the user data document itself becomes a versioned file rather than a paste into a web form.

Points to Remember

  • Begin the document with #cloud-config on line one, with nothing before it.
  • Validate the YAML before launch; indentation errors fail silently.
  • Create your user and install keys here, not after first login.
  • Disable password authentication and root login in the same document.
  • Keep secrets out of user data, since the instance and the API can both read it.
  • Diagnose with cloud-init status --long and the output log, not by guessing.
  • Test changes by creating a fresh instance, because it only runs on first boot.
  • Stop at the bootstrap and hand ongoing configuration to a purpose built tool.

Questions about a cloud account?

Ask about products, variants, availability or purchasing on either channel.