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.
blog/detect-infra-driftInfrastructure 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
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=productionA 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-runWhat you gained
| Concern | Ad-hoc plan | OrchStep |
|---|---|---|
| Run a plan on schedule | someone remembers | a check task on cron |
| Count the diff | eyeball the output | captured into changes |
| Alert only on drift | noisy or nothing | if: gt (atoi ...) 0 |
| Cover every stack | a script per env | --var stack=... |
| Preview the logic | run 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
- Branching —
if/then/elseon a computed value - Variables & Outputs — capturing a value from one step for the next
- Continuous Integration — run OrchStep from a scheduled job
Install with curl -fsSL https://orchstep.dev/install.sh | sh, then orchstep run check to watch the drift branch fire.
curl -fsSL https://orchstep.dev/install.sh | sh