TrademarkTrademark
Features
Documentation

How to Find Resources Not Managed by Terraform or OpenTofu

Native methods for finding cloud resources that were never brought under Terraform or OpenTofu management: tag-based cross-referencing, state-diffing scripts, and Terraform's built-in search-and-import workflow.
Ryan FeeJuly 24, 2026
How to Find Resources Not Managed by Terraform or OpenTofu
Key takeaways
  • Unmanaged resources are a different problem than drift: drift is a known resource whose live config diverges from state, while an unmanaged resource was never recorded in any state file at all, so terraform plan can't see it.
  • The most reliable native method is tag-based: apply a consistent ManagedBy tag to everything Terraform or OpenTofu creates, then query your cloud provider's tagging API for resources missing it.
  • AWS's Resource Groups Tagging API GetResources only returns resources that have or had at least one tag; genuinely untagged resources need a separate AWS Resource Explorer tag:none query to surface.
  • A jq-based diff of terraform show -json state against a live cloud inventory (aws resourcegroupstaggingapi get-resources or az resource list) works without any tagging convention, at the cost of a script to maintain per provider.
  • Terraform 1.14 (GA December 2025) added a native search-and-import workflow: list blocks in a .tfquery.hcl file plus the terraform query command find unmanaged resources and generate the import and resource blocks to bring them into state, no third-party tool required.
  • Terraform search runs through the plain Terraform CLI (v1.12+ for resource-identity search) independent of backend, so it works the same way against Scalr-run workspaces as it does against any other remote-operations backend.

This post is part of a series on Terraform Drift Detection and Management: A Comprehensive Guide.

An engineer spins up an RDS instance from the console to unblock an incident. A script provisions a load balancer during a migration and nobody wires it into Terraform afterward. A teammate on a different team stands up an S3 bucket for a one-off export and forgets about it. None of these show up in terraform plan, because plan only compares your configuration against resources already in state. A resource nobody ever imported isn't drift; it's invisible.

This is a different problem from the drift this guide otherwise covers, and it needs a different fix. Drift detection tells you when a known resource changed underneath you. Finding unmanaged resources means going the other direction: starting from what actually exists in your cloud account and working out what Terraform or OpenTofu never touched. This post covers three native ways to do that without adopting a third-party scanning tool, plus the tagging discipline that keeps the list from growing back.

Method 1: Tag Everything, Then Query for What's Missing

The cleanest native approach doesn't require comparing IDs at all: it relies on a consistent tag and your cloud provider's tagging API.

Set the convention

Add a ManagedBy = "terraform" (or "opentofu") tag to every resource your configuration creates. In the AWS provider, the default_tags block on the provider configuration applies it automatically without touching every resource block:

provider "aws" {
  region = "us-east-2"
  default_tags {
    tags = {
      ManagedBy = "terraform"
    }
  }
}

The azurerm provider has the same mechanism.

Query for what's missing it

With AWS, the Resource Groups Tagging API's GetResources action returns every resource matching a filter, along with its tags, so you can flag anything missing ManagedBy:

aws resourcegroupstaggingapi get-resources \
  --resource-type-filters ec2:instance rds:db s3 \
  --query "ResourceTagMappingList[?!contains(Tags[].Key, 'ManagedBy')].ResourceARN"

One gap worth knowing before you rely on this: GetResources only returns resources that currently have, or previously had, at least one tag of any kind. A resource with zero tags ever won't appear in the results regardless of your filter. For those, AWS's own documentation points you to AWS Resource Explorer with a tag:none query instead. Run both if you want full coverage.

If your account uses AWS Organizations Tag Policies, GetResources also accepts IncludeComplianceDetails and ExcludeCompliantResources, which returns a MissingTagKeys list per resource, a useful shortcut if you've already codified your tagging requirement as an org-wide policy.

Azure's equivalent is Azure Policy's built-in "Require a tag and its value on resources" definition, assigned in Audit mode (not Deny, so it reports instead of blocking deploys) against your Terraform-managed resource groups or subscriptions. Query the compliance results with Azure Resource Graph to get a list of noncompliant resource IDs.

Method 2: Diff State Against Live Cloud Inventory

If you don't want to depend on a tagging convention, you can compare your Terraform state directly against your cloud provider's resource list. This is more scripting, but it doesn't assume anything was tagged correctly.

# Export every resource ID Terraform currently manages
terraform show -json | jq -r '.values.root_module.resources[].values.id' | sort -u > managed_ids.txt
 
# Export every live resource ID from the cloud provider
aws resourcegroupstaggingapi get-resources \
  --resource-type-filters ec2:instance rds:db s3 \
  --query "ResourceTagMappingList[].ResourceARN" \
  --output text | tr '\t' '\n' | sort -u > live_ids.txt
 
# Whatever's in live_ids.txt but not managed_ids.txt is unmanaged
comm -23 live_ids.txt managed_ids.txt > unmanaged_ids.txt

