How does Terraform build its dependency graph, and what's the difference between implicit and explicit dependencies?
Quick Answer
Terraform parses every resource/module/data block and builds a directed acyclic graph so it can determine safe ordering and parallelize unrelated work. Most dependencies are **implicit**: referencing `aws_vpc.main.id` inside a subnet resource automatically tells Terraform the subnet depends on the VPC. Some dependencies aren't visible through references at all (e.g., an IAM policy that must exist before an app starts, with no attribute link between them) — for those you add an **explicit** `depends_on = [aws_iam_role_policy.x]` to force the ordering. Prefer implicit dependencies (via references) whenever possible; they're self-documenting and Terraform can reason about them precisely.
Detailed Answer
Terraform doesn't apply resources in the order they're written in a file — it builds a directed acyclic graph (DAG) from every resource, data source, and module, and uses that graph to determine both correctness (what must happen before what) and performance (what can happen in parallel).
Implicit dependencies (the common case)
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id # <- this reference creates the dependency
cidr_block = "10.0.1.0/24"
}
Simply referencing aws_vpc.main.id inside the subnet's configuration is enough — Terraform parses the expression, sees the reference, and automatically adds an edge in the graph: "subnet depends on vpc." No extra syntax needed, and this is how the vast majority of dependencies in real configurations are expressed. It's also self-documenting — anyone reading the subnet resource can see exactly why it depends on the VPC.
Explicit dependencies (depends_on)
Sometimes a real dependency exists that no resource attribute reflects. Classic example: an application needs an IAM policy attached before it starts, but the compute resource's configuration doesn't reference the policy resource at all:
resource "aws_iam_role_policy" "app_permissions" {
# ...
}
resource "aws_instance" "app" {
# no attribute here references aws_iam_role_policy.app_permissions,
# yet the app must not start until that policy exists.
depends_on = [aws_iam_role_policy.app_permissions]
}
depends_on forces the ordering edge explicitly, even without any data flowing between the two resources.
Why prefer implicit dependencies
- They're derived directly from real data flow, so they can never silently go stale the way a
depends_onlist can (nothing stops someone from refactoring away the reason for adepends_onand forgetting to remove it). - The dependency graph is exactly what enables Terraform to parallelize unrelated resource creation — resources with no path between them in the graph can be created concurrently, which is a major reason large applies aren't purely sequential.
depends_on is a valid, sometimes-necessary escape hatch, but it should be reached for only when there's genuinely no attribute reference that could express the same relationship implicitly.