skip to content

Your Terraform module needs `prevent_destroy` on the database in production but not in dev. Why can you not write `prevent_destroy = var.is_prod`, and what do you do instead?

level: seniorimportance: should knowfreq 40%

answer

  1. evaluated before expressions have values
  2. literal true or false only
  3. conditions do take expressions
  4. the provider argument is dynamic
  5. layer API-side and IAM protection

basics

~20 s

Terraform processes the lifecycle block before it evaluates expressions, so prevent_destroy and create_before_destroy accept only literal true or false — a variable reference is a configuration error. Environment-specific protection has to come from a provider argument, separate configurations, or pipeline policy.

solid answer

~50 s

The `lifecycle` block is handled earlier than ordinary arguments, before variables, locals and data sources have values, so its boolean settings must be literal — `prevent_destroy = var.is_prod` fails as invalid configuration rather than evaluating to false in dev. `ignore_changes` and `replace_triggered_by` are similarly restricted to static attribute and resource references. The practical answers are: use the provider's own guard, since `deletion_protection` on `aws_db_instance` is an ordinary argument and happily takes `var.is_prod`; keep environments as separate root modules or separate module calls so only the production one carries the literal guard; or enforce it outside Terraform, with an IAM policy that denies deletion in the production account and a CI check that rejects a plan containing a destroy of a protected resource. The one lifecycle construct that does take an arbitrary expression is a `precondition` or `postcondition` — but those guard inputs and results, not deletion.

code

hcl · 23 lines
hcl
variable "is_prod" {
  type = bool
}

variable "db_password" {
  type      = string
  sensitive = true
}

resource "aws_db_instance" "primary" {
  identifier          = "app-primary"
  engine              = "postgres"
  instance_class      = "db.t3.medium"
  allocated_storage   = 100
  username            = "app"
  password            = var.db_password
  deletion_protection = var.is_prod
  skip_final_snapshot = !var.is_prod

  lifecycle {
    prevent_destroy = true
  }
}

go deeper

for a junior

Know that lifecycle booleans have to be written as a literal true or false and cannot reference a variable — the run fails as invalid configuration if you try.

for a middle

Explain why: the block shapes how the plan graph is built, which happens before variables and data sources are resolved, so no expression is available at that point.

for a senior

Give the working alternative and the reasoning — dynamic provider-side deletion protection, environment-specific root configurations, and pipeline or IAM enforcement — and reject the count-based workaround that deletes the resource instead of guarding it.

for a principal

Own how deletion protection is guaranteed across the estate: which layer is authoritative, how it is verified rather than assumed, and what the audited break-glass path looks like when something protected genuinely must go.

## Why the restriction exists Terraform's `lifecycle` block is not evaluated like the rest of a resource. Its settings shape how Terraform builds the plan graph — whether a destroy is permitted, in what order a replacement happens — and that shaping is decided before the values of variables, locals, data sources and other resources' attributes are known. Because the decision is made too early for expression evaluation, `create_before_destroy` and `prevent_destroy` accept only literal `true` or `false`. Writing a variable reference there is not a value that comes out false; it is a configuration error, and the run stops at parse time in every workspace, including dev. The same staticness applies, in a milder form, to the other arguments: - `ignore_changes` takes a fixed list of attribute references such as `[desired_count, tags["Owner"]]`, not a list built by a `for` expression over a variable. - `replace_triggered_by` takes references to managed resources, resource instances, or their attributes — not variables or locals, which is precisely why the `terraform_data` resource exists as a bridge for a plain value. - `precondition` and `postcondition` are the exception: their `condition` and `error_message` are ordinary expressions, evaluated with everything else, because they only decide whether to fail — not how to build the graph. ## What to do instead **Prefer the provider's own guard.** Most stateful resource types have a server-side deletion guard that is an ordinary argument and therefore fully dynamic: `deletion_protection` on `aws_db_instance`, and the equivalent on cluster and other stateful types. It is enforced by the cloud API against every caller — console, CLI, another tool — not only against Terraform, so it is the stronger guard anyway. `deletion_protection = var.is_prod` and `skip_final_snapshot = !var.is_prod` express the intent exactly. ```hcl resource "aws_db_instance" "primary" { # ... deletion_protection = var.is_prod # fine: an ordinary argument lifecycle { prevent_destroy = true # must be a literal } } ``` **Separate the environments rather than parameterising them.** If the shapes genuinely differ in something as fundamental as "may this be deleted", that is a signal the production estate is a different root configuration, or that the shared module is called from a thin prod-only wrapper that adds the guard. A module that must behave structurally differently per environment is often two modules wearing one name. **Move the guard out of the configuration entirely.** Two mechanisms are stronger than a lifecycle flag: an IAM policy in the production account that denies the delete API calls to the pipeline role except under a break-glass path, and a check in CI that inspects the plan and fails if it contains a destroy of a resource that is tagged or named as protected. Both survive the failure mode `prevent_destroy` cannot cover — someone deleting the resource block from the configuration, which removes the guard with it. ## The trap to name in the interview Candidates often propose the workaround of putting `prevent_destroy = true` behind a `count` or a conditional module call so the resource only exists in prod. That does not do what they think: the guard is not conditional, the *resource* is, and in the environment where `count` evaluates to zero the object is destroyed rather than protected. Similarly, keeping two copies of the resource block behind a condition duplicates the resource address, which makes any later move between them a destroy-and-create of real data. ## The shape of a good answer Say the mechanical reason first — lifecycle settings are consumed before expressions are evaluated, so they must be literal. Then show that you know the guard was never the strong protection anyway: `prevent_destroy` stops Terraform, `deletion_protection` stops the API, IAM stops the caller, and backups are what you actually rely on when all three are bypassed. That layered answer is what distinguishes someone who has protected a production database from someone who has read the argument reference.

  • Would putting the resource behind a count based on the environment give you a conditional guard?
    No, and it is dangerous. The condition then controls whether the resource exists, not whether it is protected: in the environment where the count is zero, Terraform destroys the object rather than leaving it unguarded. Make the protection dynamic through a provider argument instead, and keep the resource unconditional.
  • Which parts of the lifecycle block do accept arbitrary expressions?
    Only `precondition` and `postcondition`. Their `condition` and `error_message` are evaluated like any other expression, because they merely decide whether to fail rather than shaping the plan graph. The booleans, the ignore_changes list and replace_triggered_by all require static, literal content.

saying these in an interview costs you the question

  • Says the variable reference just evaluates to false in dev
  • Uses count to make the guard conditional, destroying the resource
  • Assumes prevent_destroy is the strongest available protection
  • Thinks ignore_changes can be built from a for expression
  • Duplicates the resource block per environment behind a condition

context