The lifecycle Meta-Argument
By default, Terraform plans changes in a predictable order: it creates new resources, updates existing ones in place when possible, and destroys resources that have been removed from configuration. The lifecycle block is a nested meta-argument that lets you override these default behaviors per resource. It is the tool you reach for when the default create/update/destroy sequence would cause downtime, destroy something you cannot afford to lose, or fight endlessly with changes made outside Terraform. Because it is a meta-argument, lifecycle works on every resource type and every provider, and it behaves identically in OpenTofu.
Where lifecycle lives
The lifecycle block is declared inside a resource block. Its arguments accept literal values only — you cannot reference other resources or variables inside most of them, because Terraform evaluates lifecycle settings very early, before the dependency graph is fully resolved.
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
lifecycle {
create_before_destroy = true
prevent_destroy = false
ignore_changes = [tags["LastScanned"]]
}
}
create_before_destroy
When a change forces a resource to be replaced — for example, changing an ami or instance_type that cannot be updated in place — Terraform’s default is to destroy the old resource first, then create the new one. That leaves a window with no resource at all. Setting create_before_destroy = true inverts the order: the replacement is created first, then the old resource is destroyed once the new one is healthy. This is the foundation of zero-downtime replacement.
resource "aws_launch_template" "app" {
name_prefix = "app-"
image_id = "ami-0c55b159cbfafe1f0"
instance_type = "t3.small"
lifecycle {
create_before_destroy = true
}
}
Using name_prefix instead of a fixed name matters here: if both the old and new resource must briefly coexist, a hard-coded unique name would collide and the apply would fail.
Output:
# aws_launch_template.app must be replaced
+/- resource "aws_launch_template" "app" {
~ id = "lt-08a1f..." -> (known after apply)
~ instance_type = "t3.micro" -> "t3.small"
}
Plan: 1 to add, 0 to change, 1 to destroy.
Tip:
create_before_destroypropagates. If resource A depends on resource B and A is set to create-before-destroy, Terraform will also apply create-before-destroy semantics to B during replacement so the dependency stays valid.
prevent_destroy
prevent_destroy = true is a safety guard. When set, any plan that would destroy the resource — whether through terraform destroy, removing it from configuration, or a forced replacement — fails with an error instead of proceeding. Use it on stateful, hard-to-recreate resources like production databases, S3 buckets holding data, or KMS keys.
resource "aws_db_instance" "primary" {
identifier = "prod-primary"
engine = "postgres"
engine_version = "16.3"
instance_class = "db.r6g.large"
allocated_storage = 100
lifecycle {
prevent_destroy = true
}
}
Output:
Error: Instance cannot be destroyed
on main.tf line 1:
1: resource "aws_db_instance" "primary" {
Resource aws_db_instance.primary has lifecycle.prevent_destroy set, but the
plan calls for this resource to be destroyed.
To actually remove the resource you must first delete the prevent_destroy line (or set it to false) and re-apply. Note that prevent_destroy cannot block destruction when the entire resource block is deleted and its lifecycle setting goes with it — keep the resource defined while you intend the guard to apply.
ignore_changes
Sometimes a resource attribute is legitimately modified outside of Terraform — an autoscaling group resizes itself, a deploy pipeline updates a task definition, or a tag is added by a cost-management tool. Without intervention, every plan would try to revert that drift. ignore_changes tells Terraform to stop tracking the listed attributes after creation.
resource "aws_autoscaling_group" "app" {
name = "app-asg"
min_size = 2
max_size = 10
desired_capacity = 2
lifecycle {
ignore_changes = [desired_capacity, tag]
}
}
Attributes are referenced by their schema names (bare identifiers, no quotes), and you can index into maps such as tags["Owner"]. To ignore drift on every attribute, use the special keyword all:
lifecycle {
ignore_changes = all
}
Warning:
ignore_changes = allis a blunt instrument. After creation, Terraform will never update the resource for any reason. Prefer an explicit list so deliberate configuration changes still apply.
replace_triggered_by
replace_triggered_by forces a resource to be replaced when another resource or attribute changes, even if the resource’s own configuration is unchanged. It accepts a list of references to managed resources, instances, or specific attributes. This is useful for rebuilding a compute resource whenever an upstream artifact changes.
resource "aws_instance" "worker" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
lifecycle {
replace_triggered_by = [
aws_launch_template.app.latest_version
]
}
}
When aws_launch_template.app.latest_version changes, aws_instance.worker is destroyed and recreated. References must point to managed resources — you cannot trigger on input variables or data sources directly.
Option reference
| Argument | Type | Effect |
|---|---|---|
create_before_destroy | bool | Create the replacement before destroying the original (zero-downtime) |
prevent_destroy | bool | Error out on any plan that would destroy the resource |
ignore_changes | list / all | Stop tracking drift on the listed attributes after creation |
replace_triggered_by | list of references | Force replacement when a referenced resource/attribute changes |
Best Practices
- Pair
create_before_destroywithname_prefix(or other generated identifiers) so the old and new resources can coexist without naming collisions. - Reserve
prevent_destroyfor genuinely irreplaceable, stateful resources; overusing it makes routine refactors painful. - Prefer an explicit
ignore_changeslist overallso intentional config changes still take effect. - Use
ignore_changesfor attributes that are managed by autoscaling, blue/green deploy tooling, or external automation rather than disabling the integration. - Remember that lifecycle arguments take literal values only — they cannot reference variables, so keep them simple and self-contained.
- Document why each lifecycle override exists with a comment; future maintainers need to know whether the drift is expected.
- These behaviors are identical in OpenTofu, so the same patterns are portable across both tools.