Drift Detection and Reconciliation
Drift is what happens when someone changes a resource in the Console or with gcloud and the Terraform code no longer matches reality — a firewall rule widened during an incident, a label added by hand, a service-account role granted out-of-band. The code still says one thing; the cloud now says another. Drift is not a bug in Terraform; it is the gap that opens whenever a change bypasses the pipeline.
A scheduled plan in CI is how you catch it. A non-empty plan on a config nobody touched means reality moved, and then you decide whether to reconcile the code to reality or revert reality to the code. The danger is not the drift itself but discovering it at the worst time — when an unrelated apply tries to revert someone's emergency fix mid-deploy.
Drift on GCP Specifically
GCP makes drift easy to create. The Console and gcloud make out-of-band changes trivial — a few clicks or one command and a managed resource has changed without going through Terraform. And GCP's eventually-consistent IAM means a hand-granted role shows up as drift on the next plan even when the grant was reasonable. The cloud does not stop anyone from editing what Terraform owns; it just records the change for the next plan to find.
The sharpest GCP drift source is the IAM _member / _binding / _policy trap from Chapter 8. Each of those resources manages a different scope — one member, one role's bindings, or the whole policy — so a role granted by hand may or may not surface as drift depending on which resource type your config uses. Manage members additively and an out-of-band grant can hide entirely, because Terraform is only watching the members it knows about.
Scheduled plan in CI
A Cloud Build trigger or a GitHub Actions cron runs terraform plan on a schedule against an unchanged config. A non-empty plan is the signal that something changed outside Terraform. The job's job is to surface that signal, not to apply — it reports drift and stops, leaving the decision to a human. Run it nightly and drift is caught in hours instead of discovered weeks later during an unrelated change.
The cron below runs a plan with a detailed exit code, so the pipeline can tell "no drift" from "drift found" from "error" and alert only on the middle one.
# exit 0 = no changes, 2 = drift detected, 1 = error terraform plan -detailed-exitcode -refresh-only -input=false if [ "$?" -eq 2 ]; then notify "drift detected in apps/prod" # alert a human; do not apply fi
Wiring the exit code to an alert is the difference between a job that runs silently and one that tells you something moved. The plan finds the drift; the notification makes someone look.
Reading Drift with -refresh-only
terraform plan -refresh-only updates state from reality without proposing config changes, showing you exactly what drifted at the state level. It is the read-only way to see drift, separate from a plan that wants to "fix" reality back to the config. A plain plan mixes two things — pending config changes you have not applied, and drift that happened in the cloud — and -refresh-only isolates the second so you see purely what reality did.
Use it when you want to understand a drift before deciding what to do about it. A normal plan answers "what would apply change"; -refresh-only answers "what changed underneath me," which is the question drift detection is actually asking.
Reconciling vs Reverting
There are two valid responses to drift, and the choice is a judgment call, not an automatic one. You reconcile the code to match a deliberate manual change — someone fixed prod correctly during an incident and you codify what they did so the code tells the truth again. Or you revert reality to the code by running apply — an unauthorized or accidental change you undo because it should never have happened.
Treating every drift as something to revert is a mistake as common as ignoring drift entirely. Sometimes the manual change was the right call and the code is what is wrong; reconciling the code, not reverting the fix, is then the correct move. The detection job surfaces the drift; a human decides which direction to close the gap.
Alerting and Why Auto-Apply Is Dangerous
Wire the scheduled job to alert when the plan is non-empty — a Cloud Monitoring signal, a Slack message, a failed build. The point is that drift gets noticed in hours, not discovered during the next unrelated change when an apply silently reverts it. An unmonitored drift is a landmine the next deploy steps on.
Do not auto-apply on a non-empty scheduled plan. Automatically reverting any drift can undo a legitimate emergency fix someone made in the Console during an incident, turning your drift-detection job into the cause of the next outage. Detection should alert a human; reversion should be a decision. That separation is the whole safety property — the job that finds drift must never be the job that blindly fixes it.
Reconcile — update the code to match a deliberate, correct manual change. Use it when someone fixed prod the right way out-of-band and the code is now the thing that is wrong; codify their change so the config tells the truth.
Revert — run apply to undo the change and return reality to the code. Use it when the drift was unauthorized or accidental and the code already reflects what should exist. Never automate this on scheduled drift, since it can undo an emergency fix.
- Having no drift detection and discovering a hand-made Console change only when an unrelated
applytries to revert it mid-deploy, causing a surprise outage. - Running a plain
planfor drift and confusing config changes with drift — use-refresh-onlyto see purely what reality did, separate from pending code changes. - Auto-applying on any non-empty scheduled plan — you silently revert an emergency fix someone made in the Console during an incident.
- Ignoring IAM drift because GCP's additive
_memberresources do not show the full policy — a role granted by hand outside Terraform's managed members goes unnoticed. - Treating every drift as something to revert, when the right move is often to reconcile the code to a deliberate, correct manual change.
- Run a scheduled
terraform planin CI — a Cloud Build trigger or an Actions cron — and alert when the plan is non-empty. - Use
-refresh-onlyto inspect what drifted at the state level without a plan that proposes reverting it. - Decide reconcile-versus-revert per drift as a human judgment, and never auto-apply scheduled drift.
- Manage IAM with authoritative resources where drift detection matters, so hand-granted roles actually surface in the plan.
- Run the scheduled plan nightly so out-of-band changes are caught in hours, not weeks later during an unrelated deploy.
Knowledge Check
Why is drift especially common on GCP?
- The Console and gcloud make out-of-band changes trivial, and additive IAM resources can hide a hand-granted role from the plan
- Terraform's GCS backend refreshes the remote state on a timer and silently rewrites the live cloud resources behind your back to match
- GCP rotates each resource's self-link ID nightly, so the config no longer matches it by the next morning
- The google provider quietly applies pending config changes even during a plan-only, read-only run
What does terraform plan -refresh-only show that a plain plan does not isolate?
- Purely what drifted at the state level, separate from any pending config changes you have not applied
- A per-resource dollar cost estimate of the cloud charges incurred while each drifted resource sat out of sync
- The exact gcloud command and timestamp of the out-of-band edit that caused the drift
- A full list of which IAM principals currently hold access to each drifted resource
A teammate fixed a prod firewall rule correctly in the Console during an incident. What is usually the right response to the resulting drift?
- Reconcile — update the code to match the deliberate, correct manual change so the config tells the truth
- Revert immediately with a terraform apply, since any unplanned manual change must always be undone
- Delete the firewall rule and recreate it from scratch through a fresh Terraform apply to clear the recorded drift
- Ignore it entirely, because firewall-rule drift is never significant enough to act on
Why should a scheduled drift-detection job alert a human rather than auto-apply?
- Auto-applying can silently revert a legitimate emergency fix, making the drift job the cause of the next outage
- Auto-apply runs slower than a manual apply on the scheduler and needlessly wastes paid CI minutes
- The GCS backend technically forbids any unattended terraform apply that is triggered on a fixed schedule rather than by a person
- A human-triggered apply produces cleaner, better-formatted plan output than the scheduled job does
You got correct