Blog

Unmanaged resource detection, and why your plan will never do it

A terraform plan compares state to reality. A resource in no state file has nothing to compare against, so it stays invisible, however clean your plan output looks. Here is what actually finds it, and what each method costs.

If you run terraform plan in CI and it comes back clean, you know one thing: every resource your state file already knows about matches what you declared.

You know nothing at all about the resources it does not know about.

That is not a gap in your pipeline. It is the shape of the comparison. plan reads state, asks the provider about each resource in it, and reports differences. Something that was never in state is never queried, never compared, and never mentioned. A clean plan and an estate full of hand-made infrastructure are the same output.

Where unmanaged resources come from

None of these are anybody being careless. They are the normal operation of a team under time pressure:

Every one of these is running, reachable, and costing money. None of them will appear in a plan.

How you can actually find them

There are three honest methods, and they differ in what they cost you.

Read the bill. Cost tooling sees every resource, because every resource is billed. It is genuinely comprehensive and it is the wrong shape: it tells you a NAT gateway exists, not that no state file declares it. Good for spend, poor for ownership.

Import everything. terraform import or terraformer against an account, then diff. Thorough, and expensive enough that it happens once, as a project, and then does not happen again. The thing you need is a repeating answer.

Compare an inventory against your state. Enumerate what exists in the cloud, enumerate what your state files declare, and treat the difference as the finding. This is the only one of the three that is cheap enough to run on a schedule.

That third method is what “unmanaged resource detection” describes, and it comes with a precondition worth being blunt about.

The precondition nobody mentions

You cannot detect unmanaged resources without first defining managed.

The difference is only meaningful against a state source. Point a tool at a cloud account with no Terraform state connected and every resource is technically undeclared, which is true, useless, and indistinguishable from noise. The answer is only worth reading when the tool knows which state files to count as authority.

That has a practical consequence for scope. A comparison is bounded by what each credential can see: one AWS account, one GCP project, or the Azure subscriptions a credential is granted. Anything outside that boundary is not “clean”, it is unexamined, and a tool that does not distinguish those two is teaching you to trust a number it has not earned.

What a good finding looks like

An unmanaged-resource finding that is worth acting on answers four questions:

  1. What is it: type, name, and where it lives, precisely enough to find it in a console.
  2. Which scope: which subscription, account, project or cluster. On a multi-scope estate, two resources with the same name are the normal case, and a finding that cannot tell them apart is a finding you cannot action.
  3. Why is it unmanaged: no state file declares it, versus a state file declares it and it has since drifted. Different problems, different fixes.
  4. What now: adopt it into Terraform, delete it, or record a decision to leave it. A finding with no ending becomes a permanent yellow badge that everyone learns to scroll past.

The fourth is the one most tools skip, and it is the one that decides whether anybody is still using the tool in three months.

Adoption is the goal, not the alert

The useful end state for an unmanaged resource is that it stops being one. That means generating the Terraform that would declare it, importing it, and having the finding close because the cause went away, not because somebody dismissed it.

Which is why the honest measure of a drift tool is not how many findings it raises. It is how many of them ended.


Cloudkeel-DD does the third method: it reads your Terraform state or workspaces, enumerates what is live, and reports what nothing declares, per scope, with the adoption path attached. It is self-hosted and read-only: it runs in your own cluster, your credentials stay in your environment, and any remediation arrives as a pull request you review rather than a change it makes for you.

Scans are point-in-time and run on a schedule you set. Unmanaged detection covers an enumerated set of resource types per cloud, and the coverage page lists exactly which, including where the depth ends, because a page that says “everything” is the one you should not believe.

See what it finds in your own estate: one Helm command, no form and no account.