Launching an AWS EC2 Instance

By Vishvesh Patel · DevOps EngineerUpdated September 15, 2026

Launching an EC2 instance properly. Choosing an AMI without hardcoding IDs, launch templates, user data, IMDSv2, and what to check when it comes up wrong.

Launching an instance needs six decisions: which image, which size, which network, which credentials, what runs at boot, and how you will reach it. Getting them right the first time is considerably easier than fixing them afterwards.

Do not hardcode the AMI ID

AMI IDs are Region-specific and change with every release, so a hardcoded ID is both unportable and steadily more out of date, which means steadily more unpatched.

AWS publishes current images as SSM public parameters:

AMI=$(aws ssm get-parameter \
  --name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
  --query Parameter.Value --output text)
echo $AMI
ami-0abcdef1234567890

For Ubuntu:

/aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id

Terraform and CloudFormation can both resolve these at deploy time, so templates never carry a stale ID.

The trade-off is that a new AMI release changes the resolved value, which can trigger instance replacement. That is manageable behind a launch template and an Auto Scaling group, and a reason to pin deliberately for a pet instance.

Launch with the settings that matter

aws ec2 run-instances \
  --image-id "$AMI" \
  --instance-type t3.micro \
  --subnet-id subnet-private-a \
  --security-group-ids sg-application \
  --iam-instance-profile Name=app-instance-profile \
  --metadata-options "HttpTokens=required,HttpEndpoint=enabled" \
  --block-device-mappings '[{"DeviceName":"/dev/xvda","Ebs":{"VolumeSize":30,"VolumeType":"gp3","Encrypted":true,"DeleteOnTermination":true}}]' \
  --user-data file://bootstrap.sh \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=payments-api},{Key=Environment,Value=production}]'

Four of those deserve attention.

--metadata-options HttpTokens=required enforces IMDSv2. IMDSv1 answered a plain GET to a well-known address, which meant a server-side request forgery bug in your application could read the instance role's AWS credentials. IMDSv2 requires a token obtained by PUT, closing that path. New instances default to requiring it, but set it explicitly so the intent is in your code.

--iam-instance-profile is how the instance gets AWS permissions. Never bake access keys into an AMI or user data. The SDK picks up role credentials from the metadata service automatically.

No key pair in that command, deliberately. Use Session Manager instead, which needs no inbound port and no key to distribute.

Encrypted: true on the root volume, or better, enable EBS encryption by default at the account level so no launch can forget:

aws ec2 enable-ebs-encryption-by-default --region eu-west-1

Use a launch template

run-instances is fine for learning. For anything real, put the configuration in a launch template:

aws ec2 create-launch-template \
  --launch-template-name payments-api \
  --launch-template-data '{
    "ImageId":"'"$AMI"'",
    "InstanceType":"t3.micro",
    "IamInstanceProfile":{"Name":"app-instance-profile"},
    "MetadataOptions":{"HttpTokens":"required"},
    "SecurityGroupIds":["sg-application"],
    "UserData":"'"$(base64 -i bootstrap.sh)"'"
  }'

Templates are versioned, so changes are auditable and reversible. More importantly, they are what Auto Scaling groups consume, and putting even a single instance behind an Auto Scaling group with min=max=1 means a failed instance is replaced automatically rather than repaired by hand.

That single change converts an instance from a pet into cattle, and it is the difference between a node failure being an alert and being an incident.

User data runs once

#!/bin/bash
set -euxo pipefail

dnf update -y
dnf install -y nginx amazon-cloudwatch-agent

systemctl enable --now nginx

Three things to know:

  • It runs as root, on first boot only. Rebooting does not re-run it, and editing it on a running instance does nothing.
  • Output goes to /var/log/cloud-init-output.log. The first place to look when an instance comes up wrong.
  • It is limited to 16 KB, before base64 encoding.

set -euxo pipefail at the top is worth the habit: without -e, a failing command is ignored and the instance boots half-configured with no obvious error.

For anything substantial, bake the image with Packer instead. Boot time drops and the result is reproducible rather than dependent on package repositories being reachable and unchanged.

Setting user_data_replace_on_change in Terraform makes a script change replace the instance, which is usually what you actually want.

Reaching it

aws ssm start-session --target i-0abc123

No key pair, no inbound port 22, no bastion. It works for instances in private subnets with no public IP. Requirements are the SSM agent, preinstalled on current Amazon Linux and Ubuntu AMIs, and the AmazonSSMManagedInstanceCore policy on the instance profile.

If you do use SSH, restrict the security group to a known CIDR. An open port 22 is found by scanners within minutes.

When it comes up wrong

# Is it actually running and passing checks?
aws ec2 describe-instance-status --instance-ids i-0abc123 \
  --query 'InstanceStatuses[0].{Instance:InstanceStatus.Status,System:SystemStatus.Status}'

