BLOG/TEAM CI/CD
THEME
TEAM CI/CD

Detect infra drift on a schedule

Someone fixed prod by hand at 3am and your Terraform state no longer matches reality. Run a plan on a schedule, count what differs, and alert only when there's actual drift.

May 19, 2026 OrchStep Team 6 minROLE: SRESCALE: Any
RUNNABLE DEMO
Full source for this post: blog/detect-infra-drift
VIEW SOURCE

Infrastructure drift is the gap between what Terraform thinks is running and what's actually running. It opens the moment someone fixes prod by hand at 3am — bumps a security-group rule, scales a node pool, flips a feature flag in the console — and forgets to put it back in code. The change works. The state file is now a lie. The next terraform apply either reverts the fix or fails in a way nobody can explain.

You catch this by running terraform plan on a schedule and reacting only when the plan is non-empty. The fiddly part is the "only when non-empty" — a drift check that alerts every night, drift or not, gets muted in a week, and a muted alert is the same as no alert. Here's a workflow that counts the diff and speaks up only when there's something to say.

The workflow

orchstep.yml
name: drift
# Run terraform plan on a schedule, count what differs from state,
# and alert only when there's drift.
defaults:
  stack: production

tasks:
  # `orchstep run check`
  check:
    steps:
      - name: plan
        func: shell
        do: echo "terraform plan -detailed-exitcode for {{ vars.stack }}"
        outputs:
          changes: "2"

      - name: evaluate
        func: shell
        do: echo "{{ steps.plan.changes }} resource(s) differ from recorded state"

      - name: alert
        if: '{{ gt (atoi steps.plan.changes) 0 }}'
        then:
          - name: notify
            func: shell
            do: echo "DRIFT in {{ vars.stack }} - {{ steps.plan.changes }} change(s)"
        else:
          - name: clean
            func: shell
            do: echo "no drift in {{ vars.stack }}"

The demo stubs the diff count as 2 so it runs anywhere. In a real check the plan step captures the number of changed resources from terraform plan output into changes, and the rest of the workflow reads that one value.

Alert only on real drift

The alert step is the whole point. It branches on whether the change count is greater than zero:

- name: alert
  if: '{{ gt (atoi steps.plan.changes) 0 }}'
  then:
    - name: notify
      func: shell
      do: echo "DRIFT in {{ vars.stack }} - {{ steps.plan.changes }} change(s)"
  else:
    - name: clean
      func: shell
      do: echo "no drift in {{ vars.stack }}"

atoi turns the captured count into a number so gt ... 0 is a real comparison, not a string check. A clean plan takes the else branch and stays quiet; a non-empty plan takes the then branch and pages you with the count. Your channel only ever hears from this job when something actually drifted, which is the only way an alert keeps its meaning.

In production the notify step is your real escalation — a Slack post, a PagerDuty event, an issue with the plan output attached — and terraform plan -detailed-exitcode is the natural source for the count, since it already distinguishes "no changes" from "changes present."

Check each stack

The stack is a variable, so one workflow covers every environment you manage:

orchstep run check --var stack=staging
orchstep run check --var stack=production

A scheduled runner calls it on a cron — the same install-and-run shape as any CI job — and you can dry-run it first to confirm the branch logic before you wire it to a pager:

orchstep run check --dry-run

What you gained

ConcernAd-hoc planOrchStep
Run a plan on schedulesomeone remembersa check task on cron
Count the diffeyeball the outputcaptured into changes
Alert only on driftnoisy or nothingif: gt (atoi ...) 0
Cover every stacka script per env--var stack=...
Preview the logicrun it on prod--dry-run

OrchStep doesn't replace Terraform or read your state — terraform plan still does the detection. It's the scheduled wrapper that turns a plan into a decision: stay quiet, or alert with a number.

Where to go next

Install with curl -fsSL https://orchstep.dev/install.sh | sh, then orchstep run check to watch the drift branch fire.

#SRE#TERRAFORM#DRIFT#INFRASTRUCTURE
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — TEAM CI/CD