Terramate Stack Ordering and Change Detection: Running Only What Actually Changed
Terramate's change detection and explicit stack ordering let CI run only the Terraform stacks affected by a change, in the right order. Here's how after/before/wants work, with a worked example and the caveats.
27 Aug 2025, 17:50 UTC

The problem: monorepo Terraform gets slow and scary
Once a Terraform setup grows past a handful of root modules, two things tend to happen. First, every CI run plans everything, even the 40 stacks nobody touched. Second, apply order becomes tribal knowledge: someone remembers that the VPC must go before the EKS cluster, and that memory lives in a wiki page nobody reads.
Terramate addresses both problems with the same idea: treat each root module as a stack, declare how stacks relate to each other, and let the tool figure out what to run and in what order. This post looks at the two features that do the heavy lifting — change detection and explicit stack ordering — and where they fall short.
Stacks, and how Terramate decides what changed
A Terramate stack is a directory containing Terraform code plus a stack.tm.hcl file (or a terramate block) that marks it as a stack. Each stack keeps its own state, so blast radius stays small by construction.
Change detection uses git. When you run a command with the --changed flag, Terramate diffs your working tree (or a target branch) against a base ref and selects only stacks whose files — or files they watch — were modified:
# Run from the repo root, inside a git clone of your project.
# Needs: terramate CLI installed, terraform on PATH, and cloud
# credentials already configured for the stacks being planned.
terramate run --changed -- terraform planThe useful part is that change detection is transitive across stack relationships. If stack B declares that it depends on stack A, and A changed, B is included in the run even though none of B's own files moved. That is the behavior worth verifying before you trust it in CI: change a file in an upstream stack, run terramate list --changed, and confirm the downstream stacks appear in the output.
Declaring order with after, before, and wants
Terramate does not infer dependencies from your Terraform code. You declare them in stack configuration, and Terramate builds a directed acyclic graph (DAG) from those declarations to order execution:
# stacks/eks/stack.tm.hcl
stack {
name = "eks-cluster"
description = "Cluster; needs the VPC first"
after = ["/stacks/vpc"]
}Three directives cover most needs:
after— this stack runs only after the listed stacks. Use it for hard dependencies like networking before compute.before— the inverse; handy when the upstream stack is the natural place to document the relationship.wants— a soft dependency. The listed stacks run first if they are part of the current run, but they are not pulled in automatically. Good for optional coupling.
When you then run terramate run -- terraform apply, Terramate executes stacks in an order that satisfies the graph and fails fast if it detects a cycle. A cycle error at config-load time is exactly what you want — much better than discovering the circular dependency halfway through a deploy.
A worked example: VPC, cluster, app
Say your repo looks like this:
stacks/
vpc/stack.tm.hcl
eks/stack.tm.hcl # after = ["/stacks/vpc"]
app/stack.tm.hcl # after = ["/stacks/eks"]You edit a subnet CIDR in stacks/vpc and open a PR. In CI, the sequence is:
terramate list --changed— expect all three stacks, because the change propagates down the graph.terramate run --changed -- terraform plan— plans run in graph order, so reviewers see the VPC plan first, then the downstream impact.- After merge,
terramate run --changed -- terraform apply -auto-approveapplies in the same order.
The meaningful check: the list --changed output should contain eks and app even though you only touched vpc files. If it does not, your after declarations are not where you think they are — fix that before trusting the pipeline. Note that exact flag names and propagation semantics have evolved across Terramate releases, so confirm against the docs for the version you pin in CI.
Trade-offs and limitations
The graph is only as honest as your declarations. Terramate will not notice that your app stack reads the VPC's outputs via a terraform_remote_state data source unless you also write the after line. Undeclared dependencies mean wrong ordering, and wrong ordering means applies that fail on missing remote state — or worse, silently use stale outputs.
Two more caveats. First, state locking is not a Terramate feature; it comes from your Terraform backend (S3 with DynamoDB or lockfiles, GCS, etc.). Terramate runs Terraform, and Terraform takes the lock — so the usual advice about not using local state in CI still applies. Second, a large graph with many cross-links becomes hard to reason about. There is no substitute for keeping dependencies shallow and mostly one-directional; the DAG is a safety net, not a license to wire everything to everything.
What to do next
Pick your most-touched stack, convert it and its direct neighbors into Terramate stacks, and wire up the after edges you currently keep in your head. Then run terramate list --changed on a throwaway branch with a trivial edit and check the selection matches your expectation. If it does, you have a foundation that scales: CI runs shrink to what changed, and apply order stops being folklore.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.