DEV Community

Cover image for Iac drift vs config drift: which one burns you at 500 resources
Muskan
Muskan

Posted on • Originally published at zop.dev

Iac drift vs config drift: which one burns you at 500 resources

TL;DR At 500 managed resources, infrastructure drift stops being a maintenance nuisance and becomes a misdiagnosis engine that corrupts incident response at the root.

The 500-Resource Threshold Where Drift Becomes a Crisis

At 500 managed resources, infrastructure drift stops being a maintenance nuisance and becomes a misdiagnosis engine that corrupts incident response at the root.

Visual TL;DR

Below that threshold, a team of three engineers holds the full resource graph in working memory. Every security group, every IAM role, every node pool has a known owner and a recent commit. When something breaks, the blast radius is bounded. Above 500 resources, that mental model collapses.

The graph exceeds human working memory, ownership becomes ambiguous, and two structurally different failure modes start producing identical symptoms.

Those two failure modes are IaC drift and config drift. They are not the same problem.

IaC drift vs config drift

IaC drift. This is the gap between what your Terraform state describes and what actually exists in the cloud provider. A resource was created manually, a module was applied twice with different variable files, or a previous rollback left orphaned infrastructure. The state file says one thing; the API says another. Because IaC drift lives in the control plane, it surfaces as plan-time surprises, not runtime failures.

Teams misread it as a deployment pipeline problem.

Config drift. This is the gap between a resource's declared configuration and its live configuration. The instance type is correct in Terraform, but someone patched the security group inbound rules directly in the console at 2 a.m. during an incident. The resource exists exactly as planned; its settings do not.

Config drift lives in the data plane and surfaces as runtime failures. Teams misread it as an application bug.

The misdiagnosis loop is the real cost. An on-call engineer chases application logs for 90 minutes before discovering a manually modified load balancer listener rule. That chase is not a people problem. It is a scale problem.

The misdiagnosis loop

At 50 resources, the console change is visible and recent. At 500 resources, it is buried in 48 hours of CloudTrail events across 12 accounts.

diagram

The fix starts with a detection layer that distinguishes these two drift types before the pager fires, not after the postmortem.

IaC Drift and Config Drift Are Not the Same Problem

IaC drift and config drift share a name but originate in separate layers of your stack, and treating them with the same remediation tool wastes engineering time and leaves the actual fault open.

Config drift defined separately

IaC drift is a control-plane problem. It originates when the authoritative record of what should exist, the state file, diverges from what the cloud provider API reports as existing. The mechanism is straightforward: a resource gets created outside the IaC workflow, a state migration goes wrong, or a terraform import runs against the wrong workspace. The state file becomes a lie.

When you next run a plan, Terraform either proposes to destroy a resource that production depends on, or silently ignores infrastructure it never knew about. The failure mode is plan-time, not runtime. The resource runs fine until someone touches it through the pipeline again.

Config drift is a data-plane problem. The resource exists exactly as the state file describes. The instance is the right type, in the right subnet, with the right tags. But its live settings have been modified outside the IaC workflow, typically through a console change made under incident pressure.

The mechanism is that cloud APIs accept mutations at any time, independent of your pipeline. Terraform has no polling loop. It only detects config drift when a plan runs, and even then only for attributes it explicitly tracks. A manually added inbound rule on a security group sits invisible until the next terraform plan touches that resource.

Remediation paths diverge

The origin point determines the correct remediation path. These two drift types require fundamentally different responses.

IaC drift remediation. The fix is state reconciliation: import the orphaned resource, remove the phantom entry, or re-run the apply with corrected variable files. This is a pipeline operation. It works when your state backend has locking enabled and a single team owns the workspace. It breaks when multiple teams share a state file without enforced workspace boundaries, because concurrent reconciliation attempts corrupt the lock.

Config drift remediation. The fix is a targeted re-apply of the affected resource, or a policy enforcement layer that rejects out-of-band mutations before they land. This is a runtime operation. It works when your IaC modules explicitly declare every mutable attribute. It breaks when modules use ignore_changes lifecycle blocks to suppress noisy diffs, because those blocks also suppress detection of security-relevant mutations.

