Cloud Infrastructure · Infrastructure as Code

Resource Graphs and Dependency Ordering

How references between resources become directed edges, why a cycle is rejected before anything is created, and how ordering decisions change apply time and blast radius.

Ordering is the first thing a declarative tool has to solve, because the configuration deliberately does not contain an order. The author writes resources in whatever sequence reads well, splits them across files and modules, and expects the tool to work out what must happen before what. The answer is a directed graph whose edges come from references, and almost every surprising behaviour in a large configuration (a slow apply, a resource that gets replaced when it should not, a destroy that hangs) traces back to the shape of that graph.

The graph is not a convenience. It is the only sound way to schedule work when the input is a set of objects rather than a sequence of statements, and it is what allows independent work to run concurrently without the author having to prove independence by hand.

References are edges, and the direction is data flow

When one resource’s argument reads an attribute of another, the tool records a dependency. In Terraform, a subnet that takes a VPC identifier from a VPC resource has an edge from the VPC to the subnet. The edge means two things at once: the VPC must exist and be readable before the subnet is created, and the VPC must not be destroyed until the subnet is gone.

That single edge covers both directions of the lifecycle because destruction is scheduled by reversing the same graph. A plan that creates the VPC and then the subnet becomes, on destroy, a plan that deletes the subnet and then the VPC. There is no separate destroy-order declaration, and there does not need to be: whatever had to exist first must be removed last.

The edge is created by the expression, not by proximity. Two resources in the same file with no attribute reference between them are unrelated, and the tool may create them in either order or at the same time. This is a frequent source of confusion, because a reader looking at a file sees a top-to-bottom narrative that the tool does not see at all. It is also a source of correctness: if an ordering matters, it must be expressed as data flow, or the configuration is relying on an accident.

Implicit edges are narrower than they look

An attribute reference produces an edge only when the referenced value is actually used. A configuration that names a VPC by a literal string, or by a variable holding a string, produces no edge at all. The subnet will be created without waiting for the VPC, and if the VPC does not exist yet, the API call fails. Terraform will report the failure against the subnet, which points the reader at the wrong resource.

The remedy is depends_on, a meta-argument that adds an edge without consuming a value. It exists for the cases where the dependency is real but not visible in the data: a policy that must be attached before an object is usable, a cluster add-on that must be installed before a workload is scheduled, a IAM propagation delay that has no readable attribute to reference.

depends_on is coarse. It makes the entire referenced resource a prerequisite for the entire dependent resource, at create and at destroy, including every operation on both. Applied broadly, it converts a graph with wide parallel layers into a chain. Ten independent resources with a single unnecessary depends_on between consecutive pairs turn ten parallel creates into ten serial ones, and the apply time multiplies accordingly. The parallelism setting cannot recover that: a graph with no width has nothing to run in parallel.

Cycles are a static error, not a runtime one

A directed graph with a cycle cannot be topologically ordered, and a provisioning tool cannot execute it. Terraform detects the cycle during graph construction, before any API call, and reports the addresses of the resources involved. Nothing is created, so the failure is clean: fix the configuration and retry, with no partially applied world to reason about.

Cycles usually come from two patterns. The first is a genuine mutual reference between two objects that the API expects to be created in two steps: a security group that must reference itself to allow member traffic, or two resources that each need the other’s identifier. Providers handle this by exposing a two-phase shape: create with one field empty, then update with the other’s value. The configuration can express that only if the provider models it, so the cycle is often a signal that the provider’s schema is missing an intermediate object.

The second pattern is a reference that looks like data flow but is not. A resource that reads a value from a module output, where the module in turn takes an input from that resource, is a cycle through the module boundary. The fix is to break the loop by passing the value the other way, or by introducing a resource that owns the shared value and is referenced by both.

Concurrency is a property of the graph’s width

Given a valid graph, the tool processes it in waves. Every node whose dependencies are satisfied can run at once, subject to a concurrency limit. Terraform’s default is ten simultaneous operations, configurable with -parallelism. The limit exists because cloud APIs rate-limit, and because a burst of creates can exhaust quota in a way that is harder to diagnose than a slow apply.

The number is a ceiling, not a target. A configuration of three resources uses at most three. A configuration of three hundred arranged as a fan-out from one VPC can use the full ten, and the wall-clock time is roughly the depth of the graph times the per-resource latency divided by the width, bounded by the slowest resource in each layer. Depth is therefore the number that matters for apply time, and depth is what depends_on increases.

Some resources are slow regardless of graph shape. A managed database cluster that takes many minutes to become available dominates the layer it sits in, and everything downstream waits for it. There is no scheduling trick that helps; the only lever is to remove dependencies that are not needed, so that unrelated work proceeds during the wait.

Ordering in other models

Kubernetes solves the same problem with a different mechanism. Objects are applied to an API server that accepts them in any order, so ordering is not enforced at admission; instead, controllers watch for the objects they care about and act when they appear. A Deployment creates a ReplicaSet, which creates Pods, through controller ownership rather than through a scheduler over a graph. Garbage collection follows the ownership edges in reverse: deleting an owner deletes its dependents according to the owner reference rules, with a background or foreground policy deciding whether the dependents go first.

CloudFormation sits closer to the Terraform model: a template declares resources and the service infers dependencies from references, with an explicit DependsOn attribute for the cases inference misses. It also supports creation policies and wait conditions, which exist because some resources are ready long after the API reports them created, the same asynchronous gap that providers handle by polling.

The common thread is that every model needs an identity for each object and an edge set over those identities. Where the models differ is when the edges are evaluated: before any call is made, as in Terraform’s static graph, or continuously, as in a controller loop. Static evaluation gives a plan that can be reviewed and a failure that leaves nothing half-built. Continuous evaluation tolerates objects appearing and disappearing at any time, at the cost of never having a single moment that describes the whole system.

Where the graph is not the whole story

The graph orders operations the tool performs. It says nothing about ordering inside a single API call, and nothing about effects that escape the tool’s view. A resource that registers a DNS record which another system reads, or that emits an event consumed by an external queue, has dependents the graph cannot represent. Those dependents fail later and elsewhere, and the failure looks unrelated to the apply that caused it.

There is also the question of what happens when a node fails mid-apply. Terraform records what succeeded and stops scheduling the rest, leaving a partial graph applied and a state file that reflects it. The next plan operates on the remainder. This is why a failed apply is recoverable but a failed apply with a cycle-adjacent manual fix is not: the state records addresses, and addresses that were renamed by hand no longer match. That mapping is the subject of state as a mapping, and the graph is only as useful as the identities it connects.

References

  1. Terraform Language: Resource Dependencies — HashiCorp
  2. Kubernetes Documentation: Owners and Dependents — Kubernetes project
  3. Introduction to Algorithms, 3rd edition, Chapter 22, Elementary Graph Algorithms — Cormen, Leiserson, Rivest, Stein; MIT Press