Skip to main content

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)

Source distribution for strayform 0.1.0
File Size Uploaded
strayform-0.1.0.tar.gz 19.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for strayform 0.1.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page