skip to content

What is EC2 user data, at what point in an instance's life does the script run, and as which OS user?

level: juniorimportance: must knowfreq 65%

answer

  1. a text blob, delivered through metadata
  2. cloud-init is what reads it
  3. the first line decides everything
  4. root, not ec2-user
  5. once per instance, not per boot

basics

~20 s

User data is a text blob you attach to an EC2 instance that cloud-init fetches from the instance metadata service at first boot. A script starting with a shebang runs as root, once per instance — not on every reboot.

solid answer

~50 s

User data is a small text payload attached to an EC2 instance — up to 16 KB before base64 encoding. On Linux, cloud-init retrieves it from the instance metadata service at `http://169.254.169.254/latest/user-data` during boot. If the payload starts with a shebang such as `#!/bin/bash`, cloud-init writes it to disk and executes it **as root**, in a non-interactive shell with the working directory at `/` — not as `ec2-user`, and not with a login shell's PATH or environment. By default it runs **once per instance**: cloud-init records the instance ID it already handled, so a reboot, or a stop and start, will not run it again. Output lands in `/var/log/cloud-init-output.log`, which is where you look when the instance comes up but the app is missing. If the payload starts with `#cloud-config` instead, cloud-init interprets it as declarative YAML rather than executing it.

code

bash · 5 lines
bash
#!/bin/bash
set -euxo pipefail
dnf install -y nginx
echo "ok" > /usr/share/nginx/html/health
systemctl enable --now nginx

go deeper

for a junior

Be ready to say what user data is, that the payload needs a shebang, that it executes as root, and that it runs once when the instance is first created rather than on every reboot.

for a middle

Explain the mechanics: cloud-init pulls it from the metadata service, dispatches on the first line, records per-instance state under /var/lib/cloud, and logs output to /var/log/cloud-init-output.log.

for a senior

Show operational judgment — bootstrap failures do not fail the instance, so pair user data with a health signal; keep the script small and idempotent; keep secrets out of it and fetch them at runtime instead.

for a principal

Own the boundary decision: how much belongs in a first-boot script at all versus baked into the image or managed by an agent, and what standard the organisation holds so a failed bootstrap can never quietly serve traffic.

## What user data is When you launch an EC2 instance you can attach a block of text called **user data**. AWS does not interpret it; it merely makes it available to the instance through the **instance metadata service (IMDS)**, an HTTP endpoint reachable from inside the instance at the link-local address `169.254.169.254`, at path `/latest/user-data`. The limit is 16 KB of raw text before base64 encoding (as of 2025), which is why user data is a bootstrap hook, not a delivery mechanism for application artifacts. On most Linux AMIs, including Amazon Linux, Ubuntu and RHEL, the program that reads it is **cloud-init** — a distribution-level boot agent, not an AWS product. AWS supplies the data; cloud-init decides what to do with it. ## How cloud-init decides what the payload is cloud-init dispatches on the first line: - `#!/bin/bash` (or any shebang) → treated as a **script**. cloud-init writes it under `/var/lib/cloud/instance/scripts/` and executes it. - `#cloud-config` → treated as **declarative YAML**: it can create users, write files, install packages, add repositories and so on, without you scripting the steps. - A MIME multipart document → several parts of mixed types in one payload, which is how you combine a `#cloud-config` section with a shell script. A payload with **no** recognised first line is silently ignored. Forgetting the shebang is the single most common reason a user data script "does nothing". ```bash #!/bin/bash dnf install -y nginx systemctl enable --now nginx ``` ## Who runs it, and in what environment The script runs as **root**. There is no `sudo` needed and, conversely, no `ec2-user` home directory, no `~/.bashrc`, no interactive login PATH, and the working directory is `/`. Scripts written and tested in an SSH session frequently break here because they assumed a login shell's environment or a relative path. It also runs **late in boot but before the instance is useful** — after networking is up, which is why package installs work, but potentially before other things you might be counting on. Nothing waits for your script: the instance reaches the `running` state and passes status checks regardless of whether your bootstrap succeeded or failed. ## Once per instance, not once per boot By default the user-scripts stage has **per-instance** frequency. cloud-init keeps state under `/var/lib/cloud/instances/<instance-id>/` and remembers that this instance has already been handled. Consequences: - **Reboot**: does not re-run. - **Stop and start**: same instance ID, so it does **not** re-run either — this surprises people, because the instance did shut down completely. - **New instance from the same AMI and same user data**: runs, because the instance ID is new. This makes user data a *bootstrap* mechanism, not a configuration loop. If you genuinely need something on every boot, either make it a systemd unit installed by the bootstrap, or opt into per-boot frequency in `#cloud-config`: ```yaml #cloud-config cloud_final_modules: - [scripts-user, always] ``` ## Changing user data later On an EBS-backed instance you can change user data with `ModifyInstanceAttribute`, but the instance must be **stopped** first. And because of the once-per-instance rule, changing it and starting the instance again still will not execute the new script — a fact that produces a lot of confused debugging. ## Where to look when it fails - `/var/log/cloud-init-output.log` — the combined stdout and stderr of your script. This is the first place to look. - `/var/log/cloud-init.log` — cloud-init's own module-by-module log, useful when your script never ran at all. ## User data is not a secret Anything that can reach IMDS from inside the instance can read the user data back, and it is visible through the EC2 API to anyone with `ec2:DescribeInstanceAttribute`. Never put passwords, API keys or private keys in it. The correct pattern is to put a **reference** in user data and have the bootstrap fetch the actual secret at runtime using the instance's own IAM credentials.

  • You edit an instance's user data, start it again, and nothing happens. Why?
    The user-scripts stage runs at per-instance frequency, and stopping and starting an instance keeps the same instance ID, so cloud-init sees the work as already done. Either launch a fresh instance, opt into per-boot execution with `cloud_final_modules: [[scripts-user, always]]`, or clear cloud-init's recorded state before rebooting. Editing the text alone changes nothing.
  • Is it acceptable to put a database password in user data so the app can start?
    No. User data is readable by any process on the instance that can reach the metadata service, and by any principal holding `ec2:DescribeInstanceAttribute` on it, and it is often captured in launch configuration and logs. Put an identifier in user data instead — a parameter name or secret ARN — and let the bootstrap fetch the value at runtime with the instance's own IAM credentials.
  • What is the difference between a user data payload starting with #!/bin/bash and one starting with #cloud-config?
    The shebang tells cloud-init to execute the payload as a shell script as root. `#cloud-config` tells it to parse the payload as declarative YAML and hand it to cloud-init modules that create users, write files, install packages or add repositories. A payload with neither marker is recognised by nothing and is quietly discarded.

saying these in an interview costs you the question

  • Saying user data runs on every boot or every reboot
  • Claiming the script runs as ec2-user and needs sudo
  • Omitting the shebang and expecting the script to run anyway
  • Putting credentials in user data because "only the instance can read it"
  • Assuming the instance stays out of service until the script finishes

context