In this article, we will take a look at the various stages a Terraform resource goes through during its lifetime. We will look at the default resource behavior before looking at the lifecycle meta-argument, which can allow you to customize that behavior.
What is a Terraform Resource?
A Terraform resource block defines a piece of infrastructure with the given settings. When the resource block is defined in code, the resource object does not actually exist until terraform apply is executed. Applying a configuration can result in the creation, modification, or destruction of a resource, depending on the configuration and state of the infrastructure. Terraform will make the real infrastructure match the configured settings for the resource.
Terraform Resource Lifecycle
Once an object is created, it is saved in the Terraform state. Terraform can then update the object if its settings are changed in the configuration or destroy it if the resource is removed from the configuration.
Depending on the settings defined in the configuration, Terraform will take one of the following actions when applying the configuration:
Create
Creates the object with the defined settings.
Destroy
Destroys the object when the configuration no longer exists.
Update-in-place
Updates the object accordingly when the settings in the resource block are changed. For example, adding a disk to a VM in Azure can be created and added without destroying the VM first.
Destroy and recreate
Destroys the object before re-creating it, if certain setting changes within the resource configuration block means, this must happen on the given platform. For example, changing the name of a VM in Azure is not possible without first destroying the VM. It is destroyed and then recreated with the new VM name specified in the settings of the resource block.
Terraform state can contain very sensitive data. Sometimes this is unavoidable because of the design of certain Terraform providers or because the definition of what is sensitive isn’t always simple and may vary between individuals and organizations. Spacelift provides two different approaches for sanitizing values when resources are stored or passed to Plan policies:
- Default Sanitization: All string values are sanitized.
- Smart Sanitization: Only the values marked as sensitive are sanitized.
Learn more about how Spacelift can help you with Resource Sanitization, and get started on your journey by creating a free trial account.
What is Terraform Lifecycle Meta-Argument?
The Terraform lifecycle is a nested configuration block within a resource block. The lifecycle meta-argument can be used to specify how Terraform should handle the creation, modification, and destruction of resources. Meta-arguments are arguments used in resource blocks.
Terraform Lifecycle Meta-Argument Example
The lifecycle meta-argument can appear inside any resource block and lets you customize how Terraform creates, updates, and destroys that resource. For example, you can tell Terraform to ignore tag changes that are managed by an external policy engine:
resource "azurerm_resource_group" "example_rg" {
name = "example-rg"
location = "westeurope"
lifecycle {
ignore_changes = [
tags["department"]
]
}
}Managing the Resource Lifecycle Using the Lifecycle Meta-Argument
Controlling the flow of Terraform operations is possible using the lifecycle meta-argument. This is useful in scenarios when you need to protect items from getting changed or destroyed.
A common scenario that requires the use of a lifecycle meta-argument occurs when the Terraform provider itself does not handle a change correctly and so can be safely ignored, rather than the provider attempting to update an object necessarily. With the provider version updates, these “bugs” are slowly ironed out, at which point the lifecycle meta-argument can be removed from the resource.
There are several attributes available for use with the lifecycle meta-argument:
create_before_destroy
When Terraform determines it needs to destroy an object and recreate it, the normal behavior will create the new object after the existing one is destroyed. Using this attribute will create the new object first and then destroy the old one. This can help reduce downtime. Some objects have restrictions that the use of this setting may cause issues with, preventing objects from existing concurrently. Hence, it is important to understand any resource constraints before using this option.
lifecycle {
create_before_destroy = true
}prevent_destroy
This lifecycle option prevents Terraform from accidentally removing critical resources. This is useful to avoid downtime when a change would result in the destruction and recreation of resource. This block should be used only when necessary as it will make certain configuration changes impossible.
lifecycle {
prevent_destroy = true
}Terraform will error when it attempts to destroy a resource when this is set to true:
Error: Instance cannot be destroyed
resource details...
Resource [resource_name] has lifecycle.prevent_destroy set, but the plan calls for this resource to be destroyed. To avoid this error and continue with the plan, either disable lifecycle.prevent_destroy or reduce the scope of the plan using the -target flag.ignore_changes
The Terraform ignore_changes lifecycle option can be useful when attributes of a resource are updated outside of Terraform.
It can be used, for example, when an Azure Policy automatically applies tags. When Terraform detects the changes the Azure Policy has applied, it will ignore them and not attempt to modify the tag. Attributes of the resource that need to be ignored can be specified.
In the example below, the department tag will be ignored:
lifecycle {
ignore_changes = [
tags["department"]
]
}If all attributes are to be ignored, then the all keyword can be used. This means that Terraform will never update the object but will be able to create or destroy it.
lifecycle {
ignore_changes = [
all
]
}replace_triggered_by
The replace_triggered_by argument forces Terraform to replace a resource whenever one or more other managed resources change. This is useful when a resource depends on another resource’s identity in a way that requires a full replacement instead of an in-place update.
You can only reference managed resources or their attributes in replace_triggered_by. When Terraform plans an update or replacement for any of those references, it will also plan a replacement for the resource that declares replace_triggered_by.
For example, you might want an auto scaling target to be recreated whenever the ECS service it scales is replaced:
resource "aws_ecs_service" "svc" {
name = "example-svc"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.main.arn
desired_count = 2
# ...
}
resource "aws_appautoscaling_target" "ecs_target" {
max_capacity = 10
min_capacity = 2
resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.svc.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
lifecycle {
replace_triggered_by = [
aws_ecs_service.svc.id
]
}
}precondition and postcondition
You can also use custom condition checks with the lifecycle meta-argument. By adding precondition and postcondition blocks with a lifecycle block, you can specify assumptions and guarantees about how resources and data sources operate.
A precondition is evaluated before Terraform creates or updates the resource. If the condition is false, Terraform aborts the operation with a custom error message.
A postcondition is evaluated after the resource is created or updated. If the condition fails, Terraform will fail the apply and prevent dependent resources from proceeding.
In the example below, a precondition ensures the selected AMI has the correct CPU architecture, and a postcondition asserts that the instance is launched in the expected availability zone:
resource "aws_instance" "example" {
instance_type = "t2.micro"
ami = data.aws_ami.example.id
availability_zone = "eu-central-1a"
lifecycle {
# The AMI ID must refer to an AMI that contains an operating system
# for the `x86_64` architecture.
precondition {
condition = data.aws_ami.example.architecture == "x86_64"
error_message = "The selected AMI must use the x86_64 architecture."
}
# After creation, ensure the instance is really in the desired AZ.
postcondition {
condition = self.availability_zone == "eu-central-1a"
error_message = "Instance was not launched in the expected availability zone."
}
}
}action_trigger
Starting with Terraform 1.14, you can bind provider “actions” to resource lifecycle events using the action_trigger block inside lifecycle. This is useful when you want Terraform to trigger side-effectful automations (for example, invoking a Lambda function or running an external integration) whenever a resource is created or updated, without relying on provisioners or ad-hoc scripts.
The action_trigger block supports three core arguments:
- events – A list of lifecycle events that should trigger the actions. You can use:
- before_create
- after_create
- before_update
- after_update
- actions – An ordered list of actions to invoke when the event fires. Each action is referenced by its full address, for example
action.aws_lambda_invoke.notify_team. - condition (optional) – An expression that must evaluate to true for the actions to run. If the condition is false, Terraform skips the action even if the event occurs.
In the example below, Terraform invokes an AWS Lambda action after an EC2 instance is created or updated, but only if notifications are enabled:
action "aws_lambda_invoke" "notify_team" {
config {
function_name = "notify-team"
payload = jsonencode({
message = "Instance ${aws_instance.example.id} was created or updated."
})
}
}
resource "aws_instance" "example" {
ami = "ami-abc123"
instance_type = "t2.micro"
lifecycle {
action_trigger {
events = [after_create, after_update]
actions = [action.aws_lambda_invoke.notify_team]
condition = var.enable_notifications
}
}
}Actions themselves do not modify Terraform state. Instead, they are designed for day-two operations and other side effects that should run alongside the normal create and update workflow.
Key Points
Understanding the default behavior of the Terraform resource lifecycle can help avoid unwanted downtime when Terraform executes operations. The lifecycle of every resource can be manipulated as needed using the lifecycle meta-argument.
Note: New versions of Terraform are placed under the BUSL license, but everything created before version 1.5.x stays open-source. OpenTofu is an open-source version of Terraform that expands on Terraform’s existing concepts and offerings. It is a viable alternative to HashiCorp’s Terraform, being forked from Terraform version 1.5.6.
Manage Terraform Better and Faster
If you are struggling with Terraform automation and management, check out Spacelift. It helps you manage Terraform state, build more complex workflows, and adds several must-have capabilities for end-to-end infrastructure management.
Frequently asked questions
What is the Terraform lifecycle?
The Terraform lifecycle refers to the process Terraform uses to manage infrastructure resources through three core phases: create, update, and destroy. It compares your current configuration to the real infrastructure state, then plans and applies changes to align them. You can control this behavior using the lifecycle block, which supports options like create_before_destroy, prevent_destroy, and ignore_changes to fine-tune how Terraform handles resource updates and deletions.
What are the 5 steps of Terraform?
The five steps of Terraform start with writing infrastructure code using HCL. You then initialize the working directory with terraform init, which sets up required providers. Next, terraform plan lets you preview changes before applying them. With terraform apply, Terraform provisions or updates infrastructure. Finally, terraform destroy removes all managed resources when they’re no longer needed.
