Terraform lifecycle Patterns: create_before_destroy, prevent_destroy, ignore_changes, and replace_triggered_by

lifecycle Changes Terraform's Behavior, Not Provider Reality

The lifecycle block is a precision tool for resource replacement and ownership boundaries. It cannot make an API support two objects with the same unique name, turn an in-place provider update into a zero-downtime deployment, or protect a resource after its block disappears from configuration.

Use lifecycle rules only with a clear invariant and test the plan. The lifecycle safety lab combines all four patterns against disposable FakeCloud resources.

---

create_before_destroy: Preserve Capacity During Replacement

Terraform normally destroys an object before creating its replacement when both cannot coexist by default. For a service instance, that may cause an outage:

resource "fakecloud_instance" "api" {
  name  = "checkout-api-${var.release_id}"
  image = var.image
  size  = var.size

  lifecycle {
    create_before_destroy = true
  }
}

Terraform creates the replacement, updates dependencies, then destroys the old object. This requires spare quota and a naming scheme that permits coexistence. If the provider enforces a unique fixed name such as checkout-api, creation still fails. Pair the rule with release-specific names, load balancer registration, and health checks where the provider supports them.

Review propagation through dependencies: Terraform may preserve create-before-destroy behavior for dependent resources to avoid cycles. A plan is the authority, not an assumption based on one block.

---

prevent_destroy: A Guardrail With Boundaries

For a stateful production database:

resource "fakecloud_database" "orders" {
  name    = "orders-prod"
  engine  = "postgres"
  size_gb = 500

  lifecycle {
    prevent_destroy = true
  }
}

Terraform rejects a plan that would destroy or replace this resource. That catches a changed force-new argument and an explicit terraform destroy while the block remains present.

It is not an external deletion lock. It does not stop an operator deleting the database in a cloud console, and removing the entire resource block also removes the lifecycle rule from configuration. Combine it with provider-native deletion protection, backups, access control, policy checks, and review.

When destruction is intentional, remove the rule in a separately reviewed change, plan, verify backups, then perform the deletion. Do not normalize bypassing safeguards during an emergency.

---

ignore_changes: Define Shared Ownership Narrowly

Suppose an autoscaler changes desired capacity while Terraform owns the rest of the service:

resource "fakecloud_instance" "worker_pool" {
  name     = "worker-pool"
  replicas = var.initial_replicas
  size     = var.size

  lifecycle {
    ignore_changes = [replicas]
  }
}

Terraform uses replicas during creation but ignores later remote changes for update planning. This is a legitimate ownership boundary.

Bad uses include suppressing unexplained drift, ignoring security settings, or adding all to silence a noisy provider. Every ignored attribute is an attribute Terraform will stop correcting. Add a comment naming the external owner and monitor that owner separately.

Ignoring one nested field requires the provider schema's exact attribute path. Validate with a plan after both Terraform changes and external-controller changes.

---

replace_triggered_by: Rebuild on a Meaningful Dependency Change

References create ordering, but a dependency update does not always replace its consumer. replace_triggered_by makes replacement explicit:

resource "terraform_data" "bootstrap_version" {
  input = var.bootstrap_version
}

resource "fakecloud_instance" "runner" {
  name  = "ci-runner"
  image = var.base_image

  lifecycle {
    replace_triggered_by = [terraform_data.bootstrap_version]
  }
}

When the bootstrap version changes, Terraform plans to replace the runner even if no provider argument changed. This suits immutable instances whose initialization runs only at creation.

Use resource references here, not arbitrary expressions. terraform_data can represent a plain value as a managed lifecycle signal. Avoid timestamps and constantly changing hashes; they create perpetual replacements.

---

precondition and postcondition: Assert What Must Be True

Lifecycle also supports custom conditions:

resource "fakecloud_certificate" "api" {
  domain = "api.example.com"
  ca_id  = var.ca_id

  lifecycle {
    precondition {
      condition     = endswith(var.ca_id, "-prod")
      error_message = "Production certificates must use the production CA."
    }

    postcondition {
      condition     = self.expires_in_days > 30
      error_message = "Issued certificate has less than 30 days of validity."
    }
  }
}

Conditions turn assumptions into plan or apply failures close to the affected resource. Variable validation checks an input in isolation; a precondition can combine values and data; a postcondition validates provider results.

Review Questions

Before approving any lifecycle rule, ask:

  1. Which failure does this prevent?
  2. Can the provider support the requested behavior?
  3. Who owns ignored attributes?
  4. Does protection survive deletion outside Terraform?
  5. What is the reviewed escape procedure?
  6. Does a representative plan prove the intended ordering?

Lifecycle rules are most valuable when they encode a sentence the team already agrees on: keep old capacity until replacement is ready; never destroy this database casually; let the autoscaler own replica count; rebuild runners when bootstrap changes.