How to Configure Cloud-Init for Automated Server Provisioning and User Provisioning on Ubuntu Server

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: true and inject SSH public keys. If passwords are mandatory for local consoles, generate salted SHA-512 hashes using mkpasswd -m sha-512.
  • Keep runcmd Arrays Idempotent: Write shell scripts passed into runcmd so they can execute multiple times without producing errors (e.g., use mkdir -p, systemctl enable --now, and check file existence with test -f).
  • Sanitise Image Templates with cloud-init clean: When preparing golden VM templates (AMI, QCOW2, or OVA), always execute sudo cloud-init clean --machine-id before shutting down. This ensures cloned machines generate unique machine IDs and fetch fresh network metadata.

Get the best tech tips delivered straight to your inbox.

Join thousands of readers mastering Apple, Google, Microsoft, and Linux.