Injecting Personality
Beyond basic network setup, you want the server to actually do something: install Nginx, create a deploy user, write a config file, start a service. You provide these instructions via User Data — a blob of text you attach to the instance at launch time. Cloud-init reads this blob and acts on it.
You have two primary formats:
1. Shell Script (#!/bin/bash)
The simplest option. Cloud-init detects the shebang line and executes the file as a script under root — a handful of package-manager and systemctl calls to install and start a service, for instance.
- Pros: Familiar to anyone who knows Bash. Easy to test locally. Full control over error handling with
set -e,set -o pipefail, etc. - Cons: Imperative — you describe how to do things, not what the end state should be. Complex scripts become hard to maintain. No built-in idempotency.
2. Cloud-Config (#cloud-config)
A YAML document that describes the desired state of the system rather than the steps to reach it — a packages key listing nginx, say, instead of a package-manager install command. Cloud-init has built-in modules that interpret each key and handle the implementation details.
- Pros: Declarative and readable. Built-in modules handle users, files, packages, and more. Easier to review in pull requests.
- Cons: Strict YAML syntax — one indentation error silently breaks the entire boot. Less flexible than a full scripting language for complex logic.
The Header Is Not Optional:
Cloud-init determines the format entirely from the first line of the User Data blob:
| First Line | Interpretation |
|---|---|
#!/bin/bash (or any shebang) |
Execute as a script |
#cloud-config |
Parse as YAML cloud-config |
#include |
Treat as a list of URLs to fetch and process |
#cloud-boothook |
Run very early, before cloud-init's own stages |
| Anything else | Usually ignored or treated as opaque text |
If you omit the header or misspell #cloud-config (e.g., # cloud-config with a space), cloud-init will silently do nothing with your User Data. This is one of the most common causes of "my script didn't run" confusion.
Combining Both — Multi-Part MIME:
You can actually send both a shell script and a cloud-config in the same User Data blob using MIME multipart encoding. This is useful when you want cloud-config to handle users and packages, and a shell script to handle application-specific logic. Tools like write-mime-multipart (from the cloud-utils package) can assemble these for you.
Interviewers sometimes ask: "What's the difference between runcmd in cloud-config and just using a shell script?" The key distinction is when they run and how errors are handled. Both execute in the Final stage, but runcmd commands run as a list within cloud-init's managed environment — cloud-init logs each command's output and exit code. A shell script runs as a child process; if it exits non-zero, cloud-init logs the failure but the instance still boots. Neither approach retries on failure by default. For production, prefer a configuration management tool (Ansible, Chef) invoked from User Data rather than complex inline scripts.