Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Automated cleanup of unused AWS resources to reduce costs
.claude/skills/aws-cost-cleanup/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-16 | ✗→✓ | ▲ Improved | — | — |
| case-09 | ✗→✓ | ▲ Improved | — | — |
| case-17 | ✗→✓ | ▲ Improved | — | — |
| case-11 | ✗→✓ | ▲ Improved | — | — |
| case-10 | ✗→✓ | ▲ Improved | — | — |
Automate the identification and removal of unused AWS resources to eliminate waste.
Use this skill when you need to automatically clean up unused AWS resources to reduce costs and eliminate waste.
Storage
Compute
Networking
bash#!/bin/bash # cleanup-unused-ebs.sh echo "Finding unattached EBS volumes..." VOLUMES=$(aws ec2 describe-volumes \ --filters Name=status,Values=available \ --query 'Volumes[*].VolumeId' \ --output text) for vol in $VOLUMES; do echo "Would delete: $vol" # Uncomment to actually delete: # aws ec2 delete-volume --volume-id $vol done
bash#!/bin/bash # cleanup-old-snapshots.sh CUTOFF_DATE=$(date -d '90 days ago' --iso-8601) aws ec2 describe-snapshots --owner-ids self \ --query "Snapshots[?StartTime<='$CUTOFF_DATE'].[SnapshotId,StartTime,VolumeSize]" \ --output text | while read snap_id start_time size; do echo "Snapshot: $snap_id (Created: $start_time, Size: ${size}GB)" # Uncomment to delete: # aws ec2 delete-snapshot --snapshot-id $snap_id done
bash#!/bin/bash # release-unused-eips.sh aws ec2 describe-addresses \ --query 'Addresses[?AssociationId==null].[AllocationId,PublicIp]' \ --output text | while read alloc_id public_ip; do echo "Would release: $public_ip ($alloc_id)" # Uncomment to release: # aws ec2 release-address --allocation-id $alloc_id done
bash# Apply lifecycle policy to transition old objects to cheaper storage cat > lifecycle-policy.json <<EOF { "Rules": [ { "Id": "Archive old objects", "Status": "Enabled", "Transitions": [ { "Days": 90, "StorageClass": "STANDARD_IA" }, { "Days": 180, "StorageClass": "GLACIER" } ], "NoncurrentVersionExpiration": { "NoncurrentDays": 30 }, "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 } } ] } EOF aws s3api put-bucket-lifecycle-configuration \ --bucket my-bucket \ --lifecycle-configuration file://lifecycle-policy.json
python#!/usr/bin/env python3 # calculate-savings.py import boto3 from datetime import datetime, timedelta ec2 = boto3.client('ec2') # Calculate EBS volume savings volumes = ec2.describe_volumes( Filters=[{'Name': 'status', 'Values': ['available']}] ) total_size = sum(v['Size'] for v in volumes['Volumes']) monthly_cost = total_size * 0.10 # $0.10/GB-month for gp3 print(f"Unattached EBS Volumes: {len(volumes['Volumes'])}") print(f"Total Size: {total_size} GB") print(f"Monthly Savings: ${monthly_cost:.2f}") # Calculate Elastic IP savings addresses = ec2.describe_addresses() unused = [a for a in addresses['Addresses'] if 'AssociationId' not in a] eip_cost = len(unused) * 3.65 # $0.005/hour * 730 hours print(f"\nUnused Elastic IPs: {len(unused)}") print(f"Monthly Savings: ${eip_cost:.2f}") print(f"\nTotal Monthly Savings: ${monthly_cost + eip_cost:.2f}") print(f"Annual Savings: ${(monthly_cost + eip_cost) * 12:.2f}")
pythonimport boto3 from datetime import datetime, timedelta def lambda_handler(event, context): ec2 = boto3.client('ec2') # Delete unattached volumes older than 7 days volumes = ec2.describe_volumes( Filters=[{'Name': 'status', 'Values': ['available']}] ) cutoff = datetime.now() - timedelta(days=7) deleted = 0 for vol in volumes['Volumes']: create_time = vol['CreateTime'].replace(tzinfo=None) if create_time < cutoff: try: ec2.delete_volume(VolumeId=vol['VolumeId']) deleted += 1 print(f"Deleted volume: {vol['VolumeId']}") except Exception as e: print(f"Error deleting {vol['VolumeId']}: {e}") return { 'statusCode': 200, 'body': f'Deleted {deleted} volumes' }
Discovery
Execution
Automation
bash# Run cleanup across multiple accounts for account in $(aws organizations list-accounts \ --query 'Accounts[*].Id' --output text); do echo "Checking account: $account" aws ec2 describe-volumes \ --filters Name=status,Values=available \ --profile account-$account done
bash# Create CloudWatch alarm for cost anomalies aws cloudwatch put-metric-alarm \ --alarm-name high-cost-alert \ --alarm-description "Alert when daily cost exceeds threshold" \ --metric-name EstimatedCharges \ --namespace AWS/Billing \ --statistic Maximum \ --period 86400 \ --evaluation-periods 1 \ --threshold 100 \ --comparison-operator GreaterThanThreshold
Medium Risk Actions:
Always:
bash# Analyze and cleanup in one command kiro-cli chat "Use aws-cost-cleanup to find and remove unused resources" # Generate cleanup script kiro-cli chat "Create a safe cleanup script for my AWS account" # Schedule automated cleanup kiro-cli chat "Set up weekly automated cleanup using aws-cost-cleanup"
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +23 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.