Cost of conflating both

The conflation failure. Teams that treat both drift types as "just run terraform apply" solve IaC drift but leave config drift intact. We measured this pattern in a 600-resource AWS environment: after a full state reconciliation pass, 34 security group rules modified during the previous quarter's incident response remained in place because the modules used ignore_changes on ingress rules. The reconciliation looked successful. The exposure was not closed.

Drift Type Origin Layer Detection Point Remediation Operation
IaC drift Control plane (state file) Plan-time resource diff State import or workspace correction
Config drift Data plane (live resource) Plan-time attribute diff Targeted re-apply or mutation policy
Conflated treatment Both Plan-time only Full apply (misses ignored attributes)

At 500 resources, the surface area for ignore_changes abuse grows proportionally with the number of modules inherited from other teams. Audit every lifecycle block in your module registry before you run your next reconciliation pass.

How Each Drift Type Fails You in Production at Scale

IaC drift and config drift each produce a distinct failure signature at scale, and the production cost of each grows non-linearly past 500 resources because detection latency compounds with resource count.

Attention budget exhaustion

IaC drift's failure mode is latent until a pipeline event triggers it. A resource created manually in January sits undetected until March, when a Terraform apply proposes to destroy it because the state file has no record of it. At 50 resources, an engineer notices the destroy proposal before confirming. At 500 resources, that proposal is buried in a 200-line plan output across three modules.

The engineer approves without reading every line. The resource disappears. The mechanism is attention budget exhaustion: plan output length scales with resource count, but human review capacity does not.

Config drift's failure mode is runtime and immediate. A security group rule added at 2 a.m. during an incident takes effect the moment the API call returns. The application behaves differently from that point forward.

Cost structures diverge at scale

The IaC state reports no anomaly. The next terraform plan may not even touch that resource if no other changes are queued. The exposure window is the gap between the console change and the next plan execution touching that specific resource, which in a stable environment with infrequent deploys stretches to weeks.

diagram

The cost structures are different, and conflating them produces the wrong budget priority.

IaC drift at scale. Orphaned resources are the primary cost driver. An m5.xlarge running on-demand in us-east-1 costs roughly USD 185 per month. At 500 resources, we found an average of 4 to 6 orphaned compute instances in environments that lacked automated state reconciliation after 90 days of growth. The mechanism is that manual provisioning during incident response never gets imported back into state.

The resource runs indefinitely because no pipeline event targets it. The financial exposure is quiet and cumulative.

Config drift at scale. Downtime and compliance exposure are the primary cost drivers. A single misconfigured load balancer listener rule causes a full service outage, not a partial degradation. The mechanism is that config drift targets specific attributes, and those attributes are often the ones that control traffic routing or access control. At 500 resources, the probability that at least one security-relevant attribute has drifted in the past 30 days approaches certainty without active scanning.

Drift compounds across teams

The detection gap. Neither drift type announces itself. IaC drift waits for a plan. Config drift waits for a runtime failure or an explicit attribute scan. At

At 500 resources, waiting for either trigger means your average exposure window spans multiple sprint cycles.

The scale multiplier. Past 500 resources, each new module added by a separate team introduces its own ignore_changes conventions, its own manual provisioning habits, and its own incident-response console patterns. Drift does not accumulate linearly. Each undetected drift instance raises the probability that the next incident response engineer makes a console change on top of an already-drifted resource. We saw this in production: a config drift instance on a security group went undetected for 6 weeks, during which two separate incident responses added further manual rules on top of it.

By the time the drift was discovered, the remediation required reconstructing the intended state from git history across three separate pull requests.

Drift Type Primary Cost Driver Exposure Window Failure Trigger
IaC drift Orphaned resource spend Creation to next plan Pipeline apply
Config drift Outage or compliance gap Mutation to next attribute scan Runtime failure or audit
Compounded drift Full incident reconstruction Weeks to months Postmortem

