In modern cloud and virtualised infrastructure, provisioning Ubuntu Server instances manually via keyboard input is obsolete. When deploying virtual machines across public cloud providers (AWS, Azure, Google Cloud, DigitalOcean) or on-premises hypervisors (Proxmox, VMware vSphere, OpenStack, KVM), servers must bootstrap autonomously. Without automation, configuring hostnames, generating SSH keys, provisioning administrative users, and bootstrapping software runtimes introduces severe configuration drift and deployment latency.
The industry-standard engine for early operating system initialisation is cloud-init. Developed primarily by Canonical, cloud-init executes during the initial seconds of the Ubuntu boot pipeline, consuming declarative YAML metadata to configure the server from a pristine generic image into a fully functional production node. This guide provides an in-depth operational breakdown of authoring cloud-config payloads, managing systemd boot stages, and troubleshooting cloud-init on Ubuntu Server.
Architectural Overview: The cloud-init Boot Stages
To avoid race conditions between storage, network interfaces, and system services, cloud-init breaks initialisation down into four sequential systemd phases:
| Stage | Systemd Service Unit | Operational Mandate | Network Availability |
|---|---|---|---|
| 1. Local | cloud-init-local.service |
Identifies local data sources (NoCloud, ConfigDrive, kernel command line), extracts network configuration, and writes netplan YAML. | Offline (No network interfaces active). |
| 2. Network | cloud-init.service |
Fetches user-data from cloud metadata services (e.g. http://169.254.169.254/), sets machine hostname, configures storage partitions, and mounts filesystems. |
Online (IP address acquired). |
| 3. Config | cloud-config.service |
Executes declarative configuration modules: provisions local users, injects SSH public keys, configures timezone/locale, and adds custom apt repositories. | Online. |
| 4. Final | cloud-final.service |
Runs late-stage tasks: installs apt packages, writes arbitrary configuration files (write_files), and executes custom shell arrays (runcmd). |
Online. |
Step 1: Anatomy of a Cloud-Config Payload (user-data)
Every cloud-init configuration file must begin with the exact magic header comment #cloud-config on the very first line. Without this header, cloud-init ignores the payload.
Below is a production-grade user-data YAML payload automating user creation, SSH key injection, security patching, package installation, and Nginx deployment:
#cloud-config
# Author: Enterprise Systems Engineering
# Target: Ubuntu Server 24.04 LTS / 22.04 LTS
# 1. Hostname and Timezone Configuration
hostname: web-prod-lon-01
fqdn: web-prod-lon-01.corp.internal
manage_etc_hosts: true
timezone: Europe/London
# 2. Administrative User Provisioning
users:
- default
- name: sysadmin
gecos: System Administrator
primary_group: sysadmin
groups: [sudo, adm]
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
lock_passwd: true
ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGX1Q8aK4bC7eM9V9pL0xY3zW6sN8vD1uT5qR2mJ4kL9 sysadmin@workstation
# 3. System Updating and Package Management
package_update: true
package_upgrade: true
package_reboot_if_required: true
packages:
- ufw
- curl
- git
- jq
- htop
- nginx
# 4. Writing Production Configuration Files
write_files:
- path: /etc/nginx/conf.d/health_check.conf
owner: root:root
permissions: '0644'
content: |
server {
listen 80 default_server;
server_name _;
location /healthz {
access_log off;
return 200 "OK\n";
}
}
# 5. Post-Boot Orchestration Commands
runcmd:
# Configure Uncomplicated Firewall (UFW)
- ufw default deny incoming
- ufw default allow outgoing
- ufw allow OpenSSH
- ufw allow 'Nginx HTTP'
- ufw --force enable
# Enable and verify Nginx
- systemctl restart nginx
- systemctl enable nginx
# Signal successful initialisation to cloud monitor
- echo "Cloud-init deployment complete at $(date)" >> /var/log/bootstrap.log
Step 2: Validating Syntax via the cloud-init Schema Tool
YAML indentation errors or unsupported schema keys frequently cause silent initialisation failures. Always validate user-data payloads prior to provisioning:
# Validate syntax against official cloud-init schema:
cloud-init schema --config-file user-data.yaml
# Expected output on valid schema:
# Valid cloud-config: user-data.yaml
If the validator outputs warnings (such as deprecated directives or invalid types), correct the YAML indentation before launching virtual machine instances.
Step 3: Inspecting Boot Status and Profiling Execution
Once an Ubuntu server boots, query cloud-init’s runtime status directly from the terminal:
1. Checking Initialisation State:
# Query current status
cloud-init status
# Wait synchronously until all stages complete (ideal for automation scripts)
cloud-init status --wait
The status returns done on success, or error if a module failed.
2. Profiling Boot Latency with Analyze:
To identify which initialisation stage or package installation slowed down server provisioning:
# Display total execution duration across stages
cloud-init analyze show
# Profile individual modules ranked by slowest execution duration
cloud-init analyze blame
Step 4: Reading and Triaging cloud-init Log Streams
When a cloud-init script fails to configure a user, mounts the wrong disk, or aborts a runcmd command, inspect two primary system log files:
| Log Path | Log Contents | Diagnostic Purpose |
|---|---|---|
/var/log/cloud-init.log |
Detailed python execution trace, handler events, datasource discovery. | Diagnosing schema parse errors, module failures, and datasource connection timeouts. |
/var/log/cloud-init-output.log |
Raw stdout and stderr capture from all packages, apt updates, and runcmd scripts. |
Diagnosing why custom bash scripts or package installations failed. |
# Filter for fatal errors and tracebacks:
sudo grep -i 'traceback' /var/log/cloud-init.log
sudo grep -i 'failed' /var/log/cloud-init.log
# Inspect raw output of custom shell commands:
sudo cat /var/log/cloud-init-output.log
Step 5: Testing and Forcing cloud-init to Re-run
By design, cloud-init marks itself as executed after the first boot (creating semaphores in /var/lib/cloud/instance/sem/) and will never re-run on subsequent reboots. To test modifications on an existing template VM without destroying the instance:
# Purge runtime state artifacts and logs:
sudo cloud-init clean --logs
# Re-run specific initialisation modules manually:
sudo cloud-init init
sudo cloud-init modules --mode=config
sudo cloud-init modules --mode=final
# Or clean and reboot to simulate a fresh cloud launch:
sudo cloud-init clean --logs --reboot
Troubleshooting Common cloud-init Operational Faults
| Observed Issue | Root Cause | Diagnostic & Remediation Step |
|---|---|---|
cloud-init status returns error |
One of the directives in runcmd returned a non-zero exit code or apt package failed to install |
Inspect /var/log/cloud-init-output.log. By default, runcmd continues even if an individual command fails, but the global status flags an error. |
| SSH public key not injected; password authentication remains locked | Incorrect YAML list syntax under ssh_authorized_keys or invalid public key string |
Ensure ssh_authorized_keys is a YAML list prefixed with -. Verify key contains no embedded linebreaks. Inspect /var/log/cloud-init.log under cc_ssh.py. |
write_files creates files with blank content or corrupt formatting |
Incorrect YAML multiline block scalar operator (used > folded style instead of | literal style) |
Use the pipe symbol (|) after content: to preserve literal line breaks and formatting. Quote octal permissions explicitly (e.g., permissions: '0644'). |
| Instance takes 5+ minutes to boot due to cloud-init timeout | Network metadata datasource lookup timing out (e.g., looking for AWS metadata on a local KVM node) | Restrict datasources in /etc/cloud/cloud.cfg.d/90_dpkg.cfg: set datasource_list: [ NoCloud, ConfigDrive, None ] to disable slow cloud metadata probes. |
Operational Governance and Best Practices
- Never Embed Plaintext Passwords: Avoid passing raw passwords in user-data. Always use
lock_passwd: trueand inject SSH public keys. If passwords are mandatory for local consoles, generate salted SHA-512 hashes usingmkpasswd -m sha-512. - Keep runcmd Arrays Idempotent: Write shell scripts passed into
runcmdso they can execute multiple times without producing errors (e.g., usemkdir -p,systemctl enable --now, and check file existence withtest -f). - Sanitise Image Templates with cloud-init clean: When preparing golden VM templates (AMI, QCOW2, or OVA), always execute
sudo cloud-init clean --machine-idbefore shutting down. This ensures cloned machines generate unique machine IDs and fetch fresh network metadata.