strayform
Find Google Cloud resources that live outside Terraform — and adopt them in one step.
Someone created a VM "just for debugging". A bucket was spun up from the console for a one-off export. A node pool was added by hand during an incident. Months later nobody knows who owns them, they're not in code review, not in your cost model, and not covered by your policies.
strayform compares what actually runs in GCP (via Cloud Asset Inventory) with what your Terraform state says you manage, and reports:
- Unmanaged — resources created outside Terraform (ClickOps).
- Ghosts — resources in your state that no longer exist in GCP (deleted by hand).
- IaC coverage — the % of your supported infrastructure that is under Terraform.
Then it writes Terraform 1.5+ import {} blocks so you can bring the strays under management with terraform plan -generate-config-out.
$ strayform scan -s gs://acme-tfstate/prod -p acme-prod
IaC coverage 66.7% — managed 6, unmanaged 3, ghosts 1, ignored 3
Unmanaged resources (ClickOps)
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Terraform type ┃ ID ┃ Created ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ google_compute_instance │ projects/acme-prod/zones/us-central1-a/instances/debug-vm │ 2026-09-01 │
│ google_container_node_pool │ projects/acme-prod/locations/us-central1/clusters/core/... │ │
│ google_storage_bucket │ acme-prod-tmp-export │ │
└────────────────────────────┴──────────────────────────────────────────────────────────────┴────────────┘
Why another drift tool?
terraform plan only sees resources that are already in state — it can't tell you what was created around it. driftctl, the best-known tool for this, has been in maintenance mode since 2023 and its GCP coverage was limited. strayform is GCP-first:
- Uses Cloud Asset Inventory, so one API call covers a project, a folder or a whole organization.
- Knows GCP's noise: GKE nodes and boot disks, PVC disks, Kubernetes load balancer rules, the default VPC, Cloud Functions/Cloud Build buckets and other resources that Google creates for you are ignored by default (see
rules.py). - Normalizes project numbers vs IDs and the different ID formats Terraform uses (self links, bare names,
project/location/cluster/pool). - Generates ready-to-use import blocks, not just a report.
- CI-friendly: JSON / Markdown output and
--fail-on-drift.
Install
pip install strayform # or: pipx install strayform
Requires Python 3.10+ and Application Default Credentials:
gcloud auth application-default login
The identity needs roles/cloudasset.viewer on the scope, roles/browser (to resolve project numbers) and read access to the state bucket. Enable the API once: gcloud services enable cloudasset.googleapis.com.
Usage
# One project, local state
strayform scan -s terraform.tfstate -p my-project
# Several states (a directory or a GCS prefix) against a whole folder
strayform scan -s gs://my-tfstate/ -p folders/123456789012
# Generate import blocks, then let Terraform write the HCL
strayform scan -s gs://my-tfstate/prod -p my-project --generate-imports imports.tf
terraform plan -generate-config-out=generated.tf
# CI: markdown for a PR comment, non-zero exit when drift exists
strayform scan -s gs://my-tfstate/prod -p my-project -o markdown --fail-on-drift > drift.md
# Supported resource types
strayform types
Exit codes: 0 no drift (or drift without --fail-on-drift), 1 error, 2 drift found.
Remote state
strayform reads state files directly (*.tfstate, format v4) from local paths or gs:// — the default layout of the Terraform gcs backend (<prefix>/<workspace>.tfstate). For other backends, export first: terraform state pull > current.tfstate.
Ignore rules
Add a .strayform.yaml next to your code:
ignore:
- type: storage.googleapis.com/Bucket
name: "*-scratch-*"
reason: personal scratch buckets
- type: "compute.googleapis.com/*"
label: managed-by=config-connector
reason: owned by Config Connector
name is a glob matched against the resource's short name or full path; label is key or key=value. Use --no-default-ignores to see everything, including the Google-managed resources.
Supported resources
Compute (instances, disks, networks, subnets, firewalls, addresses, routers, global and regional instance templates, backend services and forwarding rules), Cloud Storage, BigQuery datasets and tables, Cloud SQL, Memorystore Redis, Pub/Sub, GKE clusters and node pools, Cloud Run (v1 and v2), Artifact Registry, Secret Manager and Cloud KMS key rings. Run strayform types for the full mapping.
A type is only added when its Cloud Asset Inventory name and its Terraform import ID map to each other deterministically, so the tool never guesses. Contributions for more types are welcome — see CONTRIBUTING.md.
Limitations
- Cloud Asset Inventory is eventually consistent: resources created or deleted in the last few minutes may not be reflected yet.
- Only resources whose type is supported are compared; coverage is computed over those types.
- Ghosts are only reported for projects inside the scanned scope.
Development
pip install -e ".[dev]"
pytest
ruff check .
License
Apache 2.0
Release files for strayform 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| strayform-0.1.0.tar.gz | 19.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| strayform-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 38.9 kB
Release files / strayform-0.1.0.tar.gz
| Download URL | strayform-0.1.0.tar.gz |
|---|---|
| Size | 19.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9ec324a36e7e85c19b71ec88b6e7aa1ec1961a2045f8645644e13d90b248f403
|
|
BLAKE2b-256 checksum How to use checksums |
f5d1ad3f1da155dfee6fa142da1c24dd90225d798021e04da40d4d371fbbd8f7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.4
|
Release files / strayform-0.1.0-py3-none-any.whl
| Download URL | strayform-0.1.0-py3-none-any.whl |
|---|---|
| Size | 19.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bb4b2949c554066a414b19b05fb604279f9805b3cbecf60a821b07c7de28e761
|
|
BLAKE2b-256 checksum How to use checksums |
e1c58af1506b3acf86dd8f62522e9678b049dc223a9bc5e86fa3cd4e1d4952bc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.4
|