# AWS CLI Cheat Sheet — Compute: EC2, ECS & EKS

> **Tool:** AWS CLI v2
> **Category:** Cloud CLIs
> **Verified against:** aws-cli/2.33.6, flags verified via `aws <cmd> help` run locally, plus current
> AWS docs for EKS access entries, 2026-08-29
> **Official docs:** https://docs.aws.amazon.com/cli/

## What it is and where it fits

EC2, ECS, and EKS represent three different points on AWS's "how much do you want to manage yourself"
spectrum for running compute: EC2 gives you a virtual machine and everything above the hypervisor is
your problem; ECS is AWS's own container orchestrator, opinionated and tightly integrated with the
rest of AWS; EKS is managed Kubernetes, the same API surface you'd get from any Kubernetes cluster,
with AWS running the control plane. All three are covered here rather than split apart because in
practice most infrastructure/platform engineers move between them in the same debugging session — an
ECS task that won't start is often an EC2 capacity or IAM problem underneath, and an EKS node group is
still, underneath, an EC2 Auto Scaling group. This page assumes the profile/credential setup from the
companion Configuration & IAM page is already working.

## Listing and inspecting EC2 instances

```bash
aws ec2 describe-instances
aws ec2 describe-instances --instance-ids i-0123456789abcdef0
aws ec2 describe-instances --filters "Name=instance-state-name,Values=running"
aws ec2 describe-instances --query 'Reservations[].Instances[].[InstanceId,State.Name,PublicIpAddress]' --output table
```

`describe-instances` returns nested `Reservations[].Instances[]` — a single call can group multiple
instances under one reservation (historically tied to how a batch `run-instances` call was issued), so
most useful `--query` expressions need to drill through both levels, not just `Instances[]` alone.

## Launching, starting, and stopping instances

```bash
aws ec2 run-instances \
  --image-id ami-0123456789abcdef0 \
  --instance-type t3.micro \
  --key-name my-key \
  --security-group-ids sg-0123456789abcdef0 \
  --subnet-id subnet-0123456789abcdef0 \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=my-instance}]'

aws ec2 start-instances --instance-ids i-0123456789abcdef0
aws ec2 stop-instances --instance-ids i-0123456789abcdef0
aws ec2 terminate-instances --instance-ids i-0123456789abcdef0
```

`stop-instances` preserves the instance (EBS-backed data survives, you keep paying for attached
storage); `terminate-instances` deletes it permanently along with any EBS volumes set to
delete-on-termination. Confusing the two in a script is one of the more expensive mistakes to make
twice — a `stop` you meant as `terminate` just leaves a bill running; a `terminate` you meant as `stop`
can destroy data with no undo.

## Waiting for state transitions instead of polling by hand

```bash
aws ec2 run-instances --image-id ami-0123456789abcdef0 --instance-type t3.micro --count 1 --query 'Instances[0].InstanceId' --output text > /tmp/iid
aws ec2 wait instance-running --instance-ids "$(cat /tmp/iid)"
aws ec2 wait instance-status-ok --instance-ids "$(cat /tmp/iid)"    # waits for status checks too, not just "running"
```

`aws ec2 wait` subcommands (`instance-running`, `instance-status-ok`, `instance-terminated`, and more)
poll the underlying `describe-*` API on a fixed interval until the condition is met or a timeout is
hit, and exit non-zero on timeout — this is the correct building block for a deploy script that needs
to block until an instance is actually usable, instead of hand-rolling a `sleep`-and-`describe` loop.
`instance-running` only means the instance has started booting; `instance-status-ok` additionally waits
for both system and instance status checks to pass, which is the stronger, usually-more-correct signal
that the instance is actually ready to receive traffic.

## Security groups

```bash
aws ec2 describe-security-groups --group-ids sg-0123456789abcdef0
aws ec2 describe-security-groups --filters "Name=vpc-id,Values=vpc-0123456789abcdef0"
aws ec2 authorize-security-group-ingress --group-id sg-0123456789abcdef0 --protocol tcp --port 443 --cidr 0.0.0.0/0
```

See the companion Networking page for the full rule-authoring reference (CIDR vs. security-group
sources, egress rules, and the VPC/subnet context these attach to).

## Building and managing AMIs

```bash
aws ec2 create-image --instance-id i-0123456789abcdef0 --name "my-app-2026-08-21" --no-reboot
aws ec2 describe-images --owners self --filters "Name=name,Values=my-app-*"
aws ec2 copy-image --source-image-id ami-0123456789abcdef0 --source-region us-east-1 --name "my-app-dr-copy"
aws ec2 deregister-image --image-id ami-0123456789abcdef0 --delete-associated-snapshots
```

