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?
answer
- evaluated before expressions have values
- literal true or false only
- conditions do take expressions
- the provider argument is dynamic
- layer API-side and IAM protection
basics
~20 sTerraform 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 sThe `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 linesvariable "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
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.
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.
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.
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