WintelGuy.com

How Terraform Manages Dependencies and Resource Lifecycle

Contents

Introduction: Terraform Lifecycle and Dependency Basics

One of Terraform's core strengths is its ability to discover relationships between resources and apply infrastructure changes in a safe, predictable order. Rather than executing configuration files line by line, Terraform analyzes the entire configuration, builds a dependency graph, and determines how resources should be created, updated, or destroyed. This approach allows Terraform to manage complex infrastructure while minimizing the risk of outages or unintended side effects.

In most cases, Terraform handles dependency and lifecycle management automatically. By analyzing links between resources, Terraform infers dependencies and ensures that resources are provisioned or modified in the correct sequence. It also determines whether a change can be applied in place or requires a resource to be replaced. For many configurations, such default behavior is sufficient and requires little to no additional input from the user.

However, real-world infrastructure often introduces scenarios where explicit control over resource lifecycle and dependencies becomes necessary. Examples include enforcing zero-downtime replacements, protecting critical resources from accidental deletion, tolerating externally managed changes, or coordinating updates across loosely coupled components. To address these needs, Terraform provides a set of lifecycle and dependency management features that allow users to fine-tune how changes are planned and applied.

Terraform's lifecycle and dependency management capabilities include:

  • Implicit dependencies derived from attribute references between resources and data sources
  • Explicit dependencies set with the depends_on meta-argument when relationships cannot be inferred automatically
  • Resource lifecycle meta-arguments, such as:
    • create_before_destroy for safer replacements
    • prevent_destroy to protect critical resources
    • ignore_changes to tolerate controlled configuration drift
    • replace_triggered_by to coordinate resource replacements
  • Provider and resource behavior that determines whether changes are applied in place or require replacement
  • Terraform CLI commands and options that influence lifecycle behavior, including:
    • Targeted operations (-target)
    • Forced replacements (-replace)
    • State refresh operations (-refresh-only)

These features are essential for designing Terraform configurations that are both flexible and safe. The sections that follow explore Terraform's default behavior in more detail, then examine each lifecycle and dependency mechanism, along with practical use cases where explicit control is required.

Back to Top

Terraform's Default Dependency and Lifecycle Behavior

Most Terraform configurations do not require explicit dependency or lifecycle settings. By default, Terraform analyzes the configuration and determines how resources relate to one another, how changes should be applied, and in what order operations must occur. This behavior is driven by Terraform's dependency graph, which is built during the planning phase.

Dependency Graph and Implicit Dependencies

When terraform plan is executed, Terraform constructs a directed acyclic graph (DAG) of all resources and data sources in the configuration. Each node in the graph represents a resource, and each edge represents a dependency. Terraform uses this graph to determine creation order, update order, and safe deletion sequences.

Dependencies are inferred implicitly through attribute references. When one resource references an attribute of another resource, Terraform automatically ensures that the referenced resource is created or updated first. For example, a compute instance that references a subnet ID will not be created until the subnet exists. This implicit dependency mechanism is the primary way Terraform manages ordering and is both reliable and expressive.

Because of this design, the arrangement of resources in configuration files has no effect on execution order. Terraform relies entirely on the dependency graph rather than file structure or declaration order.

Parallelism and Ordering

Terraform executes operations in parallel whenever possible. Resources that do not depend on one another may be created, updated, or destroyed simultaneously. This parallelism improves performance while still preserving correctness through dependency enforcement.

The default number of concurrent resource operations is 10 and can be changed with the -parallelism CLI option.

When dependencies exist, Terraform strictly honors them. A resource will not be modified or destroyed until all dependent resources are in a safe state. During destruction, Terraform reverses the dependency order, ensuring that dependent resources are removed before the resources they rely on.

Default Resource Lifecycle Behavior

For each resource change, Terraform determines whether the change can be applied in place or whether the resource must be replaced. This decision is primarily driven by the provider and the resource type.

By default:

  • Terraform updates resources in place whenever the provider supports it.
  • If a change requires replacement, Terraform performs a destroy-and-recreate operation.
  • Replacement operations are typically destroy before create, unless explicitly overridden.
  • Resources are destroyed only when they are no longer defined in the configuration or when replacement is required.

Terraform presents these decisions clearly in the plan output, using actions such as + create, ~ update in-place, - destroy, +/- create replacement and then destroy, or -/+ destroy and then create replacement.

