2026-06-16
    8 min read

    tfdrift: Detect Drift Across All Your Terraform Modules at Once

    Hardik Shah

    Hardik Shah

    Cloud Architect & AWS Expert

    Terraform
    Go
    DevOps
    Infrastructure as Code
    CI/CD
    Drift Detection
    Terragrunt
    IaC
    Open Source
    Automation
    tfdrift: Detect Drift Across All Your Terraform Modules at Once

    Terraform drift is one of those problems that sneaks up on you. Someone makes a quick console fix during an incident. A colleague tweaks a security group rule by hand. A managed service auto-updates a parameter. None of it shows up in your state file — until your next terraform apply does something unexpected, or worse, until there's an incident and you can't tell what changed.

    I built tfdrift to solve this for repositories that have grown beyond a single module. It's a Go CLI that scans an entire directory tree for Terraform and Terragrunt root modules, runs init and plan across all of them in parallel, and gives you a consolidated view of what has drifted — with HTML and PDF reports, JSON output for CI, and exit codes that mirror Terraform's own convention.

    Why Drift Is Hard to Track in Real Repos

    The official answer to drift is terraform plan. Run it, read the output, see what changed. That works fine for a single module. It stops working when you have a monorepo with fifteen environments and forty modules. You'd need to:

    1. Find every root module in the tree
    2. Run terraform init in each one
    3. Run terraform plan -detailed-exitcode and capture the exit code
    4. Aggregate the results into something readable
    5. Do it fast enough that it's useful

    That's exactly what tfdrift does. All of it, in a single command.

    Install

    Download a prebuilt binary from the Releases page. Each release ships archives for linux / darwin / windows × amd64 / arm64 plus achecksums.txt:

    bash
    # Linux amd64, v1.0.1
    curl -sSL https://github.com/hardik-aws/tfdrift/releases/download/v1.0.1/tfdrift_1.0.1_linux_amd64.tar.gz | tar xz
    sudo mv tfdrift /usr/local/bin/
    tfdrift --version

    Or install with go install:

    bash
    go install github.com/hardik-aws/tfdrift/cmd/tfdrift@latest

    Requires the terraform and/or terragrunt binary on PATH.

    Basic Usage

    Point tfdrift at your infrastructure directory and it does the rest:

    bash
    tfdrift ./infra

    It recursively walks ./infra, identifies every directory that contains*.tf files (or terragrunt.hcl if you use Terragrunt), and runs init + plan concurrently across all of them. Hidden directories,.terraform/, and .git/ are skipped automatically.

    The key flags:

    FlagDefaultWhat it does
    --toolterraformSwitch to terragrunt for Terragrunt repos
    --parallelism4Concurrent module workers
    --detailedfalseParse plan JSON to list each drifted resource
    --formatconsoleconsole or json on stdout
    --reporthtmlnone, html, pdf, or both
    --timeout10mPer-module init+plan timeout

    Per-Resource Drift Detail

    By default, tfdrift tells you which modules have drifted. Add --detailedand it tells you which resources and what changed:

    bash
    tfdrift --detailed ./infra

    When --detailed is set, tfdrift re-runs plan to a tfplan file, then uses show -json to parse every resource whose change.actionsis not ["no-op"], plus captures the human-readable diff block for each one. The HTML and PDF reports always collect per-resource detail automatically — you don't need the flag for reports, only for the console output.

    CI Gating with Exit Codes

    The exit code design mirrors terraform plan -detailed-exitcode exactly, so any CI system that already handles Terraform exit codes works without modification:

    Exit CodeMeaning
    0All modules clean
    2At least one module drifted (no errors)
    1At least one module errored (or bad flags / unreadable path)

    A GitHub Actions drift-gate job looks like this:

    yaml
    name: Drift Gate
    
    on:
      schedule:
        - cron: "0 6 * * *"   # daily at 06:00 UTC
      workflow_dispatch:
    
    jobs:
      drift:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
    
          - name: Install tfdrift
            run: |
              curl -sSL https://github.com/hardik-aws/tfdrift/releases/download/v1.0.1/tfdrift_1.0.1_linux_amd64.tar.gz | tar xz
              sudo mv tfdrift /usr/local/bin/
    
          - name: Configure AWS credentials
            uses: aws-actions/configure-aws-credentials@v4
            with:
              role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
              aws-region: us-east-1
    
          - name: Check for drift
            run: tfdrift --format=json --report=html ./infra > drift.json
            # exit 2 = drift found — job fails, team gets notified
    
          - name: Upload drift report
            if: failure()
            uses: actions/upload-artifact@v4
            with:
              name: drift-report
              path: report/drift-report.html
    Exit code 2 fails the job, so the team gets a notification when drift is found. The HTML report is uploaded as an artifact — engineers can download and search it without needing access to the CI runner.

    HTML and PDF Reports

    By default, tfdrift writes a styled HTML report to report/drift-report.html. The report groups output by module, with a header band showing the directory, tool, and status badge, followed by a per-resource table with columns for action, resource address, and the raw plan diff.

    The HTML report includes:

    • Client-side search — type an address like aws_iam_policyto filter to matching resource rows, collapsing unmatched modules
    • Status filter buttons — All / Drift / Error / Clean, no server required
    • Full plan diff per resource, so you can see exactly what changed

    The PDF report is generated by a pure-Go engine (no external binary required). It lacks the search UI but is suitable for audit trails and asynchronous review. Use --report=bothto generate both formats in one run.

    bash
    # Scan a Terragrunt repo, per-resource detail, 8 workers, both report formats
    tfdrift --tool=terragrunt --detailed --parallelism=8 --report=both ./live
    
    # CI: quiet logs, JSON stdout, fail on drift
    tfdrift --log-level=error --format=json ./infra > drift.json

    Why This Matters Beyond a Single Team

    Drift is not just a developer productivity issue — it is a security and compliance issue. A security group with an extra inbound rule that nobody remembers adding. An IAM role that accumulated policies through break-glass access that was never revoked. An S3 bucket with versioning disabled after a one-off storage optimization. None of these show up in your code review process, and none of them are visible until someone runs a plan.

    In multi-account AWS environments, this compounds. Drift in a development account is usually low risk. Drift in a production account that runs financial transactions or holds customer data is an entirely different conversation. Running tfdrift on a schedule across all of your environments — not just production — gives you a continuous signal rather than a periodic surprise.

    The goal is to make drift visible as early as possible, on a cadence that matches how often your infrastructure changes. That way, when something does drift, you find out within hours rather than weeks.

    Getting Started

    Install the binary, point it at your infra directory, and run it once to see the current state of your modules. Then wire it into a scheduled CI job so drift never goes undetected.

    bash
    # First run — see what's out there
    tfdrift --detailed ./infra
    
    # Quiet run for CI gating
    tfdrift --format=json --report=none ./infra; echo "Exit: $?"
    Hardik Shah

    About Hardik Shah

    Hardik is a dedicated Cloud Architect specializing in AWS solutions and DevOps automation. With years of industry experience, he focuses on building scalable, resilient architectures and sharing technical insights to help teams optimize their cloud-native journeys.