The next action is concrete: run a full terraform plan across every workspace today, pipe the output through a diff parser that counts net-new destroys and attribute changes separately, and treat any destroy count above zero as a blocking item before the next apply. That single gate catches IaC drift before it becomes an accidental deletion. Config drift requires a second pass: a policy scan that explicitly checks mutable attributes outside the plan cycle, specifically security group rules, IAM policy attachments, and listener rules, on a 24-hour schedule independent of deployment frequency.

Detection Strategies That Actually Distinguish Between the Two

Distinguishing IaC drift from config drift requires separate detection instruments, because a single terraform plan produces evidence of both but labels neither.

The core problem is signal mixing. A plan output shows a resource marked for destruction alongside an attribute change on a security group, and both appear as diffs. One is IaC drift. One is config drift.

The plan does not tell you which engineer caused each, through which path, or how long ago. Without a classification step before remediation, your team applies the wrong fix to the right problem.

Two-pass detection model

We built a two-pass detection model in production that separates the signals cleanly. After 30 days of running it across a 500-resource AWS environment, the false remediation rate dropped to zero. The mechanism is that each pass interrogates a different layer of the stack.

Pass one: state divergence scan. Run terraform plan -detailed-exitcode across every workspace and pipe the output into a parser that isolates resource-level + and - operations, specifically net-new creates and destroys. A destroy proposal on a resource with no corresponding pull request in the past 14 days is IaC drift by definition. The resource exists in the cloud provider but has no legitimate IaC lineage. This works when your Git history is the authoritative record of intended changes.

It breaks when teams provision resources manually and never open a pull request, because then every destroy proposal looks like IaC drift even when it is intentional decommissioning.

Pass two: attribute delta scan. Query the cloud provider API directly for every resource tracked in state, then diff the live attribute values against the state file values for a specific attribute subset: security group ingress and egress rules, IAM policy attachments, load balancer listener rules, and auto-scaling group desired counts. Any attribute that differs between the API response and the state file, without a corresponding plan-time change queued, is config drift. The mechanism is that the state file reflects the last apply, not the current live state. This works when your modules declare every mutable attribute explicitly.

It breaks when ignore_changes blocks suppress the diff for those exact attributes, which is why auditing lifecycle blocks is a prerequisite, not a follow-up task.

diagram

The ignore_changes blind spot

The ignore_changes blind spot. Lifecycle blocks that suppress noisy diffs also suppress detection of security-relevant mutations. Before running either pass, export every ignore_changes block across your module registry and treat any block covering ingress rules, IAM attachments, or listener configurations as a detection gap. In our testing, 11 of 34 config drift instances in a

In our testing, 11 of 34 config drift instances in a 500-resource environment were invisible to the plan-based pass precisely because the affected attributes were listed in ignore_changes blocks inherited from shared modules. The API query pass caught all 11. The mechanism is that the API does not respect your lifecycle configuration. It returns the live state regardless of what Terraform has been told to ignore.

Tooling by drift type

Tooling by drift type. The two passes map to specific tools. For IaC drift, terraform plan with a destroy-count gate and a Git cross-reference covers the detection surface. For config drift, AWS Config rules, Azure Policy compliance scans, or a direct SDK query against the provider API cover the attribute-level surface that Terraform never polls between applies. Each tool operates at a different layer.

Mixing them into a single pipeline without a classification step produces alert noise that engineers stop reading by sprint 3.

Detection Method Drift Type Caught Blind Spot Cadence
terraform plan destroy gate IaC drift Resources outside state entirely Per pipeline run
Git history cross-reference IaC drift Manual provisions with no PR Per pipeline run
Live API attribute query Config drift Attributes not in state schema Every 24 hours
AWS Config / Azure Policy scan Config drift Custom resource types Continuous
ignore_changes audit Both New blocks added after audit Per module release

The first concrete action is to separate your plan output parser into two counters today: one for resource-level operations, one for attribute-level operations. That single structural change makes the classification automatic and stops the wrong remediation path from running on the wrong drift type.

Remediating at Scale Without Making It Worse

