Should you maintain a single Terraform repository or multiple repositories for different environments, and what branching strategy works best?
Quick Answer
Use a single monorepo with environment-specific directories (envs/dev, envs/prod) and shared modules, combined with a trunk-based branching strategy where main is always deployable. Feature branches target dev first, promotion happens through directory-level changes, and protected branch rules prevent direct pushes to main without PR approval and passing CI checks.
Detailed Answer
Choosing between a monorepo and multi-repo for Terraform is like choosing between a single warehouse with organized aisles (monorepo) versus separate warehouses per product line (multi-repo). The single warehouse is easier to navigate and keeps inventory consistent, but a forklift accident in one aisle can block the whole building. Separate warehouses provide isolation but make it harder to share parts and coordinate shipments.
The monorepo approach stores all Terraform code in a single repository: shared modules in modules/, environment configurations in envs/dev/, envs/qa/, envs/prod/, and CI/CD pipeline definitions alongside the code. The primary advantage is atomic changes: when you update a shared module and its callers, everything is in one PR, reviewable together. Cross-cutting concerns like provider version upgrades, backend configuration changes, or tagging policy updates are a single commit. Module development and consumption happen in the same repository, eliminating the version publishing ceremony.
The multi-repo approach creates separate repositories per environment or per infrastructure domain: infra-networking, infra-eks, infra-databases. Each repository has its own CI/CD pipeline, its own access controls, and its own release cadence. The advantage is strict blast radius isolation: a broken CI pipeline in the networking repo cannot block database deployments. Access control is more granular — you can give database administrators write access to infra-databases without exposing networking code.
For most teams at the scale of 4 environments (Dev, QA, UAT, Prod), the monorepo with environment directories is the pragmatic choice. The branching strategy that works best is trunk-based development with short-lived feature branches. Main (or trunk) is always deployable — it represents the current desired state of all environments. Engineers create feature branches from main, make changes in the relevant environment directory, open a PR, and the CI pipeline runs terraform plan against the affected environments.
The promotion flow works through directory-level changes, not branch-based promotion. An engineer develops a new VPC peering configuration in envs/dev/peering.tf, merges to main, and the dev pipeline applies it. After validation in dev, they create a new PR that adds the same configuration to envs/qa/peering.tf (possibly with different tfvars). This continues through UAT and Prod. Each environment change is a separate PR with its own review and approval cycle.
The critical branching anti-pattern to avoid is environment branches (dev branch, qa branch, prod branch) where you promote by merging dev into qa into prod. This creates merge conflicts, diverging code paths, and the nightmare scenario where a hotfix to prod must be cherry-picked back through all environment branches. Trunk-based development with directory separation eliminates this entirely.
For the multi-repo approach, use Git tags or releases to version shared modules. Consumer repositories reference modules via Git source URLs with pinned tags: source = "git::https://github.com/org/tf-modules.git//vpc?ref=v2.2.0". Promotion happens by bumping the version tag in each environment's module source, creating a clear audit trail of what version each environment runs.