cloud-init can apply the account, SSH key and package configuration needed at a VM’s first boot. This example prepares an Ubuntu 24.04 LTS cloud image with a login account and Nginx, then checks the resulting service response. It also separates the documented configuration examples from the boot paths actually tested.
The basic lab is QEMU and NoCloud approach on a Linux managed PC. NoCloud is a data source that delivers configuration files to a small disk without a separate cloud service. We first succeeded with DHCP and then changed it to a static IP, and we also summarized the points to distinguish when transferring settings from AHV, VMware, etc.
On this page
First distinction: OS image and first boot settings
cloud-init handles the initial setup of an already installed OS. If you think of it as a tool that replaces the interactive installation of the Ubuntu installation ISO, the starting point is different. Here we use noble-server-cloudimg-amd64.img with cloud-init. The autoinstall: setting from Ubuntu Installation Automation is not included in this example.
| file | What it contains | Values of this exercise |
|---|---|---|
user-data | Account·Public key·Package·File·Execution command | YAML starting with #cloud-config |
meta-data | Instance identifier/host name | instance-id: lab-web-01-v1 |
network-config | NIC·IP·Gateway·DNS | Add in static IP step |
These input files are specific to NoCloud. Other virtualization platforms can supply metadata and network configuration through their own interfaces. Do not assume all three YAML files belong in one input field. See the NoCloud input file documentation for the delivery structure.
Minimum example for each purpose
Each example below is a separate file for one configuration task. Changing only the hostname does not require including the account and web server configuration. Image defaults, platform settings and cloud-init’s default behavior still apply independently.
Here’s an example of passing it on the first boot of a new VM. This is distinct from attaching a file to a running VM and rebooting it. The example without a connection account and public key does not prepare SSH connection, so use the connection settings of the image/platform or apply the public key example together.
Example 1. Changing only the hostname
Enter only the contents below into user-data. Replace app-01 with your desired hostname. preserve_hostname: false allows cloud-init to set the hostname.
#cloud-config
preserve_hostname: false
hostname: app-01
There are no IP·Package·User settings in this example. Leave only instance-id in meta-data to prevent different local-hostname values from being mixed. After booting, check whether hostnamectl --static is app-01. If you need to manage /etc/hosts, please review the manage_etc_hosts settings separately. Referred to the official host name example .
Example 2. Setting only the network IP as a static IP
In NoCloud, puts the static IP into a separate network-config file. Below is an example address for an Ubuntu 24.04 VM connecting to a real LAN. Replace the MAC address, IP, prefix, gateway, and DNS with the assigned values. We also added a gateway and DNS to allow access to out-of-band servers and package repositories.
version: 2
ethernets:
lan:
match:
macaddress: '52:54:00:12:34:56'
dhcp4: false
addresses:
- 192.168.10.50/24
routes:
- to: default
via: 192.168.10.1
nameservers:
addresses: [192.168.10.1]
The top of the file is version: 2. It doesn’t add network: to the outside like a regular Netplan file. To avoid specifying a host name, we only store the instance ID in meta-data.
instance-id: minimal-02-static-ip-01
If you do not add any accounts or packages, leave user-data empty as follows: {} is an empty YAML mapping.
#cloud-config
{}
Place the three files in the same directory and create a seed. This is not a method of pasting network-related files into the user-data input box.
cloud-localds --network-config=network-config \
seed-static-only.img user-data meta-data
Attach the seed disk you created to the VM before the first boot. A connection account and SSH public key are not created in this example. After booting, check the address, default route, and DNS at ip -br address, ip route, and resolvectl status.
192.168.10.x in this LAN example cannot be used as is in the QEMU default user network below. The QEMU NAT exercise uses the 10.0.2.x example from Section 6. network setup document also distinguishes between user-data and network input.
Example 3. Registering only the SSH public key in the default account
Register your public key with the default user of your distribution without creating a new account. Replace the placeholders below with an entire line from your .pub file.
#cloud-config
ssh_authorized_keys:
- REPLACE_WITH_YOUR_SSH_PUBLIC_KEY
The default account for Ubuntu cloud images is usually ubuntu and may vary depending on your image settings. Check system_info.default_user.name of /etc/cloud/cloud.cfg. This setting does not separately change SSH password authentication or sudo policies. Referred to the official SSH key example.
Example 4. Changing only the time zone to Korean time
#cloud-config
timezone: Asia/Seoul
After booting, check whether timedatectl is Time zone: Asia/Seoul. Since this is a setting that determines the time zone display, the NTP server and time synchronization policy are separate.
Example 5. Installing only curl and jq
#cloud-config
package_update: true
packages:
- curl
- jq
Update the package list as of Ubuntu 24.04 and install the two packages. It requires DNS and repository connectivity, and any required dependent packages may also be installed. After booting, wait for cloud-init to complete and check with dpkg-query -W curl jq.
Example 6. Create just one configuration file
#cloud-config
write_files:
- path: /etc/maruops-lab.conf
owner: root:root
permissions: '0644'
content: |
environment=lab
owner=infra
/etc/maruops-lab.conf is the file defined in this example. This is not a configuration file that the OS automatically reads; it shows how the file was created. Existing files at the same path will be overwritten and the new file path will be used. After booting, check the contents and 644 root:root permissions with cat /etc/maruops-lab.conf and stat -c '%a %U:%G' /etc/maruops-lab.conf.
Example 7. Running just one command on first boot
#cloud-config
runcmd:
- [sh, -c, 'date -Iseconds > /var/tmp/first-boot.txt']
Save the first run time to a file. runcmd runs once per instance, so it does not update history on each reboot. Confirm with cat /var/tmp/first-boot.txt. To avoid overlap with the /tmp theorem, we used /var/tmp here. The setting keys for time zone, package, file, and execution command were created based on module-specific setting document .
Common ways to seed minimal examples
Each folder from minimal/01-hostname to minimal/07-first-boot-command in the download ZIP contains user-data and meta-data with a unique ID. Run the command below in the required folder. If used on multiple VMs, set instance-id differently for each VM.
cloud-localds seed.img user-data meta-data
For static IP folders only, add the --network-config=network-config option described previously. When combining with other items, write #cloud-config of user-data just once at the top and collect the necessary keys in the same YAML document. Network settings will still be in a separate file.
At least 7 examples were examined for cloud-init schema and NoCloud seed creation, and static IP files were checked for Netplan conversion/configuration creation. Unlike the Nginx integration exercise below, the first boot of each minimal example was not run and verified separately.
1. Prepare tools and SSH keys on the management PC
Run the following commands on an Ubuntu 24.04 lab management PC. The example boots an x86-64 cloud image, and the guest needs access to Ubuntu package repositories. The QEMU guest disk has a logical capacity of 12GB.
sudo apt-get update
sudo apt-get install -y qemu-system-x86 qemu-utils cloud-image-utils \
curl openssh-client
mkdir -p ~/cloud-init-lab
cd ~/cloud-init-lab
umask 077
ssh-keygen -t ed25519 -f ./lab_ed25519 -C cloud-init-lab
cat ./lab_ed25519.pub
You can specify a private key password during key creation. Please use a practice path that does not overlap with existing keys. In the next step, put the entire line lab_ed25519.pub. lab_ed25519 is a private key, so we will not put it in user-data or distribute it.
2. Define accounts and services in user-data
Create a file user-data in your working directory and save the contents below. Replace REPLACE_WITH_YOUR_SSH_PUBLIC_KEY with the public key line you printed earlier. Keep the first line of the file #cloud-config and space indentation and no tabs.
#cloud-config
hostname: lab-web-01
preserve_hostname: false
manage_etc_hosts: true
timezone: Asia/Seoul
disable_root: true
ssh_pwauth: false
users:
- name: ops
groups: [adm, sudo]
shell: /bin/bash
lock_passwd: true
sudo: ['ALL=(ALL:ALL) NOPASSWD:ALL']
ssh_authorized_keys:
- REPLACE_WITH_YOUR_SSH_PUBLIC_KEY
package_update: true
packages:
- nginx
- curl
write_files:
- path: /var/www/html/index.html
owner: root:root
permissions: '0644'
content: |
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>cloud-init lab</title>
<h1>lab-web-01 ready</h1>
</html>
- path: /usr/local/sbin/bootstrap-web
owner: root:root
permissions: '0750'
content: |
#!/bin/sh
set -eu
nginx -t
systemctl enable --now nginx
curl --retry 5 --retry-connrefused --retry-delay 1 -fsS \
http://127.0.0.1/ | grep -Fq 'lab-web-01 ready'
install -d -m 0755 /var/lib/maruops
date -Iseconds > /var/lib/maruops/bootstrap.done
runcmd:
- [sh, -eu, /usr/local/sbin/bootstrap-web]
The ops account in this example is password-locked and logs in with an SSH public key. ssh_pwauth: false is a setting that turns off SSH password authentication, and disable_root: true applies root SSH access restrictions to cloud-init. This setting does not delete other accounts already included in the existing image.
NOPASSWD:ALL is an option that gives the lab management account full sudo privileges without a password. In a production environment, administrator key distribution and sudo policies are set separately. If you only turn on sudo’s password requirement while your password is locked, you may not be able to run necessary administrative commands.
package_update updates the package list, and packages installs Nginx and curl. Create web documents and execution scripts with write_files, and call one script with runcmd. It is easy to find the point of failure if you separate configuration files, service startup, and response checks.
The script stops on command failure with set -eu. It logs /var/lib/maruops/bootstrap.done only after passing the Nginx configuration check and local HTTP response verification. This file is a check mark for this example and does not determine the success of cloud-init as a whole. The setting key and execution time were created based on the cloud-init module document .
3. Create meta-data and seed disks
Save the following two lines in the meta-data file. If you create multiple VMs, set instance-id and the host name appropriately for each VM. This identifier is used to determine whether a per-instance operation has already run.
instance-id: lab-web-01-v1
local-hostname: lab-web-01
Place both files in the same directory to create a disk for delivery of settings. This step uses cloud-init’s DHCP default configuration as no separate network settings are included.
cloud-localds seed.img user-data meta-data
ls -lh seed.img
seed.img is not an OS boot disk. This is a separate disk that contains the files that NoCloud will read and maintains the file names. When creating an ISO manually, you must label the volume cidata and put user-data, meta-data in the root. NoCloud disk creation example also uses the same configuration.
4. Boot VM with cloud image
Download the image from the management PC and compare the checksums. The image and checksum of the current path may be updated and must be received at the same time. Checksum checking checks for file consistency and corruption during transmission, and stores received images and checksums in environments that need to be reproduced.
curl -fLO https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
curl -fLO https://cloud-images.ubuntu.com/noble/current/SHA256SUMS
awk '$2 ~ /^\*?noble-server-cloudimg-amd64\.img$/ {print}' SHA256SUMS | sha256sum -c -
qemu-img create -f qcow2 -F qcow2 \
-b "$PWD/noble-server-cloudimg-amd64.img" boot.qcow2 12G
Proceed after confirming that the checksum result is OK. boot.qcow2 is a disk that reads the original image and saves only the changes. Keep the original files together as moving or deleting them will prevent normal use of the boot disk.
The terminal where you run the following command will be used as the VM console. SSH and HTTP inspection run from different terminals on the management PC. Ports 2222, 8080 on the management PC must be empty.
qemu-system-x86_64 \
-machine accel=kvm:tcg -cpu max -m 1024 -smp 2 -nographic \
-drive file=boot.qcow2,if=virtio,format=qcow2 \
-drive file=seed.img,if=virtio,format=raw,readonly=on \
-netdev user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22,hostfwd=tcp:127.0.0.1:8080-:80 \
-device virtio-net-pci,netdev=net0,mac=52:54:00:12:34:56
If KVM is not available, it runs in TCG software emulation, which may result in slow initial boot and package installation. The port was only connected to 127.0.0.1 on the management PC. SSH is 127.0.0.1:2222 → VM:22, HTTP is 127.0.0.1:8080 → VM:80. For the configuration method, refer to the official QEMU practice guide .
5. After connecting to SSH, check completion status and HTTP response.
Go to the same working directory in another terminal and connect. The SSH connection may be rejected at first. Check the VM console to see if booting is in progress and reconnect after the SSH service is up. The host key fingerprint from the first connection is checked against the fingerprint printed on the VM console.
cd ~/cloud-init-lab
ssh -i ./lab_ed25519 -p 2222 ops@127.0.0.1
From here, the next code block runs inside the guest VM. A successful SSH login does not mean that the package installation or the last command is finished. Wait for completion with cloud-init status --wait and check the detailed status.
sudo cloud-init status --wait
echo "cloud-init exit=$?"
sudo cloud-init status --long
hostnamectl --static
id ops
sudo nginx -t
systemctl is-active nginx
curl -fsS http://127.0.0.1/
sudo cat /var/lib/maruops/bootstrap.done
If normal, the host name is lab-web-01, the service status is active, and the HTTP body contains lab-web-01 ready. The completion file records the time the scan was successful. Next, run curl -fsS http://127.0.0.1:8080/ on the management PC to check port forwarding.
| exit code | judgment | Next check |
|---|---|---|
0 | cloud-init completed without error | Check account/service/actual response |
1 | Error not recoverable | Check failure steps and logs |
2 | Completed but with recoverable errors | Check recoverable_errors |
It also reads the exit code and extended_status. Don’t ignore an error just because you see done on the screen. This distinction is shown in the failure status and the exit codes and Detailed status description. The version of the image being checked is recorded as cloud-init --version.
6. Static IP forwarded to network-config
If the DHCP example is successful, you can try changing to a static IP. The network does not mix user-data into network:, but delivers it as a separate network-config file in NoCloud. The top of the file below is version: 2, not wrapped once more with network:.
The next value is , the default user network only in the above QEMU command, . Instead of guessing the NIC name, it finds the device by the MAC address you specify in QEMU. VMs connected to a physical LAN will not use this address as is.
version: 2
ethernets:
labnic:
match:
macaddress: '52:54:00:12:34:56'
dhcp4: false
addresses:
- 10.0.2.15/24
routes:
- to: default
via: 10.0.2.2
nameservers:
addresses: [10.0.2.3]
| item | QEMU practice values | Values to change in bridge/virtualization platform |
|---|---|---|
| MAC | 52:54:00:12:34:56 | Physical MAC of target VM NIC |
| address | 10.0.2.15/24 | Assigned IP and prefix length |
| gateway | 10.0.2.2 | router for that subnet |
| DNS | 10.0.2.3 | DNS used in your environment |
After running sudo poweroff in the guest and exiting QEMU, create a new test disk and seed on the management PC. Instead of erasing the boot disk you used, create it with a new name: boot-static.qcow2. meta-data-static contains a separate identifier.
cat > meta-data-static <<'EOF'
instance-id: lab-web-01-static-v1
local-hostname: lab-web-01
EOF
cloud-localds --network-config=network-config \
seed-static.img user-data meta-data-static
qemu-img create -f qcow2 -F qcow2 \
-b "$PWD/noble-server-cloudimg-amd64.img" boot-static.qcow2 12G
In the preceding QEMU command, change boot.qcow2 to boot-static.qcow2 and seed.img to seed-static.img. Port and MAC address use the same value. The new VM’s host key is different from the old VM, so if a warning occurs, check the console for a new fingerprint and replace the existing [127.0.0.1]:2222 entry.
Check the following values in the connected guest: Even if only the address is correct, package installation may fail if the default path or DNS is missing.
ip -br address
ip route
resolvectl status
getent hosts archive.ubuntu.com
sudo cloud-init status --long
This configuration targets a Netplan-based Ubuntu 24.04 environment. On other distributions, check the renderer and supported settings. Consult the network configuration input priorities and Version 2 format. Changing an existing Ubuntu host’s address is covered separately in the Netplan static IP and SSH recovery guide.
7. Order to check when not applicable
Check YAML and actual delivered files
In a checking environment with cloud-init installed, you can check the file schema before deployment. The first command is run in the test environment, the second and third commands are run in the booted guest. If you do not have the cloud-init command on your management PC, you can use a separate lab VM installed for this test.
# 배포 전: cloud-init이 설치된 검사 환경
cloud-init schema --config-file user-data --annotate
# 부팅 후: 게스트 VM
sudo cloud-init schema --system --annotate
sudo less /var/lib/cloud/instance/user-data.txt
Schema checking verifies the YAML structure and setup keys. There is no guarantee that the public key matches the real key, that the repository can be reached, or that the script will succeed. If the user-data.txt of the modified file and the guest are different, the seed disk and data source are checked first before the execution command. official user-data inspection guide also provides the same inspection command.
Distinguish between processing log and command output
sudo cloud-init status --long
sudo cloud-id
sudo tail -n 100 /var/log/cloud-init-output.log
sudo tail -n 100 /var/log/cloud-init.log
sudo journalctl -b -u cloud-final.service --no-pager
sudo cat /run/cloud-init/ds-identify.log
cloud-init-output.log examines package installation and script output, and cloud-init.log examines data source and module processing errors. Checking if cloud-id is nocloud can help you quickly narrow down whether the intended delivery method was selected. If you cannot find the data source, check ds-identify.log, seed connection status, and volume label.
| symptoms | What to look at first | direction of action |
|---|---|---|
| Public key login failure | ops username/key pair/guest console | One line of public key and indentation, check SSH port |
| Settings are completely ignored | #cloud-config·Data Source | Check first line, file name, seed connection |
| Package installation failed | DNS/default path/storage error | After fixing the network, retest on a new VM |
| Nginx not loading | cloud-init-output.log | Check nginx -t and service log |
| Fixed IP not applied | MAC Match·network-config | Check file structure and seed regeneration |
| Even if you reboot after modification, it stays the same. | Instance ID/execution cycle | Retest with new boot disk |
Depending on your distribution, the systemd unit name may be different. The cloud-final.service used here is the last processing step in the Ubuntu 24.04 example. The log collection location can also be found in the cloud-init log file document . Authentication information may remain in user-data and logs, so check the contents before sharing them externally.
8. Distinguish between reboot, rerun, and template replication
runcmd is a task that runs once per instance. Modifying user-data and simply rebooting will not cause it to take effect again from scratch. If you change the file, it is convenient to repeat this exercise by recreating the seed and creating a new boot disk from the original cloud image.
Long-term tasks that need to run on each VM reboot are managed by the systemd service or timers. bootcmd runs at an earlier boot stage, so it is not suitable for carrying over service tasks after package installation. It is easier to read the log if you remember that runcmd writes the script and the actual execution is handled by scripts_user in the last step.
The following initialization commands are for use only on disposable lab VMs. Do not run on production servers. SSH host keys or default settings may be re-created, affecting connectivity and services. First, keep the logs needed to analyze the cause, and prepare the seed that will transfer the changed settings before use.
# 폐기 가능한 실습 VM 내부에서만 실행
sudo cloud-init clean --logs --reboot
This command initializes the running state of cloud-init and reboots. It does not return previously installed packages, users, or data to their original state. Therefore, clean reproduction experiments are performed on a new boot disk. Official relaunch document also prohibits relaunching the operating system.
If you clone a booted VM as an image template, you need to prepare separately to organize the execution status and machine ID for each instance. cloud-init clean --machine-id is the option used at that time, and a new instance-id must also be supplied for each clone. Machine ID cleanup alone does not remove all secret values and accounts. For detailed options, check the clean CLI document and the image creation procedure for the virtualization platform.
When moving examples from AHV·VMware
The common part to take in the above exercise is user-data‘s cloud-config. The cloud image must have cloud-init, and the image must support the data source provided by the platform. AHV’s guest customization input or VMware’s GuestInfo/guest customization have different delivery paths, so select them according to the VM image and platform version.
It is best to first check SSH connection by entering only the account and public key and then add packages and services to narrow down the cause. If the platform provides a network, configure it so that its settings do not compete with the settings of a separate seed. Apply the file name and label rules in this article only if you are using NoCloud disks. Supported data sources can be found in the cloud-init data source list.
Example file and verification scope
The minimum 7 examples for each purpose and the existing Nginx integration exercises user-data, meta-data, and DHCP/static IP examples were grouped into cloud-init practice example ZIP. Private key and VM disk are not included. First, check SSH and HTTP responses with DHCP, save that configuration, and then try adding a static IP or other package.
Terms of use for documents and photos were confirmed on October 22, 2026. We booted cloud-init 26.1-0ubuntu1~24.04.1 of the Ubuntu 24.04 cloud image with QEMU TCG and confirmed NoCloud recognition, public key SSH connection of ops account, Nginx automatic installation, guest internal/port forwarding HTTP response, and creation of completion file. The static IP example examined Netplan conversion and configuration creation with cloud-init 26.2-0ubuntu1~24.04.1. The first boot using a static IP and AHV/VMware deployment were not included in the execution verification.



