The default Atlantis setup watches leaf files. When a leaf changes, Atlantis re-plans that project; when a shared module changes, nothing happens — until somebody applies a consumer leaf weeks later and discovers the breakage. The contract Atlantis advertises ("plans run on file changes") quietly excludes the most consequential file changes you make.
Adding modules/**/*.tf to when_modified flips the contract. A shared-module PR now re-plans every consumer in the same PR. Regressions surface before merge, not at apply time. The change is one or two lines of YAML per project, and it is the single highest-impact edit I have made to an Atlantis setup in the last year.
The problem with leaf-only watching
A typical Atlantis project configuration looks like this:
# atlantis.yaml
projects:
- name: brand-a-prod-api
dir: live/brand-a/prod/api
when_modified: ["*.tf", "*.tfvars"]
This watches the leaf's own files. It does not watch the module source the leaf calls. So when you fix a bug in modules/ecs-service/main.tf, Atlantis does not know that brand-a/staging/api and brand-b/prod/api both call that module and might be affected.
The failure mode that follows is mechanical and unsurprising. The fix lands, CI goes green, the PR merges. Two days later somebody runs atlantis apply on brand-b/prod/api for an unrelated reason — a config bump, a tag refresh — and hits the regression you did not catch. The blame conversation that follows is unhelpful, because nobody set out to ship the regression; the tooling simply did not surface it.
This is the easiest class of bug to dismiss as "human error" and the hardest to actually prevent that way. The reviewer cannot see what they cannot see. The CI cannot fail on a plan it never ran.
The fix: include the module source in when_modified
You have two reasonable ways to express this. The narrow version targets a specific module:
when_modified:
- "*.tf"
- "*.tfvars"
- "../../../../modules/ecs-service/**/*.tf"
The broad version covers every module in the repo:
when_modified:
- "*.tf"
- "*.tfvars"
- "modules/**/*.tf"
Paths are relative to the repository root, which is what Atlantis uses for change detection. Every leaf that lists modules/**/*.tf in its when_modified will re-plan when any module changes. The narrow version reduces noise but multiplies the number of glob lines you have to maintain; the broad version is one glob per project and triggers re-plans on every module PR.
I default to the broad glob. The cost is a handful of "no changes" plans on shared-module PRs; the benefit is that every consumer is plan-verified, automatically, against the new module version.
If your repo has a layered Terragrunt structure, the same idea expresses itself through the layout: each leaf's when_modified lists the module path, and the module path is stable because the layout is layered. The combination of "leaves discover modules through brand.hcl" and "Atlantis re-plans on module changes" is what makes shared modules safe to evolve.
The dependency graph this expresses
The graph already exists; the when_modified glob simply teaches Atlantis to walk it. Each leaf that lists modules/**/*.tf is asserting, "I depend on this module surface; re-plan me when it moves." The PR comment becomes a summary of blast radius: every project that calls the module shows its plan diff, and the diff that matters (a behavioural change to one consumer) jumps out against the diffs that do not (eight identical no-op plans).
What this changes about PR review
A shared-module fix that previously generated one plan — the leaf you happened to touch in the same PR — now generates plans for every consumer. The Atlantis PR comment becomes a summary of blast radius: every project that calls the module shows its plan diff. Three things follow from that.
First, the reviewer can see, in the PR, whether the module change broke any consumer. No manual cross-referencing, no hoping somebody ran terragrunt run-all plan locally before merging, no slack thread that begins "did you check brand-b". The proof is in the comment.
Second, the author of the module change is the person who explains any surprise diffs. If a behavioural change shows up in a consumer they did not expect, they own the explanation. The accountability lands on the right side of the merge.
Third, "no changes" plans become a positive signal. When the comment is a wall of "no changes" lines, the reviewer learns that the module change is genuinely backwards-compatible. That is exactly the information a reviewer needs and the hardest information to get any other way.
The trade-off: plan noise and timeout pressure
The honest counter is that this multiplies plan volume on shared-module PRs. A repo with thirty leaves that all call modules/ecs-service will post thirty plans on a one-line module change. That is real cost in three places: CI minutes, Atlantis server load, and reviewer attention.
I have lived with each. CI minutes are cheap relative to the cost of a single bad apply. Atlantis server load is manageable with parallel_plan: true and a sensibly sized runner. Reviewer attention is the actual constraint, and the way to manage it is collapsed-by-default plan output: Atlantis comments use <details> blocks, and reviewers learn to scan for the non-empty diff lines.
The timeout concern is more real. If a single project's plan takes ninety seconds and you have thirty of them serialised, you blow past the default Atlantis plan timeout. The fix is parallelism, not avoidance — set parallel_plan: true at the repo level and confirm your runner has enough concurrent slots.
Counter-argument: this turns every module PR into a release
A serious counter is that re-planning every consumer makes a shared-module PR feel like a release event, and release events have higher friction. That is true and intended. A shared-module change is a release event, because it has multi-consumer blast radius. The previous setup hid that fact and let module changes feel cheap; the new setup matches the social cost of the change to its actual blast radius.
If the friction is too high for the kind of module changes you make, the answer is not to dial down the when_modified glob — it is to separate "module change" from "thin abstraction over inputs". The kinds of module changes that should not feel like releases are usually changes to inputs or outputs that should have been a Terraform variable or output block in the consumer, not a new module field.
So what
If you maintain Atlantis on a repo with shared modules, add modules/**/*.tf to one project's when_modified today and open a module PR. Watch the plan output. If you find a regression in a consumer you did not expect, that single PR has already paid for the change. If you do not, you have proof that the module change is safe — and that is a stronger merge signal than any number of green checks on the leaf you happened to touch.
The cost is plan noise. The benefit is never again discovering a module regression at apply time. That is a trade I would re-make every time.
[VERIFY: Atlantis parallel_plan is a project-level setting on most recent versions; confirm the exact key name and version threshold against the Atlantis docs before quoting verbatim.]
Comments
Sending…