Chapter 36 closed with an important note that Ansible excels at configuring servers that are already powered on and reachable via SSH. However, there is one gap left untouched: how the server is initially provisioned before Ansible ever gets a chance to log in. Every time we created a new VM using virt-install in Chapter 28, the post-installation steps were repetitive: creating a user, copying an SSH key, and installing a few base packages manually before the server was truly ready for use. This process feels manageable for one or two experimental VMs, but becomes a real burden as soon as we need to provision dozens of instances simultaneously, especially in cloud environments where VMs can be created and destroyed within minutes. Cloud-init bridges this gap by automating the entire initial provisioning process right from the very first second the VM boots, without requiring a single manual touch or an external SSH connection.
This scenario is most evident for Sysadmins working in cloud environments or those who routinely generate new VMs for testing and staging needs. A developer requests five identical VMs to test a new version of an application prior to release, complete with their respective user access and several supporting packages pre-installed from the start. Without cloud-init, this request implies five identical manual installation processes. Cloud-init enables all five VMs to be born from a single cloud image, differentiated only by a small configuration file injected during the first boot, ensuring that the base image itself never needs to be modified or manually populated with packages one by one.
This chapter starts with the core concepts of cloud-init and datasources, proceeds to the structure of the user-data file that serves as the core of its configuration, and then walks through full provisioning—creating users, embedding SSH keys, and installing initial packages—on a KVM VM using skills mastered in Chapter 28. The chapter concludes with an overview of cloud-init usage outside local environments, including its relationship with real cloud providers, which will be practiced in greater detail in Chapter 41.
37.1 Cloud-init Concepts and Use Cases
37.1.1 What Are Cloud-init and Datasources
Cloud-init is an open source tool that has become the de facto standard for cross-platform instance initialization, running from inside the guest operating system itself upon its first boot. Unlike Ansible in Chapter 36, which operates externally over SSH against a running and reachable server, cloud-init operates internally as part of the VM boot process, long before the SSH service is ready to accept external connections. Almost all official Ubuntu cloud images, including those used by AWS, Azure, GCP, and OpenStack, come with cloud-init pre-installed and enabled out of the box, making it a universal bridge between a generic image and the specific configuration required by each instance.
To operate across diverse platforms, cloud-init uses the concept of a datasource, which is the mechanism cloud-init uses to identify the environment it is running in and locate its configuration data sources. This detection process is handled automatically by an internal component named ds-identify during the earliest boot stage. Once the datasource is located, cloud-init typically receives data in the form of two files: user-data, which contains the complete instance configuration definition, and meta-data, which contains instance-specific details such as instance-id and hostname (generally provided automatically by the underlying platform). This chapter uses a datasource named NoCloud, allowing us to inject user-data and meta-data manually via local media labeled cidata, making it ideal for a local KVM environment that lacks a dedicated metadata service found in true cloud environments.
37.1.2 Use Cases: When to Use Cloud-init
Cloud-init provides the most value in the following scenarios commonly encountered by Sysadmins in the field:
- Mass deployments of identical instances at a cloud provider (e.g., launching ten VMs at once for an autoscaling group behind a load balancer, as covered conceptually in Chapter 16) without logging into each instance individually for initial setup.
- The golden image concept, where one tested and validated base cloud image is reused across different environments simply by altering the content of
user-data, eliminating the need to build separate images for every configuration variant. - Setting up testing and staging environments that need to be recreated from scratch quickly and consistently—a process that is far more repetitive and error-prone when executed manually via console as done in Section 28.3.3.
- Serving as a bridge to Ansible from Chapter 36. Cloud-init handles the baseline foundation, such as user creation and SSH access setup; once the instance is reachable, Ansible takes over for advanced configurations and ongoing changes outside the scope of cloud-init.
This final point deserves emphasis. Cloud-init is designed for initial boot provisioning, not as a replacement for configuration management tools meant for routine day-to-day changes. Re-running the entire cloud-init cycle on an instance that has been running for a long time is not a standard pattern, unlike Ansible playbooks which are designed to be run repeatedly throughout a server's lifecycle.
37.1.3 Cloud-init Boot Stages and Status on Installed Servers
Cloud-init does not run as a single process; instead, it is divided into several sequential boot stages, each mapped to a specific systemd unit.
| Stage | Systemd Unit | Main Function |
|---|---|---|
| Generator | cloud-init-generator | Determines whether cloud-init needs to run at all during this boot cycle |
| Local | cloud-init-local.service | Locates the earliest local datasource before networking is up |
| Network | cloud-init-network.service | Configures networking and fetches data from datasources requiring a connection |
| Config | cloud-config.service | Executes main configuration modules, including user creation and package installations |
| Final | cloud-final.service | Executes late-stage modules, including custom commands specified via runcmd |
Practical Steps
- Check the cloud-init status on the server installed manually back in Chapter 2:
cloud-init status --long
Verification and Troubleshooting
- Servers installed interactively via Subiquity starting in Chapter 2 will likely display a status of
disabledrather thandone. This does not indicate a system failure.ds-identifydid not detect a valid datasource during such a manual installation and consequently wrote a marker file at/etc/cloud/cloud-init.disabled, preventing the above units from executing on subsequent boots. This behavior confirms that cloud-init is intended for environments that supply a proper datasource, such as the one we will build ourselves in Section 37.3. - On instances where the datasource is detected correctly, a healthy status displays
status: donealong with execution time details for each stage. Thecloud-init analyze blamecommand is useful for identifying which modules took the longest to execute if the boot process feels slow.
37.2 The user-data File Structure
37.2.1 The #cloud-config Format and Section Roles
The user-data file supports several content formats, but the most common and recommended format for daily use is cloud-config, written in YAML syntax and strictly required to begin with the literal header #cloud-config on its very first line. This line is not a simple YAML comment; it is a mandatory marker read by cloud-init to identify the file content type before processing it. Without this header, cloud-init will ignore the entire file content, even if the internal YAML syntax is valid.
Several high-level keys frequently used and practiced in Section 37.3 include users (to define accounts along with their ssh_authorized_keys), package_update and package_upgrade (to refresh package lists and upgrade the system prior to installation), packages (to list packages to install), and runcmd (to execute custom shell commands during the Final stage). A subtle yet critical detail is the instance-id value in meta-data. Cloud-init tracks previously processed instance-ids, and single-run modules will not re-run if the instance-id matches one recorded earlier. This is a common pitfall when cloning VM disks without updating the instance-id.
37.2.2 Writing and Validating a Minimal user-data File
Practical Steps
- Create an experimental working directory on the same KVM host used in Chapter 28 to inspect its structure before crafting a full configuration:
mkdir -p ~/cloud-init-lab cd ~/cloud-init-lab - Create a test
user-datafile containing a minimal configuration:nano user-data - Add the following two lines:
#cloud-config hostname: cloud-init-test - Validate the syntax and schema of the file without creating a VM:
cloud-init schema --config-file user-data --annotate
Verification and Troubleshooting
- A healthy output displays
Valid schema user-data:user-data. If the#cloud-configline is intentionally omitted for testing, the same command will reject the file because it cannot identify the content type. - The
cloud-init schemacommand validates key names and structures against each module's official schema; it does not guarantee logical correctness. For instance, a misspelled package name under thepackageskey will pass schema validation because its structural layout is correct, only to fail later when executed byaptinside the VM. - Common errors such as
mapping values are not allowed in this contextpoint to YAML indentation issues, identical to those discussed during Ansible playbook creation in Section 36.3.3.
37.3 Automated Provisioning: Users, SSH Keys, and Initial Packages
This section walks through a end-to-end scenario: going from a plain cloud image to a VM configured with a sysadmin user, SSH key-based authentication, and the nginx package installed—all without touching the VM console manually. We will rely on the KVM/QEMU/libvirt stack installed in Section 28.2, but this time we boot directly from a cloud image rather than a Subiquity installer ISO like vm-app01 in Section 28.3.
37.3.1 Preparing the Cloud Image and VM Disk
Ubuntu provides official cloud images: base OS images pre-packaged with an active cloud-init installation, ready for immediate use without going through the Subiquity installer process.
Practical Steps
- Download the official Ubuntu Server 26.04 LTS cloud image to the storage pool configured in Section 28.3.1:
cd /var/lib/libvirt/images sudo wget https://cloud-images.ubuntu.com/releases/26.04/release/ubuntu-26.04-server-cloudimg-amd64.img - Create a dedicated directory to store the
user-dataandmeta-datafiles, separate from the VM disk file:sudo mkdir -p /var/lib/libvirt/images/cloud-init - Create a new copy-on-write VM disk image utilizing the downloaded cloud image as a backing file, then extend its capacity to 10 GB:
sudo qemu-img create -f qcow2 -F qcow2 \ -b /var/lib/libvirt/images/ubuntu-26.04-server-cloudimg-amd64.img \ /var/lib/libvirt/images/vm-cloudinit01.qcow2 10G
Verification and Troubleshooting
- Confirm that the new disk image correctly points to the backing file and reflects the expanded size:
Theqemu-img info /var/lib/libvirt/images/vm-cloudinit01.qcow2backing filefield should point to the original cloud image, whilevirtual sizeshould display10 GiB, even though the actual file size on the host disk (disk size) remains small, as qcow2 format only allocates blocks that are actively written. - This backing file approach avoids duplicating the base cloud image file whenever a new VM is instantiated. The original cloud image stays intact as a shared base, while each individual VM retains its unique changes (deltas) in its assigned qcow2 file, saving storage compared to performing a full file copy.
- Default cloud images are intentionally distributed with a small footprint (typically under 3 GB). They rely on the
growpartandresizefsmodules within cloud-init to expand the root partition and filesystem automatically on first boot up to the 10 GB limit set viaqemu-img create. Both modules are enabled by default in official Ubuntu cloud images and do not require manual configuration inuser-data.
37.3.2 Writing a Complete user-data File for Provisioning
Practical Steps
- Create a
meta-datafile defining the instance identity:sudo nano /var/lib/libvirt/images/cloud-init/meta-data - Add the
instance-idand local hostname:instance-id: vm-cloudinit01 local-hostname: vm-cloudinit01 - Create the primary
user-dataconfiguration file:sudo nano /var/lib/libvirt/images/cloud-init/user-data - Define user parameters, SSH key configuration, and package installations. Replace the placeholder string in
ssh_authorized_keyswith the actual contents of your public key located at~/.ssh/id_ed25519.pubgenerated back in Section 3.2.1:
The#cloud-config users: - name: sysadmin groups: sudo shell: /bin/bash sudo: ALL=(ALL) NOPASSWD:ALL lock_passwd: true ssh_authorized_keys: - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGXampleKeyMaterialGantiDenganPublicKeyAsli package_update: true package_upgrade: true packages: - nginx - curl runcmd: - [ sh, -c, "echo Provisioned via cloud-init on $(date) > /var/www/html/provisioned.txt" ]userssection creates an account namedsysadminbelonging to thesudogroup. Thesudodirective configures passwordless sudo access equivalent to an entry under/etc/sudoers.d/as seen in Section 4.2.1. Thelock_passwd: truedirective explicitly disables password login so that access is restricted to SSH key authentication, aligning with system hardening guidelines in Chapter 30. Directivespackage_updateandpackage_upgradeperform tasks equivalent to runningapt updateandapt upgradeprior to package installation viapackages. Finally,runcmdexecutes commands during the Final boot stage, which here writes a verification file checked viacurlto confirm provisioning completed.
Verification and Troubleshooting
- Re-validate the configuration schema prior to booting the target VM:
cloud-init schema --config-file /var/lib/libvirt/images/cloud-init/user-data --annotate - Because the
usersentry above is explicitly defined without including the keyworddefaultin its list, cloud-init will suppress the creation of the cloud image's default user (typically namedubuntu). Only thesysadminaccount will exist on the provisioned VM. - Ensure the entire
ssh-ed25519 ...public key string is formatted on a single unbroken line withinssh_authorized_keys. Line breaks introduced during copy-paste operations are a frequent cause of SSH authentication failures in Section 37.3.4.
37.3.3 Booting the VM with virt-install and Seed Cloud-init
The --cloud-init flag in virt-install automatically packages the user-data and meta-data files into a seed media labeled cidata according to the NoCloud datasource specification, attaching it to the target VM as a virtual CD-ROM. This mirrors the behavior of using cloud-localds from the cloud-image-utils package manually. Using virt-install automates this step natively, though cloud-localds remains useful for standalone QEMU setups or hypervisors lacking equivalent flags.
Practical Steps
- Run
virt-installtargeting the disk created in Section 37.3.1, supplying the--importflag to boot directly from the existing OS disk without invoking an installer interface:virt-install \ --name vm-cloudinit01 \ --memory 2048 \ --vcpus 2 \ --disk path=/var/lib/libvirt/images/vm-cloudinit01.qcow2,bus=virtio \ --os-variant detect=on,require=off \ --network network=default,model=virtio \ --graphics none \ --import \ --cloud-init user-data=/var/lib/libvirt/images/cloud-init/user-data,meta-data=/var/lib/libvirt/images/cloud-init/meta-data - Allow time for the initial boot and provisioning routines to complete, then inspect the assigned IP address issued by the default network's DHCP service:
virsh domifaddr vm-cloudinit01
Verification and Troubleshooting
- The
--importflag differs from--locationused in Section 28.3.2. While--locationboots the Subiquity installer from an ISO,--importinstructsvirt-installto boot immediately from an OS-populated disk image, suitable for pre-installed cloud images. - The IP address reported by
virsh domifaddrfalls within the default192.168.122.0/24subnet configured for libvirt NAT networking in Section 28.2.4. - If
virsh domifaddrreturns no network details immediately, the target VM may still be executing early boot routines or waiting on a DHCP lease response. Re-query after a short delay, or monitor console output directly usingvirsh console vm-cloudinit01.
37.3.4 Verifying Provisioning Results
Practical Steps
- Initiate an SSH session to the VM using the IP assigned via
domifaddrand the authorized SSH key defined inuser-data:ssh [email protected] - Upon successful login, confirm that all cloud-init stages have completed execution:
cloud-init status --wait - Verify that the
nginxservice is active and that the marker file created viaruncmdexists:systemctl is-active nginx curl http://localhost/provisioned.txt
Verification and Troubleshooting
- A successful SSH login does not automatically guarantee that all provisioning steps have finished executing. Modules handling
ssh_authorized_keysexecute early, while package management operations andruncmdroutines run in later stages (Config and Final). Executingcloud-init status --waitensures all tasks have completed before declaring the instance operational. - The command
systemctl is-active nginxshould returnactive, andprovisioned.txtshould contain the textProvisioned via cloud-init onalongside the boot timestamp, confirming that theruncmddirectives were executed. - If errors occur during module execution, consult
/var/log/cloud-init-output.loginside the VM for standard output and error details. Comprehensive execution details are written to/var/log/cloud-init.logfor deeper troubleshooting. - Receiving a
Permission denied (publickey)error during SSH connection attempts usually points to key truncation issues or invalid YAML indentation under thessh_authorized_keysblock within theuser-datafile.
37.4 Using Cloud-init in Local VMs and Cloud Environments
37.4.1 Alternative Datasources Beyond NoCloud: Comparison Summary
NoCloud is one of many datasources supported by cloud-init. Each major platform provides its own implementation mechanism to feed configuration parameters into instances, while the underlying user-data syntax remains portable across targets.
| Datasource | Common Environment | User-Data Retrieval Method |
|---|---|---|
| NoCloud | Local KVM/QEMU, VirtualBox, custom images | Local media labeled cidata (ISO/vfat) attached directly to the VM |
| ConfigDrive | OpenStack, select private cloud providers | Attached block device structured with an openstack/ directory layout |
| EC2 | AWS EC2, EC2-compatible platforms | Internal HTTP metadata service queried at 169.254.169.254 |
| Azure | Microsoft Azure | Combination of ovf-env.xml via virtual CD-ROM and Azure Instance Metadata Service |
| GCE | Google Compute Engine | Internal metadata server accessed via HTTP requests |
Differences among datasources center on how configuration payloads are retrieved by cloud-init. Key names such as users, packages, and runcmd maintain identical behavior regardless of the underlying platform datasource.
37.4.2 Checking the Active Datasource Inside a VM
Practical Steps
- From within the active SSH session on
vm-cloudinit01, query the runtime datasource detected byds-identifyduring boot:cloud-id --long
Verification and Troubleshooting
- The output should return
nocloud, confirming that the seed media attached using the--cloud-initflag in Section 37.3.3 was processed via the NoCloud datasource mechanism. - The
cloud-idcommand is a utility utility bundled with thecloud-initpackage that reports the active datasource without requiring manual parsing of underlying state JSON files.
37.4.3 Porting user-data to Public Cloud Providers
The user-data file authored in Section 37.3.2 can be repurposed when launching instances on public cloud platforms like AWS, Azure, or GCP. The file content can be pasted directly into the provider's launch console user data fields. Cloud platforms manage payload delivery natively through their metadata services without requiring manual ISO generation. Platform specifics—such as cloud security groups taking precedence over local UFW firewall configurations—are covered in Chapter 41.
The primary difference when deploying to public cloud instances lies in meta-data handling. Cloud platforms inject values like instance-id and hostname dynamically using internal metadata services, removing the need to supply a manual meta-data file as required in local NoCloud environments.
You now understand core datasource concepts, how to validate user-data files, and how to automate VM provisioning from a bare cloud image into a configured server equipped with users, SSH keys, and base packages upon initial boot. Cloud-init completes the automation toolset presented in Part IX: shell scripts in Chapter 35 handle routine tasks on individual servers, Ansible in Chapter 36 manages ongoing configuration across fleets, and cloud-init provisions unconfigured instances into accessible nodes. As rapid provisioning scales up node counts across environments, maintaining system visibility becomes essential—a topic explored in Part X through system monitoring concepts in Chapter 38.

