AWS EC2 User Data Explained: Scripts, Cloud-Init, Examples

EC2 user data is a script or configuration block you attach to an instance at launch. The instance reads it on its first boot and configures itself automatically: installing packages, writing config files, starting services. It is the standard way to bootstrap a new EC2 instance without logging in manually.

Simple explanation

Think of user data as a first-day instructions sheet you leave on the desk before a new employee arrives. The employee (your EC2 instance) reads it on day one and follows the steps. After that, the sheet is done. It does not run again on its own.

In practice: you write a shell script or a cloud-config YAML file, attach it to the instance at launch, and AWS runs it automatically as root before the instance starts serving traffic. No manual login. No SSH commands after the fact. No waiting for a configuration tool to connect.

Quick mental model

User data is your instance’s first-day instructions. Cloud-init is the assistant that reads and executes those instructions. The log file at /var/log/cloud-init-output.log is the paper trail showing exactly what happened.

Why EC2 user data matters

When you launch an EC2 instance from a base Amazon Machine Image (AMI), you get a clean operating system with nothing extra installed. Without user data, you would need to SSH in after launch and set everything up by hand. That is slow, error-prone, and impossible to automate at scale.

User data lets you define the setup once and run it automatically every time a new instance launches. This matters especially for Auto Scaling Groups: when AWS adds a new instance to handle increased load, the user data script runs and prepares it to serve traffic with no human involvement.

  • Repeatability: the same script runs every time, producing a consistent instance.
  • Speed: no manual SSH steps after launch.
  • Version control: the bootstrap logic lives in your repo alongside your infrastructure code.

How EC2 user data works

When an EC2 instance boots for the first time, cloud-init (a standard Linux bootstrapping tool included in most AWS-provided AMIs) contacts the instance metadata service (IMDS) at http://169.254.169.254/ and retrieves the user data you attached at launch.

Cloud-init inspects the first line of the payload and decides how to process it:

  • A line starting with #!/bin/bash (or any shebang) causes cloud-init to run it as a shell script, as root.
  • A line starting with #cloud-config causes cloud-init to treat it as declarative YAML configuration.
  • Multiple content types can be combined using MIME multi-part format (covered below).

Boot sequence at a glance

  1. You launch an instance and attach user data.
  2. AWS stores the user data and makes it available via IMDS.
  3. cloud-init runs during the first boot and detects the content type.
  4. Your script or cloud-config executes as root.
  5. The instance finishes bootstrapping and becomes available.
  6. All output is written to /var/log/cloud-init-output.log.

Key behaviors and limits

BehaviorDetail
When it runsFirst boot only, by default
Who runs itcloud-init, as root
Size limit16 KB of raw user data, before base64 encoding
Output log/var/log/cloud-init-output.log and /var/log/cloud-init.log
User data and AMIsUser data is attached to the instance. It is not baked into any AMI you create from that instance
Updating user dataEditing user data on a stopped instance makes the new content visible, but cloud-init will not re-run it automatically on Linux
Size limit detail

The 16 KB limit applies to the raw, unencoded user data. If your bootstrap logic exceeds this, store the main script in S3 and use a short user data script to download and run it: aws s3 cp s3://my-bucket/setup.sh /tmp/ && bash /tmp/setup.sh. The instance’s IAM role needs s3:GetObject permission on the bucket.

When to use EC2 user data

User data is a good fit for:

  • Installing packages: install Nginx, Python, or any other dependency on a fresh AMI.
  • Writing config files: write an app config or an Nginx virtual host before the service starts.
  • Starting services: enable and start a systemd service so it is running on first boot.
  • Bootstrapping Auto Scaling Group instances: register with a load balancer target group, or pull the latest app artifact from S3.
  • Simple one-time application setup: clone a repo and run a setup script on a development or staging instance.

When not to use EC2 user data

User data is a simple tool. When the bootstrap gets complex, other approaches work better:

  • Use a golden AMI when you want fast launches, predictable state, and no dependency on external package repositories at boot time. Pre-bake everything into the AMI and use user data only for last-mile configuration, like injecting an environment name or running a registration command.

  • Use a systemd service for any logic that needs to run on every boot, not just the first. User data is not the right tool for recurring startup behavior.

  • Use a CI/CD image-build pipeline (such as Terraform with Packer or CodeBuild) when your bootstrap is complex, needs automated testing, or must be reproducible across multiple regions.

If your user data script is over 60 lines and doing complex work, that is usually a sign you should pre-bake the heavy steps into a golden AMI instead.

User data vs cloud-init vs golden AMI vs systemd

OptionBest forRuns whenLaunch speedComplexity
User data (shell script)Simple one-time bootstrapFirst boot onlySlower (runs at boot)Low
User data (cloud-config)Declarative package and file setupFirst boot onlySlower (runs at boot)Low to medium
Golden AMIProduction fleets, fast scalingNever (pre-baked)FastHigher (needs build pipeline)
systemd serviceLogic that must run on every bootEvery bootNo extra delayMedium

User data vs golden AMI