Run terraform show -json per workspace (state only covers what that workspace manages), and swap in az resource list --query "[].id" for Azure. The output format differs between resource IDs and ARNs, so you'll need to normalize before diffing; matching on the resource's unique ID substring rather than the full string is usually more reliable than an exact match.

This method scales about as well as any hand-rolled script: fine for a handful of workspaces, tedious once you're running it across dozens.

Method 3: Terraform's Native Search and Import Workflow

Terraform search, generally available in the Terraform 1.14 release (December 2025), is HashiCorp's first-party answer to this problem. Instead of tagging conventions or diff scripts, you write a query directly in HCL and let Terraform find and generate the import configuration for you.

Create a .tfquery.hcl file with a list block describing what to search for:

# search.tfquery.hcl
list "aws_instance" "unmanaged" {
  provider = aws
 
  config {
    region = "us-east-2"
    filter {
      name   = "tag:ManagedBy"
      values = ["unmanaged"]
    }
    filter {
      name   = "instance-state-name"
      values = ["running"]
    }
  }
}

Then run terraform query to see the matches, and add -generate-config-out=generated.tf to have Terraform write the import and resource blocks for every result:

terraform query -generate-config-out=generated.tf

Copy the generated blocks into your configuration, review them (generated config often needs edits for conflicting or computed arguments), and run terraform apply to bring the matched resources into state in one pass.

Two things worth knowing before you reach for it. First, resource-identity search requires Terraform v1.12 or newer, and provider support for the resources you want to query varies, so check your provider's documentation. Second, this runs through the plain Terraform CLI, not an HCP Terraform-exclusive API. An HCP Terraform account only adds an optional UI comparison against what other workspaces already manage; the search and import itself is backend-independent. That means it works the same way against a Scalr-run workspace as it does anywhere else, since the query talks directly to your cloud provider regardless of where your state lives.

At the time this is being written, the OpenTofu community is investigating the equivalent of this feature.

Keep the List From Growing Back

Finding unmanaged resources once is a cleanup project. Keeping the list empty is a policy. The most durable fix is enforcing your tagging convention at the provider level so it doesn't depend on every engineer remembering to add it to every resource block.

Scalr's AWS provider configuration supports the same default-tags mechanism directly: set a ManagedBy tag (and a merge strategy for handling conflicts with resource-level tags) once on the provider configuration, and every workspace that uses it inherits the tag automatically going forward. It's scoped to the AWS provider today and needs agent version 0.50.0 or higher for agent-based runs; check the Scalr AWS provider docs for current provider coverage. Pair that with a recurring GetResources or Resource Explorer query (Method 1, on a schedule) and the tag stays trustworthy instead of decaying the moment someone forgets it.

Further Reading

Frequently asked questions

What's the difference between drift and an unmanaged resource?

Drift is a resource already recorded in your Terraform or OpenTofu state whose live configuration has diverged from what's declared in code. An unmanaged resource was never recorded in state at all, so there's no baseline for terraform plan to compare against. Detecting drift means running a plan; finding unmanaged resources means cross-referencing your cloud inventory against state from the outside.

How do I find AWS resources that Terraform didn't create?

Tag everything Terraform creates with a consistent key like ManagedBy = "terraform", then call the Resource Groups Tagging API's GetResources action and filter for resources missing that tag or value. One gap: GetResources only returns resources that currently have, or previously had, at least one tag. For resources with zero tags ever, use AWS Resource Explorer with a tag:none query instead.

Can Azure Policy detect resources without a Terraform tag?

Yes. Assign the built-in "Require a tag and its value on resources" policy definition in Audit mode (not Deny, so it reports rather than blocks) scoped to your Terraform-managed resource groups or subscriptions, then query compliance results with Azure Resource Graph to list every noncompliant resource by ID.

What is Terraform search and how is it different from terraform import?

terraform import (or an import {} block) brings a specific, already-identified resource into state one at a time. Terraform search, generally available in Terraform 1.14 (December 2025), finds resources you haven't identified yet: you write a list block in a .tfquery.hcl file describing what to search for, run terraform query, and Terraform generates the import and resource blocks for every match so you can bulk-import them.

Does Terraform search require HCP Terraform?

No. The underlying list block and terraform query command are core Terraform CLI functionality (resource-identity search requires Terraform v1.12+; the broader workflow GA'd in 1.14), and they run the same way through the plain CLI regardless of backend. An HCP Terraform account is only needed for the optional UI comparison against resources other workspaces already manage, not for running the query itself.

How do I keep newly discovered resources from going unmanaged again?

Enforce the tag at the provider level instead of relying on every engineer to hand-tag resources. Both the AWS and azurerm Terraform providers support a default_tags block that auto-applies a tag to everything created through that provider configuration. Scalr's AWS provider configuration supports the same default-tag enforcement centrally, so every workspace using that provider configuration inherits it without per-workspace setup.
About the author
Ryan Feedirector of platform engineering at Scalr
Ryan Fee is the director of platform engineering at Scalr, with over 15 years of experience improving infrastructure experiences at companies large and small.