Bootstrapping with User Data
When you launch a new EC2 (Elastic Compute Cloud — Amazon’s virtual server service) instance, it starts as a blank operating system with nothing installed. “User data” is a block of text — usually a shell script — that you hand to the instance at launch, and the instance runs it automatically the very first time it boots. This is how you go from a fresh server to a ready-to-use web server, database, or app node without ever logging in by hand. It is the simplest form of provisioning (automatically setting up a server), and it scales: the same script bootstraps one instance or a thousand.
What user data actually is
User data is just a string you attach to an instance. EC2 stores it and exposes it to the instance through the Instance Metadata Service (IMDS — a special internal address 169.254.169.254 that an instance can query about itself). On boot, a program called cloud-init (the standard Linux bootstrapping tool, pre-installed on Amazon Linux, Ubuntu, and most modern AMIs) reads that user data and executes it.
If the user data starts with #! (a “shebang” line, like #!/bin/bash), cloud-init treats it as a script and runs it as the root user. There is no need to add sudo to commands — you are already root. You can also pass cloud-init’s own YAML format (a file starting with #cloud-config) for declarative setup like creating users or writing files.
When to use this (and when not to)
| Approach | Best for | Avoid when |
|---|---|---|
| User data script | One-time setup at first boot: install packages, pull code, start a service | You need repeated config changes after launch |
| Custom AMI (pre-baked image) | Fast, identical boots where software is already installed | Software changes often (rebuilding AMIs is slow) |
| Config management (Ansible, SSM) | Ongoing configuration and drift correction over an instance’s life | You only need a quick first-boot setup |
Use user data for lightweight, run-once bootstrapping. For heavy software you reboot often, bake a custom AMI instead so boots are fast. For long-lived config, combine a small user data script with a tool like AWS Systems Manager.
Passing user data in the Console
- Open the EC2 console and choose Launch instances.
- Pick your Amazon Machine Image (AMI — the OS template), instance type, key pair, and network settings as usual.
- Expand the Advanced details panel at the bottom of the launch form.
- Scroll to the User data text box.
- Paste your script. Do not base64-encode it — the Console encodes it for you automatically.
- Click Launch instance.
A typical script to install and start the Nginx web server:
#!/bin/bash
dnf update -y
dnf install -y nginx
systemctl enable nginx
systemctl start nginx
echo "<h1>Hello from $(hostname -f)</h1>" > /usr/share/nginx/html/index.html
Passing user data with the CLI
With AWS CLI v2, pass the script with --user-data and the file:// prefix. The CLI reads the file and base64-encodes it for you, so point it at a plain-text script.
aws ec2 run-instances \
--image-id ami-0abcdef1234567890 \
--instance-type t3.micro \
--key-name my-key \
--security-group-ids sg-0a1b2c3d \
--subnet-id subnet-0a1b2c3d \
--user-data file://bootstrap.sh \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=web-01}]'
Output:
{
"Instances": [
{
"InstanceId": "i-0a1b2c3d4e5f",
"ImageId": "ami-0abcdef1234567890",
"InstanceType": "t3.micro",
"State": { "Name": "pending" },
"SubnetId": "subnet-0a1b2c3d",
"PrivateIpAddress": "10.0.1.42"
}
]
}
You can view the user data attached to an existing instance at any time:
aws ec2 describe-instance-attribute \
--instance-id i-0a1b2c3d4e5f \
--attribute userData \
--query "UserData.Value" --output text | base64 -d
This decodes and prints the original script — which is exactly why secrets do not belong here (more on that below).
It runs once — and quietly
Two behaviors trip people up constantly.
It runs only on the first boot. By default cloud-init runs the script one time, on the instance’s very first start. If you stop and start the instance, or reboot it, the script does not run again. This is usually what you want, but if you edit user data on a stopped instance expecting it to re-run, it will not. (Advanced: you can force re-runs with a #cloud-config directive cloud_final_modules or a MIME boothook, but that is rarely needed.)
Failures are silent. If a command in your script fails, the instance still boots and reaches the running state — EC2 does not surface the error anywhere in the Console. Your app simply will not be there. The output and errors live in a log file on the instance:
sudo cat /var/log/cloud-init-output.log
Always check this file first when “the user data didn’t work.” A good habit is to add set -euxo pipefail near the top of your script so it stops on the first error and logs every command.
Gotcha: A blank Console “User data” box plus a missing shebang line means cloud-init treats your text as plain data and runs nothing. The first line must be
#!/bin/bash(or#cloud-config). No silent fallback — it just does nothing.
Never put secrets in user data
User data is not secret storage. Anyone or any process on the instance can read it freely from the metadata service:
curl http://169.254.169.254/latest/user-data
Because user data is readable from IMDS (and via describe-instance-attribute to anyone with EC2 permissions), passwords, API keys, and database credentials in user data are effectively exposed. Instead, store secrets in AWS Secrets Manager or SSM Parameter Store and have the script fetch them at boot using the instance’s IAM role:
#!/bin/bash
DB_PASS=$(aws secretsmanager get-secret-value \
--secret-id prod/db/password \
--query SecretString --output text)
Security tip: Enable IMDSv2 (the token-based, session-oriented version of the metadata service) on your instances so the metadata endpoint — including user data — cannot be reached by simple server-side request forgery (SSRF) attacks. New launches default to IMDSv2.
There is no extra charge for user data itself; you pay only normal instance and storage rates while the instance runs.
Best practices
- Start every script with
#!/bin/bashandset -euxo pipefailso failures stop the boot and are logged. - Check
/var/log/cloud-init-output.logfirst whenever bootstrapping appears to fail. - Keep user data small and fast — bake heavy or slow-to-install software into a custom AMI instead.
- Never embed secrets; fetch them at boot from Secrets Manager or SSM Parameter Store via an IAM role.
- Make scripts idempotent (safe to run more than once) in case you later force re-runs or reuse them in launch templates.
- Reuse the same script across many instances by storing it in a launch template for consistent, repeatable boots.
- Tag instances on launch so you can tell which bootstrap version produced which server.