Home / AWS

AWSCheat sheetBeginner

AWS CLI --query and --filters cheat sheet: fast inventory answers for EC2, EBS, RDS, Lambda and IAM

Copy-paste AWS CLI --filters and JMESPath --query patterns for everyday questions such as which instances are running, which volumes are orphaned and what still allows IMDSv1.

Deepak Bhardwaj6 min read

Applies toAWS CLI version 2; bash and PowerShell

The two filters, and when to use each

Option Where it runs Use it for
--filters (or --filter, --filter-expression, depending on the service) Server side, by the AWS API Cutting down what AWS sends back: state, tags, type, VPC
--query Client side, by the CLI (JMESPath) Choosing fields, renaming columns, sorting, counting, conditions the API can't filter on

Filter on the server first, then shape the result with --query. Filter names vary by service and command, so check aws <service> <command> help for the list.

aws ec2 describe-instances \
  --filters "Name=instance-state-name,Values=running" "Name=tag:Env,Values=prod" \
  --query "Reservations[].Instances[].{Id:InstanceId,Type:InstanceType,AZ:Placement.AvailabilityZone}" \
  --output table

JMESPath syntax in one table

Pattern Meaning Example
List[] Project and flatten a list Reservations[].Instances[]
List[0], List[-1], List[:5] Index and slice Images[:5]
.[a,b] Multiselect list (good for text) Volumes[].[VolumeId,Size]
.{A:a,B:b} Multiselect hash (good for table) Volumes[].{Id:VolumeId,GiB:Size}
[?expr] Filter Volumes[?State=='available']
'text' Raw string literal [?VolumeType=='gp2']
`20` JSON literal (numbers, booleans) [?Size > `500`]
| Pipe the result into another expression Tags[?Key=='Name'].Value | [0]
!, &&, || Not, and, or [?!AssociationId]
Functions length, sort_by, max_by, reverse, contains, starts_with, not_null length(Volumes)

Getting the Name tag

Tags are a list of Key/Value pairs, so you have to filter them:

--query "Reservations[].Instances[].{Id:InstanceId,Name:Tags[?Key=='Name']|[0].Value}"

Output formats and paging

--output json     # default; query runs once on the full result
--output table    # readable; best with a {} hash
--output text     # tab-separated; for shell loops and grep
--output yaml
--no-cli-pager    # don't open less; or: export AWS_PAGER=""
--max-items 50    # stop after N items (returns a NextToken)
--page-size 100   # smaller API pages if calls time out; still returns everything

Warning

With --output text, the CLI runs --query on each page separately. Expressions like [0], length() or sort_by() then return one result per page instead of one overall. Use --output json for counting and sorting.

EC2 inventory

# Running instances with name, type, private IP, launch time
aws ec2 describe-instances --filters "Name=instance-state-name,Values=running" \
  --query "Reservations[].Instances[].{Name:Tags[?Key=='Name']|[0].Value,Id:InstanceId,Type:InstanceType,IP:PrivateIpAddress,Launched:LaunchTime}" \
  --output table

# Count running instances by type (json output for correct counts)
aws ec2 describe-instances --filters "Name=instance-state-name,Values=running" \
  --query "Reservations[].Instances[].InstanceType" --output json | jq -r '.[]' | sort | uniq -c

# Stopped instances (still paying for their EBS)
aws ec2 describe-instances --filters "Name=instance-state-name,Values=stopped" \
  --query "Reservations[].Instances[].[InstanceId,Tags[?Key=='Name']|[0].Value]" --output text

# Instances still allowing IMDSv1
aws ec2 describe-instances --filters "Name=metadata-options.http-tokens,Values=optional" \
  --query "Reservations[].Instances[].InstanceId" --output text

# Instances missing an Owner tag
aws ec2 describe-instances \
  --query "Reservations[].Instances[] | [?!not_null(Tags[?Key=='Owner'].Value|[0])].InstanceId" --output json

# Latest AMI you own
aws ec2 describe-images --owners self \
  --query "max_by(Images,&CreationDate).{Id:ImageId,Name:Name,Date:CreationDate}"

EBS, snapshots and Elastic IPs (the clean-up list)

# Unattached volumes, largest first
aws ec2 describe-volumes --filters "Name=status,Values=available" \
  --query "reverse(sort_by(Volumes,&Size))[].{Id:VolumeId,GiB:Size,Type:VolumeType,Created:CreateTime}" \
  --output table