A useful analogy: user data is like baking bread from scratch every time a new server launches. A golden AMI is like keeping a pre-baked loaf in the freezer and just defrosting it. The bread-from-scratch approach is flexible and easy to change. But it takes time, and if the oven (the package repository or the network) is unavailable, you get nothing.

User data is flexible: change the script and the next launch picks it up immediately. But it runs at boot, depends on external services, and is harder to test systematically.

A golden AMI snapshot has everything pre-installed. Launches are fast and predictable. The tradeoff is a pipeline to build and validate new AMI versions whenever your software changes.

Where to start

For early-stage projects, user data is fine. For production Auto Scaling at scale, build golden AMIs and keep user data minimal, handling only what genuinely changes between launches.

How to add user data

In the AWS Console

  1. Open the EC2 console and choose Launch Instance.
  2. Scroll to Advanced details at the bottom of the form.
  3. Paste your script into the User data text area.
  4. The console base64-encodes the data automatically before launch.

To update user data on an existing instance: stop the instance, go to Actions → Instance Settings → Edit User Data, make your changes, then start the instance. The updated data is stored and attached, but on Linux, cloud-init will not re-run it automatically.

With the AWS CLI

Pass a local script file using the —user-data flag:

aws ec2 run-instances \
  --image-id ami-0abcdef1234567890 \
  --instance-type t3.micro \
  --key-name my-key \
  --security-group-ids sg-0abc123 \
  --subnet-id subnet-0abc123 \
  --user-data file://startup-script.sh

The file:// prefix reads the script from disk. The AWS CLI handles base64 encoding for you.

In a launch template

Launch templates store user data alongside the rest of your instance configuration, making it easy to reuse across Auto Scaling Groups and keep it versioned. When creating a launch template via the CLI, base64-encode the script first.

# Linux (GNU coreutils) — -w 0 disables line wrapping
USERDATA=$(base64 -w 0 startup-script.sh)

# macOS — omit -w 0, it is not supported by BSD base64
# USERDATA=$(base64 startup-script.sh)

aws ec2 create-launch-template \
  --launch-template-name web-template \
  --launch-template-data "{
    \"ImageId\": \"ami-0abcdef1234567890\",
    \"InstanceType\": \"t3.micro\",
    \"UserData\": \"$USERDATA\"
  }"

In the AWS Console, you can paste the script directly into the launch template editor and encoding is handled for you.

Example: working bash user data script

This script installs Nginx, fetches the instance ID from the metadata service using IMDSv2, writes a simple landing page, and starts Nginx:

#!/bin/bash
set -e

# Update package cache and install Nginx
dnf update -y
dnf install -y nginx

# Fetch the instance ID using IMDSv2 (required on newer instances)
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/instance-id)

# Write a simple landing page.
# The heredoc uses <<EOF (unquoted) so $INSTANCE_ID expands in the body.
cat > /usr/share/nginx/html/index.html <<EOF
<!DOCTYPE html>
<html>
<head><title>Hello from EC2</title></head>
<body>
  <h1>Instance is running</h1>
  <p>Instance ID: ${INSTANCE_ID}</p>
</body>
</html>
EOF

# Enable and start Nginx in one step
systemctl enable --now nginx

echo "Bootstrap complete on ${INSTANCE_ID}"

A few things worth noting:

  • set -e stops the script immediately if any command fails, so later steps never run against a broken environment.
  • IMDSv2 requires a session token. The two-step curl pattern above is the current AWS-recommended approach. Plain IMDSv1 calls may be blocked on instances where IMDSv2-only mode is enforced.
  • The heredoc uses <<EOF (unquoted) so ${INSTANCE_ID} expands. A quoted <<‘EOF’ would suppress all variable expansion and print the literal string ${INSTANCE_ID} in the HTML.
  • systemctl enable —now nginx both enables the service for future reboots and starts it immediately.

This example uses dnf, the package manager on Amazon Linux 2023 and Fedora-based AMIs. On Ubuntu or Debian, replace dnf install with apt-get install -y and run apt-get update -y first.

Example: cloud-config version

The same setup expressed as cloud-config YAML:

#cloud-config
packages:
  - nginx

write_files:
  - path: /usr/share/nginx/html/index.html
    content: |
      <!DOCTYPE html>
      <html>
      <head><title>Hello from EC2</title></head>
      <body><h1>Instance is running</h1></body>
      </html>
    owner: root:root
    permissions: '0644'

runcmd:
  - systemctl enable --now nginx

Cloud-config is cleaner than bash for tasks that map naturally to its built-in directives: installing packages (with automatic retry on failure), writing files atomically, creating users, or setting the hostname. For anything that needs conditionals, loops, or complex error handling, a shell script is usually easier to reason about.

Example: combining cloud-config and a shell script

If you need both a cloud-config block and a shell script in the same user data payload, use MIME multi-part format. Cloud-init processes each part according to its declared content type.

Content-Type: multipart/mixed; boundary="//"
MIME-Version: 1.0

--//
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit

#cloud-config
packages:
  - nginx

--//
Content-Type: text/x-shellscript; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit

