Skip to content
Preptima
hardDesignScenarioSeniorStaffLead

A Terraform plan in CI wants to destroy resources in production. What has to happen before anyone is allowed to approve it?

Read what is being destroyed and what is being replaced, since a replacement of a stateful resource is a data loss dressed as an update. Then make the reviewed plan the artefact that is applied, put guard rails below the level a run can edit, and confirm the restore path matches the granularity of the damage.

6 min readUpdated 2026-07-29Target archetype: Big Tech, Enterprise Captive, Product Startup
Practice answering out loud

What the interviewer is scoring

  • Whether the candidate reads which resources are being replaced rather than only counting the destroys
  • Does the answer make the saved plan the applied artefact so the approved change and the executed change are provably identical
  • That splitting state is argued as blast-radius control rather than as tidiness
  • Whether guard rails are placed somewhere the same pull request cannot remove them
  • Does the candidate check that the restore path exists at the granularity the destroy would require

Answer

Read the plan, not the diff

The pull request tells you what someone intended. The plan tells you what will happen, and they diverge often enough that reviewing only the code is how destructive changes get waved through. So the first action is to read the plan output properly, and specifically to separate its four verbs. Creates are usually uninteresting. Updates in place are usually fine. Destroys are visible and therefore rarely the thing that hurts you. The dangerous class is the replacement, because it is a destroy followed by a create presented as a single change to one resource, and it slips past a reviewer who is scanning for the word destroy.

Ask of each replacement which attribute forced it and whether the resource holds state. A replaced security group is an outage of some seconds. A replaced managed database, disk, bucket or stateful volume is data loss, and Terraform will carry it out without complaint because you asked for a resource with those properties and the provider cannot produce one by mutating what exists. This is the single most useful skill in reviewing infrastructure changes, and it is why "the plan shows one change" is never a sufficient summary.

The second thing to look for is scope creep. A plan that touches resources unrelated to the pull request means either drift has accumulated and this run will reconcile it, or a module version moved, or a data source now resolves differently. In each case the change being approved is larger than the change being discussed, and the right response is to find out why before applying, not after.

The approved plan must be the applied plan

If the pipeline runs a plan for the reviewer and then runs a fresh plan at apply time, the reviewer approved a forecast and the pipeline executed a different one. Between the two, someone could have merged another change, drift could have appeared, a provider or module version could have resolved upwards, or a data source could have started returning something new. Usually the two agree. The case where they do not is exactly the case you built the review for.

So save the plan to a file, publish that file as the artefact under review, and apply that same file. The apply then either succeeds against the state it was computed from or refuses because state has moved on, and refusing is the desirable behaviour. Have the pipeline also render the saved plan in machine-readable form so review is not limited to a human reading terminal output.

# Everything that decides the outcome is pinned, so a re-run resolves the same way.
terraform {
  required_version = "<the CLI version your pipeline installs>"
  required_providers {
    aws = {
      source = "hashicorp/aws"
      # An exact version, not a range: a range lets a re-run plan differently.
      version = "<exact provider version>"
    }
  }
}

resource "aws_db_instance" "primary" {
  # ...
  lifecycle {
    # A plan that would replace or remove this fails instead of proceeding.
    prevent_destroy = true
  }
}

The lock file for provider versions belongs in the repository for the same reason: a run that resolves a different provider version can produce a different plan from identical configuration, and that is not a difference you want discovered at apply time.

Guard rails have to sit below the thing being reviewed

Any control implemented in the same configuration the pull request is editing can be removed by that pull request. A lifecycle rule preventing destruction is genuinely useful, because it converts a catastrophic apply into a failed plan, but it protects you only until someone deletes the three lines, possibly with the honest intention of getting their change through. So layer it.

Provider-level deletion protection, where a resource supports it, lives in the cloud rather than in your repository, which means an apply cannot bypass it without an explicit separate change. Policy-as-code evaluated against the rendered plan is the layer that scales, because it can express rules that no single resource can: reject any plan that destroys or replaces a resource of a stateful type, reject a plan whose destroy count exceeds a threshold, require a specific approval label when a protected tag is involved. And permissions on the identity the pipeline assumes are the layer of last resort: if the deploy role has no authority to delete a particular class of resource, no plan can do it regardless of what the code says.

Splitting state is the structural version of the same idea. One state file covering the whole estate means every run has the authority to destroy everything in it, and the only thing standing between you and that is the correctness of the configuration. Separating state by environment first, then by lifecycle - long-lived networking and data separate from frequently changed application resources - bounds what any single run can reach. The cost is real: cross-stack references become explicit inputs or remote state lookups, and there is more orchestration to own. That cost is the price of a smaller blast radius, and stating the trade honestly is part of a good answer.

When automation deletes exactly what you told it to

In April 2022 an Atlassian maintenance script intended to remove a deprecated application was run in the wrong deletion mode and permanently deleted data for around 400 customer sites. Restoring them took weeks, because the tooling had been built to restore everything at once rather than one tenant at a time.

Two things in that generalise directly to infrastructure as code. The first is that automation multiplies whatever intent you hand it, faithfully and quickly, so the reviewable artefact has to be the intent as the machine understood it rather than as the author described it. A destructive mode that is reachable from a pipeline is a destructive mode that will eventually be selected, so ask whether it needs to be reachable at all, and whether decommissioning should instead be a deliberate, separately authorised operation with its own approval and its own audit trail.

The second is that the restore path has to match the granularity of the damage. A recovery capability designed for total loss does not help when the loss is a subset, and discovering that mid-incident converts hours into weeks. Before approving a destructive plan, the specific question to answer is not whether backups exist but whether these resources can be brought back individually, by whom, and in what elapsed time. If the honest answer is that nobody has tried, that is the reason to withhold approval.

The approval that means something

A human clicking approve on a wall of plan output is theatre. Make the approval carry information by making the pipeline extract the facts that decide it: the list of resources being destroyed or replaced, which attribute forced each replacement, whether any of them are of a protected type, and the count against whatever threshold policy allows. Post that summary as the thing being approved. The reviewer's job then becomes answering a specific question rather than skimming for danger.

Add one dependency that people forget. The apply must hold a lock on the state for its whole duration, and the review is only valid while nothing else applies against that state. A pipeline that serialises applies per state file gives you that; a team where anyone can apply from a laptop does not, and no amount of plan review compensates for it.

The dangerous line in a plan is a replacement rather than a destroy, because a replaced stateful resource is data loss labelled as an update. Review the saved plan that will be applied, keep the guard rails somewhere the pull request cannot delete them, and refuse the approval until you know what restoring these particular resources would actually take.

Likely follow-ups

  • The plan wants to replace your production database because a provider attribute changed. Walk me through your options in order.
  • You split state to bound the blast radius and now three stacks need each other's outputs. How do you wire that without recreating the coupling?
  • A plan was approved yesterday and applied this morning. What could have changed in between, and does it matter?
  • How would you make a destroy unreachable from the pipeline while still being able to decommission things deliberately?

Related questions

terraformiacblast-radiuspolicy-as-codechange-review