Running Scripts on EC2 Startup: Automating Nginx Installation with User Data
When you need every new EC2 instance to arrive pre-configured — Nginx installed, services running, directories created — manually SSHing in after launch is a workflow that doesn't scale. EC2 User Data solves this by executing a shell script exactly once, the first time the instance boots, before it ever accepts traffic.
TL;DR: EC2 User Data at a Glance
| Concern | Answer |
|---|---|
| When does it run? | Once, on first boot (by default) |
| What user executes it? | root |
| Where do I paste the script? | EC2 Launch Wizard → Advanced Details → User Data |
| Size limit | 16 KB (raw, unencoded data) |
| Logs location | /var/log/cloud-init-output.log |
| Larger scripts | Host on S3, fetch via aws s3 cp or curl |
How EC2 User Data Works Internally
User Data is served to the instance through the EC2 Instance Metadata Service (IMDS) at http://169.254.169.254/latest/user-data. The cloud-init daemon — present on all AWS-provided AMIs — reads this endpoint during the boot sequence and executes the payload. If the script begins with #!/bin/bash, cloud-init treats it as a shell script and runs it as root.
Think of cloud-init as a hotel concierge that reads your pre-submitted checklist the moment you check in — it acts before you even reach the room.
The execution happens during the cloud-init boot stage, which runs after networking is available but before the instance is marked healthy by any load balancer. This ordering matters: your script can safely call package repositories and AWS APIs without racing against network initialization.
http://169.254.169.254/latest/user-data"] D --> E["Script Executes as root"] E --> F["stdout/stderr → /var/log/cloud-init-output.log"] F --> G["Instance enters running state"] G --> H["Health Checks Pass"]
- Instance Launch: EC2 provisions hardware and starts the OS boot sequence.
- Networking Up: The instance obtains its IP and can reach the internet or VPC endpoints.
- cloud-init reads IMDS: Fetches the User Data payload from the metadata endpoint.
- Script Executes as root: Your shell script runs; all stdout/stderr goes to
/var/log/cloud-init-output.log. - Instance Ready: The instance enters the
runningstate and passes health checks.
Running Scripts on EC2 Startup: Installing Nginx via User Data
The script you provide depends on the AMI's package manager. Below are verified scripts for the two most common Amazon Linux variants and Ubuntu. Each script installs Nginx, enables it to start on future reboots, and starts it immediately.
Amazon Linux 2023
Amazon Linux 2023 uses dnf as its package manager. The amazon-linux-extras command is not available on AL2023 — do not use it here.
#!/bin/bash
yum update -y
dnf install -y nginx
systemctl enable nginx
systemctl start nginx
Amazon Linux 2
Amazon Linux 2 ships with amazon-linux-extras, which provides a curated Nginx package. This command is specific to Amazon Linux 2 and does not exist on AL2023.
#!/bin/bash
yum update -y
amazon-linux-extras install -y nginx1
systemctl enable nginx
systemctl start nginx
Ubuntu 22.04 / 24.04
#!/bin/bash
apt-get update -y
apt-get install -y nginx
systemctl enable nginx
systemctl start nginx
Where to Paste the Script: Console Walkthrough
If you're launching through the AWS Management Console, the User Data field is not on the main launch page — it's nested inside Advanced Details.
- Open EC2 → Instances → Launch Instances.
- Configure AMI, instance type, key pair, and network settings as normal.
- Scroll to the bottom of the launch wizard and expand Advanced Details.
- Locate the User Data text area. Select "As text" (not base64).
- Paste your script, including the
#!/bin/bashshebang on line 1. - Complete the launch. The script runs automatically on first boot.
Launching with User Data via AWS CLI
For repeatable, automated launches, the CLI is more reliable than the console. Store your script in a local file and reference it with the file:// protocol.
🔽 Click to expand: CLI launch command
aws ec2 run-instances \
--image-id ami-0abcdef1234567890 \
--instance-type t3.micro \
--key-name my-key-pair \
--security-group-ids sg-0123456789abcdef0 \
--subnet-id subnet-0123456789abcdef0 \
--user-data file://nginx-install.sh \
--region us-east-1
Replace ami-0abcdef1234567890, my-key-pair, sg-0123456789abcdef0, and subnet-0123456789abcdef0 with your actual resource IDs. The file:// prefix tells the CLI to read the script from disk and transmit it as the User Data payload.
Updating User Data on an Existing Instance
User Data can be changed after launch, but only while the instance is stopped. The instance must be stopped — not just rebooted — before the attribute can be modified.
# Step 1: Stop the instance
aws ec2 stop-instances \
--instance-ids i-0123456789abcdef0 \
--region us-east-1
# Step 2: Wait until stopped
aws ec2 wait instance-stopped \
--instance-ids i-0123456789abcdef0 \
--region us-east-1
# Step 3: Update User Data
aws ec2 modify-instance-attribute \
--instance-id i-0123456789abcdef0 \
--user-data Value=fileb://updated-script.sh \
--region us-east-1
# Step 4: Start the instance
aws ec2 start-instances \
--instance-ids i-0123456789abcdef0 \
--region us-east-1
Note the Value=fileb:// syntax in Step 3 — this is the correct structure for modify-instance-attribute. Using file:// instead of fileb:// here will cause a parameter validation error because the attribute expects binary-safe encoding.
By default, cloud-init does not re-execute User Data on subsequent boots. If you need the updated script to run after the restart, you must configure cloud-init's scripts-user module to run on every boot — which is a separate cloud-init configuration concern beyond the scope of User Data alone.
The 16 KB Size Limit and What to Do When You Hit It
EC2 enforces a 16 KB limit on the raw, unencoded User Data payload. This limit applies to the actual script content before any encoding. When you submit User Data through the console as text, the console base64-encodes it internally before transmission — and base64 encoding increases the payload size by approximately 33%. This means a script approaching 16 KB of raw content will exceed the limit after encoding.
Scripts larger than 16 KB must be hosted externally. The standard pattern is to store the full script in S3 and use a minimal bootstrap script as User Data:
#!/bin/bash
aws s3 cp s3://my-bootstrap-bucket/full-setup.sh /tmp/full-setup.sh
chmod +x /tmp/full-setup.sh
/tmp/full-setup.sh
The instance profile attached to the EC2 instance must grant s3:GetObject on the target bucket. Without this permission, the aws s3 cp call will fail silently from a User Data perspective — the instance will boot, but the setup script will never run.
Diagnosing Failures: Reading the cloud-init Log
Here's a failure pattern that catches engineers off guard: the instance reaches running state, the health check passes, but Nginx isn't running. The instinct is to check the Security Group — port 80 blocked, right? In practice, the Security Group is fine. The script failed silently because amazon-linux-extras was called on an AL2023 AMI where the command doesn't exist.
The authoritative log for User Data execution is /var/log/cloud-init-output.log. Every line your script prints to stdout or stderr ends up here.
# SSH into the instance and inspect the log
sudo cat /var/log/cloud-init-output.log
# Or tail it in real time during boot (from Systems Manager Session Manager)
sudo tail -f /var/log/cloud-init-output.log
If you don't have SSH access, AWS Systems Manager Session Manager provides shell access without opening port 22 — provided the instance has the SSM Agent installed (all current AWS-provided AMIs include it) and an instance profile with the AmazonSSMManagedInstanceCore managed policy attached.
on stopped instance → relaunch"] C -- No --> E{"S3 fetch in script?"} E -- Yes --> F["Verify instance profile
has s3:GetObject"] E -- No --> G{"systemctl start nginx
in script?"} G -- No --> H["Add systemctl enable
and start to script"] G -- Yes --> I["Check Security Group
inbound port 80/443"]
- Check cloud-init log: Confirm the script was received and started executing.
- Script error? Fix the script, update User Data on the stopped instance, relaunch.
- S3 fetch failed? Verify the instance profile has
s3:GetObjectand the bucket name is correct. - Service not running? Confirm
systemctl enableandsystemctl startare both present in the script. - Port unreachable? Only after confirming the service is running should you check the Security Group inbound rules.
IAM: Minimum Permissions for S3-Hosted Bootstrap Scripts
If your User Data script fetches from S3, the EC2 instance needs an instance profile with at least this policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bootstrap-bucket/full-setup.sh"
}
]
}
Scope the Resource to the specific object or a prefix — not arn:aws:s3:::*. If the bucket uses SSE-KMS encryption, add kms:Decrypt on the KMS key ARN as a second statement.
Wrap-Up: Running Scripts on EC2 Startup — Next Steps
User Data is the right tool for first-boot configuration of individual instances. For fleet-scale configuration management, consider AWS Systems Manager State Manager or a configuration management tool like Ansible, which can enforce desired state continuously rather than just at launch. For immutable infrastructure patterns, baking Nginx directly into a custom AMI with EC2 Image Builder eliminates the runtime dependency on package repositories entirely.
- AWS Docs: Run commands on your Linux instance at launch
- AWS Systems Manager State Manager
- EC2 Image Builder
Glossary
| Term | Definition |
|---|---|
| User Data | A script or configuration payload passed to an EC2 instance at launch, executed by cloud-init on first boot. |
| cloud-init | An open-source initialization tool included in AWS-provided AMIs that processes User Data and performs early boot configuration. |
| IMDS | Instance Metadata Service — an HTTP endpoint at 169.254.169.254 that provides instance-specific data including User Data. |
| Instance Profile | An IAM container that attaches a role to an EC2 instance, granting it permissions to call AWS APIs. |
| amazon-linux-extras | A package management tool specific to Amazon Linux 2 for installing curated software topics. Not available on Amazon Linux 2023. |
Comments
Post a Comment