Terraform depends_on: Model Hidden Dependencies Without Hiding the Graph
Use depends_on for hidden dependencies, understand conservative plans and data-source deferral, and avoid confusing ordering with readiness.
Terraform builds a dependency graph from references between resources, data sources, modules, and outputs. If one resource argument reads another resource’s attribute, Terraform can infer that the producer must be processed first. The depends_on meta-argument adds an edge when the dependency exists operationally but is not represented by a value reference.
An explicit edge is easy to add and difficult to see in the resulting plan. It can make Terraform wait for every action on the dependency, including reads, and may cause values to become unknown until later in the run. A broad edge can reduce parallelism, hide the real data flow, delay data-source reads, and make plans more conservative. Use depends_on only when the dependency is real, hidden, and cannot be expressed by a normal reference.
Let value references express normal dependencies
Prefer a resource argument that references the value it consumes. That tells Terraform both the ordering and the data relationship:
resource "aws_security_group" "application" {
name = "application"
vpc_id = var.vpc_id
}
resource "aws_instance" "application" {
ami = var.ami_id
instance_type = var.instance_type
vpc_security_group_ids = [aws_security_group.application.id]
}
The instance cannot be planned with the final security group ID until Terraform has enough information about that security group. The reference gives Terraform a precise edge and a value to propagate. Do not add depends_on to repeat a relationship already visible through an argument; redundant dependencies make the configuration noisier without adding a stronger guarantee.
Terraform does not use the order of resource declarations or filenames as an execution sequence. Reordering blocks is not a dependency mechanism. A reviewer should be able to understand why an edge exists from the configuration rather than relying on the visual placement of two blocks.
Use explicit ordering for hidden behavior
An explicit dependency is appropriate when one object needs another object’s behavior but does not consume one of its values. For example, an instance may need an IAM policy attachment to exist before startup, while its arguments reference only the instance profile. The profile and policy attachment are distinct objects; Terraform may otherwise create them concurrently.
resource "aws_instance" "application" {
ami = var.ami_id
instance_type = var.instance_type
iam_instance_profile = aws_iam_instance_profile.application.name
depends_on = [
aws_iam_role_policy_attachment.application,
]
}
The resource argument already creates a dependency on the instance profile. The explicit edge documents the additional operational requirement that the policy attachment API call complete before instance creation begins. Keep the reference narrow and add a comment describing why the value reference alone is insufficient.
This edge does not guarantee that the cloud control plane has finished propagating the policy to every data plane or that the guest operating system has obtained working credentials. Terraform waits for provider operations to report completion; it is not a distributed readiness protocol. If the application needs a service to become healthy, use health checks, retry logic, deployment orchestration, or a provider-supported readiness signal rather than extending the Terraform graph and assuming ordering equals readiness.
Dependencies on modules are broad
A module block can use depends_on, but that edge applies to the module’s resources and data sources as a whole. If module A depends on module B, Terraform can conservatively delay the work in A until all work in B has completed. This may be necessary for a genuine hidden module-level prerequisite, but it is broader than a reference to one output from one resource.
Prefer passing a dependency value through the module interface when a real data relationship exists. A network module can expose a subnet ID, and an application module can consume it. This makes the contract explicit and limits planning uncertainty to the value that actually matters. An output with a dependency can be used when an operational requirement is not otherwise visible, but document that exceptional edge.
Avoid attaching depends_on to a whole module because “the database should be ready before the application.” If the application needs a database endpoint, pass the endpoint output. If it needs schema migration or service readiness beyond resource creation, model that as an application deployment step with a reliable health or migration signal. A module-wide edge can order Terraform operations but cannot prove a database accepts queries or that a migration completed.
Data-source reads can move from plan to apply
Terraform normally reads data sources during planning when their arguments are known. If a data-source argument depends on a resource that is changing in the same plan, Terraform may defer the read until apply. Its attributes then appear as unknown during planning, and resources that depend on those attributes can become more conservative.
An explicit depends_on on a data block can also delay the read. This is sometimes required when the query depends on a hidden side effect, but it may hide data that Terraform could otherwise read before planning resource changes. A data source that selects a latest image or looks up a policy can cause downstream values to become unknown if Terraform cannot safely evaluate the lookup until the dependency has been applied.
When a data source queries something Terraform itself manages, prefer referencing the managed resource directly rather than reading it back through a provider API. Direct references create a more precise dependency and avoid a second lookup. Use data sources for external information or provider-defined read queries, not as a generic indirection layer for objects already in state.
Static dependency references and cycles
The depends_on value must be known before Terraform builds the graph. It accepts references to resources or modules in the same calling module, not arbitrary computed expressions that select a dependency at runtime. This is necessary because Terraform must know which actions can run concurrently before it starts planning or applying them.
Dependencies form a directed graph. If A depends on B and B depends on A, Terraform cannot find a valid execution order and reports a cycle. Removing one edge may make the graph acyclic but still be incorrect if a real dependency was hidden. Find the underlying requirement and express it as a value flow where possible. For graph inspection, use terraform graph in a controlled checkout and focus on the edges around the resources involved in the cycle.
Do not build a chain of artificial dependencies to serialize an entire deployment. Serialization may appear to avoid intermittent failures, but it increases runtime and obscures which components actually rely on one another. If a remote system needs eventual consistency or an external readiness gate, model that specific operation explicitly and keep Terraform’s infrastructure graph limited to infrastructure dependencies.
Plan conservatism and unknown values
Terraform uses dependencies to decide what it must refresh, create, update, or destroy before it can evaluate downstream values. When an explicit edge is broad, Terraform may assume an upstream object could affect more downstream values than the configuration directly shows. This can lead to “known after apply” values and a plan that replaces or updates more objects conservatively.
A more conservative plan is not automatically safer. It can be harder to review, can hide whether a particular input change truly affects a resource, and can trigger a replacement that would not occur with a more precise reference. Before accepting an edge, ask whether the downstream block needs an actual upstream value or merely relies on an undocumented side effect. If the latter is true, add an explanatory comment and test the plan before and after the dependency change.
When a large plan becomes unknown after adding depends_on, compare it with the prior plan, identify which values changed from known to unknown, and trace each value back through the graph. Check whether an entire module was made dependent, whether a data source read was deferred, or whether the edge duplicated a reference. Do not respond by adding more edges or targeting resources without first understanding the resulting plan.
Test and review dependency changes
For a hidden dependency, document the behavior that requires ordering and the reason Terraform cannot infer it. Review the specific edge, the plan ordering, and whether any data-source reads move to apply. A focused test can verify that a module exposes the value downstream code needs. A controlled integration test is needed to verify external service readiness; a plan test can show graph intent but not production propagation timing.
When using output-level depends_on, include a comment because the dependency is not obvious from the output value itself. When using module-level depends_on, verify that the broader barrier is intentional. Keep the smallest edge that captures the real prerequisite and remove obsolete edges when the underlying provider or module begins exposing a direct value reference.
Ordering is not a substitute for orchestration
Terraform’s graph coordinates provider operations for one Terraform operation. It does not coordinate arbitrary application state transitions, cross-repository releases, or long-running readiness conditions. A create call can return before a service is healthy, and a dependency edge does not make a remote API transactional. Design bootstrap scripts and applications to tolerate retries, define health probes, and use a deployment controller when readiness and rollout policy matter.
The best dependency graph is not the one with the most edges. It is the one that exposes real value flow, names exceptional hidden dependencies, and keeps unrelated changes parallel. Use depends_on as a precise exception, understand its effects on planning and data reads, and keep service readiness in the system that can actually observe and manage it.
Related:
- Terraform for_each: Stable Resource Identity, Safe Refactors, and Plan-Time Keys
- Terraform Conditions: Put Each Invariant at the Right Evaluation Boundary
Sources: