Stage 4 · Provision
Advanced Terraform Patterns
Monorepo vs Multi-Repo
Structuring Terraform codebases for teams — when to consolidate and when to split.
The Monorepo Approach
A monorepo stores all Terraform code in a single Git repository. Modules, environments, and shared libraries live in the same codebase. This simplifies dependency management — every team sees every change in one place.
terraform-infra/
modules/
networking/
compute/
database/
environments/
dev/
staging/
prod/
shared/
variables.tf
providers.tf
.github/workflows/
plan.yml
apply.ymlThe monorepo keeps all Terraform code in one repository. Changes to shared modules are visible immediately. CI/CD pipelines can validate the entire infrastructure in a single run.
The Polyrepo Approach
A polyrepo approach uses separate Git repositories for each team, service, or infrastructure component. Each repo has its own CI/CD pipeline, state, and approval process.
# Separate repositories per team/component
terraform-networking/ # Network team owns this
terraform-compute/ # Platform team owns this
terraform-database/ # Database team owns this
terraform-shared/ # Shared modules consumed by allEach repository is independently versioned, tested, and deployed. Teams have full ownership of their infrastructure code without stepping on each other's changes.
The Hybrid Approach
Most mature organizations use a hybrid approach: shared modules in a dedicated repo, environment configurations in another, and service-specific infrastructure in service repos. This balances reuse with team autonomy.
# In the service-specific repo
module "networking" {
source = "git::https://github.com/org/terraform-modules.git//modules/networking?ref=v2.3.1"
vpc_cidr = "10.0.0.0/16"
env = var.environment
}The source argument uses a Git URL with a ref parameter for versioning. This lets the service repo pin to a specific module version while the shared module repo evolves independently.
Tradeoffs
| Factor | Monorepo | Polyrepo |
|---|---|---|
| Code discovery | Easy — everything in one place | Hard — must search across repos |
| Cross-cutting changes | One PR changes everything | Requires coordinated PRs across repos |
| Module updates | Instant — same repo | Requires version bumps and ref updates |
| Team autonomy | Lower — shared CI, shared reviews | Higher — each team controls their pipeline |
| Access control | Coarse — everyone sees everything | Fine-grained per repository |
| CI complexity | One pipeline validates all | Multiple pipelines with different configs |
Decision Framework
- Small teams (under 5 engineers): Use a monorepo. The overhead of polyrepo is not worth it.
- Platform teams with shared modules: Put modules in a dedicated repo, environments in another.
- Regulated industries: Use polyrepo for separation of duties and audit trails.
- Multiple cloud providers: Consider separate repos per cloud with a shared module repo.
- Fast-moving startups: Start monorepo, split when team size or compliance requires it.
Real-World Examples
# terragrunt.hcl in environments/prod/vpc/
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../../modules//vpc"
}
inputs = {
cidr_block = "10.0.0.0/16"
env = "prod"
}Terragrunt in a monorepo reduces boilerplate. The include block inherits shared configuration. The source argument points to the module in the same repo. This keeps environment configs minimal and consistent.
Begin with a monorepo. You can always split into polyrepo later using Git subtree or by moving modules to a separate repo. Starting with polyrepo creates premature complexity that slows down small teams.
In polyrepo, module source references create cross-repo dependencies. A breaking change in the module repo can break every environment that consumes it. Use semantic versioning and CI checks to prevent this.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.