| 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.
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.
~/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 references —
terraform_data.first.outputinside another block creates an implicit dependency. depends_onmeta-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:
RESOURCE_TYPE.RESOURCE_NAME.ATTRIBUTEExample from the opening lab:
terraform_data.first.outputterraform_data— resource typefirst— local name from the block headeroutput— 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:
mkdir -p ~/terraform-labs/terraform-resource-dependencies/mainMove into it — the implicit-dependency walkthrough assumes you are here:
cd ~/terraform-labs/terraform-resource-dependencies/mainWrite configuration where second reads first's output:
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
}
EOFInitialize the working directory:
terraform initApply the configuration to see implicit ordering in the log:
terraform apply -auto-approveSample output (trimmed):
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:
terraform graph -type=planSample output (trimmed):
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:
terraform graph -type=plan | dot -Tsvg > graph.svgGraphviz 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.
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:
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
]
}
EOFInitialize and confirm the graph edge exists even though second does not reference first's attributes:
terraform init && terraform graph -type=plan | grep 'second.*first'Sample output:
"[root] terraform_data.second (expand)" -> "[root] terraform_data.first (expand)"Apply when you want to confirm both resources are created:
terraform apply -auto-approvedepends_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:
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:
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 thedatablock when a read must wait for upstream work; referencedata.TYPE.NAME.attributefrom resources when you need the looked-up value. - Procedural ordering — "run this before that" when no real infrastructure dependency exists.
- Redundant edges —
depends_on = [terraform_data.first]when the block already referencesterraform_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:
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
}
EOFWrite the upstream resource in a separate file:
cat > b.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
resource "terraform_data" "first" {
input = "from-b-tf"
}
EOFInitialize and apply:
terraform init && terraform apply -auto-approveSample output (trimmed):
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:
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
}
EOFValidate in the cycle directory:
terraform init && terraform validateSample output:
Error: Cycle: terraform_data.a, terraform_data.bterraform 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:
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
}
EOFInitialize and plan before any apply:
terraform init && terraform planSample output (trimmed):
+ 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:
terraform_data.first
└── terraform_data.second
└── output.chainMultiple branches are common:
terraform_data.first
├── terraform_data.second
└── terraform_data.third
└── module.childTerraform 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
- Resource behavior — dependencies
- depends_on meta-argument
- terraform graph command
- terraform_data resource
- Module depends_on
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.

