---
cuppy_hub: aws
cuppy_path: guides/ec2/launch-instance.md
cuppy_section: docs
cuppy_doc: F3GE-HT3Q
cuppy_doc_uuid: 44a56447-f005-48d8-9867-e1a4d8e6bc75
cuppy_permalink: https://cuppy.io/aws/d/F3GE-HT3Q
cuppy_version: 4
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/aws/docs/guides/ec2/launch-instance
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: c206418a82e96f199c380c275bd1e43280edc66e53ba65315a6a283941e45b81
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# Launch an EC2 instance safely

**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>}]'
```

```ts
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 `…-arm64` parameter. List the variants with `aws ssm get-parameters-by-path --path /aws/service/ami-amazon-linux-latest`.
- `HttpTokens=required` enforces IMDSv2. A hop limit of 2 lets containers on the instance reach the metadata service (`reference/ec2/instance-metadata-and-roles.md`).
- `t3.micro` is 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`.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 131 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/aws/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