#!/bin/bash
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/instance-id)
echo "Bootstrapped: $INSTANCE_ID" > /var/log/bootstrap-id.txt

--//--

The cloud-config part runs first (package installs, file writes), then the shell script runs. This pattern lets you use cloud-config for standard declarative setup and bash for anything that needs scripting logic, without mixing the two.

How to debug user data failures

User data failures are common, and cloud-init logs everything. When an instance launches but the software is not installed or configured correctly, start with the logs before relaunching.

# Main log: stdout and stderr from your script
sudo cat /var/log/cloud-init-output.log

# Detailed cloud-init log: module execution, timing, errors
sudo cat /var/log/cloud-init.log

# Quick status check: returns done, running, or error
cloud-init status

Access the instance via SSH to read these logs. If the instance is unreachable because the user data script failed before the SSH service started, use Session Manager instead. It does not depend on SSH or open inbound ports in your security group.

Common failure causes

SymptomLikely causeFix
Software not installedScript exited early due to an errorFind the first error in the output log. Everything after it was skipped
Nothing in the output logMissing or misspelled shebang lineFirst line must be exactly #!/bin/bash with no leading whitespace
Script appears to hangPackage manager waiting for input, or network timeoutAdd -y to all package install commands; check that outbound 80/443 is open
Instance terminates immediatelyASG health check fires before bootstrap finishesIncrease the health check grace period in your ASG settings
Config applied but service not runningService not enabled, or failed silentlyCheck with systemctl status nginx after logging in

Bootstrap time and Auto Scaling

User data scripts run synchronously during boot. If your script takes two minutes, the instance is not available for two minutes after launch. In an Auto Scaling Group, this delays scale-out events. If the health check grace period is shorter than the bootstrap time, the ASG will terminate the instance as unhealthy before it finishes starting.

Auto Scaling grace period trap

If your bootstrap script takes 90 seconds and your ASG health check grace period is 60 seconds, the ASG will kill the instance before it finishes. Either increase the grace period or reduce bootstrap time by pre-baking slow steps into a golden AMI.

You can track actual launch durations from Amazon CloudWatch metrics to spot patterns and set alarms if bootstrapping starts taking longer than expected.

Common mistakes

  1. Forgetting the shebang line. Without #!/bin/bash as the exact first line, cloud-init does not execute the payload as a shell script. The instance launches fine but nothing happens and the output log may appear empty.

  2. Scripts that are not idempotent. An idempotent script produces the same result whether it runs once or ten times. If you need to rerun the script manually for debugging, non-idempotent steps like useradd will fail with errors. Write scripts that check before acting: id -u myuser &>/dev/null || useradd myuser.

  3. Putting secrets in user data. User data is stored in AWS and is readable by anyone with ec2:DescribeInstanceAttribute permission on the instance. It is also visible in launch templates and the EC2 console. The instance’s IAM role should have permission to access only the specific secrets it needs.

    Never put credentials in user data

    API keys, passwords, and private keys in user data are visible to anyone who can describe the instance or read the launch template. Use AWS Secrets Manager and fetch secrets at runtime using the instance’s IAM role.

  4. Assuming it runs on every boot. Linux user data runs on the first boot only. Stopping and restarting the instance, or editing user data on a stopped instance, does not re-run the script. Cloud-init has already marked the first-boot stage as complete. For recurring startup logic, create a systemd service.

  5. Exceeding the 16 KB raw size limit. The limit applies to the raw user data before base64 encoding. For complex bootstrap logic, store the main script in S3 and use a short user data script to download and run it.

  6. Not checking the logs first. The most common debugging mistake is re-launching the instance repeatedly without reading /var/log/cloud-init-output.log. This log almost always shows exactly what went wrong. Check it before changing anything.

Frequently asked questions

What is EC2 user data?

EC2 user data is a script or configuration block you attach to an instance at launch. When the instance first boots, the cloud-init service reads it and executes it automatically as root. Use it to install packages, write config files, and start services without logging in manually.

Does EC2 user data run on every boot?

No. By default, user data runs only on the first boot. If you stop and start the instance, the script does not run again. Editing user data on a stopped instance makes the new content visible, but cloud-init will not re-run it automatically because it has already marked the first-boot stage as complete. For recurring startup logic, use a systemd service instead.

How do I debug a user data script that is not working?

SSH into the instance and read /var/log/cloud-init-output.log. This file contains all stdout and stderr from your script. Also run "cloud-init status" to see whether cloud-init finished, is still running, or exited with an error. If you cannot reach the instance via SSH, use Session Manager instead.

Can I update user data after the instance is launched?

Yes. Stop the instance, edit the user data in the console or CLI, then start it again. The updated user data is stored and becomes visible on the instance, but cloud-init will not re-execute it on Linux because the first-boot mark is already set.

When should I use a golden AMI instead of user data?

Use a golden AMI when you need fast and predictable launches, when your bootstrap is complex or slow, or when you are running Auto Scaling at production scale. Pre-bake everything that does not change between launches into the AMI, and use minimal user data only for instance-specific configuration like environment variables or a registration step.

Last verified: 1 April 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.