Launch an EC2 instance safely
Launch an EC2 instance from the CLI or TypeScript with the current Amazon Linux AMI resolved from a public parameter, IMDSv2 required, an instance profile instead of stored keys, a private subnet and a narrow security group — and what to check when the launch is refused.
markdownguidev4F3GE-HT3Qstage: buildtrust: officialservice: ec2interface: cliinterface: sdkrun-instancesRunInstancesCommandresolve:ssmami-amazon-linux-latestal2023IMDSv2HttpTokens requiredhop limitinstance profileiam:PassRolesecurity groupUnauthorizedOperationdecode-authorization-messageInsufficientInstanceCapacityVcpuLimitExceeded
Resolve the AMI from the public parameter (never hard-code an AMI ID), require IMDSv2, give the instance a role through an instance profile, and launch it into a private subnet with a security group that opens only what it serves. If the launch is refused with an encoded message, decode it. It names the missing permission, often iam:PassRole.
1. Instance profile (the instance's credentials)
An instance gets AWS credentials from a role wrapped in an instance profile. Do not copy access keys onto it.
aws iam create-role --role-name <app>-ec2 \
--assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
aws iam attach-role-policy --role-name <app>-ec2 \
--policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore # Session Manager instead of SSH
aws iam create-instance-profile --instance-profile-name <app>-ec2
aws iam add-role-to-instance-profile --instance-profile-name <app>-ec2 --role-name <app>-ec2
The identity that launches the instance needs iam:PassRole on that role, as well as ec2:RunInstances. AmazonSSMManagedInstanceCore and the ec2.amazonaws.com trust principal are standard (UNVERIFIED in the collected sources; see reference/iam/roles-and-trust.md). A new instance profile can take a few seconds before run-instances accepts it.
2. Security group
aws ec2 create-security-group --group-name <app>-sg --description "<app>" --vpc-id <vpc>
aws ec2 authorize-security-group-ingress --group-id <sg> --protocol tcp --port 443 --source-group <lb-sg>
A new group allows all outbound traffic and no inbound traffic. Open inbound only from a load balancer's group or a known range. With Session Manager there is no need for port 22. Right after creating the group, run-instances can briefly say InvalidGroup.NotFound, because the EC2 API is eventually consistent. Retry with backoff.
3. Launch
aws ec2 run-instances \
--image-id resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
--instance-type t3.micro --count 1 \
--subnet-id <private-subnet> --security-group-ids <sg> \
--iam-instance-profile Name=<app>-ec2 \
--metadata-options "HttpEndpoint=enabled,HttpTokens=required,HttpPutResponseHopLimit=2" \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=<app>}]'
import { EC2Client, RunInstancesCommand } from "@aws-sdk/client-ec2";
const ec2 = new EC2Client({ region: "<region>" });
const out = await ec2.send(new RunInstancesCommand({
ImageId: "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64",
InstanceType: "t3.micro",
MinCount: 1,
MaxCount: 1,
SubnetId: "<private subnet id>",
SecurityGroupIds: ["<sg id>"],
IamInstanceProfile: { Name: "<instance profile name>" },
MetadataOptions: { HttpEndpoint: "enabled", HttpTokens: "required", HttpPutResponseHopLimit: 2 },
TagSpecifications: [{ ResourceType: "instance", Tags: [{ Key: "Name", Value: "<name>" }] }],
}));
console.log(out.Instances?.[0]?.InstanceId);
resolve:ssm:makes EC2 look up the current AMI ID for the Region at launch. For Arm instance types, use the…-arm64parameter. List the variants withaws ssm get-parameters-by-path --path /aws/service/ami-amazon-linux-latest.HttpTokens=requiredenforces IMDSv2. A hop limit of 2 lets containers on the instance reach the metadata service (reference/ec2/instance-metadata-and-roles.md).t3.microis an example. Choose by workload.
Make IMDSv2 the account default
aws ec2 modify-instance-metadata-defaults --region <region> \
--http-tokens required --http-put-response-hop-limit 2
aws ec2 modify-instance-metadata-defaults --region <region> --http-tokens-enforced enabled
The first command sets the default for new launches in that Region. The second refuses launches that try to allow IMDSv1.
4. Wait and connect
aws ec2 wait instance-status-ok --instance-ids <id>
aws ssm start-session --target <id>
Session Manager needs the instance to reach Systems Manager: a NAT gateway, or VPC endpoints for ssm, ssmmessages and ec2messages (UNVERIFIED endpoint list in the collected sources).
If the instance launches but nothing can connect to it (or it can't reach out), see guides/vpc/diagnose-connection-timeout.md.
When the launch is refused
| Error code | Do this |
|---|---|
UnauthorizedOperation with Encoded authorization failure message | aws sts decode-authorization-message --encoded-message <msg> --query DecodedMessage --output text. You need sts:DecodeAuthorizationMessage. Look for the action that was denied, often iam:PassRole on the role, or a tag condition |
InsufficientInstanceCapacity | AWS has no capacity of that type in that Availability Zone right now. Try another AZ (subnet) or instance type, or wait |
VcpuLimitExceeded | The account's vCPU quota for that instance family is reached. Request an increase in Service Quotas or use a smaller instance |
InvalidAMIID.NotFound | Wrong Region for the AMI, the AMI is no longer available, or the account's Allowed AMIs setting excludes it |
InvalidGroup.NotFound, InvalidParameterValue for the instance profile | Just created: retry. Otherwise the wrong VPC or name |
The full list is in reference/ec2/errors.md.