List Running EC2 Instances as a Clean Table
Use aws ec2 describe-instances with a JMESPath query to print every running instance with ID, IP, instance type, and Name tag.
aws ec2 describe-instances is the canonical "what's running in this account" command. Combined with a JMESPath --query and --output table, it produces a clean human-readable table showing every running instance with its ID, type, IP, and Name tag. This is the snippet I run before any change to an EC2 environment.
Tested on AWS CLI v2.17.
When to Use This
- Quick inventory check before making changes to an account
- Finding the public IP of an instance you need to SSH into
- Auditing which instances are running outside business hours
- Building a daily "what's running" Slack report
Don't use this when you need historical data (use Cost Explorer or AWS Config) or when you need detailed instance metadata beyond the basics (drop the --query and read the full JSON).
Code
# Running instances only, table format with the most useful columns
aws ec2 describe-instances \
--filters "Name=instance-state-name,Values=running" \
--query 'Reservations[*].Instances[*].[
InstanceId,
InstanceType,
PublicIpAddress,
PrivateIpAddress,
Tags[?Key==`Name`]|[0].Value
]' \
--output tableOutput looks like:
-------------------------------------------------------------------------------
| DescribeInstances |
+----------------------+-------------+----------------+----------------+------+
| i-0123456789abcdef0 | t3.medium | 54.123.45.67 | 10.0.1.42 | web |
| i-0123456789abcdef1 | t3.large | None | 10.0.1.43 | api |
| i-0123456789abcdef2 | m6i.xlarge | None | 10.0.2.10 | db |
+----------------------+-------------+----------------+----------------+------+
The Tags[?Key==\Name`]|[0].Value` JMESPath idiom is the standard way to extract the Name tag from EC2's awkward tag list format. Memorize it — you'll use it constantly.
Usage
A few useful variations:
# Just the instance IDs (for piping into another command)
aws ec2 describe-instances \
--filters "Name=instance-state-name,Values=running" \
--query 'Reservations[*].Instances[*].InstanceId' \
--output text
# Stopped instances (find idle resources to delete)
aws ec2 describe-instances \
--filters "Name=instance-state-name,Values=stopped" \
--query 'Reservations[*].Instances[*].[InstanceId,InstanceType,Tags[?Key==`Name`]|[0].Value]' \
--output table
# Instances in a specific VPC
aws ec2 describe-instances \
--filters "Name=vpc-id,Values=vpc-12345678" "Name=instance-state-name,Values=running" \
--query 'Reservations[*].Instances[*].[InstanceId,InstanceType,PrivateIpAddress]' \
--output table
# Instances with a specific tag
aws ec2 describe-instances \
--filters "Name=tag:Environment,Values=production" \
--query 'Reservations[*].Instances[*].[InstanceId,InstanceType,Tags[?Key==`Name`]|[0].Value]' \
--output tableA daily "still running" check, run from cron, that posts to Slack if anything is up outside business hours:
#!/usr/bin/env bash
COUNT=$(aws ec2 describe-instances \
--filters "Name=instance-state-name,Values=running" \
--query 'length(Reservations[*].Instances[*][])' \
--output text)
if [ "$COUNT" -gt 0 ]; then
curl -X POST -H 'Content-Type: application/json' \
-d "{\"text\":\"⚠️ $COUNT EC2 instances still running outside business hours\"}" \
"$SLACK_WEBHOOK_URL"
fiCaveats
Reservations[*].Instances[*]is a nested list, not flat. Each "reservation" can contain multiple instances. The double splat flattens it for the query, but the JSON shape trips people up.- JMESPath backticks
`are NOT shell backticks. They're literal characters in the query string. Wrap the entire--queryin single quotes so the shell doesn't try to evaluate them. - Filters and
--queryare different things.--filtersruns server-side before the response is sent.--queryruns client-side on the response. Use filters when you can — they're cheaper and faster. PublicIpAddressisNonefor instances without one. That's a string, not a missing field. Pipe throughgrep -v Noneif you only want public-facing instances.- The
Nametag is just a tag like any other. EC2 has no special concept of an instance "name" — it's a convention. If someone forgot to tag an instance, you getNonein that column. - Pagination matters for large fleets.
describe-instancesreturns up to 1000 instances per call. Use--max-itemsand--starting-tokenfor accounts with more than that.
Related Snippets & Reading
- Debug Which AWS Identity You're Using — first check before any EC2 inventory
- Tail CloudWatch Logs Live — for live debugging once you find the right instance
- aws ec2 describe-instances reference — every available filter and field
Frequently Asked Questions
Why use --query instead of jq?
--query uses JMESPath, which runs server-side in the AWS CLI before the response is returned. That means smaller responses, faster commands, and no jq dependency. jq is more powerful for complex transformations, but for simple flat outputs, --query is the right tool.
Why is the Name tag so awkward to extract?
EC2 stores tags as a list of {Key, Value} objects, not a map. To extract the Name tag, you have to query into the Tags array, filter where Key equals 'Name', and pluck the Value. It's verbose, but it's the price of EC2's flexible tag model.