State, Drift Detection, and Refresh

Terraform's default lifecycle behavior is tightly coupled with the state file. Before planning changes, Terraform refreshes the state by querying the providers and comparing the real infrastructure with the recorded state. Any differences are identified as drift and factored into the plan.

If a resource differs from the configuration, Terraform attempts to reconcile the difference by applying the desired configuration. This ensures that the declared configuration remains the single source of truth, unless explicitly instructed otherwise.

Why Defaults Are Usually Enough

Terraform's default dependency and lifecycle behavior is sufficient for the majority of use cases. Implicit dependencies reduce configuration complexity, parallel execution improves efficiency, and provider-driven lifecycle decisions handle most infrastructure changes safely.

However, some scenarios, such as preventing accidental deletions, enforcing zero-downtime replacements, or managing resources with external side effects require behavior that deviates from these defaults. Understanding Terraform's baseline behavior is important before introducing explicit lifecycle rules or dependency overrides, which are covered in the sections that follow.

Back to Top

Lifecycle Management Features

Terraform lifecycle management features allow you to control how individual resources are created, updated, replaced, and destroyed. While Terraform's default behavior is safe and efficient for most scenarios, lifecycle meta-arguments provide precise control when infrastructure changes must follow stricter rules, such as avoiding downtime, protecting critical resources, or tolerating controlled drift.

Lifecycle settings are defined within a resource's lifecycle block and apply only to that resource. They do not change Terraform's dependency graph directly, but they strongly influence how Terraform plans and executes changes for that resource.

Resource Lifecycle Meta-Arguments Overview

Terraform supports the following lifecycle meta-arguments:

  • create_before_destroy
  • prevent_destroy
  • ignore_changes
  • replace_triggered_by

Each of these addresses a specific class of operational or safety concerns and should be used deliberately.

create_before_destroy

