kodebeat / papers / cost

Finding cloud resources that cost money and do nothing

A zombie-resource scanner with confidence scores.

Scans AWS for unattached volumes, unassociated Elastic IPs, load balancers with no targets, idle NAT gateways and RDS instances, orphaned snapshots and unused AMIs. Each finding carries a deterministic confidence score that never reaches 100, because a scanner cannot know your intent, and the monthly cost it would reclaim. It never deletes anything: it prints the commands and running them is on you.

The problem

Cloud waste accumulates by subtraction. Somebody deletes an EC2 instance and its 200 GiB volume survives, because the volume was created separately and deletion is not cascading. Somebody tears down a load balancer's targets and leaves the load balancer. A NAT gateway outlives the subnet that needed it. An Elastic IP is disassociated from an instance and stays allocated, billing hourly for doing nothing.

None of these has an owner, because they are all residue: what was left behind, not what anyone created in its current state. Nobody looks for residue, because looking requires knowing what to look for across a dozen resource types in every region.

The scale is unremarkable per item and material in aggregate: a 200 GiB unattached volume is about $16 a month, indefinitely. Fifty of them is $800 a month for storage nothing reads.

Deletion is also dangerous, and the danger is asymmetric. Deleting a NAT gateway that really was idle saves about $33 a month ($0.045 an hour in us-east-1). Deleting an unattached volume that somebody detached last week, pending a restore, destroys data with no undo. So the rational response to a list of deletion candidates is to do nothing, and that is what happens: the quarterly cleanup produces a list, the list is not acted on, and the next quarter produces a longer one.

There is a second trap. Deleting a resource that Terraform or CloudFormation manages does not stick: the next apply recreates it, the drift is confusing, and the person who tried concludes the cleanup was pointless.

The method

Score confidence, per type, from stated rules. Every finding carries a 0–100 confidence-to-delete. The README publishes one worked example, the unattached EBS volume:

an unattached volume starts at 50, gains +5 per week unattached (capped) and +10 if unnamed, capped at 95

The other seven types have rules of their own, set out in Table 1. Each term encodes a signal: an older resource is less likely to be pending something, and an unnamed one less likely to be wanted. No rule reaches 100, because a scanner cannot know your intent.

Penalise IaC-managed resources. Resources whose tags say CloudFormation, Terraform, CDK, Pulumi, Beanstalk, EKS or Kubernetes owns them, or whose managed-by-style tag names an IaC tool (Ansible included), score 30 lower, with a floor of 5. The fix for an IaC-managed zombie is a change to the code, and scoring it down keeps it out of the click-and-delete list.

Never delete. Print the commands. --commands emits ready-to-review AWS CLI invocations. The repository README shows:

$ idle-hunter scan --region eu-west-1 --min-confidence 80 --commands
# idle-hunter report — 3 finding(s), ~$34/mo reclaimable

[ 95%] ebs-unattached  vol-0abc…  (eu-west-1)  ~$16.0/mo
       200 GiB, created 210d ago, status=available
       $ aws ec2 delete-volume --region eu-west-1 --volume-id vol-0abc…
[ 90%] eip-unassociated  eipalloc-0def…  (eu-west-1)  ~$3.6/mo
...

The evidence line (200 GiB, created 210d ago, status=available) sits between the score and the command, so the decision is made with the facts in view. A scanner with delete permissions could cause the incident it exists to prevent, so running the command is left to a person.

A lost region is reported, not skipped. --all-regions scans in parallel (--workers, default 8). A region that fails is named on stderr and the run exits 3; a permissions error that silently dropped one region's findings would report a cleaner account than exists. expiry-radar applies the same rule to its expiry sources, exiting 3 on partial results.

Read-only IAM. ec2:Describe*, elasticloadbalancing:Describe*, rds:Describe*, cloudwatch:GetMetricStatistics, plus pricing:GetProducts for --live-pricing. There is no permission in the set that could delete anything.

The headline results

  • Every type has its own rule.
  • Age is capped early.
  • Pricing is labelled.
  • What the tests assert.

The full results, with the tables and the reasoning behind each figure, are in the PDF.

What is in the PDF

  • The problem — what goes unanswered without it, and who notices first.
  • Why the obvious alternative falls short — stated plainly, including where it is the better choice.
  • How it works — the method, not a feature list.
  • Concrete use cases — with console output quoted from the repository, never reconstructed.
  • The methodology behind any number it emits — every term shown, so the figure survives a question.
  • What it deliberately does not do — the section most papers leave out.

Part of the Cost & FinOps theme.

Get the PDF

This paper already selected; the download starts when you send the form. No follow-up sequence.

Get the PDF