Chapter 3: The Core Workflow
Topic 18

Dependencies

ConceptOrdering

Terraform does not run your resources in file order — it builds an order from the references between them. When a Pub/Sub IAM member references google_service_account.processor.email, Terraform knows the service account must exist first; that reference is the dependency. You almost never declare ordering by hand, because the act of using one resource's value in another declares it for you.

Explicit depends_on exists for the minority of cases where a real ordering requirement leaves no reference to infer it from — most often an IAM binding that must be in place before the resource that uses that permission runs. It is a sharp tool with a narrow purpose, and over-using it is exactly how a clean, parallel graph turns into a slow tangle.

Implicit Dependencies from References

Any expression that reads another resource's attribute creates an edge. Write bucket = google_storage_bucket.raw.name and Terraform records that this resource depends on the bucket, and so the bucket is created first. This is the normal, preferred way ordering is established — you express what a resource needs by referencing it, and the order falls out for free.

A reference is the dependency — no depends_on needed
resource "google_storage_notification" "raw" {
  bucket = google_storage_bucket.raw.name   # edge: bucket before notification
  topic  = google_pubsub_topic.events_ingest.id
}

Two edges are declared here, both implicitly: the notification needs the hatch-events-raw bucket and the events-ingest topic, so both are created before it. You wrote no ordering instruction — the references carry it.

When There Is No Reference to Infer From

Sometimes A must exist before B, but B's configuration never reads anything from A. The classic GCP case is an IAM binding: the event-processor Cloud Run service must run with a service account that already holds its BigQuery write role, but the Cloud Run block references neither the binding nor the role — it references the service account's email, which exists whether or not the role is attached.

So there is no implicit edge from the binding to the service, even though one is genuinely needed. Terraform may create the service before the role is granted, the service starts, and its first write to analytics returns permission denied. Nothing in the config told Terraform to wait, because no value flowed from the binding into the service.

depends_on for Hidden Dependencies

This is what depends_on is for. Adding depends_on = [google_project_iam_member.processor_bq] to the Cloud Run service forces it to be created only after the service account has its BigQuery write role — supplying the ordering edge that no reference could express.

depends_on supplies the edge a reference cannot
resource "google_cloud_run_v2_service" "processor" {
  name = "event-processor"
  # ... config that references the SA email but not its roles ...
  depends_on = [google_project_iam_member.processor_bq]
}

The rule of thumb: use depends_on when there is a real ordering requirement and no attribute of the dependency appears anywhere in the dependent's config. If a reference exists, the edge already exists and depends_on is noise.

Two ways an ordering edge is created
Implicit dependency
A reference like google_storage_bucket.raw.name is the edge; the order falls out for free. The normal, preferred way.
Explicit dependency
depends_on supplies an edge no reference could express, for hidden ordering like IAM-before-use. A sharp tool with a narrow purpose.

API Enablement and Eventual Consistency

A resource often needs an API enabled before it can be created — a Cloud Run service needs the Run API on. You express that with depends_on the relevant google_project_service. But because API enablement on GCP is eventually consistent, the edge alone is sometimes not enough: enablement is acknowledged before it has fully propagated, and the next resource can still race it.

The pragmatic fix where this race actually appears is a short time_sleep between the enablement and the dependent resource, giving propagation a window to finish. Add it only where the race shows up in practice, not preemptively on every API — a sleep on every enablement slows every apply for a problem most of them do not have.

Over-Using depends_on

Adding depends_on "to be safe" when a reference already exists is redundant noise — it obscures the real inferred edge and implies a problem that is not there. Worse is a broad depends_on: putting it on an entire module, or between two resources that did not need ordering, serializes more than necessary. Every resource that now waits has lost its chance to run in parallel, and the apply slows for no benefit. Scope every depends_on to the single resource that genuinely must wait.

Common Mistakes
  • Creating event-processor with a service account that lacks its BigQuery role at create time because no reference forced the ordering — the service starts, the first write to analytics returns permission denied, and the failure is intermittent and confusing.
  • Adding depends_on to a resource that already references the dependency's attribute — pure noise that obscures the real, inferred edge and suggests a problem that is not there.
  • Using depends_on to paper over a missing reference instead of just referencing the attribute — you lose the value the reference would have carried and keep a brittle manual edge.
  • Putting depends_on on an entire module when only one resource inside needs to wait — every resource in the module now waits, serializing the apply and erasing parallelism.
  • Assuming depends_on google_project_service.run alone guarantees the API is usable — enablement propagates asynchronously, so the next resource can still race it without a brief time_sleep.
  • Wiring a literal GCP name into a dependent resource instead of referencing the attribute — the literal carries the value but creates no edge, so the ordering you assumed never exists.
Best Practices
  • Express dependencies by referencing attributes; let Terraform infer order and reserve depends_on for genuinely hidden requirements.
  • Use depends_on for the IAM-before-use case, where a resource needs a permission granted by a binding it does not reference.
  • Scope depends_on to the narrowest resource that actually must wait, never a whole module, to preserve parallelism.
  • Pair depends_on on a google_project_service with a short time_sleep only where an enablement race actually appears, not preemptively everywhere.
  • Audit existing depends_on lines periodically and delete any whose dependency is already expressed by a reference.
Comparable tools Pulumi infers dependencies from references and offers dependsOn identically Config Connector relies on the Kubernetes controller's continuous reconciliation, not an ordered graph Ansible orders by task position — the opposite model

Knowledge Check

How does Terraform learn that the bucket must be created before the notification that points at it?

  • The notification references google_storage_bucket.raw.name, and that reference creates the ordering edge
  • It reads the two resource blocks top to bottom as they appear in the file and creates them in that written order
  • Both resource blocks carry an explicit depends_on that lists the other one
  • The provider hardcodes that storage buckets always precede their notifications

Why does the IAM-before-use case need an explicit depends_on?

  • The service references the account's email but never the role binding, so no reference creates the required edge
  • IAM role bindings are always applied dead last in the run, after every other resource, unless something forces them earlier
  • A Cloud Run service is forbidden from referencing any other resource by attribute
  • The role binding and the service are declared in two different modules

What is the cost of putting depends_on on an entire module instead of one resource?

  • Every resource in the module now waits, serializing the apply and erasing parallelism
  • The whole module is destroyed and recreated from scratch on every apply
  • Terraform refuses to run because a module-level depends_on is rejected outright as invalid syntax
  • The module's outputs become (known after apply) permanently on every run

Why might depends_on on a google_project_service still let the next resource fail?

  • API enablement is eventually consistent and is acknowledged before it propagates; a short time_sleep covers the gap
  • depends_on is silently unsupported and quietly ignored on the google_project_service resource type, so the ordering never takes hold
  • Enabling the API requires a second, separate apply before it takes effect
  • The dependent resource quietly ignores its depends_on edge at apply time

You got correct