← All one-liners·#039·cost·aws·power

Unattached EBS volumes + snapshots → claude cleanup plan

Lists every `available` (unattached) EBS volume with size, age, and snapshot count, then asks claude to bucket each one into delete-now / archive-to-snapshot / keep with a dollar estimate.

Setup
  • → brew install awscli jq
  • → aws configure # or export AWS_PROFILE=prod
  • → IAM: ec2:DescribeVolumes, ec2:DescribeSnapshots (read-only; no delete perms needed for the audit)
  • → claude /login OR export ANTHROPIC_API_KEY=sk-…
Cost per run
<$0.01
The one-liner
$ REGION=${AWS_REGION:-eu-central-1}; NOW=$(date -u +%s); SNAPS=$(aws ec2 describe-snapshots --owner-ids self --region $REGION --query 'Snapshots[].VolumeId' --output json | jq 'group_by(.) | map({key: .[0], value: length}) | from_entries'); aws ec2 describe-volumes --region $REGION --filters Name=status,Values=available --query 'Volumes[].{id:VolumeId,size:Size,type:VolumeType,created:CreateTime,az:AvailabilityZone,tags:Tags}' --output json | jq --argjson snaps "$SNAPS" --argjson now $NOW -r '.[] | [.id, .size, .type, ((($now - (.created | fromdateiso8601)) / 86400) | floor), ($snaps[.id] // 0), ((.tags // []) | map(select(.Key=="Name")) | .[0].Value // "untagged")] | @tsv' | sort -k4 -rn | head -50 | claude -p "You are a FinOps engineer reporting to a CFO. Columns: volume_id, size_gib, type, age_days, snapshot_count, name_tag. gp3 is ~\$0.08/GiB-month, io2 ~\$0.125/GiB-month. For each row decide DELETE_NOW / ARCHIVE_THEN_DELETE / KEEP and give a one-line reason. End with a table sorted by monthly \$ saved, then total annualised savings."
What each stage does
  1. [01] awsSNAPS=$(aws ec2 describe-snapshots --owner-ids self ... | jq 'group_by(.) | map(…
    Builds a {volumeId: snapshotCount} map in one shot — `--owner-ids self` is mandatory or you get every public snapshot in AWS.
  2. [02] awsaws ec2 describe-volumes --filters Name=status,Values=available
    `status=available` is the canonical 'unattached' filter; `Attachments == []` is a noisy client-side check that misses attaching/detaching transient states.
  3. [03] jqjq --argjson snaps "$SNAPS" --argjson now $NOW -r '.[] | [.id, .size, .type, (((…
    Joins the snapshot map onto each volume, computes age_days from CreateTime, flattens to TSV so claude sees a tidy grid instead of 6KB of JSON noise.
  4. [04] jqsort -k4 -rn | head -50
    Sorts by age descending and caps at 50 rows — oldest unattached volumes are almost always the safest deletes and keeps the prompt under ~2k tokens.
  5. [05] claudeclaude -p "You are a FinOps engineer ... DELETE_NOW / ARCHIVE_THEN_DELETE / KEEP…
    Forces a 3-bucket taxonomy plus a dollar-ranked table — the CFO framing kills the usual hedging and produces an actionable memo.
Expected output (sample)
vol-0a1b2c3d4e	500	gp3	412	0	legacy-jenkins-data
vol-0f5e6d7c8b	100	io2	287	2	staging-pg-old
vol-09a8b7c6d5	30	gp2	91	1	untagged

# claude memo:
# vol-0a1b2c3d4e → DELETE_NOW (500 GiB gp3 unattached >1y, no snapshots — pure waste, $40/mo)
# vol-0f5e6d7c8b → ARCHIVE_THEN_DELETE (io2 staging, 2 snaps exist, delete vol keep snaps, $12.50/mo)
# vol-09a8b7c6d5 → KEEP (untagged, <90d — ping owner first)
# Total annualised: ~$630
Caveats & tips
  • Date math is portable here (`date -u +%s` works on BSD and GNU); CreateTime is parsed inside jq with fromdateiso8601 so no `date -v` vs `date -d` trap.
  • `describe-snapshots --owner-ids self` is REQUIRED — without it you pull every public snapshot in the AWS marketplace and the jq map explodes.
  • Volumes in `creating` or `error` state are skipped by the `available` filter; sweep them separately with `Name=status,Values=error`.
  • Encrypted volumes count toward your KMS key usage — deleting frees that quota too, worth mentioning to security.