tf-drift

CLI Drift Detection Tool (tf-drift)

tf-drift is a Go-based CLI tool designed to recursively scan, detect, filter, and display configuration drift across multiple Terraform or OpenTofu workspaces/layers in a workspace (like your-infrastructure-dir).

Architecture & Workflow

The tool operates in four distinct phases:

  1. Pre-Discovery (Recursive Scanning): Walk the target directory (defaulting to the current working directory .) to identify all directories containing .tf files and a backend configuration block.
  2. Selection: Apply -env, -layer, -include, and -exclude, then optionally show a checkbox picker in interactive mode.
  3. Parallel Processing (Worker Pool): Resolve the selected IaC engine, queue selected layers into a concurrent worker pool of a bounded size (configured via -concurrency), and for each layer run <engine> init (if needed), <engine> plan -detailed-exitcode -lock=false, and <engine> show -json when the plan exits with code 2.
  4. Classification and display: Parse structured plan JSON. Classify resource_drift entries as EXTERNAL_DRIFT and ordinary resource_changes entries as PLANNED_CHANGE, then apply the selected -mode filter before reporting. Interactive mode presents a live Bubble Tea dashboard; non-interactive mode emits text, JSON, markdown, Slack, or SARIF output.

Workflow Diagram

The workflow now includes a selection step before workers start.

Architecture Diagram

TUI Layout & Features

Display Paths

TUI views and human-readable reports shorten paths under the current user’s home directory with ~. This keeps app output readable and avoids displaying /Users/<name>, /home/<name>, or /root in normal UI surfaces. Engine execution, layer selection, rules evaluation, and JSON output keep raw paths for compatibility.

Engine Selection

The -engine flag selects which executable runs plans:

Value Behavior
auto Default. Prefer tofu when present, then fall back to terraform.
terraform Require the terraform executable.
opentofu or tofu Require the tofu executable.

The resolved executable is used consistently for init, plan, and show -json. Error messages must name the selected executable so CI logs explain whether Terraform or OpenTofu failed. Non-interactive runs set TF_IN_AUTOMATION=1 for child commands.

OpenTofu compatibility depends on the same plan JSON contract used by Terraform-compatible versions. The runner keeps TF_PLUGIN_CACHE_DIR behavior and continues to treat .terraform as the default data directory because OpenTofu preserves those compatibility surfaces.

OpenTofu-specific suggestions should focus on known migration failures: missing explicit provider source addresses, registry resolution changes, provider version jumps, state encryption key configuration, and saved plan sensitivity.

Scan Mode and Classification

The -mode flag controls which classified changes are returned and therefore which changes trigger exit code 2:

Value Behavior
both Default. Report external drift and pending config changes.
drift Report only external infrastructure drift from plan JSON resource_drift.
plan Report only pending configuration changes from plan JSON resource_changes.

When the same resource address appears in both resource_drift and resource_changes, tf-drift reports the EXTERNAL_DRIFT entry and skips the same-address planned entry. This avoids double-counting drift remediation as an ordinary pending config change.

Non-Interactive Output

The -format flag supports:

Value Behavior
text Human-readable stdout summary for local runs and plain CI logs.
json Structured machine-readable result list with raw paths, status, counts, and classified changes.
markdown GitHub-flavored table with detail sections for comments and release notes.
slack Slack-oriented text with status icons and a scan summary.
sarif SARIF 2.1.0 log for GitHub code scanning and CI annotation uploads.

SARIF output treats layer errors as execution errors, external drift as tf-drift.external-drift, and pending config changes as tf-drift.planned-change. Severity maps to SARIF levels with CRITICAL and HIGH as error, MEDIUM as warning, and LOW as note.

Rules Configuration

Rules keep backward compatibility with severity_classification, where a resource type maps directly to a severity. Ordered severity_rules add predicates for more precise classification:

Predicate Match
resource_types Terraform/OpenTofu resource type such as aws_iam_policy.
attributes Any changed top-level attribute.
actions Any managed action from the plan JSON, such as create, update, or delete.
classifications EXTERNAL_DRIFT or PLANNED_CHANGE.
layer_patterns Glob pattern against the slash-normalized layer path.
address_patterns Glob pattern against the resource address.

All configured predicates on a rule must match. The first matching rule wins, then severity_classification, then the default MEDIUM severity.

Large Monorepo Layouts

Large infrastructure repositories should run against explicit directory sets instead of the whole repository root when teams need faster, more reviewable output. -dir accepts direct directories, glob patterns, and brace choices, then discovery finds backend-enabled Terraform/OpenTofu config layers under each resolved root. Typical layouts:

tf-drift -dir "infra/{prod,stage}/aws" -include "network,shared-*"
tf-drift -dir "platform/accounts/*" -exclude "sandbox-*"
tf-drift -dir "services/*/infra" -mode drift -format sarif

Selection filters run after discovery and preserve discovery order. Use -include to build focused CI jobs for owned layers, -exclude to remove noisy sandboxes or generated fixtures, and -mode drift when CI should annotate external drift without failing on ordinary unmerged config changes.

Version Reporting

The CLI exposes -version and -v as aliases. Both print tf-drift <version> and exit before discovery or engine resolution.

Official release binaries receive the exact release tag from GoReleaser through -ldflags "-X main.version=", so tf-drift -version reports values such as tf-drift v1.0.0. Local make build and make install inject git describe --tags --always --dirty so source builds report the nearest tag, commit, and dirty marker instead of plain dev.

Decision Log

ID Date Decision Rationale
DEC-001 2026-06-15 Use native engine plan Ensures compatibility with custom providers and engine versions.
DEC-002 2026-06-15 Default to -lock=false Prevents blocking active deployment pipelines.
DEC-003 2026-06-15 Use Charm CLI bubbletea Gold standard for interactive, modern Go terminal interfaces.
DEC-004 2026-06-15 Worker pool reports via p.Send() Safely queues UI updates into the Bubble Tea runtime thread.
DEC-005 2026-06-19 Default -engine to auto Supports OpenTofu first when installed while keeping Terraform fallback for existing users.
DEC-006 2026-06-19 Shorten displayed home paths with ~ Keeps terminal and app output compact without changing execution paths.
DEC-007 2026-06-19 Keep version reporting build-time driven Release tags come from GoReleaser ldflags, while source builds use git metadata without runtime git calls.
DEC-008 2026-06-22 Split external drift from pending plan changes Terraform plan JSON exposes resource_drift separately from resource_changes, so reports should classify them instead of labeling every non-no-op plan delta as drift.
DEC-009 2026-06-22 Add SARIF and severity predicates CI annotations need a standard upload format, while teams need severity rules that can include layer, action, classification, address, and changed attributes.

References