COR
COR (Cloud Orphan Resources) is a CLI tool to find (and optionally delete) potentially orphaned AWS resources.
What it does
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)
Cost Impact
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
Note: Prices are approximate and vary by region. Check current AWS pricing for your region.
Install
From the repo:
make build # produces ./cor
./cor --help
make 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, …).
Authentication
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_ID and AWS_SECRET_ACCESS_KEY
Configuration (file + env)
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-1
COR_PROFILE=prod
COR_AUTH_METHOD=AWS_CREDENTIALS_FILE
COR_DELETE=true
COR_YES=true
COR_ON_ERROR=continue
COR_DRY_RUN=true
COR_FORMAT=json
COR_LOG_FORMAT=json
COR_METRICS_FILE=/var/log/cor.ndjson
COR_STATE_FILE=/var/lib/cor/state
COR_SORT_BY=Name
COR_SORT_DESC=true
Common flags
--region, -r: AWS region (default: us-east-1)
--profile, -p: shared config profile (default: default)
--auth-method, -a: AWS_CREDENTIALS_FILE or ENV_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 (default stop)
--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 (default table)
--log-format text|json: log line format (default text)
--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)
Examples
List orphaned EBS volumes:
./cor volumes --region us-east-1 --profile default
Delete orphaned EBS volumes (will prompt):
./cor volumes --delete
Filter ELB by name pattern:
./cor elbv1 --filter-by-name "prod-*" --show-tags
Filter 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 volumes
Preview deletes without calling AWS (--dry-run):
./cor volumes --delete --dry-run
Non-interactive delete for automation (--yes):
./cor volumes --delete --yes --on-error continue
Machine-readable output (--format):
./cor volumes --format json
./cor snapshots --format csv --sort-by Size --sort-desc > orphans.csv
Resumable 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.state
Per-run metrics + JSON logs (good for CI / cron):
./cor volumes --log-format json --metrics-file /var/log/cor-metrics.ndjson
List orphan AMIs, including those used by instances:
./cor images --include-used-by-instance
Check ECR for old/untagged images (30+ days):
./cor ecr --days-old 30
Find Route53 zones with only NS/SOA records:
./cor route53zones
List RDS instances and manual snapshots:
./cor rds
Find Lambda functions not invoked in 90+ days:
./cor lambda
./cor lambda --days-since-invocation 180 # Custom threshold
Find idle ElastiCache clusters:
./cor elasticache
./cor elasticache --hours-zero-connections 48 # Zero connections for 48+ hours
Find orphaned OpenSearch domains:
./cor opensearch
./cor opensearch --days-no-indexing 14 --hours-no-searches 48 # Custom thresholds
Find idle DynamoDB tables:
./cor dynamodb
./cor dynamodb --days-no-activity 60 # No activity for 60+ days
Find orphaned S3 buckets:
./cor s3buckets
./cor s3buckets --check-lifecycle # Also check for missing lifecycle policies
Find empty ECS clusters:
./cor ecs
Troubleshooting
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
--timeout flag 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
--region or -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
--profile to 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-by for very large result sets (requires buffering all results)
- If issues persist, check for resource leaks or file an issue on GitHub
Development
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 targets
Architecture, command pattern, and contributor notes live in CLAUDE.md,
CONTRIBUTING.md, and (for deeper backgrounder) research.md.
Profiling (optional)
This binary can expose pprof endpoints only when enabled:
COR_PPROF=1 enables the server
COR_PPROF_ADDR=:6060 sets the bind address (default :6060)
License
Apache-2.0 — see LICENSE and NOTICE.