8.3 Dependencies, depends_on, and Lifecycle Rules
Key Takeaways
- A reference such as subnet_id = aws_subnet.app.id creates an implicit dependency; Terraform will not create the consumer until the producer exists
- depends_on is only for hidden dependencies — side effects you rely on but do not reference — and overusing it serializes the graph and makes plans more conservative
- create_before_destroy reverses replacement order: Terraform creates the new object first, then destroys the old one; 004 added this rule and depends_on as explicit 4f topics
- prevent_destroy rejects a plan that would destroy the object but does nothing if you delete the resource block; ignore_changes skips listed attributes on update; replace_triggered_by forces replacement when a referenced managed resource changes
- An in-place update mutates the existing object; a replacement destroys and creates (or create-before-destroy) because the remote API cannot change that argument on the live object
8.3 Dependencies, depends_on, and Lifecycle Rules
Quick Answer: A reference is an implicit dependency. Write
depends_ononly when one resource relies on another resource's side effect and does not read any of its attributes. Overusingdepends_onserializes work and marks more values(known after apply).lifecycle { create_before_destroy = true }creates the replacement first.prevent_destroy,ignore_changes, andreplace_triggered_byare the other lifecycle rules. Replacement is not an in-place update.
Objective 4f on Terraform Associate (004) is: define resource dependencies in configuration. HashiCorp called this out as a new 004 topic versus 003, and the two named additions are depends_on and create_before_destroy. Product version is Terraform 1.12. Official pages: depends_on and lifecycle (unversioned: depends_on, lifecycle).
Implicit dependencies from references
Terraform walks every expression and draws a graph edge from the referenced object to the object that mentioned it.
resource "aws_subnet" "app" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
}
resource "aws_instance" "web" {
ami = data.aws_ami.al2023.id
instance_type = "t3.micro"
subnet_id = aws_subnet.app.id
}
aws_instance.web depends on aws_subnet.app, which depends on aws_vpc.main. You do not write depends_on for that chain. Apply creates the VPC, then the subnet, then the instance. Destroy walks the other way. Parallelism is whatever the graph still allows — two instances that both reference the same subnet can still create together.
That implicit edge is precise. Terraform knows you depended on aws_subnet.app.id. If a later plan changes only a subnet tag that is not part of that id, the instance does not need to wait on a replacement. Expression references are the recommended style for that reason.
Explicit depends_on — hidden dependencies only
Official definition: use depends_on when a resource or module relies on another resource's behavior but does not access any of that resource's data in its arguments. The classic 1.12 example is an EC2 instance that has an instance profile (implicit — it references the profile) but also needs an IAM role policy to exist before the guest's boot software can call S3. Nothing on the instance block interpolates the policy's id, so Terraform cannot see that edge:
resource "aws_iam_role_policy" "example" {
name = "example"
role = aws_iam_role.example.name
policy = jsonencode({
Statement = [{
Action = "s3:*"
Effect = "Allow"
}]
})
}
resource "aws_instance" "example" {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
iam_instance_profile = aws_iam_instance_profile.example
# Hidden: guest software talks to S3 at boot. No attribute reference exists.
depends_on = [aws_iam_role_policy.example]
}
depends_on is a list of references to other resources or child modules in the same calling module. It cannot be an arbitrary expression. Terraform must know the graph before it can safely evaluate expressions.
It is legal on resource, module, data, output, check, and ephemeral blocks. On a data source it forces Terraform to finish the listed operations — including reads — before the query. On a whole module it orders every resource and data source in that module, which is why module-level depends_on is so blunt.
Why overuse serializes the graph
Official planning note: use depends_on as a last resort because it can cause Terraform to create more conservative plans that replace more resources than necessary. Terraform may treat more values as (known after apply) because it is uncertain what will change on the upstream object. That is especially likely when depends_on points at a module.
Practically, depends_on = [aws_iam_role_policy.example] means: finish all actions on that policy, including reads, before touching the instance. The instance can no longer create in parallel with unrelated policy updates. Spray depends_on onto every resource and you have turned a DAG into a nearly straight line. Prefer a real attribute reference whenever one exists.
| Kind | How you write it | When |
|---|---|---|
| Implicit | subnet_id = aws_subnet.app.id | Any time you already need the value |
| Explicit | depends_on = [aws_iam_role_policy.example] | Hidden side effect, no attribute to interpolate |
| Output ordering | depends_on on an output | The value is ready only after a related resource exists (for example, a VPC plus its internet gateway) |
Replacement versus update in place
When you apply, Terraform does one of four things to each managed object (official lifecycle intro):
- Create it if configuration has it and state does not.
- Destroy it if state has it and configuration does not.
- Update in place if arguments changed and the remote API can mutate the live object.
- Replace (destroy then create, by default) if arguments changed and the API cannot update that argument in place.
Changing an EC2 instance_type on many types is an in-place update. Changing ami is usually a replacement — a new instance id. Plan prints -/+ (destroy and create) or +/- when create-before-destroy is on. That distinction is the rest of 4f.
The lifecycle block
All lifecycle settings affect how Terraform builds the graph, so they accept literal values only. You cannot write prevent_destroy = var.protect. Processing happens too early for arbitrary expressions.
resource "aws_instance" "web" {
ami = data.aws_ami.al2023.id
instance_type = "t3.micro"
lifecycle {
create_before_destroy = true
prevent_destroy = true
ignore_changes = [tags]
replace_triggered_by = [aws_launch_template.web.id]
}
}
create_before_destroy — 004's named new rule
Default replacement order is destroy the old object, then create the new one. That leaves a gap: no instance, no DNS record, no target-group member. create_before_destroy = true flips the order: create the replacement first, then destroy the original.
It is opt-in because many remote types have unique names. Two objects cannot share the same IAM role name or the same Route 53 record set. Some resource types offer a name-prefix plus a random suffix so both can exist at once; Terraform CLI will not turn those features on for you.
Terraform propagates this behavior. If resource A has create_before_destroy and depends on resource B, Terraform enables the same rule on B (and stores it in state) so the graph does not cycle. You cannot then set create_before_destroy = false on B. A destroy-time provisioner also will not run when this flag is true.
prevent_destroy
When prevent_destroy = true, Terraform rejects plans that would destroy that infrastructure object and returns an error. The argument must be present in configuration. Official catch: this rule does not protect you if you remove the resource block. Once the block is gone, the lifecycle rule is gone, and apply will plan a destroy. It also blocks terraform destroy for as long as the block remains. Use it sparingly on costly objects such as databases, and do not treat it as a backup policy.
ignore_changes
Terraform normally plans an update whenever real state differs from configuration. ignore_changes lists attributes that should be honored on create and then ignored on update. Typical case: an autoscaler or a management agent rewrites tags or desired_capacity after create.
lifecycle {
ignore_changes = [tags, tags["Name"]] # relative addresses; indexes allowed
# ignore_changes = all # create and destroy only; never update
}
all means Terraform may still create and destroy the object but will never propose an update. You cannot ignore meta-arguments or lifecycle itself. Only resource-type attributes.
replace_triggered_by
Terraform replaces this resource when a referenced managed resource, instance, or attribute is planned for update or replace. You may use count.index or each.key inside the list when this resource uses count or for_each.
resource "aws_appautoscaling_target" "ecs_target" {
lifecycle {
replace_triggered_by = [aws_ecs_service.svc.id]
}
}
You can only reference managed resources. Locals and input variables have no planned actions of their own; wrap one in terraform_data if you need a resource-like trigger. A reference to a whole multi-instance resource fires when any instance updates or replaces.
| Rule | Literal? | Effect |
|---|---|---|
create_before_destroy | Yes | Replacement creates the new object first |
prevent_destroy | Yes | Error if the plan would destroy, but only while the block exists |
ignore_changes | Yes (list or all) | Create uses the values; later updates skip those attributes |
replace_triggered_by | Resource addresses | Replace this object when that managed resource changes |
004 traps for dependencies and lifecycle
- A reference already is a dependency. Adding
depends_onon top of it only makes the plan more conservative. depends_onis a last resort for hidden side effects, not a style preference.- Module-level
depends_onorders the entire child module. create_before_destroydoes not turn a replacement into an in-place update. It only changes the order of a replacement.prevent_destroydies with the resource block. Deleting the block is still a destroy.ignore_changesis notprevent_destroy. The object can still be replaced for other reasons.replace_triggered_bydoes not accept a variable or a local. It accepts managed resource addresses.- Lifecycle arguments are literals.
prevent_destroy = var.safeis not valid HCL for this block.
When should a Terraform 1.12 resource set depends_on?
What does lifecycle { create_before_destroy = true } change on Terraform 1.12?
A database resource is configured with prevent_destroy = true. An operator deletes the entire resource block from configuration and runs apply. What does Terraform 1.12 do?