AWS for DevOps — User Data Scripts
3.4 — User Data Scripts
User data is a script that runs automatically when an EC2 instance first boots. It is the simplest way to automate instance setup — install packages, configure services, and deploy applications without manual SSH.
How User Data Works
Instance Launch
│
▼
OS boots
│
▼
cloud-init runs user data script (as root)
│
▼
Instance ready with software installed
- Runs once at first boot (by default)
- Runs as root — no
sudoneeded - Output is logged to
/var/log/cloud-init-output.log - Maximum size: 16 KB (use S3 for larger scripts)
Basic User Data Example
Install Nginx and start it:
#!/bin/bash
yum update -y
yum install -y nginx
systemctl enable nginx
systemctl start nginx
echo "<h1>Hello from $(hostname)</h1>" > /usr/share/nginx/html/index.html
Launching with User Data (CLI)
# Method 1 — From a file
$ aws ec2 run-instances \
--image-id ami-0abcdef1234567890 \
--instance-type t3.micro \
--key-name my-key-pair \
--security-group-ids sg-0abc123def456 \
--user-data file://setup.sh
# Method 2 — Inline (base64 encoded automatically by CLI)
$ aws ec2 run-instances \
--image-id ami-0abcdef1234567890 \
--instance-type t3.micro \
--user-data '#!/bin/bash
yum update -y
yum install -y nginx
systemctl start nginx'
Real-World DevOps User Data Script
A production-ready script that installs Docker, pulls an app image, and starts it:
#!/bin/bash
set -euxo pipefail
# Variables
REGION="ap-south-1"
ECR_REPO="123456789012.dkr.ecr.${REGION}.amazonaws.com/my-app"
IMAGE_TAG="latest"
# Install Docker
yum update -y
yum install -y docker
systemctl enable docker
systemctl start docker
# Install CloudWatch Agent
yum install -y amazon-cloudwatch-agent
/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
-a fetch-config -m ec2 \
-s -c ssm:AmazonCloudWatch-linux
# Login to ECR and pull the image
aws ecr get-login-password --region ${REGION} | \
docker login --username AWS --password-stdin ${ECR_REPO}
docker pull ${ECR_REPO}:${IMAGE_TAG}
# Run the container
docker run -d \
--name my-app \
--restart unless-stopped \
-p 80:8080 \
${ECR_REPO}:${IMAGE_TAG}
Note: The instance needs an IAM role with ECR pull permissions for this to work.
Checking User Data Execution
# SSH into the instance
$ ssh -i key.pem ec2-user@<ip>
# Check the user data log
$ sudo cat /var/log/cloud-init-output.log
# Check cloud-init status
$ cloud-init status
status: done
# If it failed:
$ cloud-init status --long
status: error
detail: ...
Retrieving User Data from an Instance
# From outside (via CLI)
$ aws ec2 describe-instance-attribute \
--instance-id i-0abc123def456 \
--attribute userData \
--query 'UserData.Value' \
--output text | base64 --decode
# From inside the instance (metadata)
$ TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 300")
$ curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/user-data
User Data vs AMI vs Configuration Management
| Approach | When to Use | Pros | Cons |
|---|---|---|---|
| User Data | Simple setup, environment-specific config | No custom AMI needed | Slow boot (installs at launch) |
| Custom AMI | Software is pre-installed | Fast boot | Must rebuild AMI for updates |
| AMI + User Data | AMI has base software, user data does config | Best of both worlds | Two things to maintain |
| Config Management (Ansible, Chef) | Complex multi-step setup | Idempotent, testable | Extra tooling to manage |
Best Practice: AMI + User Data
AMI (golden image): User Data (per-instance):
┌───────────────────┐ ┌──────────────────────┐
│ OS + patches │ │ Pull latest app │
│ Docker engine │ │ Set environment vars │
│ CloudWatch agent │ + │ Register with ALB │
│ Monitoring tools │ │ Start the app │
└───────────────────┘ └──────────────────────┘
The AMI handles things that rarely change (base OS, Docker, agents). User data handles things that change per deployment (app version, environment configuration).
Common Mistakes
| Mistake | Why It Hurts | Fix |
|---|---|---|
Not using set -e in the script |
Script continues after errors | Always start with set -euxo pipefail |
| Installing heavy packages in user data | Slow instance boot (5-10 minutes) | Bake them into the AMI |
| Hard-coding secrets in user data | User data is visible via metadata | Use SSM Parameter Store or Secrets Manager |
| Forgetting to check logs | You don’t know why it failed | Always check /var/log/cloud-init-output.log |
| Exceeding 16 KB limit | Script silently truncated | Download large scripts from S3 |
Quick Reference
| Task | Command |
|---|---|
| Launch with user data | aws ec2 run-instances ... --user-data file://setup.sh |
| View user data of instance | aws ec2 describe-instance-attribute --instance-id ID --attribute userData |
| Check cloud-init logs (on instance) | sudo cat /var/log/cloud-init-output.log |
| Check cloud-init status (on instance) | cloud-init status --long |
Next → Auto Scaling