COR (Cloud Orphan Resources) is a CLI tool to find (and optionally delete) potentially orphaned AWS resources.
COR currently supports:
- EBS volumes: unattached volumes (
volumes) - EBS snapshots: snapshots not referenced by AMIs/volumes and not created by lifecycle policy (
snapshots) - AMIs: images not used by instances or launch templates (
images) - Elastic IPs: unassociated EIPs (
elasticips) - ENIs: unattached network interfaces (
enis) - ELBv1: classic ELBs with unhealthy instances (
elbv1) - ELBv2: application/network LBs with target groups that have no valid targets (
elbv2) - ELBv2 target groups: target groups not attached to any LB (
targetgroups) - Auto Scaling Groups: “empty” ASGs (no instances, min/desired=0, no LB/TG) (
autoscaling) - NAT gateways: list and optionally delete (
natgateways) - RDS: stopped instances + manual snapshots (
rds) - CloudWatch Logs: log groups (optionally delete) (
logs) - EFS: file systems with zero mount targets (
efs) - ECR: untagged/old container images (
ecr) - Route53: hosted zones with only NS/SOA records (
route53zones) - VPC Endpoints: interface endpoints with no ENIs (
vpcendpoints) - Client VPN: endpoints with zero active connections (
clientvpn) - Site-to-Site VPN: connections with no tunnels up (
vpnconnections) - Transit Gateway: VPC attachments without route table association (
tgwattachments) - Lambda Functions: functions not invoked in 90+ days, old versions, unused provisioned concurrency (
lambda) - ElastiCache: clusters with zero connections for 24+ hours (
elasticache) - OpenSearch: domains with no indexing/search activity (
opensearch) - DynamoDB Tables: tables with zero read/write activity, empty tables (
dynamodb) - S3 Buckets: empty buckets, incomplete multipart uploads (
s3buckets) - ECS Clusters: clusters with no services or running tasks (
ecs) - EC2 Instances: instances stopped for too long, or running with near-zero CPU (
ec2) - Bedrock: idle provisioned-throughput model units (
bedrock) - IAM Roles: customer-managed roles unused for 90+ days, excluding AWS service-linked roles (
iamroles) - IAM Policies: customer-managed policies attached to no principal and used as no permissions boundary (
iampolicies)
COR helps identify resources that continue to incur AWS charges even when orphaned or unused:
- EBS Volumes: ~$0.08-0.10/GB-month (gp3), ~$0.10/GB-month (gp2)
- EBS Snapshots: ~$0.05/GB-month
- AMIs: Includes underlying snapshot costs
- Elastic IPs: $0.005/hour when unassociated (~$3.60/month)
- NAT Gateways:
$0.045/hour + data transfer ($32/month minimum) - ALB/NLB:
$0.0225/hour ($16/month per load balancer) - Classic ELB:
$0.025/hour ($18/month) - RDS Instances: Varies by instance type (even when stopped, snapshots incur costs)
- RDS Snapshots: ~$0.095/GB-month
- CloudWatch Logs: $0.50/GB ingestion + $0.03/GB-month storage
- EFS: $0.30/GB-month (Standard), $0.016/GB-month (Infrequent Access)
- ECR: $0.10/GB-month for storage
- Route53 Hosted Zones: $0.50/month per zone
- VPC Endpoints:
$0.01/hour per AZ ($7/month) - Client VPN:
$0.10/hour ($72/month) - Site-to-Site VPN:
$0.05/hour per connection ($36/month) - Lambda Functions: Provisioned concurrency $100-500/month per function; storage for old versions
- ElastiCache: cache.m5.large ~$105/month, cache.r6g.xlarge ~$217/month
- OpenSearch: t3.small.search ~$26/month, r6g.large.search ~$101/month + $0.135/GB-month storage
- DynamoDB Tables: Provisioned mode $0.00065/hour per WCU, $0.00013/hour per RCU; storage $0.25/GB-month
- S3 Buckets: Storage $0.023/GB-month (Standard); request costs; incomplete uploads consume storage
- ECS Clusters: Free, but prevents cleanup of NAT Gateway, ALB, and related infrastructure
- IAM Roles / Policies: No direct charge — cleanup reduces attack surface and reclaims account entity quotas (default 1,000 roles / 1,500 customer-managed policies)
Note: Prices are approximate and vary by region. Check current AWS pricing for your region.
From the repo:
make build # produces ./cor
./cor --helpmake build injects version/commit/buildDate from git via -ldflags.
Plain go build -o cor . also works.
Run make help for the full target list (test, lint, cover, dist, …).
Global flag: --auth-method (default: AWS_CREDENTIALS_FILE)
- AWS_CREDENTIALS_FILE: uses your standard AWS shared config/credentials files with
--profile - ENV_SECRET: uses
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEY
COR reads config from:
--config /path/to/file.yaml, or$HOME/.cor.yaml
It also supports env vars prefixed with COR_ (hyphens become underscores).
Precedence: CLI flags > config file > env vars > defaults
Example env vars:
COR_REGION=us-east-1COR_PROFILE=prodCOR_AUTH_METHOD=AWS_CREDENTIALS_FILECOR_DELETE=trueCOR_YES=trueCOR_ON_ERROR=continueCOR_DRY_RUN=trueCOR_FORMAT=jsonCOR_LOG_FORMAT=jsonCOR_METRICS_FILE=/var/log/cor.ndjsonCOR_STATE_FILE=/var/lib/cor/stateCOR_SORT_BY=NameCOR_SORT_DESC=true
--region, -r: AWS region (default:us-east-1)--profile, -p: shared config profile (default:default)--auth-method, -a:AWS_CREDENTIALS_FILEorENV_SECRET--delete: actually delete/release resources (prompts for confirmation)--yes: skip the delete confirmation prompt — use only in automation--dry-run: print what would be deleted without calling AWS delete APIs--on-error stop|continue: per-item delete-error policy (defaultstop)--state-file <path>: persist deleted-resource keys; skip them on re-run--sort-by: sort output by a column name (buffers results; disables streaming)--sort-desc: descending sort--format table|json|csv: output format (defaulttable)--log-format text|json: log line format (defaulttext)--metrics-file <path>: append a per-run JSON metrics summary to this path--timeout: overall command timeout (e.g.5m)--config: config file path (default:$HOME/.cor.yaml)--all-regions: scan every enabled region for the account (one unified table)--min-cost <usd>: only show orphans estimated at or above this monthly cost--top-n <n>: show only the N most expensive orphans--rank: add a Rank column ordered by estimated monthly cost--save-baseline <path>/--diff-baseline <path>: snapshot orphans+costs, then diff a later run--with-ce: prorate snapshot/AMI/RDS estimates against actual Cost Explorer spend ($0.01 per call)
List orphaned EBS volumes:
./cor volumes --region us-east-1 --profile defaultDelete orphaned EBS volumes (will prompt):
./cor volumes --deleteFilter ELB by name pattern:
./cor elbv1 --filter-by-name "prod-*" --show-tagsFilter resources by tags:
./cor volumes --filter-by-tags "Environment=dev,Owner=team-a"
./cor elbv2 --filter-by-tags "CostCenter=engineering"Sort output for easier analysis:
./cor snapshots --sort-by "Size" --sort-desc
./cor volumes --sort-by "CreateTime"Use config file for consistent settings:
./cor --config ./prod-config.yaml volumesPreview deletes without calling AWS (--dry-run):
./cor volumes --delete --dry-runNon-interactive delete for automation (--yes):
./cor volumes --delete --yes --on-error continueMachine-readable output (--format):
./cor volumes --format json
./cor snapshots --format csv --sort-by Size --sort-desc > orphans.csvResumable runs with a state file:
# first run records every deleted ID; interrupt at any time
./cor volumes --delete --yes --state-file /tmp/cor-volumes.state
# re-run skips already-deleted IDs
./cor volumes --delete --yes --state-file /tmp/cor-volumes.statePer-run metrics + JSON logs (good for CI / cron):
./cor volumes --log-format json --metrics-file /var/log/cor-metrics.ndjsonList orphan AMIs, including those used by instances:
./cor images --include-used-by-instanceCheck ECR for old/untagged images (30+ days):
./cor ecr --days-old 30Find Route53 zones with only NS/SOA records:
./cor route53zonesList RDS instances and manual snapshots:
./cor rdsFind Lambda functions not invoked in 90+ days:
./cor lambda
./cor lambda --days-since-invocation 180 # Custom thresholdFind idle ElastiCache clusters:
./cor elasticache
./cor elasticache --hours-zero-connections 48 # Zero connections for 48+ hoursFind orphaned OpenSearch domains:
./cor opensearch
./cor opensearch --days-no-indexing 14 --hours-no-searches 48 # Custom thresholdsFind idle DynamoDB tables:
./cor dynamodb
./cor dynamodb --days-no-activity 60 # No activity for 60+ daysFind orphaned S3 buckets:
./cor s3buckets
./cor s3buckets --check-lifecycle # Also check for missing lifecycle policiesFind empty ECS clusters:
./cor ecsCost analysis (cost / pricing):
Every orphan command shows an estimated monthly cost per row. Two extra commands work on the cost dimension itself:
# total estimated monthly cost of every orphan type, in one table
./cor cost
./cor cost --all-regions
# rank the most expensive orphans of one type
./cor snapshots --rank --top-n 20
./cor volumes --min-cost 25
# inspect the rate table in effect (bundled defaults, or a refreshed cache)
./cor pricing show
./cor pricing show eu-west-1
# fetch live prices from the AWS Pricing API into ~/.cor/prices/<region>.json
./cor pricing refresh
./cor pricing refresh --all-regionsEstimates use bundled us-east-1 list prices (no Reserved Instance or Savings
Plan discounts). A region without a refreshed cache silently falls back to
us-east-1 rates, so run cor pricing refresh for accurate non-us-east-1 numbers.
A cost of — means the rate is unknown, not that the resource is free.
Track cost drift between runs:
./cor volumes --save-baseline /tmp/volumes-week1.ndjson
# ... a week later
./cor volumes --diff-baseline /tmp/volumes-week1.ndjson # added / removed / changedIAM hygiene (security, not cost):
iamroles and iampolicies are account-global (they run once even with
--all-regions) and have no Est $/mo column — they exist to shrink attack
surface and reclaim IAM entity quotas. Always audit first; IAM deletion is
irreversible.
# unused roles > 180 days, audit only (JSON for review)
./cor iamroles --max-unused-days 180 --format json
# also flag roles with no attached/inline policies and no instance profile
./cor iamroles --include-empty
# unattached customer-managed policies, preview deletion without touching AWS
./cor iampolicies --delete --dry-run
# clean up unused app roles under a path, non-interactive + resumable
./cor iamroles --path-prefix /application/ --delete --yes \
--state-file /var/lib/cor/iamroles.state --on-error continue
# safety allowlist: only touch entities you explicitly tagged
./cor iampolicies --require-tag cor-managed=true --delete --dry-runAWS service-linked roles (/aws-service-role/…) and AWS-managed policies are
always excluded and never deleted.
--require-tag key[=value] (comma-separated for multiple) is an opt-in
allowlist: when set, only entities carrying all the given tags are eligible —
anything else is not even listed. Recommended for shared accounts to restrict
COR to entities you own (e.g. cor-managed=true).
Required IAM permissions. Read-only (listing) needs:
iam:ListRoles, iam:GetRole, iam:ListAttachedRolePolicies, iam:ListRolePolicies,
iam:ListInstanceProfilesForRole, iam:ListPolicies, iam:ListPolicyVersions,
iam:ListPolicyTags
(iam:ListPolicyTags is only needed when using --require-tag on iampolicies;
role tags come back with iam:GetRole.)
--delete additionally needs:
iam:DetachRolePolicy, iam:DeleteRolePolicy, iam:RemoveRoleFromInstanceProfile,
iam:DeleteRole, iam:DeletePolicyVersion, iam:DeletePolicy
Permission Denied Errors
- Ensure your IAM user/role has read permissions for the resource type you're checking
- For delete operations, write/delete permissions are required
- Common required permissions:
ec2:Describe*,elasticloadbalancing:Describe*,rds:Describe*, etc. - Use AWS IAM Policy Simulator to verify permissions
Timeout Issues
- Use the
--timeoutflag to increase execution time for large environments - Consider filtering by region or resource tags to reduce scope
- Some commands (like snapshots, images) can take longer in accounts with many resources
No Resources Found
- Verify you're using the correct region with
--regionor-r - Check that resource state filters match your use case (e.g., volumes must be in 'available' state)
- Ensure your credentials have permission to view the resources
- Try running with
--profileto verify you're using the correct AWS account
Rate Limiting / Throttling
- AWS APIs have rate limits; COR implements retries with exponential backoff
- If you consistently hit limits, consider running during off-peak hours
- Use filtering options to reduce the number of API calls
Memory Issues
- COR uses streaming output by default to keep memory usage low
- Avoid using
--sort-byfor very large result sets (requires buffering all results) - If issues persist, check for resource leaks or file an issue on GitHub
The repo ships a Makefile covering the common workflows:
make build # compile ./cor with ldflags-injected version
make test # go test ./...
make test-race # go test -race ./...
make cover # write coverage.out + print the summary
make lint # golangci-lint run ./...
make vet # go vet ./...
make fmt # gofmt -s -w .
make tidy # go mod tidy
make vendor # go mod vendor
make run ARGS='volumes --region us-east-1'
make dist # cross-compile linux+darwin x amd64+arm64 into ./dist
make clean # remove ./cor, coverage.out, dist/
make help # list all targetsArchitecture, command pattern, and contributor notes live in CLAUDE.md,
CONTRIBUTING.md, and (for deeper backgrounder) research.md.
This binary can expose pprof endpoints only when enabled:
COR_PPROF=1enables the serverCOR_PPROF_ADDR=:6060sets the bind address (default:6060)
Apache-2.0 — see LICENSE and NOTICE.