Skip to content

Dependencies and cascade

Declare that one Stack depends on another, cascade an upstream apply to its dependents, and pass an upstream's root outputs downstream — safely.

Real estates are layered — a network Stack feeds a dns Stack feeds your app Stacks. Tell Stacks about that with a dependency, and an upstream change can flow to everything that builds on it.

Stack dependencies

A dependency is an edge you author on the downstream Stack — “this Stack depends on that one” — from the Console, API, lbr, or the Terraform provider.

  • Dependencies are within one organization; both Stacks are yours.
  • They form a graph — one upstream can feed many downstreams, and one downstream can depend on many upstreams. It stays acyclic: an edge that would close a loop is rejected when you declare it, naming the loop, so a Run never trips over a bad graph.
  • You cannot delete an upstream Stack while anything still depends on it — remove the edges first.
  • Authoring an edge needs a grant on the downstream Stack.

Cascade on apply

Each edge carries a cascade mode:

  • trigger (default) — when the upstream applies, the downstream runs.
  • ordering — the edge expresses order and freshness only; it never starts a Run on its own.

When an upstream Change Run applies with real changes, each trigger-edge downstream gets a Run enqueued. Whether that Run applies is decided by the downstream’s own execution mode: an auto_apply downstream applies; a merge_then_apply or branch_apply downstream parks at approval; a manual one does not fire. A cascade never applies something you would want to review.

Cascades propagate one hop at a time — an upstream apply wakes its direct dependents, and each of their applies wakes theirs. Order falls out naturally, and any Stack that parks for approval is a checkpoint in the chain. A no-op apply wakes nothing, and a Destroy never cascades in either direction. If an apply fails, the cascade stops there: the subtree below it does not run, sibling branches are unaffected, and nothing already applied is rolled back. Fix the Stack and re-run it to continue.

Seeing the reach before you apply

Downstreams do not speculatively run on a pull request — that would be noisy and, because an upstream’s new outputs are unknown until it applies, often misleading. Instead, a pull request that changes an upstream shows a blast-radius summary: which downstreams the apply would reach, and in what mode. The Console shows the same view for any Stack.

Cross-Stack outputs

Turn on output exposure for a dependency edge and the downstream’s Runs can read the upstream’s root outputs — never its resources or their attributes, and never the full state. Exposure is all-or-nothing (root outputs are your deliberate public interface) and off by default; enabling it also needs a read-outputs grant on the upstream Stack, live-evaluated at every Run. Revoke the grant and the next downstream Run stops reading.

Every exposed edge carries a downstream-local alias (say network). Both consumption idioms bind through it, and you pick one:

  • Injected variables. The control plane injects each upstream output as a TF_VAR_network_<output>, or a name you map — so your config reads it as an ordinary input variable.

  • terraform_remote_state. The control plane serves a read-only, resources-stripped, outputs-only view of the upstream state, and injects the address and credentials as <alias>_remote_state_{address,username,password}:

    data "terraform_remote_state" "network" {
      backend = "http"
      config = {
        address  = var.network_remote_state_address
        username = var.network_remote_state_username
        password = var.network_remote_state_password
      }
    }

    sensitive markings survive this path natively. Pointing terraform_remote_state at another Stack’s real state backend is unsupported — the outputs-only endpoint is the only cross-Stack read.

Values are read at the downstream plan and frozen into the pinned plan, so the apply uses exactly the upstream values you reviewed. An upstream that has not applied yet fails the downstream Run fast, with a clear reason, rather than handing back a null.