`--no-reboot` skips the default reboot-before-snapshot step for a faster, non-disruptive image build —
but it's only safe if the instance's on-disk state is already consistent (nothing mid-write when the
snapshot is taken); an application with in-flight writes to its own disk can produce a corrupted AMI
this way. `deregister-image` removes the AMI but leaves its backing EBS snapshot behind by default —
pass `--delete-associated-snapshots` to avoid quietly accumulating orphaned, still-billed snapshots
every time an old AMI is cleaned up.

## Spot instances

```bash
aws ec2 describe-spot-price-history --instance-types t3.micro --product-descriptions "Linux/UNIX" --start-time 2026-08-20T00:00:00Z
aws ec2 request-spot-instances --instance-count 1 --type "persistent" --launch-specification file://spot-spec.json
aws ec2 describe-spot-instance-requests --filters "Name=state,Values=active"
aws ec2 cancel-spot-instance-requests --spot-instance-request-ids sir-0123456789abcdef0
```

`--type "persistent"` re-requests a new spot instance automatically after an interruption reclaims the
previous one; `"one-time"` (the default) does not. Cancelling a spot request does *not* terminate an
already-running instance created from it — that's a separate `terminate-instances` call, a distinction
that produces a real, ongoing bill if missed in a cleanup script.

## Launch templates

```bash
aws ec2 create-launch-template --launch-template-name my-template --launch-template-data '{"ImageId":"ami-0123456789abcdef0","InstanceType":"t3.micro","KeyName":"my-key"}'
aws ec2 describe-launch-templates --launch-template-names my-template
aws ec2 run-instances --launch-template LaunchTemplateName=my-template,Version='$Latest' --min-count 1 --max-count 1
```

A launch template can hold multiple versions — `create-launch-template-version` adds one without
re-specifying every field. `Version='$Latest'` and `Version='$Default'` are both valid magic values in
`run-instances`, distinct from a specific version number: `$Default` only moves when you explicitly
call `modify-launch-template --default-version`, so pinning an Auto Scaling group to `$Default` rather
than `$Latest` is a deliberate way to stage a new template version before it goes live everywhere.

## How ECS and EKS actually differ operationally

```mermaid
flowchart LR
    subgraph ECS["ECS — AWS-native orchestration"]
        direction TB
        TD["Task definition<br/>(JSON, ECS-specific)"] --> Svc["Service<br/>(desired count, ALB target group)"]
        Svc --> Task["Running task<br/>on Fargate or EC2"]
    end
    subgraph EKS["EKS — managed Kubernetes"]
        direction TB
        Manifest["Kubernetes manifest<br/>(Deployment, Service YAML)"] --> Ctrl["kube-apiserver<br/>(AWS-managed control plane)"]
        Ctrl --> Pod["Running pod<br/>on a managed/Fargate node"]
    end

    classDef accent fill:#f0e9fb,stroke:#6d43c0,color:#10161c
    classDef alt fill:#e9eafb,stroke:#4550c4,color:#10161c
    class TD,Svc,Task accent
    class Manifest,Ctrl,Pod alt
```

The practical difference: `aws ecs` commands are the whole interface — there's no second control-plane
API to learn. EKS instead hands you a real Kubernetes API server, so `aws eks` commands mostly exist to
get you *access* to that API (credentials, `kubeconfig`, node/Fargate capacity) — the actual workload
management afterward happens through `kubectl`, not the AWS CLI.

## ECS clusters, services, and tasks

```bash
aws ecs list-clusters
aws ecs describe-clusters --clusters my-cluster
aws ecs list-services --cluster my-cluster
aws ecs update-service --cluster my-cluster --service my-service --desired-count 3
aws ecs update-service --cluster my-cluster --service my-service --force-new-deployment
```

`--force-new-deployment` (no argument, a flag) is the standard way to roll a service onto new task
instances without changing the task definition — useful for picking up a new container image tagged
`:latest` without bumping a revision, though pinning image tags to an immutable digest is the safer
long-term practice (see the pitfalls section).

## Registering an ECS task definition

```bash
aws ecs register-task-definition \
  --family my-task \
  --container-definitions '[{"name":"app","image":"myrepo/app:latest","memory":512,"cpu":256}]' \
  --requires-compatibilities FARGATE \
  --network-mode awsvpc \
  --cpu "256" --memory "512"

aws ecs describe-task-definition --task-definition my-task:5
```

Each `register-task-definition` call creates a *new revision* under the same family — it never edits
an existing one in place, which is exactly what makes ECS deployments safely reversible: rolling back
is just pointing the service at an older revision number, no rebuild required.

## ECS task inspection and one-off runs

