Terraform Resource References and Dependencies

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
Applies to Any host with Terraform installed
Lab environment Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu
Privilege Normal user; sudo only if Terraform is not installed yet
Scope Terraform resource references, implicit dependencies from attribute expressions, explicit depends_on on resources and child modules, depends_on inside data blocks, terraform graph, file-order misconception, dependency cycles, unknown values during planning, and common dependency mistakes. Does not cover create_before_destroy, prevent_destroy, ignore_changes, replace_triggered_by, count or for_each depth, or module design patterns.
Related guides Terraform resources
Terraform data sources
terraform plan command
terraform validate command
Terraform Associate certification course

When one Terraform resource needs another to exist first, Terraform must know about that relationship before it can plan or apply safely. Most of the time you express that relationship with a resource reference — not with file order or a manual checklist.

hcl
resource "terraform_data" "first" {
  input = "first"
}

resource "terraform_data" "second" {
  input = terraform_data.first.output
}

The expression terraform_data.first.output does two jobs at once: it passes a value into second, and it tells Terraform that first must be handled before second. That inferred ordering is an implicit dependency. When no attribute reference captures the relationship, you add an explicit dependency with depends_on.

This guide focuses on how Terraform builds that dependency graph, when depends_on is actually necessary, and how to read terraform graph output. Examples use terraform_data so you can practice without external providers.

NOTE
Work in ~/terraform-labs/terraform-resource-dependencies/ on the Terraform lab environment on Ubuntu. Run terraform init before validate, plan, or apply in each new subdirectory. For resource block anatomy, see Terraform resources.

How Terraform determines resource order

Terraform is declarative. You describe desired infrastructure; Terraform compares configuration to state, builds a dependency graph of resource instances, and chooses an order that respects every edge in that graph. It does not walk .tf files top to bottom the way a shell script runs line by line.

Each edge in the graph comes from:

  • Attribute referencesterraform_data.first.output inside another block creates an implicit dependency.
  • depends_on meta-arguments — explicit dependencies when references alone are not enough.

Once the graph is built, Terraform schedules create, update, and destroy operations so upstream nodes complete before downstream nodes that depend on them. Parallelism is possible only where no dependency edge exists between two instances.


Resource references and implicit dependencies

Reference syntax

A resource attribute reference uses three dot-separated parts:

text
RESOURCE_TYPE.RESOURCE_NAME.ATTRIBUTE

Example from the opening lab:

hcl
terraform_data.first.output
  • terraform_data — resource type
  • first — local name from the block header
  • output — attribute exported by that instance

The same pattern applies to provider-backed resources (docker_container.web.id) and to data sources with a data. prefix (data.docker_network.lab.name). This lesson stays on managed resources; Terraform data sources covers the data. form in depth.

Implicit dependency from a reference

Create the main lab directory:

bash
mkdir -p ~/terraform-labs/terraform-resource-dependencies/main

Move into it — the implicit-dependency walkthrough assumes you are here:

bash
cd ~/terraform-labs/terraform-resource-dependencies/main

Write configuration where second reads first's output:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

resource "terraform_data" "first" {
  input = "first"
}

resource "terraform_data" "second" {
  input = terraform_data.first.output
}

output "chain" {
  value = terraform_data.second.output
}
EOF

Initialize the working directory:

bash
terraform init

Apply the configuration to see implicit ordering in the log:

bash
terraform apply -auto-approve

Sample output (trimmed):

output
terraform_data.first: Creating...
terraform_data.first: Creation complete after 0s [...]
terraform_data.second: Creating...
terraform_data.second: Creation complete after 0s [...]

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

Outputs:

chain = "first"

Terraform always created first before second because the reference in second's input argument requires first's output attribute. No depends_on was needed — the expression itself communicated the relationship.

Inspect the graph with terraform graph

terraform graph prints the dependency graph Terraform uses internally. Run it after init:

bash
terraform graph -type=plan

Sample output (trimmed):

output
digraph {
  subgraph "root" {
    "[root] terraform_data.first (expand)" [label = "terraform_data.first", shape = "box"]
    "[root] terraform_data.second (expand)" [label = "terraform_data.second", shape = "box"]
    "[root] terraform_data.second (expand)" -> "[root] terraform_data.first (expand)"
  }
}

The arrow from second to first is the implicit dependency edge. Reading DOT syntax is optional — the important part is that second points at first, not the reverse.

If Graphviz is installed on your workstation, you can render a diagram:

