Architecting Multi‑Stack Terraform with Terramate: Code Generation, Change Detection, and Safe CI
Use Terramate to turn a monorepo of Terraform root modules into independent, independently plannable stacks. Learn how to generate shared backends, detect changes via git, and run CI safely with minimal design and clear trust boundaries.
07 Aug 2026, 04:59 UTC

Problem Statement
Managing dozens of Terraform root modules in a single monorepo quickly becomes unwieldy. Running terraform plan and apply against every directory is slow, and accidental cross‑stack changes can slip through. Terramate offers a way to treat each directory as an independent stack while still allowing shared configuration and efficient change detection.
Requirements
- Terraform 1.5+ (or the version your organization relies on).
- Terramate 1.0+ pinned in CI; the
terramatebinary should be in the build image. - Remote backend per stack (S3, Azure Blob, GCS, etc.) to avoid state collisions.
- CI system capable of running Git commands and Terraform commands (GitHub Actions, GitLab CI, Jenkins, etc.).
- Cloud credentials supplied to CI via environment variables or OIDC federation; never in source.
Minimal Design
The smallest viable architecture consists of a single repository with the following layout:
├── terramate.tm.hcl # Repository‑wide globals and code‑generation rules
├── stacks
│ ├── network
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── stack.tm.hcl
│ └── compute
│ ├── main.tf
│ ├── variables.tf
│ └── stack.tm.hcl
└── .gitignore
Each stack.tm.hcl declares its own name, optional dependencies, and any stack‑specific overrides. The root terramate.tm.hcl contains two key blocks:
globals {
backend = {
type = "s3"
bucket = "infra-state"
key = "${stack_path}/${stack_name}.tfstate"
region = "us-east-1"
}
providers = {
aws = {
region = "us-east-1"
}
}
}
generate_file "backend.tf" {
content = <
The generate_file block writes a backend.tf into every stack directory, ensuring consistent backend settings without manual copy‑paste.
Trust & Data Boundaries
Terramate is an orchestrator, not a state manager. It never stores or reads Terraform state; that responsibility remains with Terraform and the chosen remote backend. Credentials are supplied at runtime by the CI environment, so Terramate never touches secrets. The only data Terramate writes to the repository is the generated backend.tf (or any other files you choose to generate). Keeping these files in version control allows peer review of the exact configuration that will be applied.
Operational Checks
- Validate Generation Idempotence – Run
terramate generatelocally, commit the files, and in CI verify that a second run produces no diff.terramate generate git diff --exit-code - Format & Validate Terramate Config – Enforce
terramate fmtandterramate validatein a pre‑commit hook or CI step.terramate fmt --check terramate validate - Change‑Detection Planning – In CI, run
terramate run --changed -- terraform plan. This command only plans stacks whose files or declared dependencies have changed.terramate run --changed -- terraform plan - Apply Gatekeeping – Require that plan outputs are reviewed before
applyis executed. A typical workflow is to runrun --changed -- terraform planas a PR check, then a separaterun --changed -- terraform applystep gated on merge.
Failure Modes & Mitigations
- Stale Generated Files – If the CI environment does not run
generatebefore a plan, the stack may use an outdated backend configuration. Mitigation: enforce generation as part of the CI pipeline and commit the generated files. - Shallow Clone or Rebase Skipping Detection –
--changedrelies on git history. A shallow clone or a rebased branch can cause Terramate to miss changes. Mitigation: configure CI to perform a full clone or use--full-historyflag. - Hidden Coupling via Globals – Overusing globals can make stack behavior opaque. Keep globals minimal (backend, providers) and document any stack‑specific overrides in
stack.tm.hcl. - Version Drift – Terramate’s CLI flags and HCL syntax evolve. Pin the tool version in CI and reference the changelog when upgrading.
Practical Example: Two‑Stack Repository
Below is a minimal terramate.tm.hcl and two stack definitions that illustrate the design.
# terramate.tm.hcl
globals {
backend = {
type = "s3"
bucket = "infra-state"
region = "us-east-1"
}
}
generate_file "backend.tf" {
content = <
CI Integration
Sample GitHub Actions workflow snippet:
name: Terraform CI
on: [push, pull_request]
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.5.0
- uses: hashicorp/setup-terramate@v1
with:
terramate_version: 1.0.0
- name: Generate
run: terramate generate
- name: Validate
run: terramate fmt --check && terramate validate
- name: Plan Changed Stacks
run: terramate run --changed -- terraform plan
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
When a PR modifies stacks/network/main.tf, only the network stack is planned. If compute declares after = ["network"], Terramate will automatically order the apply so that the network is created first.
Verification Checklist
| Check | Command | Expected Result |
|---|---|---|
| Generate idempotent | terramate generate && git diff --exit-code | No diff output |
| Format & validate | terramate fmt --check && terramat validate | Exit code 0 |
| Changed detection | Modify a stack file, run terramate run --changed -- terraform plan | Only modified stack appears in plan |
| Dependency ordering | Declare after in compute, run terramate run --changed -- terraform apply | Network stack applied before compute |
Summary
Terramate’s stack abstraction, combined with code generation and git‑based change detection, gives a clean, scalable way to manage many Terraform root modules in a monorepo. By keeping generated files in version control, enforcing format/validate checks, and gating applies behind reviewed plans, you can achieve fast CI cycles without sacrificing safety.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.