← Recipes

阅读中文版本

Detect Terraform Drift on a Schedule

Run a read-only plan nightly, use detailed exit codes, and alert on real drift instead of waiting for the next apply to fail.

~30 minterraformdriftmonitoring

Task

Find out that infrastructure was changed by hand before a deploy either reverts it silently or fails halfway through.

When to use this

An engineer opens a security group in the console during an incident and forgets to revert it; the next apply closes it again in the middle of a release.

Prerequisites

  • Terraform 1.6 or newer
  • Read access to remote state from CI
  • A notification channel for alerts

Steps

1. Plan with detailed exit codes and without taking the lock.

terraform plan -detailed-exitcode -lock=false -input=false -out=drift.tfplan

Expected output: Exit code 0 means no changes, 2 means drift or pending changes, 1 means the plan failed.

2. Turn a drift exit code into a readable report for the alert.

terraform show -no-color drift.tfplan | head -60

Expected output: The report names each changed attribute and resource, which is attached to the alert.

3. Run the check nightly and notify only when the exit code is 2.

on:
  schedule:
    - cron: "17 3 * * *"
jobs:
  drift:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: terraform init -input=false
      - id: plan
        run: terraform plan -detailed-exitcode -lock=false -input=false -out=drift.tfplan
        continue-on-error: true
      - if: steps.plan.outputs.exitcode == '2'
        run: terraform show -no-color drift.tfplan >> "$GITHUB_STEP_SUMMARY"

Expected output: A matching plan produces no notification; real drift produces one alert with the summary.

4. Plan the deployed release instead of main so pending code changes are not reported as drift.

git checkout v1.4.0 && terraform plan -input=false -detailed-exitcode -lock=false

Expected output: Exit code 0 proves the deployed release matches the infrastructure; 2 means something outside Git changed it.

Troubleshooting

Problem

Every nightly run reports drift

Solution

Provider defaults and computed attributes can produce permanent diffs; after review, add a narrow lifecycle ignore_changes instead of muting the alert.

Problem

Alert storms from autoscaling or controller-managed resources

Solution

Exclude resources that controllers own from the drift scope, or plan only the modules your team manages.

Limitations

  • A read-only plan does not detect drift in resources that were removed from state.
  • Refresh is slow and rate limited on large estates; split stacks or use a cloud-native drift service.

Drift is silent until it hurts

Terraform assumes it is the only writer. When a console change or another tool modifies a resource, state still describes the old configuration and nothing complains until the next plan. That plan may revert the hand-made change during a release, or fail because the resource no longer matches what the provider expects. A scheduled read-only plan makes the divergence visible on the day it happens, while the person who made the change still remembers why.

Use exit codes, not parsed text

terraform plan has three meaningful outcomes, and -detailed-exitcode exposes them: 0 for no changes, 2 for changes, 1 for errors. Treat 2 as drift and 1 as a broken check, because a failed plan is not the same as a modified resource. Run with -lock=false so the nightly job does not block a deploy, and never apply the saved plan from a drift check. The plan is evidence, not a release artifact.

terraform plan -detailed-exitcode -lock=false -input=false -out=drift.tfplan
terraform show -no-color drift.tfplan | head -60

Separate drift from pending work

Plan the commit that is actually deployed, not the tip of main, otherwise a merged but unreleased change looks like drift. Store the deployed release tag in the pipeline so the drift job can check it out. When a diff is legitimate, codify the change in Terraform instead of accepting it; when it is provider noise, add a targeted ignore_changes with a comment. Review the drift report weekly, because an alert nobody reads is the same as no alert.