bash
terraform graph -type=plan | dot -Tsvg > graph.svg

Graphviz is optional for understanding this article. The text graph above is enough to confirm ordering.


Explicit dependencies with depends_on

depends_on declares a dependency Terraform cannot infer from attribute references alone. Use it for hidden behavioral dependencies — ordering requirements that are not expressed through values passed between blocks.

hcl
resource "terraform_data" "second" {
  input = "second"

  depends_on = [
    terraform_data.first
  ]
}

depends_on declares explicit dependencies on upstream resources or child modules in the same calling module. The meta-argument can also be used inside data blocks when a data read must wait for an upstream resource or module. Do not put arbitrary strings, timestamps, or procedural steps in the list — only addresses Terraform can resolve in the graph.

Implicit vs explicit dependencies

Aspect Implicit (reference) Explicit (depends_on)
How you declare it Use another object's attribute in an expression Add depends_on = [ ... ]
Terraform inference Automatic from the reference You supply the edge manually
Communicates data flow Yes — shows which value is consumed No — ordering only
When to prefer it Whenever a reference naturally exists When ordering is required but no attribute should be passed

Prefer expression references when they represent the real relationship. A reference documents both data flow and order. Reserve depends_on for cases where the dependent block does not need any attribute from the upstream object but must still wait for it.

Use depends_on only for hidden dependencies Terraform cannot infer. Explicit dependencies can make plans more conservative because Terraform knows only that the upstream object may affect the downstream object, not which specific attribute matters. This can cause more values to remain (known after apply) than with a direct expression reference — especially when the list includes a whole module.

Explicit dependency between resources

Switch to a clean directory for the explicit-only demo:

bash
mkdir -p ~/terraform-labs/terraform-resource-dependencies/explicit && cd ~/terraform-labs/terraform-resource-dependencies/explicit && cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

resource "terraform_data" "first" {
  input = "first"
}

resource "terraform_data" "second" {
  input = "second"

  depends_on = [
    terraform_data.first
  ]
}
EOF

Initialize and confirm the graph edge exists even though second does not reference first's attributes:

bash
terraform init && terraform graph -type=plan | grep 'second.*first'

Sample output:

output
"[root] terraform_data.second (expand)" -> "[root] terraform_data.first (expand)"

Apply when you want to confirm both resources are created:

bash
terraform apply -auto-approve

depends_on in data sources

When a resource argument already references data.TYPE.NAME.attribute, Terraform infers the read must complete first — the usual case in Terraform data sources. You do not add depends_on on the resource to point at a data source address.

Put depends_on on the data block when a real ordering requirement exists that its arguments do not express — deferring the read until an upstream resource finishes:

hcl
resource "docker_network" "created" {
  name = "tf-deps-net"
}

data "docker_network" "created" {
  name = "tf-deps-net"

  depends_on = [docker_network.created]
}

When Terraform itself creates the network, reference docker_network.created.name directly instead of re-querying through a data source. The data-sources lesson walks through deferred reads; this article introduces the depends_on shape on the data block only.

depends_on with modules

A parent module can wait for a resource before instantiating a child module:

hcl
resource "terraform_data" "first" {
  input = "before-module"
}

module "child" {
  source = "./child"
  label  = "inside-module"

  depends_on = [
    terraform_data.first
  ]
}

Module-to-module dependency design — outputs feeding inputs, nested depends_on, when to split modules — belongs in a modules lesson. For Associate exam tasks, remember that depends_on on a module block waits for upstream resources or other modules, and that child module resources become separate nodes inside the expanded graph.

What not to put in depends_on

  • Non-reference values — strings, numbers, or local variables that are not resource or module addresses.
  • Data source addresses on resource depends_on — put ordering on the data block when a read must wait for upstream work; reference data.TYPE.NAME.attribute from resources when you need the looked-up value.
  • Procedural ordering — "run this before that" when no real infrastructure dependency exists.
  • Redundant edgesdepends_on = [terraform_data.first] when the block already references terraform_data.first.output; the reference already created the edge.

Lifecycle meta-arguments such as create_before_destroy solve a different problem — replacement ordering during destroy/create — and belong in a lifecycle lesson, not in depends_on.


File order, cycles, and unknown values

Terraform does not follow file order

Split the same implicit chain across two files with the dependent resource listed first alphabetically:

bash
mkdir -p ~/terraform-labs/terraform-resource-dependencies/file-order && cd ~/terraform-labs/terraform-resource-dependencies/file-order && cat > a.tf <<'EOF'
resource "terraform_data" "second" {
  input = terraform_data.first.output
}
EOF