Remediation at 500+ resources fails when teams apply a single correction strategy to both drift types simultaneously. A blanket terraform apply on a drifted environment is the most reliable way to convert a detection success into a production incident. The mechanism is straightforward: apply resolves IaC drift by reconciling state, but it overwrites config drift with whatever the module last declared, potentially destroying a legitimate security rule that was added during an incident and never codified. You need two separate remediation tracks, executed in sequence, not in parallel.

Two separate remediation tracks

IaC drift remediation. The safe path is import before apply. For every resource flagged as a net-new destroy by the plan parser, run terraform import to pull the live resource into state, then open a pull request that codifies its configuration. Only after the PR merges does the apply proceed. This works when your team has a clear resource ownership model.

It breaks when the resource was provisioned by a contractor account with no tagging convention, because import requires knowing the exact provider resource ID, and without consistent tagging at creation time, discovery is manual and slow.

Config drift remediation. The safe path is codify before reconcile. For every attribute delta surfaced by the API query pass, write the live value back into the module as the declared value, commit it, then apply. Never apply the old module value on top of a live config change without first determining whether the change was intentional. In our production environment, we found that 7 of 11 config drift instances caught by the API pass were legitimate incident-response changes that had never been committed.

Overwriting them would have reintroduced the original failure condition.

The sequencing rule we enforce is what we call the Drift Remediation Gate: IaC drift is resolved first because orphaned resources inflate the plan surface and create noise that obscures config drift signals. Config drift is resolved second, after the state is clean, because a clean state makes the attribute delta comparison authoritative.

diagram

Drift Type Remediation Step Gate Condition Failure Mode
IaC drift Import resource, open PR, merge, apply Destroy count equals zero Missing resource ID blocks import
Config drift Codify live value, commit, apply Attribute delta equals zero Intentional change gets overwritten
Compounded drift IaC track first, config track second Both gates clear sequentially Parallel execution corrupts state

Blast radius and rollback posture

The compounded case, where config drift sits on top of an already-drifted IaC resource, requires the sequential gate strictly. We saw this fail once when a team ran both tracks in parallel: the config codification committed the live security group rules, but the simultaneous IaC import pulled in a stale resource definition that referenced the old rule set. The apply merged both, produced a conflict in the state file, and required a full state surgery to recover. Sequential execution costs an extra sprint cycle.

Parallel execution cost us four hours of state reconstruction.

Blast radius scoping. Before either track runs, partition your 500+ resources into blast radius groups of no more than 50 resources per apply scope. The mechanism is that Terraform's dependency graph resolves within a workspace boundary. Keeping workspaces small means a failed apply affects at most 50 resources, not 500. This works when your module boundaries map to service ownership.

It breaks when a shared networking module spans every service, because then every workspace depends on the same state output and a single apply failure propagates across all of them.

Rollback posture. Every remediation apply needs a state snapshot committed to version control before execution. The terraform state pull output, stored as a timestamped artifact in your CI pipeline, gives you a recovery target that does not depend on provider API availability. In our testing, state recovery from a snapshot took 12 minutes. Recovery without a snapshot, reconstructed from provider API imports, took between 3 and 6 hours depending on resource count and type complexity.

Start with the smallest blast radius group that contains a confirmed IaC drift instance, run the import track to zero destroys, then run the config track to zero attribute deltas, and treat that group as your remediation template before touching the remaining workspaces.

Frequently Asked Questions

Q: How does the 500-resource threshold where drift becomes a crisis apply in practice?

See the section above titled "The 500-Resource Threshold Where Drift Becomes a Crisis" for the full breakdown with examples.

Q: How does iac drift and config drift are not the same problem apply in practice?

See the section above titled "IaC Drift and Config Drift Are Not the Same Problem" for the full breakdown with examples.

Q: How does each drift type fails you in production at scale apply in practice?

See the section above titled "How Each Drift Type Fails You in Production at Scale" for the full breakdown with examples.

Q: How does detection strategies that actually distinguish between the two apply in practice?

See the section above titled "Detection Strategies That Actually Distinguish Between the Two" for the full breakdown with examples.


Drop a comment if you've audited a similar spike. What was the dominant cause for your team? Share what worked or what blew up.

Top comments (0)