Study guide

8.3 Dependencies, depends_on, and Lifecycle Rules

HashiCorp Certified: Terraform Associate 004

Track your progress as you read

Finished sections get a check mark.

1 Introduction & Exam Overview

1.1 Current Exam Facts 1.2 Format, Delivery, Retakes, and Recertification 1.3 004 Changes, Terraform 1.12, and Study Plan

2 Infrastructure as Code with Terraform

2.1 What Infrastructure as Code Is 2.2 Advantages of IaC Patterns 2.3 Multi-Cloud, Hybrid, and Service-Agnostic Workflows

3 Terraform Fundamentals: Providers & State

3.1 Install and Version Terraform Providers 3.2 How Terraform Uses Providers 3.3 Configuration with Multiple Providers 3.4 How Terraform Uses and Manages State

4 Core Terraform Workflow: Init & Validate

4.1 The Terraform Write–Plan–Apply Workflow 4.2 Initialize a Working Directory 4.3 Validate a Configuration

5 Plan, Apply, Destroy, and Formatting

5.1 Generate and Review an Execution Plan 5.2 Apply Changes to Infrastructure 5.3 Destroy Terraform-Managed Infrastructure 5.4 Formatting and Style Adjustments

6 Resources, Data Sources, and References

6.1 Resource Blocks vs Data Blocks 6.2 Resource Attributes and Cross-Resource References 6.3 Resource Addressing and Interpolation Patterns

7 Variables, Outputs, and Complex Types

7.1 Input Variables, tfvars, and Precedence 7.2 Output Values 7.3 Complex Types: list, map, object, tuple, set

8 Dynamic Configuration and Dependencies

8.1 Expressions, count, for_each, and dynamic Blocks 8.2 Built-in Functions 8.3 Dependencies, depends_on, and Lifecycle Rules

9 Conditions, Secrets, and Sensitive Data

9.1 Custom Conditions: validation, precondition, postcondition, check 9.2 Sensitive Data Handling 9.3 Vault, Ephemeral Values, and Write-Only Arguments

10 Terraform Modules

10.1 How Terraform Sources Modules 10.2 Variable Scope within Modules 10.3 Using Modules in Configuration 10.4 Managing Module Versions

11 Terraform State Management

11.1 The Local Backend 11.2 State Locking 11.3 Remote State and the backend Block 11.4 Resource Drift and State Operations

12 Maintain Infrastructure with Terraform

12.1 Import Existing Infrastructure 12.2 Inspect State with the CLI 12.3 Verbose Logging and Diagnostics

13 HCP Terraform

13.1 Create Infrastructure with HCP Terraform 13.2 Collaboration and Governance Features 13.3 Workspaces and Projects 13.4 HCP Terraform Integrations

8.3 Dependencies, depends_on, and Lifecycle Rules

Key Takeaways Explain This Topic AI

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

Last updated: August 2026

8.3 Dependencies, depends_on, and Lifecycle Rules

Quick Answer: A reference is an implicit dependency. Write depends_on only when one resource relies on another resource's side effect and does not read any of its attributes. Overusing depends_on serializes work and marks more values (known after apply) . lifecycle { create_before_destroy = true } creates the replacement first. prevent_destroy , ignore_changes , and replace_triggered_by are 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_on on top of it only makes the plan more conservative.

depends_on is a last resort for hidden side effects, not a style preference.

Module-level depends_on orders the entire child module.

create_before_destroy does not turn a replacement into an in-place update. It only changes the order of a replacement.

prevent_destroy dies with the resource block. Deleting the block is still a destroy.

ignore_changes is not prevent_destroy . The object can still be replaced for other reasons.

replace_triggered_by does not accept a variable or a local. It accepts managed resource addresses.

Lifecycle arguments are literals. prevent_destroy = var.safe is not valid HCL for this block.

Loading diagram... Implicit references versus a hidden depends_on edge