Write the upstream resource in a separate file:

bash
cat > b.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

resource "terraform_data" "first" {
  input = "from-b-tf"
}
EOF

Initialize and apply:

bash
terraform init && terraform apply -auto-approve

Sample output (trimmed):

output
terraform_data.first: Creating...
terraform_data.first: Creation complete after 0s [...]
terraform_data.second: Creating...
terraform_data.second: Creation complete after 0s [...]

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

first still ran before second even though a.tf appears before b.tf and second is declared first in the directory. Filename and declaration order do not override the reference graph.

Dependency cycles

A cycle forms when dependencies loop — A needs B and B needs A — so no valid ordering exists:

bash
mkdir -p ~/terraform-labs/terraform-resource-dependencies/cycle && cd ~/terraform-labs/terraform-resource-dependencies/cycle && cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

resource "terraform_data" "a" {
  input = terraform_data.b.output
}

resource "terraform_data" "b" {
  input = terraform_data.a.output
}
EOF

Validate in the cycle directory:

bash
terraform init && terraform validate

Sample output:

output
Error: Cycle: terraform_data.a, terraform_data.b

terraform plan reports the same cycle. Break the loop by removing one reference, introducing an intermediate resource, or restructuring so values flow in one direction only.

Unknown values and dependencies

References create dependencies even when attribute values are not known during planning. On a first create, computed attributes often show as (known after apply) in the plan.

Use a fresh directory to see that behavior:

bash
mkdir -p ~/terraform-labs/terraform-resource-dependencies/unknown-after-apply && cd ~/terraform-labs/terraform-resource-dependencies/unknown-after-apply && cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

resource "terraform_data" "first" {
  input = "seed"
}

resource "terraform_data" "second" {
  input = terraform_data.first.output
}

output "chain" {
  value = terraform_data.second.output
}
EOF

Initialize and plan before any apply:

bash
terraform init && terraform plan

Sample output (trimmed):

output
+ resource "terraform_data" "first" {
      + output = (known after apply)
    }

  + resource "terraform_data" "second" {
      + input  = (known after apply)
      + output = (known after apply)
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + chain = (known after apply)

The dependency edge still exists — second waits for first — even though terraform_data.first.output is unknown until apply computes it. Terraform carries unknown values through the graph rather than dropping the ordering requirement.


Dependency graph intuition

Think of the graph as a tree of prerequisites, not a list of files:

text
terraform_data.first
└── terraform_data.second
    └── output.chain

Multiple branches are common:

text
terraform_data.first
├── terraform_data.second
└── terraform_data.third
    └── module.child

Terraform may run independent branches in parallel. It never runs a downstream node before its dependencies complete. terraform graph is a useful way to inspect whether Terraform has inferred a dependency edge between objects.


Common dependency mistakes

Mistake Why it fails Better approach
Unnecessary depends_on Duplicates an edge references already create; makes plans more conservative Reference the attribute directly
Circular references validate / plan report Error: Cycle Restructure so values flow one way
Assuming file order a.tf before b.tf does not imply create order Use references or justified depends_on
Wrong instance address terraform_data.app[0] vs terraform_data.app["web"] typo Match the count or for_each key exactly
depends_on instead of lifecycle Tries to fix replacement timing Use lifecycle meta-arguments in a dedicated lesson
Explicit dependency without reason Hides the real data relationship Prefer upstream.attribute when the value matters

References


Summary

Terraform dependencies come from the graph Terraform builds before every plan and apply. A resource reference such as terraform_data.first.output creates an implicit dependency — Terraform orders first before second and can still schedule that edge when the attribute is (known after apply) on the first run.

Use depends_on only when ordering is required but attribute references do not express it — hidden side effects on resources or child modules, deferred data reads declared on the data block, or module instantiation order. Prefer references when they communicate both data flow and sequence; unnecessary depends_on makes plans more conservative and can leave more values as (known after apply).

Filenames do not determine order; cycles fail fast at validate with a clear Error: Cycle message. terraform graph shows the edges Terraform actually uses — optional Graphviz rendering is a nice extra, not a requirement. When a plan surprises you, check whether the graph edge you expected came from a reference or from depends_on, then fix the expression before adding meta-arguments. Next: deepen read-only lookups in Terraform data sources or lifecycle replacement rules in a dedicated lifecycle lesson.

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)