tforphans

Finds live infrastructure that no Terraform state manages: resources created
by hand, left behind by deleted configurations or made by other tools. Then
writes the import blocks to bring them under Terraform.
How It Works in 30 Seconds
- List what is live. Every provider with
list resources
lists its objects through
terraform query (Terraform 1.14+), with the
root module's own provider configuration. Providers without them are
listed through their plural data sources (-scan) or a plugin.
- Read every state. The roots' own states, every root under
-state-roots, and any -state file or URL, for every workspace.
- Match. Each live object is looked up in the states by its
resource identity
(or by attributes for older state).
- Report. What matches nothing is unmanaged: a table, JSON, or import
blocks with
tforphans adopt. The last line says whether the check was
complete.
$ tforphans -state-roots ~/work/infra
SCOPE TYPE NAME IDENTITY SOURCE
account_id 123456789012 aws_instance debug-box account_id=123456789012, id=i-0abc, region=eu-north-1 .
account_id 123456789012 aws_s3_bucket tmp-export-2024 account_id=123456789012, bucket=tmp-export-2024, ... .
512 live, 498 managed, 0 uncertain, 12 ignored, 2 unmanaged; 41 states read, 187 resource types queried, 58 skipped (-v to list)
coverage: complete
(Illustrative output.)
Supported Providers
| Provider |
How it is listed |
What is covered |
Used on real infrastructure |
AWS (hashicorp/aws) |
list resources |
247 resource types in 6.68 |
not yet |
Google Cloud (hashicorp/google, google-beta) |
list resources |
152 resource types in 8.6 |
yes |
Azure (hashicorp/azurerm) |
list resources |
118 resource types in 5.9 |
not yet |
| Any other provider with list resources |
list resources |
whatever the installed version lists |
|
Cloudflare (cloudflare/cloudflare 5.x) |
-scan cloudflare, plural data sources |
about 95 of 271 resource types; the rest are mostly settings |
yes |
Proxmox VE (bpg/proxmox; telmate/proxmox in state) |
-scan proxmox, plural data sources |
VMs, containers, pools, users, groups, roles, HA, replication, SDN vnets, backup jobs, ACME accounts |
yes |
Vault (hashicorp/vault) |
plugin vault.sh |
auth methods, roles, secrets engines, policies |
|
Junos (jeremmfr/junos) |
plugin junos.py |
interfaces, BGP, policies, prefix lists, security zones, applications |
new |
| Anything else |
your own plugin: one JSON object per line |
|
|
List resource coverage grows with every provider release, without changes
here: tforphans -v lists the types a run skipped and why. Planned next:
GitHub (-scan github), then plugins for Helm and Kubernetes.
Quick Start
-
Install (macOS, Linux):
brew install 5xxeu/tap/tforphans
Other ways: Install.
-
Go to an initialized root module, with read-only credentials for its
provider in the environment, and run it:
cd infra/aws/prod && terraform init
tforphans
-
Give it the states of every other root that manages the same accounts,
or their objects show as unmanaged:
tforphans -state-roots ~/work/infra
-
Adopt what should be in Terraform:
tforphans adopt -state-roots ~/work/infra -o imports.tf
terraform plan -generate-config-out=generated.tf
For Cloudflare or Proxmox, no root module is needed:
export CLOUDFLARE_API_TOKEN=... # "Read all resources" template
tforphans -scan cloudflare -state-roots ~/work/cloudflare
Contents
- Quick Start, Why, Features, When Not to Use It, Install, Requirements
- How it works: listing, reading states, matching by identity or attributes,
-refresh
- Output, exit status and coverage, JSON output, config file
- Roots, modules and states:
-state-roots, -state, workspaces, Terragrunt
- Regions, zones and other list arguments:
-arg, zone=auto, your own list blocks, -types
- Ignoring known resources: the ignore file and the built-in presets
- Bringing orphans under Terraform:
tforphans adopt
- Providers without list resources:
-scan cloudflare, -scan proxmox
- Plugins: Vault, Junos, your own
- In CI, Security, Limitations
Why
Infrastructure created outside Terraform skipped code review, policy checks
(checkov, tfsec, OPA) and cost estimates (Infracost), because all of those
look at code. tforphans finds it, which helps with:
- security: hand-made access policies, identity providers, tokens, members,
tunnels, DNS records that may dangle (subdomain takeover);
- cost: forgotten instances, disks, IPs and other resources nobody owns;
- IaC coverage: knowing what still has to be brought under Terraform, and
doing it with
tforphans adopt.
It does not judge how a resource is configured (that is what CSPM tools
such as Prowler do) and knows nothing about prices; -format json hands
the findings to tools that do.
Features
- 🔎 No Provider Knowledge to Maintain: Resource types come from the providers' own list resources, so coverage follows provider releases.
- 🗂 Every State Counts:
-state-roots finds and reads every root module and workspace in a tree, through any backend Terraform supports; Terragrunt units too.
- 🪪 Exact Matching: Resource identity from state (Terraform 1.12+), checked against the provider that listed the object; older state is matched by attributes, and a match that cannot confirm the account or cluster is reported as uncertain, not as managed.
- ✅ Honest Coverage: The last line says whether "nothing unmanaged" can be trusted, with every failed listing, unread state and skipped type counted;
-strict turns gaps into a failed CI job.
- 📥 Adoption:
tforphans adopt writes import blocks for terraform plan -generate-config-out, with addresses that never collide with existing ones.
- 🧹 Little Noise: Objects the cloud creates on its own (default security groups, GKE nodes, managed rulesets) are hidden by default presets that go only by marks users cannot set; an ignore file covers the rest.
- 🌍 All Regions and Zones:
-arg region=... fans list queries out; -arg zone=auto reads every Google Cloud zone.
- ☁️ Beyond List Resources: Plural data sources for Cloudflare and Proxmox VE, with crash isolation; plugins for anything else.
- 🔐 Read Only: Never applies your configuration and plans it only with
-refresh, which writes nothing; read-only credentials are enough; no telemetry.
- 📄 Scriptable: Versioned JSON output, counts per type and per scope, a config file per audit.
When Not to Use It
- The objects belong to another tool by design (Argo CD, Helm, Kubernetes operators, CloudFormation): they show as unmanaged, because to Terraform they are. Ignore them, or use that tool's own drift detection.
- You want to know how resources are configured (public buckets, open security groups): that is a CSPM tool's job; feed it
-format json if you want both.
- The provider has neither list resources nor plural data sources: only a plugin can list it.
- OpenTofu without
terraform: OpenTofu has no query command yet; only -no-query with -scan and plugins can work (not tested).
Install
Binaries for Linux, macOS and Windows are on the
releases page, with
checksums, an SBOM and a build provenance attestation
(gh attestation verify tforphans_*.tar.gz -R 5xxeu/tforphans). With
Homebrew (macOS, Linux):
brew install 5xxeu/tap/tforphans
Or build it (Go 1.26 or newer):
go install github.com/5xxeu/tforphans@latest
Requirements
Terraform in PATH (or -terraform /path/to/bin):
| What |
Terraform |
Listing with terraform query (the default) |
1.14 or newer; tforphans checks and says so |
| Matching by resource identity |
state written by 1.12 or newer with a provider that records identities; older state is matched by attributes (less certain, see -refresh) |
adopt import blocks with identity |
1.12; with id 1.5 |
-no-query with -state, -state-roots, -scan, -plugin |
no query needed; tested only with 1.15 and 1.16 so far |
The tests run against 1.15 and 1.16. Roots that are queried must allow 1.14
in their required_version (-ignore-required-version overrides it for
the run); roots whose state is only read need not.
Credentials: whatever the providers and backends need; read-only is enough
(AWS ReadOnlyAccess, GCP Viewer, Azure Reader, a Cloudflare "Read all
resources" token, a Proxmox PVEAuditor token).
Exit status: 0 nothing found, 2 unmanaged resources found, 1 an error
or, with -strict, incomplete coverage. See Output, exit status and
coverage.
- driftctl solved the same problem with
its own scanners for every resource type; it is in maintenance mode.
tforphans keeps no provider knowledge of its own and relies on
terraform query, so coverage follows the providers.
- tfimport works in the opposite
direction: you write the configuration for existing infrastructure and it
works out the import IDs. It does not look for unknown resources.
Development
go test ./...
go test -tags integration . # needs terraform; builds testdata/fakeprovider
CI (.github/workflows/ci.yml) runs gofmt, go vet, the unit tests with
the race detector, golangci-lint, govulncheck and the integration tests
against Terraform 1.15 and 1.16. The integration tests use a fake provider
(things are files in a temporary directory) that implements list resources
and resource identity, and run the whole flow including the import round
trip; the Proxmox test runs the real bpg/proxmox against a fake Proxmox VE
API.
A v* tag builds the release with GoReleaser (.goreleaser.yaml) as a
draft, with checksums, SBOMs and a build provenance attestation, and pushes
the Homebrew cask to 5xxeu/homebrew-tap (secret HOMEBREW_TAP_TOKEN: a
fine-grained token with contents write on that repository only). The cask
points at the release files, so publish the draft soon after.
License
MIT, see LICENSE.