Two status checks, and which one fails tells you where to look:

CheckFailing means
System statusAWS infrastructure: the underlying host. Stop and start to move to new hardware
Instance statusYour instance: kernel panic, full disk, misconfigured network, failed boot

A stop and start moves the instance to different physical hardware and resolves most system status failures. A reboot does not, since it stays on the same host.

# What did it print while booting?
aws ec2 get-console-output --instance-id i-0abc123 --output text

Console output is the closest thing to a monitor attached to the machine, and it shows kernel messages and boot failures that no agent could report.

Two more things that catch people: a stopped instance gets a new public IP when restarted unless an Elastic IP is attached, and instance store data is lost on stop.

Quick reference

TaskCommand
Latest Amazon Linux AMIaws ssm get-parameter --name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 --query Parameter.Value
Launchaws ec2 run-instances --image-id <ami> --instance-type <t> --subnet-id <s>
Create a launch templateaws ec2 create-launch-template --launch-template-name <n> --launch-template-data <json>
Status checksaws ec2 describe-instance-status --instance-ids <id>
Boot outputaws ec2 get-console-output --instance-id <id> --output text
Shell without SSHaws ssm start-session --target <id>
Require IMDSv2aws ec2 modify-instance-metadata-options --instance-id <id> --http-tokens required
Terminateaws ec2 terminate-instances --instance-ids <id>

Key takeaways

  • Resolve the AMI from an SSM public parameter rather than hardcoding a Region-specific ID that goes stale.
  • Use a launch template, and put even a single instance behind an Auto Scaling group so failures self-heal.
  • Attach an instance profile; never put access keys in an AMI or user data.
  • Require IMDSv2, which closes the SSRF path to the instance's AWS credentials.
  • User data runs once, as root, on first boot. Check /var/log/cloud-init-output.log when it goes wrong.
  • Use Session Manager rather than a key pair: no open ports, IAM-controlled, audited.
  • System check failure is AWS hardware; instance check failure is your instance. Stop and start fixes the former.
  • Stopping loses instance store data and the public IP.

Check your understanding

Exercise

Which status check failed

An instance fails its system status check but passes the instance status check. What is the right first action?

Exercise

A bootstrap script that stopped working

A team edits an instance's user data to install an extra package and reboots. The package is not installed. Why?


Frequently Asked Questions

How do I avoid hardcoding AMI IDs?

Read them from AWS's public SSM parameters, which always resolve to the current image, /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 for Amazon Linux, with equivalents published for Ubuntu and other distributions. Terraform and CloudFormation can both resolve these at deploy time. AMI IDs are Region-specific and change with every release, so a hardcoded value is unportable and becomes progressively more unpatched.

Should I use run-instances or a launch template?

A launch template for anything beyond experimentation. Templates are versioned so changes are auditable and reversible, and they are what Auto Scaling groups consume, which matters because putting even a single instance behind an Auto Scaling group with min and max of one means a failed instance is replaced automatically. That converts a node failure from an incident into a non-event, and it is the single biggest operational improvement available when launching instances.

Why doesn't my user data script run after a reboot?

Because it is designed to run only on first boot. Cloud-init records that it has executed and skips it thereafter, so editing the script on an existing instance achieves nothing. For configuration that must apply on every boot, use a systemd unit instead. When a first boot produces the wrong result, /var/log/cloud-init-output.log on the instance contains the script's full output and almost always explains it.

How do I give an EC2 instance permission to access AWS services?

Attach an IAM instance profile at launch. The SDK inside the instance picks up temporary credentials automatically from the instance metadata service, with no configuration in your application. Never bake access keys into an AMI, user data or application configuration. Those are permanent credentials that end up in images, snapshots and repositories. Also require IMDSv2, which prevents a server-side request forgery bug from being used to read those role credentials.

Do I need a key pair to launch an EC2 instance?

No, and for new instances you generally should not use one. SSM Session Manager gives you a shell with no key pair, no inbound port 22 and no bastion host, working even for instances in private subnets with no public IP. It needs the SSM agent, which is preinstalled on current Amazon Linux and Ubuntu images, and the AmazonSSMManagedInstanceCore policy on the instance profile. Access is granted through IAM and every session is logged to CloudTrail.

What is the difference between the two EC2 status checks?

The system status check monitors AWS infrastructure. The physical host, its networking and power. The instance status check monitors your instance's own health, covering things like a kernel panic, a full root volume or a broken network configuration. A system check failure is fixed by stopping and starting the instance, which relocates it to different hardware; a reboot keeps it on the same failing host. An instance check failure means looking at the guest, starting with get-console-output.

Continue Learning

Explore Related Topics

When it breaks

Related Resources