Resource Lifecycle in OpenTofu
Control resource lifecycles with the lifecycle meta-argument: prevent_destroy, create_before_destroy, ignore_changes, replace_triggered_by, and custom conditions.
Table of Contents
By default, OpenTofu manages resource lifecycles using sensible default behaviors:
- In-place updates: Whenever an attribute change is supported directly by the target API, OpenTofu modifies the resource without destroying it.
- Destroy-then-create: If a change requires full recreation (e.g., swapping a base OS image or changing an immutable subnet), OpenTofu destroys the existing resource first, then provisions the replacement.
- Drift reconciliation: If real-world infrastructure drifts due to manual out-of-band changes, OpenTofu reverts it back to match your
.tfconfiguration. - Complete destruction: Running
tofu destroydeletes every tracked resource in the configuration without exception.
In production environments, however, these default rules can be dangerous or disruptive. Accidentally dropping a primary database, incurring downtime during a routine web server update, or fighting an autoscaler over replica counts are real operational risks.
This is where the lifecycle meta-argument comes in. Embedded directly within a resource block, it allows you to customize and override OpenTofu’s standard lifecycle behavior.
Anatomy of the lifecycle Block
The lifecycle block lives directly inside a resource definition:
resource "<RESOURCE_TYPE>" "<LOCAL_NAME>" {
# ... resource attributes ...
lifecycle {
prevent_destroy = true
create_before_destroy = true
ignore_changes = [ ... ]
replace_triggered_by = [ ... ]
}
}
Let’s break down each core argument, their mechanics, and practical real-world scenarios.
1. prevent_destroy (Accidental Deletion Guardrail)
Purpose
prevent_destroy acts as a safety circuit breaker. When set to true, OpenTofu halts execution immediately and rejects any plan (tofu plan or tofu apply) that would result in destroying the resource.
When to Use It
- Persistent storage volumes (
docker_volume, AWS EBS volumes, GCS Buckets). - Primary production databases.
- Core networking components (VPCs, transit gateways) whose deletion would bring down the entire architecture.
Implementation Example
resource "docker_volume" "db_data" {
name = "pgdata-production"
lifecycle {
prevent_destroy = true
}
}
What Happens During Accidental Deletion?
If someone runs tofu destroy:
tofu destroy
OpenTofu immediately aborts the run and displays a protective error message:
Error: Instance cannot be destroyed
on main.tf line 1, in resource "docker_volume" "db_data":
1: resource "docker_volume" "db_data" {
Resource docker_volume.db_data has lifecycle.prevent_destroy set, but the plan
calls for this resource to be destroyed. To safely destroy this resource,
remove the prevent_destroy block or set it to false and run tofu apply.
How to Intentionally Delete It: When you genuinely need to decommission the resource, you must explicitly set
prevent_destroy = false(or remove the block), runtofu applyonce to persist the change in state, and only then proceed withtofu destroy.
2. create_before_destroy (Zero-Downtime Replacements)
The Problem with Default Behavior
By default, whenever an attribute change forces resource recreation (replacement), OpenTofu follows this sequence:
- Stop & Destroy: Terminate the old resource.
- Create: Spin up the new replacement.
This sequence creates an unavoidable window of downtime where your service is unavailable while the new resource boots and passes health checks.
The create_before_destroy Solution
Setting create_before_destroy = true inverts the sequence:
- OpenTofu creates the replacement resource first and ensures it is healthy.
- References and traffic are updated (e.g., a load balancer redirects requests to the new instance).
- The old resource is safely destroyed.
Default Behavior:
[Old Resource] ──► Destroy ──► (DOWNTIME) ──► Create [New Resource]
With create_before_destroy:
[New Resource] Created & Healthy ──► Cut Over Traffic ──► Destroy [Old Resource]
Implementation Example
resource "docker_container" "web" {
name = "web-server"
image = "nginx:alpine"
lifecycle {
create_before_destroy = true
}
}
Important Caveat: Resources that require unique, static names (such as Docker containers on a single host or cloud resources with strict naming uniqueness) will encounter name collision errors if both instances coexist. To use
create_before_destroyeffectively, leverage dynamic naming (e.g., usingname_prefixor appending random unique suffixes).
3. ignore_changes (Handling Out-of-Band Drift)
The External Drift Problem
After infrastructure is provisioned, certain attributes may be legitimately modified by external systems outside OpenTofu’s purview, such as:
- Metadata tags automatically injected by cloud security or cost allocation bots.
- Replica counts dynamically adjusted by an autoscaler (Kubernetes HPA or AWS Auto Scaling Groups).
- SSL certificates or credentials rotated by secret management agents.
Without intervention, every subsequent tofu apply will detect drift and attempt to revert these values back to the .tf source code, leading to unwanted churn.
The ignore_changes Solution
ignore_changes instructs OpenTofu to ignore discrepancies on specific attributes during plan and apply:
resource "docker_container" "app" {
name = "backend-api"
image = "nginx:alpine"
# Base label declared in code
labels {
label = "managed_by"
value = "opentofu"
}
lifecycle {
# Ignore any external labels added by monitoring or audit tools
ignore_changes = [
labels,
]
}
}
If you want OpenTofu to ignore all subsequent attribute modifications after initial resource creation:
lifecycle {
ignore_changes = all
}
4. replace_triggered_by (Dependency-Driven Recreation)
The Hidden Dependency Problem
Sometimes a resource must be destroyed and recreated not because its own .tf attributes changed, but because an external dependency or file was modified (such as an nginx.conf file, a database migration script, or a TLS secret).
Historically, operators relied on manual tofu taint commands. In modern OpenTofu, you can cleanly automate this with replace_triggered_by.
Implementation Example with terraform_data
Use the built-in terraform_data resource to track the checksum of a local configuration file, triggering container recreation when the file changes:
# 1. Track checksum changes of a local configuration file
resource "terraform_data" "nginx_config" {
input = filebase64sha256("${path.module}/nginx.conf")
}
# 2. Recreate the container automatically when the checksum changes
resource "docker_container" "web" {
name = "web-service"
image = "nginx:alpine"
lifecycle {
replace_triggered_by = [
terraform_data.nginx_config
]
}
}
Whenever you modify nginx.conf, OpenTofu detects the updated hash in terraform_data.nginx_config and flags docker_container.web for replacement on the next plan.
5. Modern Validation: precondition & postcondition
In modern OpenTofu, the lifecycle block also supports custom condition validation to enforce architecture and security rules directly within resource blocks:
precondition: Evaluates assumptions before provisioning the resource.postcondition: Evaluates state guarantees after provisioning completes.
resource "docker_container" "web" {
name = "web-server"
image = "nginx:alpine"
lifecycle {
# Guardrail: disallow Alpine images in production environments
precondition {
condition = var.environment != "production" || !can(regex("alpine", var.nginx_image))
error_message = "Alpine image variants are not permitted in production environments."
}
}
}
Quick Reference: lifecycle Arguments
| Argument | Primary Effect | Common Use Cases |
|---|---|---|
prevent_destroy | Halts execution if a plan attempts to delete the resource. | Persistent storage volumes, production databases. |
create_before_destroy | Provisions replacement instances before terminating old ones. | Preventing downtime on web servers and microservices. |
ignore_changes | Excludes specific attributes from drift reconciliation. | Dynamic cloud audit tags, autoscaler replica counts. |
replace_triggered_by | Forces recreation when another resource or data changes. | Refreshing servers when config files or secrets update. |
precondition / postcondition | Enforces custom boolean checks before / after execution. | Architectural guardrails and compliance policies. |
Complete Hands-on Example
Here is a consolidated main.tf demonstrating how these lifecycle rules work together:
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 4.6.0"
}
}
}
provider "docker" {
host = "unix:///var/run/docker.sock"
}
# 1. Persistent storage protected from accidental deletion
resource "docker_volume" "app_storage" {
name = "app-production-data"
lifecycle {
prevent_destroy = true
}
}
# 2. Config version tracker
resource "terraform_data" "app_version" {
input = "v1.0.0"
}
# 3. Application container with combined lifecycle rules
resource "docker_container" "app_server" {
name = "app-server"
image = "nginx:alpine"
labels {
label = "managed_by"
value = "opentofu"
}
lifecycle {
# Provision replacement before destroying the old container
create_before_destroy = true
# Do not overwrite labels injected by external observability agents
ignore_changes = [
labels,
]
# Recreate the container whenever the app version changes
replace_triggered_by = [
terraform_data.app_version
]
}
}
Summary & Next Steps
Mastering the lifecycle meta-argument gives you precise control over how infrastructure changes are applied and protected:
- Use
prevent_destroyto safeguard critical assets against human error. - Use
create_before_destroyto deliver zero-downtime rolling updates. - Use
ignore_changesto coexist peacefully with external automation and autoscalers. - Use
replace_triggered_byfor automated, dependency-driven release cycles.
In the next part, we will explore State Management and Remote Backends—learning how engineering teams collaborate safely on shared state files using object storage (such as Amazon S3 or MinIO) with distributed state locking.