resource "<TYPE>" "<LABEL>" { # ... lifecycle { create_before_destroy = <true || false> # boolean } }

By default, when a resource must be replaced, Terraform destroys the existing resource before creating the new one. In environments that require high availability, this behavior can cause outages or service interruptions.

The create_before_destroy meta-argument reverses this behavior by instructing Terraform to create the replacement resource first and destroy the old one only after the new resource is successfully provisioned.

Common use cases include:

  • Load-balanced compute instances
  • Launch templates or instance groups
  • Resources where temporary duplication is acceptable

Important considerations:

  • The resource must support coexistence of old and new instances.
  • Naming constraints or uniqueness requirements may need additional configurations.

prevent_destroy

resource "<TYPE>" "<LABEL>" { # ... lifecycle { prevent_destroy = <true || false> # boolean } }

The prevent_destroy meta-argument acts as a safeguard against accidental deletions. When it is set to true, Terraform will fail the plan or apply if an operation would destroy the resource, whether directly or as part of a replacement.

Typical use cases include:

  • Production databases
  • DNS zones
  • Long-lived storage resources
  • Critical networking components

Key behaviors:

  • Applies to both destroy and replacement operations
  • Requires explicit removal of the prevent_destroy lifecycle rule for resource modification
  • Provides an additional layer of protection beyond code review
  • Doesn't prevent Terraform from destroying the resource if the resource is removed form the configuration

This feature is particularly useful in shared environments or production workspaces where destructive changes carry high risk.

ignore_changes

resource "<TYPE>" "<LABEL>" { # ... lifecycle { ignore_changes = [ <ATTRIBUTE> ] # attribute list or 'all' } }

The ignore_changes meta-argument tells Terraform to disregard differences between the configuration and the actual resource for specific attributes (or all attributes). This allows Terraform to tolerate controlled drift without constantly attempting to revert changes.

Common scenarios include:

  • Provider-managed attributes (timestamps, IDs, metadata)
  • Auto-scaling–managed fields
  • Attributes modified by external systems

Behavioral notes:

  • Ignored attributes are excluded from diff calculations
  • Terraform continues to manage all non-ignored attributes normally
  • Overuse can mask unintended configuration drift

Used carefully, ignore_changes helps balance Terraform's declarative model with operational realities.

replace_triggered_by

resource "<TYPE>" "<LABEL>" { # ... lifecycle { replace_triggered_by = [ <RESOURCE.ADDRESS> ] } }

The replace_triggered_by meta-argument allows you to force resource replacement when another resource or its attribute changes, even if Terraform would not normally replace the resource.

This is useful when:

  • Multiple resources must be replaced together for consistency
  • Resources have dependencies that Terraform cannot automatically infer

Examples include:

  • Replacing compute resources when shared configuration changes
  • Coordinating updates across loosely coupled components

Unlike depends_on, this feature influences replacement behavior rather than execution order.

When to Use Lifecycle Meta-Arguments

Lifecycle meta-arguments are precision tools, not general-purpose configuration options. They are most effective when:

  • Default behavior introduces unacceptable risk or downtime
  • External systems interact with managed resources
  • Infrastructure changes must follow strict operational rules

Before applying lifecycle overrides, it is important to understand Terraform's default behavior and verify that the chosen approach aligns with provider capabilities and operational constraints.

Back to Top

Dependency Management Features

Terraform dependency management determines the order in which resources are created, updated, and destroyed. Unlike lifecycle controls, which affect how or when individual resources change, dependency management governs how resources relate to one another across the entire configuration. Terraform's ability to infer and enforce dependencies is central to its declarative model and is one of the primary reasons most configurations require little explicit ordering logic.

Terraform tracks dependencies using a graph-based model and supports both implicit and explicit dependency definitions. Understanding how and when to use each approach is critical to writing maintainable and predictable Terraform configurations.

Implicit Dependencies

Implicit dependencies are the default and preferred mechanism for dependency tracking in Terraform. They are created automatically whenever a resource references an attribute of another resource or data source.

For example, if a compute resource references a subnet ID, Terraform automatically ensures that the subnet is created before the compute resource. No additional configuration is required.

Key characteristics of implicit dependencies:

  • They are derived from attribute references
  • They are easy to read and maintain
  • They scale naturally with configuration complexity
  • They accurately represent real infrastructure relationships

Implicit dependencies also apply to data sources. If a resource references a data source that queries infrastructure, Terraform ensures the data source is evaluated before the resource is deployed.

Because implicit dependencies are embedded directly in configuration logic, they provide both correct ordering and clear representation of resource relationships.

Explicit Dependencies with depends_on

resource "<TYPE>" "<LABEL>" { # ... depends_on = [ <RESOURCE.ADDRESS> ] }

depends_on can be used in the following Terraform configuration blocks:

  • check blocks
  • data blocks
  • ephemeral blocks
  • module blocks
  • output blocks
  • resource blocks

The depends_on meta-argument allows you to define an explicit dependency in situations where Terraform cannot infer it automatically. This is typically necessary when a dependency exists due to side effects or external behavior that is not captured through attribute references.

Common use cases include:

  • Resources that rely on provisioners
  • Dependencies on external systems or services
  • Ordering requirements based on operational behavior rather than configuration data

Unlike implicit dependencies, depends_on does not rely on attribute references. Instead, it enforces a strict ordering relationship between resources or modules.

Important considerations:

  • Overusing depends_on can obscure true resource relationships
  • It can reduce parallelism and increase apply times
  • It should be used only when implicit dependencies are insufficient

As a best practice, depends_on should be treated as an exception rather than a default pattern.

Module-Level Dependencies

Dependencies can also exist at the module level. When outputs from one module are consumed by another, Terraform automatically creates implicit dependencies between the modules.

In cases where module interactions rely on side effects or external processes, depends_on can be applied at the module block level to enforce ordering. This is particularly useful in larger, layered architectures where responsibilities are separated across modules.

Module-level dependency management helps Terraform orchestrate complex infrastructure without requiring tightly coupled configurations.

Dependency Behavior During Destruction

Terraform applies dependencies in reverse during destruction. Resources that depend on others are destroyed first, ensuring that dependent infrastructure is removed safely before shared or foundational components.

For example:

  • Compute resources are destroyed before networks
  • Applications are removed before shared storage or databases

This reverse-order destruction is automatic and requires no additional configuration, provided dependencies are correctly expressed.

Best Practices for Dependency Management

  • Prefer implicit dependencies over explicit ones
  • Use depends_on only when dependencies cannot be expressed through attributes
  • Do not rely on resource declaration order; Terraform orders resource creation by dependencies, not file layout
  • Minimize use of CLI targeting (-target) in regular workflows
  • Review plan outputs to confirm expected dependency ordering

Terraform's dependency management model is intentionally opinionated. By aligning configurations with its implicit dependency system, you gain safer plans, better parallelism, and more maintainable infrastructure definitions.

Back to Top

CLI-Driven Lifecycle Controls

In addition to configuration-based lifecycle rules, Terraform provides several command-line options that can influence how resources are created, updated, replaced, or destroyed. These CLI-driven controls operate outside the regular workflow and are typically used for exception handling, recovery, or surgical changes, rather than as part of normal operations.

Because these options can partially bypass Terraform's dependency graph or override planned behavior, they should be used deliberately and with a clear understanding of their impact.

Targeted Operations (-target)

terraform plan -target=RESOURCE.ADDRESS

The -target option instructs Terraform to plan and apply changes for a specific resource (or set of resources), rather than the entire configuration.

Typical use cases include:

  • Recovering from a failed apply
  • Forcing the recreation of a single resource during troubleshooting
  • Breaking a complex change into smaller steps

Important characteristics:

  • Terraform still evaluates dependencies required by the targeted resource
  • Unrelated resources are excluded from the plan
  • The resulting state may not fully reflect the desired configuration

Because -target bypasses parts of the dependency graph, it can leave infrastructure in an incomplete or transitional state. For this reason, it is best used as a short-term corrective tool rather than a standard deployment mechanism.

Forced Resource Replacement (-replace)

terraform plan -replace=RESOURCE.ADDRESS

The -replace option explicitly marks a resource for replacement during the next apply, even if Terraform would normally plan no actions on the resource.

Common scenarios include:

  • Recovering from a corrupted or misconfigured resource
  • Forcing recreation after manual changes
  • Validating replacement behavior during testing

Key behaviors:

  • Replacement follows normal lifecycle rules unless overridden
  • Dependencies are respected during replacement
  • The action is visible and explicit in the plan output

Unlike -target, -replace does not limit the scope of the plan, it simply forces a replacement decision for the specified resource.

Refresh-Only Operations (-refresh-only)

terraform plan -refresh-only

The -refresh-only option updates Terraform state to reflect the current real-world infrastructure without proposing or applying changes.

This is useful when:

  • Investigating configuration drift
  • Reconciling state after out-of-band changes
  • Validating provider behavior

Refresh-only operations:

  • Do not modify infrastructure
  • May update the state file
  • Can reveal unexpected drift that affects future plans

While refresh operations are typically automatic, -refresh-only allows explicit control when diagnosing state discrepancies.

Destructive Operations (terraform destroy)

The terraform destroy command is a lifecycle-level operation that removes all managed resources in a configuration or workspace.

Important notes:

  • Destruction follows dependency order in reverse
  • Lifecycle safeguards such as prevent_destroy are enforced
  • Targeting can be combined, but with the same risks as -target during apply

This command is typically used for ephemeral environments, testing, or controlled teardown scenarios.

Legacy Resource Tainting (terraform taint)

terraform taint RESOURCE.ADDRESS

Historically, Terraform supported resource tainting as a way to mark a resource for replacement during the next apply. A tainted resource is treated as unhealthy, causing Terraform to destroy and recreate it even if no configuration changes are detected.

Key points:

  • Tainting affects only Terraform state
  • The resource is replaced during the next apply
  • No configuration changes are required

However, terraform taint is now deprecated and has been superseded by the -replace option. The modern approach provides clearer intent, improved plan visibility, and better integration with standard workflows.

As a result, terraform taint should be avoided in new automation and used only when maintaining or migrating existing legacy workflows.

When to Use CLI Lifecycle Controls

CLI-driven lifecycle controls are most appropriate when:

  • Recovering from failed or partial applies
  • Investigating drift or state inconsistencies
  • Performing one-time corrective actions
  • Managing exceptional operational scenarios

Terraform's CLI provides powerful tools, but they should not replace configuration-based lifecycle management for repeatable or long-term behavior. Whenever possible, lifecycle intent should be captured in code rather than enforced manually via CLI options.

Back to Top

Common Use Cases and Examples

While Terraform's default lifecycle and dependency behavior works well for most configurations, certain scenarios require explicit lifecycle or dependency controls. These situations typically involve safety, ordering, controlled drift, or coordinated changes that Terraform cannot infer automatically.

This section introduces several common use cases and illustrates them with simple examples that highlight Terraform's lifecycle management behavior.

Back to Top

Controlling Replacement with create_before_destroy

Problem:
By default, Terraform destroys a resource before creating its replacement. For some critical resources this may cause temporary unavailability.

Solution:
Use create_before_destroy to ensure the replacement is created first.

Example:

resource "random_pet" "pet_cbd" { lifecycle { # create_before_destroy = true } }

Workflow:

Initialize and apply the configuration:

terraform init
terraform apply -auto-approve

Force re-creation of random_pet.pet_cbd:

terraform apply -auto-approve -replace=random_pet.pet_cbd

random_pet.pet_cbd: Refreshing state... [id=needed-bee]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # random_pet.pet_cbd will be replaced, as requested
-/+ resource "random_pet" "pet_cbd" {
      ~ id        = "needed-bee" -> (known after apply)
        # (2 unchanged attributes hidden)
    }

Plan: 1 to add, 0 to change, 1 to destroy.
random_pet.pet_cbd: Destroying... [id=needed-bee]
random_pet.pet_cbd: Destruction complete after 0s
random_pet.pet_cbd: Creating...
random_pet.pet_cbd: Creation complete after 0s [id=learning-hookworm]

Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

Note the Terraform's default replacement behavior: -/+ destroy and then create replacement.

Uncomment the line with create_before_destroy and re-run terraform apply:

terraform apply -auto-approve -replace=random_pet.pet_cbd

random_pet.pet_cbd: Refreshing state... [id=learning-hookworm]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+/- create replacement and then destroy

Terraform will perform the following actions:

  # random_pet.pet_cbd will be replaced, as requested
+/- resource "random_pet" "pet_cbd" {
      ~ id        = "learning-hookworm" -> (known after apply)
        # (2 unchanged attributes hidden)
    }

Plan: 1 to add, 0 to change, 1 to destroy.
random_pet.pet_cbd: Creating...
random_pet.pet_cbd: Creation complete after 0s [id=legible-rhino]
random_pet.pet_cbd (deposed object e145d3a4): Destroying... [id=learning-hookworm]
random_pet.pet_cbd: Destruction complete after 0s

Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

Note the change in the Terraform's replacement behavior: +/- create replacement and then destroy.

Key takeaway:
create_before_destroy helps avoid temporary unavailability by ensuring that a replacement resource is created before the existing one is removed. However, the actual replacement behavior depends on the resource type and provider capabilities. Some resources cannot safely coexist in duplicate form due to naming constraints, uniqueness requirements, or other limitations. In such cases, the apply may fail or produce an unexpected result unless additional design considerations are introduced.

Back to Top

Protecting Resources with prevent_destroy

Problem:
Some critical resources must be protected from accidental deletion or re-creation.

Solution:
Use prevent_destroy to block destructive operations.

Example:

variable "home_dir" { default = "/home/user" } variable "content_pd" { default = "version=1.0" } resource "local_file" "file_pd" { filename = "${var.home_dir}/file_pd.txt" content = var.content_pd lifecycle { prevent_destroy = true } }

Workflow:

Initialize and apply the configuration:

terraform init
terraform apply -auto-approve

Re-apply with an updated content_pd value:

terraform apply -auto-approve -var "content_pd=version-1.1"

local_file.file_pd: Refreshing state... [id=f1af4003baac597259e2d2b420470de7ce76b9e6]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform planned the following actions, but then encountered a problem:

  # local_file.file_pd must be replaced
-/+ resource "local_file" "file_pd" {
      ~ content              = "version=1.0" -> "version-1.1" # forces replacement
...
      ~ id                   = "f1af4003baac597259e2d2b420470de7ce76b9e6" -> (known after apply)
        # (3 unchanged attributes hidden)
    }

Plan: 1 to add, 0 to change, 1 to destroy.
╷
│ Error: Instance cannot be destroyed
│
│   on lifecycle_pd.tf line 25:
│   25: resource "local_file" "file_pd" {
│
│ Resource local_file.file_pd 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 option.

Any configuration changes that need to destroy the resource either to replace it or as part of the terraform destroy workflow will fail with the Instance cannot be destroyed error. Terraform requires explicit removal of the prevent_destroy lifecycle rule to proceed.

Key takeaway:
prevent_destroy acts as a safety lock for critical or long-lived resources.

Back to Top

Ignoring Controlled Drift with ignore_changes

Problem:
Some resource attributes may be modified externally or change frequently, causing Terraform to detect drift and attempt unnecessary updates.

Solution:
Use ignore_changes to exclude specific attributes from diff calculations.

Example:

variable "project_id" { default = "<project_id>" # Your GCP project ID } provider "google" { project = var.project_id } resource "google_storage_bucket" "example" { name = "tf-demo-bucket-${var.project_id}-01" location = "US" storage_class = "STANDARD" # labels = {type = "temp"} lifecycle { ignore_changes = [storage_class] } }

Workflow:

Initialize and apply the configuration:

terraform init
terraform apply -auto-approve

Change the bucket's default storage class using Cloud Console or gcloud CLI:

gcloud storage buckets update \
   gs://<bucket_name> \
   --default-storage-class=NEARLINE 

Refresh Terraform state and inspect bucket's configuration:

terraform apply -refresh-only -auto-approve

Uncomment the labels = {type = "temp"} line and re-run terraform apply.

terraform apply -auto-approve

google_storage_bucket.example: Refreshing state... [id=tf-demo-bucket-760114-01]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # google_storage_bucket.example will be updated in-place
  ~ resource "google_storage_bucket" "example" {
      ~ effective_labels            = {
          + "type"                       = "temp"
            # (1 unchanged element hidden)
        }
        id                          = "tf-demo-bucket-760114-01"
      ~ labels                      = {
          + "type" = "temp"
        }
        name                        = "tf-demo-bucket-760114-01"
      ~ terraform_labels            = {
          + "type"                       = "temp"
            # (1 unchanged element hidden)
        }
        # (15 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.
google_storage_bucket.example: Modifying... [id=tf-demo-bucket-760114-01]
google_storage_bucket.example: Modifications complete after 0s [id=tf-demo-bucket-760114-01]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

Terraform ignores manual changes to the storage_class attribute and continues to manage all other bucket parameters. The bucket's default storage class remains NEARLINE.

Key takeaway:
ignore_changes allows Terraform to coexist with external resource management systems, but should be used sparingly.

Back to Top

Coordinating Replacement with replace_triggered_by

Problem:
Sometimes a resource must be replaced when another resource changes, even if there is no direct attribute dependency.

Solution:
Use replace_triggered_by to explicitly define replacement behavior.

Example:

resource "random_id" "token_rtb" { byte_length = 4 } resource "random_pet" "pet_rtb" { lifecycle { # replace_triggered_by = [random_id.token_rtb] } }

Workflow:

Initialize and apply the configuration:

terraform init
terraform apply -auto-approve

Force re-creation of random_id.token_rtb:

terraform apply -auto-approve -replace=random_id.token_rtb

Note that replacement of random_id.token_rtb does not affect random_pet.pet_rtb.

Uncomment the line with replace_triggered_by and re-run terraform apply:

terraform apply -auto-approve -replace=random_id.token_rtb

random_id.token_rtb: Refreshing state... [id=DuDT-Q]
random_pet.pet_rtb: Refreshing state... [id=sharp-deer]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # random_id.token_rtb will be replaced, as requested
-/+ resource "random_id" "token_rtb" {
      ~ b64_std     = "DuDT+Q==" -> (known after apply)
      ~ b64_url     = "DuDT-Q" -> (known after apply)
      ~ dec         = "249615353" -> (known after apply)
      ~ hex         = "0ee0d3f9" -> (known after apply)
      ~ id          = "DuDT-Q" -> (known after apply)
        # (1 unchanged attribute hidden)
    }

  # random_pet.pet_rtb will be replaced due to changes in replace_triggered_by
-/+ resource "random_pet" "pet_rtb" {
      ~ id        = "sharp-deer" -> (known after apply)
        # (2 unchanged attributes hidden)
    }

Plan: 2 to add, 0 to change, 2 to destroy.
random_pet.pet_rtb: Destroying... [id=sharp-deer]
random_pet.pet_rtb: Destruction complete after 0s
random_id.token_rtb: Destroying... [id=DuDT-Q]
random_id.token_rtb: Destruction complete after 0s
random_id.token_rtb: Creating...
random_id.token_rtb: Creation complete after 0s [id=VMCsuQ]
random_pet.pet_rtb: Creating...
random_pet.pet_rtb: Creation complete after 0s [id=topical-kid]

Apply complete! Resources: 2 added, 0 changed, 2 destroyed.

When random_id.token_rtb changes, Terraform also forces replacement of random_pet.pet_rtb due to the link defined with the replace_triggered_by argument.

Key takeaway:
replace_triggered_by enables coordinated changes across loosely coupled resources.

Back to Top

Enforcing Ordering with depends_on

Problem:
Terraform cannot always infer dependencies when relationships are based on side effects rather than attribute references.

Solution:
Use depends_on to enforce ordering.

Example:

variable "home_dir" { type = string default = "/home/user" } resource "random_id" "token_do" { byte_length = 4 provisioner "local-exec" { command = "echo 'Sleeping 20 sec' && sleep 20" } } resource "local_file" "file_do" { filename = "${var.home_dir}/file_do.txt" content = random_id.token_do.id } resource "terraform_data" "file_do" { # depends_on = [local_file.file_do] provisioner "local-exec" { command = "(echo -n 'File content: ' && cat ${var.home_dir}/file_do.txt) || echo 'File does not exists!'" } }

Workflow:

Initialize and apply the configuration:

terraform init
terraform apply -auto-approve

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # local_file.file_do will be created
  + resource "local_file" "file_do" {
      + content              = (known after apply)
...
      + filename             = "/home/user/file_do.txt"
      + id                   = (known after apply)
    }

  # random_id.token_do will be created
  + resource "random_id" "token_do" {
      + b64_std     = (known after apply)
...
      + id          = (known after apply)
    }

  # terraform_data.file_do will be created
  + resource "terraform_data" "file_do" {
      + id = (known after apply)
    }

Plan: 3 to add, 0 to change, 0 to destroy.
terraform_data.file_do: Creating...
terraform_data.file_do: Provisioning with 'local-exec'...
terraform_data.file_do (local-exec): Executing: ["/bin/sh" "-c" "(echo -n 'File content: ' && cat /home/user/file_do.txt) || echo 'File does not exists!'"]
random_id.token_do: Creating...
random_id.token_do: Provisioning with 'local-exec'...
random_id.token_do (local-exec): Executing: ["/bin/sh" "-c" "echo 'Sleeping 20 sec' && sleep 20"]
random_id.token_do (local-exec): Sleeping 20 sec
terraform_data.file_do (local-exec): File content: cat: /home/user/file_do.txt: No such file or directory
terraform_data.file_do (local-exec): File does not exists!
terraform_data.file_do: Creation complete after 0s [id=ca6a51fb-a724-3e99-3d41-d0a11f3fafbe]
random_id.token_do: Still creating... [00m10s elapsed]
random_id.token_do: Still creating... [00m20s elapsed]
random_id.token_do: Creation complete after 20s [id=b0oE8Q]
local_file.file_do: Creating...
local_file.file_do: Creation complete after 0s [id=4936512e550300ed01d468cd8b7b699d6c3e8507]

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Note the No such file... error reported by terraform_data.file_do (local-exec). Terraform attempts to provision resources in parallel and since there is no implicit ("visible" to Terraform) dependency between terraform_data.file_do and local_file.file_do, terraform_data.file_do is provisioned before the local file file_do.txt is created.

Uncomment the line with depends_on and re-run terraform apply:

terraform apply -auto-approve \
    -replace=terraform_data.file_do \
    -replace=random_id.token_do

random_id.token_do: Refreshing state... [id=b0oE8Q]
local_file.file_do: Refreshing state... [id=4936512e550300ed01d468cd8b7b699d6c3e8507]
terraform_data.file_do: Refreshing state... [id=ca6a51fb-a724-3e99-3d41-d0a11f3fafbe]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # local_file.file_do must be replaced
-/+ resource "local_file" "file_do" {
      ~ content              = "b0oE8Q" -> (known after apply) # forces replacement
...
      ~ id                   = "4936512e550300ed01d468cd8b7b699d6c3e8507" -> (known after apply)
        # (3 unchanged attributes hidden)
    }

  # random_id.token_do will be replaced, as requested
-/+ resource "random_id" "token_do" {
      ~ b64_std     = "b0oE8Q==" -> (known after apply)
...
      ~ id          = "b0oE8Q" -> (known after apply)
        # (1 unchanged attribute hidden)
    }

  # terraform_data.file_do will be replaced, as requested
-/+ resource "terraform_data" "file_do" {
      ~ id = "ca6a51fb-a724-3e99-3d41-d0a11f3fafbe" -> (known after apply)
    }

Plan: 3 to add, 0 to change, 3 to destroy.
terraform_data.file_do: Destroying... [id=ca6a51fb-a724-3e99-3d41-d0a11f3fafbe]
terraform_data.file_do: Destruction complete after 0s
local_file.file_do: Destroying... [id=4936512e550300ed01d468cd8b7b699d6c3e8507]
local_file.file_do: Destruction complete after 0s
random_id.token_do: Destroying... [id=b0oE8Q]
random_id.token_do: Destruction complete after 0s
random_id.token_do: Creating...
random_id.token_do: Provisioning with 'local-exec'...
random_id.token_do (local-exec): Executing: ["/bin/sh" "-c" "echo 'Sleeping 20 sec' && sleep 20"]
random_id.token_do (local-exec): Sleeping 20 sec
random_id.token_do: Still creating... [00m10s elapsed]
random_id.token_do: Still creating... [00m19s elapsed]
random_id.token_do: Creation complete after 19s [id=ux50Qg]
local_file.file_do: Creating...
local_file.file_do: Creation complete after 0s [id=50861283055ea87763742843ba16e86117e810df]
terraform_data.file_do: Creating...
terraform_data.file_do: Provisioning with 'local-exec'...
terraform_data.file_do (local-exec): Executing: ["/bin/sh" "-c" "(echo -n 'File content: ' && cat /home/user/file_do.txt) || echo 'File does not exists!'"]
terraform_data.file_do (local-exec): File content: ux50Qg
terraform_data.file_do: Creation complete after 0s [id=21d28f43-eb80-eb9b-dbb8-ca6f271a0fde]

Apply complete! Resources: 3 added, 0 changed, 3 destroyed.

When dependency is explicitly defined with depends_on = [local_file.file_do], Terraform ensures that provisioning of local_file.file_do is completed before terraform_data.file_do is created. Execution order is guaranteed, even if there is no direct attribute dependency.

Key takeaway:
depends_on should be used only when implicit dependencies are insufficient.

Back to Top

Summary and Best Practices

Terraform's approach to lifecycle and dependency management is one of its most powerful features. By default, Terraform builds a dependency graph from configuration references, determines safe execution order, and applies changes in a predictable and efficient manner. For the majority of use cases, these defaults are sufficient and require no additional configuration.

Lifecycle and dependency controls exist to address specific operational needs, not to replace Terraform's core model. Resource lifecycle meta-arguments allow you to influence how individual resources are created, replaced, or destroyed, while dependency management features ensure that related resources are applied in the correct order. Used correctly, these tools make infrastructure changes safer and easier to implement.

At the same time, these features should be applied with care. Overusing explicit dependencies, masking drift unnecessarily, or relying on CLI overrides can reduce clarity and introduce fragile behavior. Terraform configurations are most maintainable when intent is expressed declaratively and consistently in code, rather than enforced manually at apply time.

Best Practices Summary

  • Rely on Terraform's defaults first.
    Implicit dependencies and provider-managed lifecycle behavior handle most scenarios correctly.
  • Prefer implicit dependencies over explicit ones.
    Use attribute references whenever possible; reserve depends_on for cases Terraform cannot infer.
  • Use lifecycle meta-arguments deliberately.
    Apply create_before_destroy, prevent_destroy, ignore_changes, and replace_triggered_by only when default behavior introduces risk or operational constraints.
  • Design for lifecycle constraints.
    Some resources cannot coexist during replacement due to naming, uniqueness, or quota limitations. Plan for these constraints at the architecture level.
  • Minimize CLI-driven overrides.
    Options such as -target, -replace, and legacy taint are best suited for recovery or troubleshooting, not routine workflows.
  • Always review plan output carefully.
    Terraform's plan is the complete representation of how lifecycle and dependency rules will be applied.
  • Capture intent in code, not in process.
    Long-term safety and predictability come from clearly expressed configuration, not repeated manual intervention.

Understanding how Terraform manages dependencies and resource lifecycle and when to override that behavior allows you to build infrastructure that is both flexible and resilient. By aligning configurations with Terraform's model and applying explicit controls only when necessary, you can safely manage change at scale while preserving clarity and confidence in every workflow step.

Back to Top