- What counts as an unmanaged resource
- Why terraform plan cannot see them
- Finding them
- Triage before you codify
- Bringing one under management
- Staying at coverage
- Frequently asked questions
Every AWS account has resources nobody wrote down. The uncomfortable part is that your tooling is structurally incapable of telling you about them: terraform plan iterates the resources in your state, so anything outside state is not a finding, it is a blind spot. Your plan comes back clean because it looked only where your code already pointed.
What counts as an unmanaged resource
Anything running in your account that no infrastructure code creates or tracks. In practice they arrive through a handful of ordinary routes.
- Console changes. Someone fixed an incident at two in the morning by clicking. It worked, and nobody went back to codify it.
- Resources AWS creates for you. Default VPCs, default security groups, service-linked roles, log groups created on first write.
- Side effects of managed services. An EKS cluster creating network interfaces, a load balancer creating target groups.
- Predecessors. Infrastructure from before your team adopted IaC, still running because it works.
- Abandoned experiments. The proof of concept in a region nobody looks at.
- Other tools. Anything created by a different pipeline, a vendor integration or a partner account.
Only some of these are problems. Distinguishing them is the actual work.
Why terraform plan cannot see them
Worth stating plainly, because it is often misunderstood as a coverage gap rather than a design property.
A plan reads your state, refreshes each resource it contains against the provider, and reports differences. The loop is over resources in state. A resource that was never in state is never visited, so no amount of running plans more often will surface it.
This produces a specific and dangerous outcome. An account can be half unmanaged while every plan reports no changes. The signal you are watching is silent not because things are fine but because it is not looking there.
The number that matters is coverage: how much of what exists is under IaC management. Terraform cannot compute it, because it only knows what it manages. Any percentage requires enumerating the account independently.
Finding them
Three approaches, with different costs.
Enumerate and diff by hand. Describe every resource type in every region, list what is in your state, and subtract. Accurate and tedious, and it needs redoing every time. Reasonable once, to size the problem.
# everything Terraform believes it manages
terraform state list
# what actually exists, one service and region at a time
aws ec2 describe-instances --region ap-south-1 \
--query 'Reservations[].Instances[].InstanceId' --output text
AWS Config. Records an inventory of supported resource types with configuration history. Often already enabled, and genuinely useful for the question "what exists". It is not IaC-aware, so it cannot tell you which of those things your Terraform manages, and it bills per configuration item recorded.
A scanner that enumerates independently. Reads the account through a read-only role and reports everything, so unmanaged resources appear as findings rather than as silence. This is the coverage half of what driftctl provided.
Triage before you codify
The instinct on seeing the list is to import everything. That is usually wrong, and importing indiscriminately creates a Terraform configuration nobody wants to own.
Four buckets, in the order worth working through.
Delete. The largest bucket in most accounts, and the only one that pays immediately. Abandoned experiments, orphaned storage, resources for services long since retired. Deleting is faster than codifying and reduces both cost and attack surface. Start here, and pair it with the obvious storage leaks.
Codify. Anything load-bearing that ought to be reproducible. This is what an IaC generator is for, and reviewing the output is where the real decisions happen.
Leave deliberately. Some things should not be in your Terraform. Resources managed by another team's pipeline, AWS-created service-linked roles, and side effects of managed services all belong to something else. Importing them means your plan proposes changes to things you do not control.
Investigate. Anything you cannot categorise. An unexplained resource in an unused region deserves a look before it gets a label, particularly if nobody can say who created it.
Record the "leave deliberately" decisions somewhere. Otherwise the next person to run a coverage report finds the same resources, reaches the same uncertainty, and repeats the analysis you already did.
Bringing one under management
For a bounded set, Terraform's own import block generates the configuration for you. You supply each resource's identity, which is fine for tens and impractical for thousands. The mechanics are here.
For hundreds or thousands, generating configuration from a scan is the realistic path. Expect the same characteristics generated code always has: literal IDs that need lifting into variables, module boundaries drawn by service rather than by application, and a first plan to read carefully rather than apply. The full guide covers reaching a clean plan.
One rule regardless of route. Import, then plan, then read the plan, before applying anything. A first plan proposing to destroy and recreate a resource is usually an artefact of the import rather than a real difference, and applying it is how a codification exercise causes an outage.
Staying at coverage
Coverage decays. Every incident fixed by hand and every service that creates its own resources moves the number down, so a one-off cleanup regresses within months.
What holds it is re-reading the account on a schedule and treating new unmanaged resources as a finding rather than as background. Scheduled scans exist for this, and pairing them with event-based detection catches the console change as it happens rather than at the next sweep.
Frequently asked questions
What are unmanaged AWS resources?
Anything running in your account that no infrastructure code creates or tracks. They arrive through console changes made during incidents, resources AWS creates for you such as default VPCs and service-linked roles, side effects of managed services, infrastructure predating your IaC adoption, abandoned experiments, and resources created by other tools or teams.
Why does terraform plan not show unmanaged resources?
Because a plan iterates the resources in your state, refreshes each one and reports differences. A resource that was never in state is never visited. This means an account can be half unmanaged while every plan reports no changes, since the signal is silent because it is not looking there.
How do I find AWS resources not managed by Terraform?
You have to enumerate the account independently of your state and subtract what Terraform manages. That can be done by hand with describe calls per service and region compared against terraform state list, with AWS Config which records an inventory but is not IaC-aware, or with a scanner that reads the account through a read-only role and reports everything it finds.
What is IaC coverage and how do I measure it?
Coverage is the proportion of what exists in your cloud account that is under infrastructure-as-code management. Terraform cannot compute it because it only knows what it manages, so any percentage requires enumerating the account independently and comparing that inventory against your state.
Should I import every unmanaged resource into Terraform?
No. Sort them first. Delete abandoned resources, which is the largest bucket in most accounts and the only one that pays immediately. Codify what is load-bearing. Deliberately leave anything owned by another pipeline, AWS-created service-linked roles and side effects of managed services, because importing those makes your plan propose changes to things you do not control.
Why does my first plan want to destroy and recreate an imported resource?
Usually because the import left a required, force-new attribute empty, so the plan reads null becoming a value and proposes replacement. Nothing actually changed in AWS. Always import, plan, read the plan and only then apply, because applying a first plan unread is how a codification exercise causes an outage.
Can AWS Config tell me what is unmanaged?
It can tell you what exists, since it records an inventory of supported resource types with configuration history, and it is often already enabled. It is not IaC-aware, so it cannot tell you which of those resources your Terraform manages. It also bills per configuration item recorded, which grows with account size.
How do I stop unmanaged resources accumulating again?
Coverage decays continuously, because every incident fixed by hand and every managed service that creates its own resources moves the number down. A one-off cleanup regresses within months. What holds it is re-reading the account on a schedule and treating a new unmanaged resource as a finding, ideally paired with event-based detection so a console change surfaces immediately rather than at the next sweep.