# gp2 volumes still to migrate to gp3
aws ec2 describe-volumes --filters "Name=volume-type,Values=gp2" \
  --query "length(Volumes)"

# Your snapshots older than a date (ISO strings compare correctly)
aws ec2 describe-snapshots --owner-ids self \
  --query "Snapshots[?StartTime<='2026-01-01'].[SnapshotId,VolumeId,VolumeSize,StartTime]" --output json

# Elastic IPs not associated with anything
aws ec2 describe-addresses --query "Addresses[?!AssociationId].[PublicIp,AllocationId]" --output text

Caution

These lists are where you start a clean-up, not proof that something can go. Check tags, AWS Backup and AMI references before you delete a volume or snapshot. A snapshot behind a registered AMI can't be deleted until the AMI is deregistered.

Networking

# Security groups with any rule open to 0.0.0.0/0
aws ec2 describe-security-groups --filters "Name=ip-permission.cidr,Values=0.0.0.0/0" \
  --query "SecurityGroups[].{Id:GroupId,Name:GroupName,Vpc:VpcId}" --output table

# Subnets with fewer than 20 free IPs
aws ec2 describe-subnets \
  --query "Subnets[?AvailableIpAddressCount < \`20\`].[SubnetId,CidrBlock,AvailabilityZone,AvailableIpAddressCount]" \
  --output text

RDS, Lambda, Auto Scaling and CloudWatch

# RDS inventory
aws rds describe-db-instances \
  --query "DBInstances[].{Id:DBInstanceIdentifier,Class:DBInstanceClass,Engine:Engine,Ver:EngineVersion,MultiAZ:MultiAZ,Encrypted:StorageEncrypted}" \
  --output table

# RDS instances without Multi-AZ
aws rds describe-db-instances --query "DBInstances[?MultiAZ==\`false\`].DBInstanceIdentifier" --output text

# Lambda functions on a given runtime
aws lambda list-functions \
  --query "Functions[?Runtime=='python3.10'].[FunctionName,LastModified]" --output text

# Lambda functions per runtime
aws lambda list-functions --query "Functions[].Runtime" --output json | jq -r '.[]' | sort | uniq -c

# Unhealthy instances in an Auto Scaling group
aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names web-asg \
  --query "AutoScalingGroups[].Instances[?HealthStatus=='Unhealthy'].InstanceId" --output text

# Alarms currently firing
aws cloudwatch describe-alarms --state-value ALARM \
  --query "MetricAlarms[].[AlarmName,StateUpdatedTimestamp]" --output text

IAM and S3

# Credential report: password and access key ages for every IAM user
aws iam generate-credential-report
aws iam get-credential-report --query Content --output text | base64 -d > cred-report.csv

# Roles not used recently (RoleLastUsed is only in get-role)
for r in $(aws iam list-roles --query "Roles[].RoleName" --output text); do
  echo "$r $(aws iam get-role --role-name "$r" --query 'Role.RoleLastUsed.LastUsedDate' --output text)"
done

# Buckets matching a prefix
aws s3api list-buckets --query "Buckets[?starts_with(Name,'prod-')].Name" --output text

Run it across every Region

for region in $(aws ec2 describe-regions --query "Regions[].RegionName" --output text); do
  n=$(aws ec2 describe-volumes --region "$region" --filters "Name=status,Values=available" \
        --query "length(Volumes)" --output json)
  echo "$region $n"
done

describe-regions returns only the Regions enabled for the account. For many accounts, run the loop with credentials for each account's role, or use AWS Config advanced queries from an aggregator.

PowerShell quoting

PowerShell treats the backtick as its escape character inside double-quoted strings. Raw string literals with single quotes work without changes. For JSON number literals, double the backtick:

aws ec2 describe-volumes --filters "Name=status,Values=available" `
  --query "Volumes[?Size > ``100``].[VolumeId,Size]" --output text

aws ec2 describe-instances `
  --query "Reservations[].Instances[].{Id:InstanceId,Name:Tags[?Key=='Name']|[0].Value}" --output table

You can also skip JMESPath and use PowerShell objects: aws ec2 describe-volumes --output json | ConvertFrom-Json.

Prevent mistakes

  • Test a new --query with --output json and --max-items 5 before you feed it to a loop that changes anything.
  • Use --output text only with plain projections, not with counts, sorts or [0] (see the warning above).
  • Wrap destructive loops in --dry-run where the EC2 API supports it, and log the IDs you act on.
  • Turn the queries you run every week into AWS Config rules or Config advanced queries so they report on their own.