```bash
aws ecs list-tasks --cluster my-cluster --service-name my-service
aws ecs describe-tasks --cluster my-cluster --tasks <task-arn>
aws ecs run-task --cluster my-cluster --task-definition my-task:5 --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-0123456789abcdef0],securityGroups=[sg-0123456789abcdef0],assignPublicIp=ENABLED}"
```

`run-task` launches a task outside of any service's desired-count management — the right tool for a
one-off migration job or a manually-triggered batch task, not for anything that should stay running
and self-heal (that's what a service is for).

## Debugging a running ECS task with execute-command

```bash
aws ecs execute-command \
  --cluster my-cluster --task <task-arn> --container app \
  --interactive --command "/bin/sh"
```

`execute-command` opens an interactive shell (or runs a one-off command) inside a running container —
the ECS equivalent of `kubectl exec`. It requires three things set up ahead of time that are easy to
miss: the task's IAM execution role needs the SSM Session Manager permissions, `enableExecuteCommand`
must be set on the service/task (it's not on by default), and the SSM Session Manager plugin must be
installed locally alongside the CLI. `AccessDeniedException` here almost always means one of those
three, not an actual permissions problem with the container's own IAM role.

## EKS cluster access and node groups

```bash
aws eks list-clusters
aws eks describe-cluster --name my-cluster
aws eks update-kubeconfig --name my-cluster --alias my-cluster    # writes/merges kubeconfig context for kubectl
aws eks list-nodegroups --cluster-name my-cluster
aws eks create-nodegroup \
  --cluster-name my-cluster --nodegroup-name workers \
  --node-role arn:aws:iam::111122223333:role/EksNodeRole \
  --subnets subnet-0123456789abcdef0 \
  --scaling-config minSize=1,maxSize=5,desiredSize=2
```

`eks update-kubeconfig` is the bridge between the AWS CLI and `kubectl` — it doesn't create anything in
the cluster, it just writes an entry into your local kubeconfig so `kubectl` can authenticate against
that cluster using your AWS credentials (via the `aws eks get-token` credential process under the
hood, invoked automatically).

## EKS Fargate profiles — running pods with no managed nodes at all

```bash
aws eks create-fargate-profile \
  --cluster-name my-cluster --fargate-profile-name default \
  --pod-execution-role-arn arn:aws:iam::111122223333:role/EksFargatePodRole \
  --subnets subnet-0123456789abcdef0 \
  --selectors namespace=default
```

A Fargate profile is a set of selectors (namespace + optional labels) — any pod matching one runs on
Fargate with no underlying EC2 node to patch or scale, at the cost of losing DaemonSets and some
node-level customization. Mixing Fargate profiles and managed node groups in the same cluster is
common: system/logging DaemonSets on managed nodes, application workloads on Fargate.

## EKS cluster access — access entries (current) vs. the aws-auth ConfigMap (legacy)

```bash
aws eks create-access-entry \
  --cluster-name my-cluster \
  --principal-arn arn:aws:iam::111122223333:role/PlatformTeamRole \
  --type STANDARD

aws eks associate-access-policy \
  --cluster-name my-cluster \
  --principal-arn arn:aws:iam::111122223333:role/PlatformTeamRole \
  --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSAdminPolicy \
  --access-scope type=cluster

aws eks list-access-entries --cluster-name my-cluster
```

> [!IMPORTANT]
> **EKS access entries are now the recommended way to grant cluster access — not hand-editing the
> `aws-auth` ConfigMap.** Older clusters and tutorials still reference editing a `ConfigMap` in
> `kube-system` to map an IAM principal to Kubernetes RBAC groups; that mechanism still works but is
> no longer the recommended path. Access entries manage the same IAM-to-Kubernetes-RBAC mapping
> through the EKS API itself (auditable in CloudTrail, no risk of a malformed YAML edit locking
> everyone out of the cluster) and let you attach AWS-managed access policies
> (`AmazonEKSAdminPolicy`, `AmazonEKSViewPolicy`, ...) instead of hand-authoring RBAC bindings for
> common cases. A cluster must have its authentication mode set to `API` or `API_AND_CONFIG_MAP`
> (checked via `aws eks describe-cluster`) for access entries to take effect at all.

## Real-world scenario: rolling out a new container image without downtime

```bash
# 1. Register the new revision (never mutates the old one — instant rollback target)
aws ecs register-task-definition --cli-input-json file://task-def-v6.json

# 2. Point the service at it and let ECS handle the rolling replacement
aws ecs update-service --cluster prod --service checkout-svc --task-definition checkout-task:6

# 3. Watch it settle before declaring victory
aws ecs wait services-stable --cluster prod --services checkout-svc
```

`services-stable` is the ECS analogue of `ec2 wait instance-status-ok` — it blocks until the service's
running count matches its desired count with no in-progress deployment, which is the actual signal a
deploy succeeded (a `desiredCount` that was merely *accepted* by the API is not evidence the new tasks
are healthy).

## Real-world scenario: EKS node group won't scale up — where to look

```bash
aws eks describe-nodegroup --cluster-name my-cluster --nodegroup-name workers \
  --query 'nodegroup.[status,scalingConfig,health]'
aws ec2 describe-instances --filters "Name=tag:eks:nodegroup-name,Values=workers" \
  --query 'Reservations[].Instances[].[InstanceId,State.Name]'
```

A managed node group is, underneath, a standard EC2 Auto Scaling group — when pods stay `Pending` and
the node group "won't scale," `describe-nodegroup`'s `health` field surfaces EKS-level issues (IAM role
problems, subnet capacity), but a genuine EC2 capacity shortfall in that Availability Zone won't show
up there at all; cross-check `describe-instances` for the actual instance states and, if needed, the
underlying ASG's own activity history.

## Real-world scenario: emergency access to a locked-out EKS cluster

A cluster migrated to access-entry–only authentication, and the one IAM role with admin access was
accidentally deleted along with its access entry:

```bash
# From an account-level IAM identity with eks:CreateAccessEntry permission (not cluster-level access)
aws eks create-access-entry \
  --cluster-name prod-cluster \
  --principal-arn arn:aws:iam::111122223333:role/BreakGlassAdmin \
  --type STANDARD
aws eks associate-access-policy \
  --cluster-name prod-cluster \
  --principal-arn arn:aws:iam::111122223333:role/BreakGlassAdmin \
  --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy \
  --access-scope type=cluster
```

> [!TIP]
> **Access entries are managed entirely through the EKS *AWS* API, using standard IAM permissions on
> the `eks:*` actions — not through cluster-internal RBAC.** This is precisely why they're a real
> improvement over the `aws-auth` ConfigMap for break-glass scenarios: recovering access never
> requires already having `kubectl` access to the cluster, only IAM permission on the EKS API, which an
> account owner always retains.

## CI/CD recipe: GitHub Actions deploying to ECS

```yaml
# .github/workflows/deploy.yml
name: Deploy to ECS
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::111122223333:role/GitHubActionsDeployRole
          aws-region: us-east-1
      - run: |
          aws ecs register-task-definition --cli-input-json file://task-def.json
          aws ecs update-service --cluster prod --service checkout-svc \
            --task-definition checkout-task --force-new-deployment
      - run: aws ecs wait services-stable --cluster prod --services checkout-svc
```

The final `wait` step matters as much as the deploy itself — without it, the workflow reports success
the instant the API *accepts* the update request, before ECS has actually finished replacing tasks,
which silently turns every deploy-failure into something only discovered later, out of band.

## Common pitfalls

- **Confusing `stop-instances` and `terminate-instances`** — see the note above; the former is
  reversible and keeps billing for storage, the latter destroys the instance and (by default) any
  delete-on-termination EBS volumes.
- **Deploying `:latest`-tagged images and relying on `--force-new-deployment` alone** — this makes
  every rollback ambiguous ("which build was actually running before?"). Pin to an immutable digest or
  a versioned tag and roll back by task-definition revision instead.
- **`execute-command` failing with `AccessDeniedException` for reasons that have nothing to do with
  the container's own permissions** — check `enableExecuteCommand`, the execution role's SSM
  permissions, and the local Session Manager plugin install before assuming an IAM policy problem.
- **Assuming `eks update-kubeconfig` grants cluster access** — it only configures `kubectl`'s
  connection details; actual authorization still comes from an access entry (or a legacy `aws-auth`
  mapping) for that IAM principal.
- **Not running the matching `wait` subcommand after a mutating call** — `run-instances`,
  `update-service`, and `create-nodegroup` all return before the resource is actually ready; scripting
  the next step immediately after is a common source of flaky automation.

## Exit codes / when to reach for something else

The CLI returns `0` on a successfully *accepted* API call and non-zero on a rejected one — for EC2/ECS
it does not mean the requested end state is reached (see the `wait` subcommands above for that).
Prefer Terraform/CloudFormation over hand-run `aws ec2 run-instances`/`aws ecs register-task-definition`
for anything meant to be a long-lived, reviewable part of your infrastructure; reach for these commands
directly for operational debugging, one-off tasks, and CI/CD deploy steps that intentionally mutate an
already-provisioned service (a task definition update, a rolling deployment) rather than provisioning
new infrastructure from scratch.
