
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.
The cleanest native approach doesn't require comparing IDs at all: it relies on a consistent tag and your cloud provider's tagging API.
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.
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.
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.txtRun 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.
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.tfCopy 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.
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.
