Cloud-Init β A Visual Blueprint
“The first 60 seconds of an EC2 instance’s life, choreographed.”
π 1. The Big Picture (Mental Metaphor)
Imagine a new hire on Day 1.
HR gives them: cloud-init gives the VM:
βββββββββββββ βββββββββββββββββββββ
β’ ID badge ββββΊ β’ hostname / SSH keys
β’ Laptop setup ββββΊ β’ packages installed
β’ Onboarding doc ββββΊ β’ user-data script
β’ Email/Slack ββββΊ β’ network + DNS
β’ Welcome lunch ββββΊ β’ final "ready" signal
After lunch β they're productive.
After cloud-init β the VM is production-ready.
Cloud-init is the universal “onboarding officer” that runs once when a cloud VM first boots, turning a generic image into your server.
πΊοΈ 2. Where Cloud-Init Lives (System Map)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CLOUD PROVIDER β
β βββββββββββββββ βββββββββββββββββ ββββββββββββββββββ β
β β AMI/Image β β Metadata β β User-Data β β
β β (Ubuntu 22) β β Service β β (your script) β β
β βββββββββββββββ β 169.254.169 β ββββββββββββββββββ β
β β β .254 β β β
β β βββββββββ¬ββββββββ β β
β βΌ β β β
β ββββββββββββββββββββββββββββΌβββββββββββββββββββββΌββββββββββ β
β β VM BOOTS β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β cloud-init (4 stages) β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Key insight: cloud-init = the glue between
the immutable image β the per-VM metadata β your runtime config.
π 3. The 4 Boot Stages (Flowchart)
Power-On
β
βΌ
ββββββββββββββββββββ
β β generator β Decide: "Do I run at all?"
β (systemd) β β reads kernel cmdline
ββββββββββ¬ββββββββββ
βΌ
ββββββββββββββββββββ Network: β not yet
β β‘ local β Tasks: detect datasource,
β cloud-init β set hostname, fs resize
ββββββββββ¬ββββββββββ
βΌ
ββββββββββββββββββββ Network: β
up
β β’ network β Tasks: fetch metadata + user-data
β cloud-init β configure NTP, mounts
ββββββββββ¬ββββββββββ
βΌ
ββββββββββββββββββββ Tasks: install packages,
β β£ config β write files, create users,
β cloud-config β add SSH keys, run cmds
ββββββββββ¬ββββββββββ
βΌ
ββββββββββββββββββββ Tasks: user scripts (runcmd),
β β€ final β boot finished signal,
β cloud-final β snapshot of all logs
ββββββββββ¬ββββββββββ
βΌ
System ready β
(~30-90 s after power-on)
Mnemonic: G-L-N-C-F β “Generator, Local, Network, Config, Final”
π§± 4. Anatomy of Inputs (Layered Framework)
WHO TELLS THE VM WHAT TO DO?
ββββββββββββββββββββββββββββ
Layer 4: USER-DATA β You write this
(cloud-config YAML, (most customization happens here)
shell script,
MIME multi-part)
Layer 3: VENDOR-DATA β Cloud provider injects
(defaults from AWS, (e.g. AWS SSM Agent setup)
Azure, GCP)
Layer 2: METADATA β Read from 169.254.169.254
(instance-id, hostname, (immutable per-instance facts)
AZ, IAM role, tags)
Layer 1: DATASOURCE β Auto-detected
(Ec2, Azure, GCE, (decides where to find layers 2-4)
NoCloud, OpenStack)
Layer 0: /etc/cloud/cloud.cfg β Image author baked this in
(which modules run, (the "rulebook")
in what order)
Causal chain:
cloud.cfg β picks datasource β pulls metadata + user-data
β
βΌ
modules execute
in defined order
π 5. The Star of the Show: #cloud-config (Cheat Sheet)
#cloud-config β THIS MAGIC HEADER is mandatory
# βββββββββββββββββββββββββββββββββββββββββββββββββ
hostname: web-01 β Identity
fqdn: web-01.example.com
users: β Who can log in
- name: deploy
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys:
- ssh-ed25519 AAAA...
package_update: true β What software
package_upgrade: true
packages:
- nginx
- jq
write_files: β What config
- path: /etc/nginx/conf.d/app.conf
content: |
server { listen 80; ... }
runcmd: β What to execute
- systemctl enable --now nginx
- curl https://example.com/register
final_message: "Booted in $UPTIME s"
Visual pattern: Identity β Users β Packages β Files β Commands β Signal done
βοΈ 6. user-data Formats (Comparison)
βββββββββββββββββββββ¬ββββββββββββββββββββββ¬βββββββββββββββββββββββ
β Format β Header β When to use β
βββββββββββββββββββββΌββββββββββββββββββββββΌβββββββββββββββββββββββ€
β cloud-config β #cloud-config β Declarative, β
β (YAML) β β idempotent β
β
βββββββββββββββββββββΌββββββββββββββββββββββΌβββββββββββββββββββββββ€
β Shell script β #!/bin/bash β Quick & dirty, β
β β β imperative β
βββββββββββββββββββββΌββββββββββββββββββββββΌβββββββββββββββββββββββ€
β MIME multipart β Content-Type: ... β Combine both β
βββββββββββββββββββββΌββββββββββββββββββββββΌβββββββββββββββββββββββ€
β Jinja template β ## template:jinja β Render with β
β β #cloud-config β metadata vars β
βββββββββββββββββββββΌββββββββββββββββββββββΌβββββββββββββββββββββββ€
β Gzip / Base64 β (binary) β Bypass 16 KB limit β
βββββββββββββββββββββ΄ββββββββββββββββββββββ΄βββββββββββββββββββββββ
Rule of thumb:
Declarative (cloud-config) for config, imperative (script) for one-shot actions.
π 7. Idempotency & Re-Runs (Mental Model)
First boot: Subsequent boots:
βββββββββββββ ββββββββββββββββββ
β² β²
β all modules β only "always" modules
β execute β (e.g. NTP, network)
β β "once" modules SKIP
β /var/lib/cloud/ β via stamp files in
β instance/sem/ β /var/lib/cloud/sem/
β *.once βββββββββββββββββ
ββββββββββββββββββββ
debug rerun ββββΊ cloud-init clean βββββΊ next reboot
β (wipes stamps) β re-runs everything
ββββββββββββββββββββ
Mental hook: Stamp file present? Skip. Missing? Run.
π 8. Forensics β Where Things Live (Map)
/etc/cloud/
βββ cloud.cfg β master config (modules + order)
βββ cloud.cfg.d/ β drop-ins (vendor + user overrides)
βββ templates/ β hostname, hosts, etc.
/var/lib/cloud/
βββ instance/ β symlink β current instance
β βββ user-data.txt β raw user-data as received
β βββ cloud-config.txt β rendered cloud-config
β βββ scripts/ β runcmd & per-boot scripts
β βββ sem/ β "I already ran" stamps
βββ instances/<iid>/ β history per instance
/var/log/
βββ cloud-init.log β every module call (verbose)
βββ cloud-init-output.log β stdout/stderr of scripts
Debug recipe:
See also: Mastering the Linux Command Line β Your Complete Free Training Guide
cloud-init status --long # current state
cloud-init query --all # all known vars (metadata, ds, etc.)
sudo cloud-init schema --system # validate config
sudo tail -f /var/log/cloud-init-output.log
β±οΈ 9. Timeline: A Real Boot
0 s β Hypervisor starts VM
2 s β Kernel loaded
4 s β systemd reaches "cloud-init.target"
5 s β β‘ local stage β hostname set
8 s β DHCP gets IP
10 s β β’ network stage β fetch user-data from
β http://169.254.169.254/latest/user-data
15 s β β£ config stage β apt update, install nginx
45 s β β€ final stage β runcmd, write final_message
46 s β EC2 status check goes β
Terraform sees "instance running" and continues
π§ 10. Cause-and-Effect: Why Things Break
Symptom Root cause
βββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββ
Hostname stays "ip-10-β¦" ββββ user-data missing #cloud-config
header β treated as garbage
SSH keys not added ββββ IAM role can't read metadata,
or instance in private subnet
without VPCe to metadata
"runcmd" never executes ββββ YAML indentation error
(silent skip β check schema!)
Re-run does nothing ββββ Stamp files exist;
need: cloud-init clean --logs
Boot takes 5 min ββββ `package_upgrade: true` over
slow NAT β upgrade megabytes
𧬠11. Cloud-Init in Your Terraform Context
[Terraform admin-host module]
β
βΌ
creates aws_instance
β
β user_data = <<-EOF
β #cloud-config
β hostname: adminhost
β write_files: β¦
β runcmd: [bootstrap-ansible.sh]
β EOF
βΌ
EC2 boots from AMI:
β
βΌ
cloud-init takes over:
β reads metadata (IAM role, AZ, tags)
β‘ applies hostname, SSH keys
β’ runs bootstrap (joins consul, registers DNS)
β
βΌ
Instance is "an adminhost" β
Insight: That’s why baking a minimal AMI + delegating to cloud-init
is more flexible than baking everything into the image β same image,
different roles across environments.
π― 12. Mental Snapshot (One Picture to Remember Everything)
βββββββββββββββββββββββββββββββββββββββββββ
β CLOUD-INIT IN ONE FRAME β
βββββββββββββββββββββββββββββββββββββββββββ€
β β
β Generic Image + Per-VM Recipe β
β β β β
β ββββββββ¬ββββββββββ β
β βΌ β
β ββββββββββββββββββ β
β β cloud-init β β runs ONCE β
β β GβLβNβCβF β (stamps it) β
β βββββββββ¬βββββββββ β
β βΌ β
β Unique, ready VM β
β β
β Inputs: metadata + user-data β
β Where: /etc/cloud + /var/lib/cloud β
β Logs: /var/log/cloud-init*.log β
β Debug: `cloud-init status --long` β
β β
βββββββββββββββββββββββββββββββββββββββββββ
One-line mantra:
“Image is the body, cloud-init is the soul that arrives on first breath.”
