AWS CLI Fundamentals for EC2
Learn to manage EC2 instances from the command line instead of the console — configuring credentials and profiles, launching, describing, and terminating instances, and filtering output with JMESPath queries — the foundation for any automation or scripting workflow.
Want a visual for this topic?
Generate a diagram tailored to AWS CLI Fundamentals for EC2 — the AI picks whichever visual (architecture, flowchart, ER diagram, etc.) best fits this specific AWS concept.
Sign in to generate a visual →🎓 Learning objectives
- •Configure AWS CLI credentials and multiple named profiles for different accounts or roles
- •Launch, describe, and terminate EC2 instances using aws ec2 commands
- •Filter and shape CLI output using --query (JMESPath) and --filters
- •Explain why scriptable, repeatable CLI access matters even when the console is more visual
- •Debug common CLI configuration and permission errors
- •Combine CLI commands into simple shell scripts for repeatable EC2 operations
What is it?
The AWS CLI is a command-line tool (aws) that wraps every AWS API call into a scriptable command, authenticated using credentials stored in ~/.aws/credentials and configuration in ~/.aws/config, organized into named profiles so a single machine can hold credentials for multiple AWS accounts or IAM roles. For EC2 specifically, the aws ec2 command group covers the full instance lifecycle: run-instances to launch, describe-instances to list and inspect, terminate-instances (and stop-instances/start-instances) to manage lifecycle, plus dozens of related commands for AMIs, security groups, key pairs, and volumes. Every command returns JSON by default, and the --query flag applies a JMESPath expression to extract and reshape exactly the fields you need from that JSON, while --filters narrows which resources the API itself returns, before any local filtering happens.
Why it exists
Infrastructure work stops being a one-off task the moment you need to do it more than once, do it identically every time, do it as part of a larger automated pipeline, or do it faster than a human can click through a UI — all of which describe the overwhelming majority of real production AWS usage. The CLI exists because 'click through the console' does not compose: it can't be triggered by a cron job, called from a CI/CD pipeline, embedded in a script that also talks to five other systems, or code-reviewed in a pull request. Any serious automation, from a nightly backup job to a full Infrastructure-as-Code pipeline, ultimately rests on the same APIs the CLI exposes directly.
Problem it solves
The CLI solves the fundamental limitation that console actions are not repeatable, not scriptable, and not automatable — any workflow that needs to run more than once, run unattended, run as part of a larger pipeline, or run identically across many resources needs a scriptable interface, which clicking through a web UI structurally cannot provide. It also solves the problem of auditability and review: a CLI command or script committed to a repository can be read, reviewed, and diffed like code, where a sequence of console clicks leaves no equivalent artifact.
Intuition
Every AWS Console click is secretly just a pre-filled API call that AWS's web UI makes on your behalf, and the CLI calls those same APIs directly, just explicitly and in text form you can save, version, repeat, and hand to a machine. --query is a way of telling the CLI 'the response has a hundred fields, I only care about these three, in this shape' instead of scrolling through a JSON dump trying to find what matters. Profiles exist because the CLI needs to know which AWS identity you mean every time you run a command, and most engineers legitimately work across more than one account or role in a single day.
Analogy
Using the AWS Console is like driving a car with a dashboard full of dials and screens you read and react to in the moment. Using the AWS CLI is like writing down the exact driving instructions once — turn left at Main Street, accelerate to 40 — and being able to hand that same instruction sheet to a hundred other drivers, or a robot, and get the identical result every time. The console is great for looking around and understanding what's there; the CLI is what you reach for the moment you need to do the same thing twice, or do it as part of something bigger than a single click.
Technical explanation
AWS CLI configuration lives in two files: ~/.aws/credentials (access key ID and secret access key per profile) and ~/.aws/config (region, output format, and for assumed roles, role ARN and source profile, per profile), with the [default] section used when no --profile flag is given. Every CLI command corresponds 1:1 to an AWS API action — aws ec2 run-instances calls the EC2 RunInstances API, describe-instances calls DescribeInstances, and so on — and the CLI handles request signing (SigV4), retries, and pagination automatically. run-instances requires at minimum an AMI ID and instance type, and typically also a key pair name (for SSH access), a security group ID, and a subnet ID; it returns a JSON response containing the new instance's ID, state, and metadata nested under Instances[]. describe-instances returns a more deeply nested structure (Reservations[].Instances[], since one API call can return multiple reservations each containing multiple instances), which is exactly why --query matters: a JMESPath expression like Reservations[].Instances[].[InstanceId,State.Name,PublicIpAddress] flattens that nesting into exactly the fields needed, and --output table or --output text controls how the result is rendered (json, the default, is best for piping into other tools like jq; table is best for humans reading a terminal). --filters differs from --query in where the work happens: filters are sent as part of the API request itself (Name=instance-state-name,Values=running), so AWS's servers do the filtering and return a smaller response, while --query only reshapes a response AWS has already sent in full — for large accounts with thousands of resources, preferring --filters over fetching everything and filtering client-side meaningfully reduces latency and response size. Exit codes and aws configure list are useful for debugging: a non-zero exit code combined with an UnauthorizedOperation or AccessDenied error message points to an IAM permissions gap, while an empty but successful response often points to a wrong region or an overly narrow filter.
Architecture
A platform team maintains a shell script library for routine EC2 operations: a launch-dev-box.sh that calls run-instances with the team's standard AMI, instance type, and tags; a cleanup-stale-instances.sh run nightly via cron that calls describe-instances --filters to find instances tagged environment=temporary older than 24 hours and terminates them; and a audit-instance-types.sh used before a cost review that calls describe-instances --query to produce a table of every running instance's type and launch time. Each script authenticates using a named profile scoped to a least-privilege IAM role, and the scripts live in the team's git repository alongside the rest of their infrastructure code.
Workflow
- Install the CLI (
aws --versionto confirm) and runaws configureto set a default profile's access key, secret key, default region, and output format — oraws configure --profile myprofileto create a named profile for a second account or role. 2) Verify identity and access withaws sts get-caller-identity, which confirms which account and IAM identity the configured credentials resolve to before you run anything destructive. 3) Launch an instance withaws ec2 run-instances --image-id ami-xxxx --instance-type t3.micro --key-name mykey --security-group-ids sg-xxxx --subnet-id subnet-xxxx, capturing the returned instance ID. 4) Check status withaws ec2 describe-instances --instance-ids i-xxxx --query 'Reservations[].Instances[].State.Name' --output text, polling until it reports 'running'. 5) List and filter instances at scale withaws ec2 describe-instances --filters 'Name=instance-state-name,Values=running' --query 'Reservations[].Instances[].[InstanceId,InstanceType,PublicIpAddress]' --output table. 6) Terminate when done withaws ec2 terminate-instances --instance-ids i-xxxx, and confirm with another describe-instances call. 7) Wrap repeatable sequences into a shell script, and once comfortable, graduate repeatable infrastructure to a proper IaC tool (CloudFormation, CDK, Terraform) rather than growing CLI scripts indefinitely.
Example
A developer needs to spin up a temporary EC2 instance every morning for integration testing and tear it down every evening. Doing this by hand in the console is 10 clicks each way, every day, forever. Written as two aws ec2 commands in a shell script triggered by cron, it becomes a zero-effort, zero-mistake, fully auditable operation — and the exact same script can be handed to a CI pipeline without any changes.
Real-world usage
The AWS CLI underlies the overwhelming majority of real-world AWS automation: CI/CD pipelines deploying application changes, scheduled cleanup and cost-optimization scripts, infrastructure bootstrapping scripts run before a full Infrastructure-as-Code tool takes over, and ad hoc operational scripts engineers write to diagnose or remediate incidents quickly. It's also the universal fallback when a console feature doesn't exist yet for a newly released AWS capability, since new APIs are typically available via the CLI/SDK before the console UI catches up.
Trade-offs
The CLI trades the console's visual immediacy and point-and-click discoverability for repeatability, scriptability, and precision — you give up being able to casually browse and discover options by clicking around, in exchange for commands that produce exactly the same result every time they run, can be checked into version control, and can run unattended. Learning JMESPath for --query has a real upfront learning curve versus just reading a console table, but pays off the moment you need to extract the same three fields from fifty resources instead of eyeballing one. Named profiles add setup overhead versus a single default profile but prevent the serious risk of accidentally running a command against the wrong AWS account.
Visual explanation
Picture the AWS Console as a window showing you a live, visual snapshot of your AWS account, and the AWS CLI as a direct telephone line to the same underlying API the console window is secretly looking through. A command like aws ec2 describe-instances picks up that phone line, asks the API 'tell me about all my instances,' and gets back a large JSON document — the same raw data the console formats into pretty tables. --filters is like telling the API operator over the phone 'only tell me about the running ones' before they even start talking, while --query is like asking your own assistant to skim the operator's full answer afterward and read back only the three facts you actually care about, in exactly the order you want them.
Advantages
- —
Commands are exactly repeatable — the same command run twice produces the same result, unlike manually clicking through a UI where steps can be missed
- —
Commands can be saved in scripts, version-controlled, and code-reviewed like any other code
- —
CLI output can be piped into other command-line tools (grep, jq, awk) or consumed by other scripts for full automation
- —
Credentials and profiles let one machine safely and explicitly switch between multiple AWS accounts or IAM roles
- —
Every CLI capability maps directly to an AWS API call, so CLI fluency transfers directly to understanding SDKs and Infrastructure-as-Code tools
Disadvantages
- —
No visual feedback — mistakes in a command (wrong instance ID, wrong region) aren't caught by a UI confirmation dialog the way some console actions are
- —
JMESPath syntax for --query has a real learning curve and is easy to get subtly wrong, producing an empty or malformed result rather than an error
- —
Managing credentials securely (not committing them to git, rotating them, scoping named profiles correctly) is an ongoing responsibility the console doesn't impose
- —
Long-running or complex multi-step workflows written as ad hoc CLI scripts can become hard to maintain compared to dedicated Infrastructure-as-Code tools
- —
Default output formats and API response shapes occasionally change between CLI versions, which can silently break scripts that assumed a specific JSON structure
Common mistakes
- —
Running a destructive command (terminate-instances) against the wrong profile because the active profile wasn't checked first, especially dangerous when managing multiple AWS accounts
- —
Hardcoding access keys directly into scripts or committing
~/.aws/credentialscontents to version control instead of relying on named profiles or IAM roles - —
Writing a --query expression that silently returns null or an empty list due to a small JMESPath syntax error, and assuming the underlying resource doesn't exist
- —
Forgetting that --filters narrows what the API itself returns (server-side), while --query only reshapes what's already been returned (client-side) — using the wrong one for large result sets wastes bandwidth and time
- —
Not setting a default region, then being confused when describe-instances returns nothing because it's silently querying the wrong region
In the AWS Console
- 1
Terminal → aws configure
Run `aws configure` and enter your AWS Access Key ID, Secret Access Key, default region, and output format; repeat with `--profile <name>` for additional accounts or roles.
Access keys belong to an IAM user; for anything beyond local learning, prefer IAM roles with temporary credentials (via aws sso or assume-role) over long-lived access keys.
- 2
Terminal → aws sts get-caller-identity
Run `aws sts get-caller-identity` to confirm exactly which account and IAM identity the active credentials resolve to before running any command that creates or destroys resources.
This single command prevents the single most common and costly CLI mistake: running a command against the wrong AWS account.
- 3
Terminal → aws ec2 run-instances / describe-instances
Launch a test instance with `aws ec2 run-instances --image-id <ami-id> --instance-type t3.micro --key-name <key> --count 1`, then confirm it's running with `aws ec2 describe-instances --instance-ids <id> --query 'Reservations[].Instances[].State.Name'`.
Always capture and check the InstanceId returned by run-instances — you'll need it for every subsequent command on that instance.
- 4
Terminal → aws ec2 terminate-instances
Terminate the test instance with `aws ec2 terminate-instances --instance-ids <id>` and verify the state transitions to 'shutting-down' then 'terminated' via another describe-instances call.
Terminated instances remain visible in describe-instances output for a period afterward with state 'terminated' — this is normal and not an error.
🎤 Interview questions
What's the practical difference between --filters and --query, and why does that difference matter for a large AWS account? (Listen for: --filters narrows results server-side as part of the API request, reducing what AWS sends back, while --query only reshapes a response already fully received — for large result sets, filtering server-side is materially faster and cheaper.)
Why would you run aws sts get-caller-identity before running a destructive CLI command? (Listen for: it confirms which AWS account and IAM identity the active credentials and profile actually resolve to, preventing the common and costly mistake of running a command against the wrong account.)
How do named profiles let a single machine safely work across multiple AWS accounts? (Listen for: each profile in ~/.aws/credentials and ~/.aws/config holds separate credentials/region/role configuration, selected explicitly via --profile, preventing accidental cross-account command execution.)
Why does aws ec2 describe-instances return a nested Reservations[].Instances[] structure instead of a flat list of instances? (Listen for: a single API call/reservation can return multiple instances launched together, so the API models that grouping explicitly, which is also exactly why JMESPath --query expressions need to flatten through both levels.)
What's a realistic failure mode if you hardcode AWS access keys directly into a script instead of using named profiles or IAM roles? (Listen for: hardcoded long-lived credentials risk accidental exposure via version control or logs, and don't rotate or scope permissions as cleanly as IAM roles with temporary credentials.)
Why does the CLI matter even for engineers who are comfortable and fast in the AWS Console? (Listen for: console actions aren't repeatable, scriptable, reviewable, or triggerable by automation like cron or CI/CD — any workflow that needs to happen more than once or run unattended needs a scriptable interface.)
A describe-instances call returns an empty result even though you're certain the instance exists. What would you check first? (Listen for: confirm the active region matches where the instance was actually launched, and confirm the --filters expression isn't unintentionally excluding it — these are the two most common causes of an unexpectedly empty result.)