Detect and Remediate State Drift in Python IaC

Drift is the gap between what your code says and what the cloud actually holds — a console edit, a manual hotfix, an out-of-band automation. This guide, part of managing IaC state under Python IaC fundamentals and strategy, shows how to detect drift with Pulumi and CDKTF and how to remediate it safely, building on idempotency and drift detection.

Problem Framing

State records what your tool believes exists. When reality diverges — someone widens a security group by hand — the next apply may revert the change, or worse, act on stale assumptions and delete something. Detecting drift on a schedule turns these surprises into reviewable diffs instead of 2 a.m. incidents.

Drift lifecycle Drift lifecycle: Declared state → Refresh → Detect diff → Remediate → repeat. Declared state Refresh Detect diff Remediate
A scheduled refresh compares declared state to reality and surfaces drift for remediation.

Prerequisites

Prerequisites Prerequisites: layered from Python interface / API down to Cloud runtime. Python interface / API Typed resource model Provider plugin State backend Cloud runtime
Prerequisites: the stack from the Python interface down to the cloud runtime.
  • A configured remote state backend so refresh reads the shared source of truth
  • Read access to the live cloud resources for the refresh credentials
  • A CI schedule (cron) able to run pulumi refresh or cdktf diff non-interactively
# CLI: dry-run a drift check without changing anything
pulumi preview --refresh --diff

Detecting Drift in CI

Run a refresh-and-preview on a schedule and fail the job when it reports changes. For Pulumi, --expect-no-changes turns any drift into a non-zero exit; for CDKTF, parse the cdktf diff output.

Scheduled drift check Scheduled drift check: cron trigger then pulumi refresh then preview diff then alert on change cron trigger pulumi refresh preview diff alert on change
A scheduled job refreshes state, diffs, and alerts when reality has diverged.
# drift_gate.py — fail CI when a Pulumi stack has drifted
# CLI: python drift_gate.py   (run from a scheduled pipeline)
import subprocess, sys

def check(stack: str) -> int:
    # State implication: --refresh updates state from reality before diffing.
    proc = subprocess.run(
        ["pulumi", "preview", "--refresh", "--expect-no-changes", "-s", stack],
        capture_output=True, text=True)
    if proc.returncode != 0:
        sys.stderr.write("DRIFT DETECTED\n" + proc.stdout)
    return proc.returncode

if __name__ == "__main__":
    raise SystemExit(check("prod"))

Remediation Strategies

There are two honest responses to drift. Revert — accept your code as the source of truth and pulumi up to bring reality back. Adopt — the manual change was correct, so encode it in code and refresh so state matches. Never leave drift unresolved: an unreconciled diff makes the next unrelated deploy dangerous.

Respond to drift Respond to drift: choose among 3 options. Was the manual changeintended? no Revert via up yes Encode in code unknown Investigate first
Decide per drift whether code or reality is correct, then reconcile.

Verification

After remediating, a fresh refresh-and-preview must report no changes — the definition of reconciled.

Verification Verification: Test → Program → Mock/Cloud. Test Program Mock/Cloud invoke declare resolve assert
Verification: the test drives the program and asserts on resolved values.
# CLI: prove the stack is reconciled
pulumi preview --refresh --expect-no-changes -s prod && echo "no drift"

Gotchas & Edge Cases

Gotchas & Edge Cases Gotchas & Edge Cases: Where it breaks with 3 facets. Where it breaks Edge Cases watch this boundary Provider watch this boundary Drift watch this boundary
Gotchas & Edge Cases: the boundaries where things break and what to check.

Refresh can mask deletions. If a resource was deleted out of band, refresh removes it from state, and the next apply recreates it — usually what you want, but confirm before applying in production.

Provider defaults look like drift. Some providers normalise values (tags, ordering), producing perpetual diffs. Pin or ignore those fields with resource options rather than fighting them every run.

Secrets in the diff. Drift output can print sensitive values; scrub logs or mark those outputs secret so drift reports stay safe to share.

FAQ

How often should I check for drift?

Daily is a good default for production; high-blast-radius environments benefit from hourly. Tie the cadence to how quickly out-of-band changes hurt you.

Does refresh change my infrastructure?

No — refresh only updates state to match reality. It is up/apply that changes cloud resources.

Can I detect drift without a schedule?

You can run it ad hoc, but scheduled checks are what make drift visible before it causes an incident.