Monorepo vs Multirepo #
One of the first decisions to make when organizing Terraform configuration at team scale is: put everything in one repository, or split it into several? There’s no universal answer that fits every context. A monorepo makes visibility and cross-configuration refactoring easier; multirepo provides clearer isolation and ownership. The choice depends on team size, infrastructure architecture, and how you want to manage dependencies between components.
flowchart LR
subgraph Mono["Monorepo"]
M1["networking/"]
M2["compute/"]
M3["database/"]
M4["shared modules/"]
end
subgraph Multi["Multirepo"]
N1["infra-network\n(repo)"]
N2["infra-compute\n(repo)"]
N3["infra-db\n(repo)"]
end
style M1 fill:#3b82f6,stroke:#1e40af,color:#fff
style M2 fill:#3b82f6,stroke:#1e40af,color:#fff
style M3 fill:#3b82f6,stroke:#1e40af,color:#fff
style M4 fill:#8b5cf6,stroke:#6d28d9,color:#fff
style N1 fill:#10b981,stroke:#059669,color:#fff
style N2 fill:#10b981,stroke:#059669,color:#fff
style N3 fill:#10b981,stroke:#059669,color:#fffMonorepo: Everything in One Place #
TYPICAL MONOREPO STRUCTURE:
infra-repo/
├── modules/ ← Shared modules
│ ├── networking/
│ ├── compute/
│ └── database/
│
├── environments/
│ ├── dev/
│ │ ├── networking/
│ │ ├── compute/
│ │ └── database/
│ ├── staging/
│ │ └── ...
│ └── production/
│ └── ...
│
├── .github/workflows/ ← One CI/CD pipeline
├── .gitignore
└── README.md
All infrastructure lives in one repository, managed together.
MONOREPO ADVANTAGES:
✓ Full visibility — all changes are visible in one place
✓ Easier refactoring — change a module and update all its callers at once
✓ Dependencies between configurations are easier to track
✓ One CI/CD pipeline for everything
✓ Easier for small teams managing all the infrastructure
✓ No versioning complexity between modules and their callers
MONOREPO DISADVANTAGES:
✗ The repository can get big and slow over time
✗ One commit can affect CI/CD for all components
✗ Access control is harder (hard to restrict specific teams to specific folders)
✗ A careless module change can break all its callers
✗ Tighter coupling between teams
Multirepo: One Repository per Component #
TYPICAL MULTIREPO STRUCTURE:
infra-networking-repo/
├── modules/vpc/
├── environments/dev/
├── environments/staging/
└── environments/production/
infra-compute-repo/
├── modules/ec2/
├── modules/asg/
└── environments/...
infra-database-repo/
├── modules/rds/
└── environments/...
infra-modules-repo/ ← Shared modules in a separate repo
├── networking/
├── compute/
└── database/
# Versions: v1.0.0, v1.1.0, v2.0.0
Each team has its own repository with separate access and pipelines.
MULTIREPO ADVANTAGES:
✓ Clear isolation — changes in one repo don't affect the others
✓ Per-repository access control — the database team can't modify networking
✓ More specific and faster CI/CD pipelines
✓ Modules can have their own versioning (v1.0, v1.1, v2.0)
✓ Teams can move more independently
MULTIREPO DISADVANTAGES:
✗ Reduced visibility — hard to see the big infrastructure picture
✗ Complex dependency management — must manage module versions across repos
✗ Cross-repo refactoring is harder and needs coordination
✗ Pattern/convention duplication if there are no shared guidelines
✗ More management overhead (many repos, many pipelines)
Choosing Based on Context #
CHOOSE A MONOREPO IF:
Small team (< 5 engineers):
Multirepo overhead isn't worth it, everyone already knows all the
infrastructure, a monorepo provides the needed visibility
Tightly coupled infrastructure:
Networking, compute, and database often change together
Cross-component refactoring happens frequently
Just starting with Terraform:
Start simple, split into multirepos later if needed
"Premature separation" is more dangerous than a monorepo that grows big
One team managing all the infrastructure:
No need for isolation between teams
CHOOSE MULTIREPO IF:
Large team with clear ownership:
Team A only touches networking, Team B only touches database
Access isolation matters for governance
Modules used by many teams:
Modules need their own versions so they can be upgraded gradually
A breaking module change doesn't directly affect all callers
Compliance requirements:
Audits must be separate per domain (network audit, data audit)
Production database config access must not be in the same repo
as compute
Mature, stable infrastructure:
Components rarely undergo cross-domain refactoring
Separation is already natural according to team ownership
Hybrid: Monorepo per Domain #
An approach often used by medium-to-large teams is a monorepo per domain — one repo for networking, one for platform, one for applications, and a dedicated repo for shared modules.
HYBRID STRUCTURE:
infra-foundation-repo/ ← Platform / Infrastructure team
├── networking/
├── security/
└── shared-services/
infra-platform-repo/ ← Platform Engineering team
├── kubernetes/
├── observability/
└── cicd-infrastructure/
infra-app-repo/ ← Application team
├── environments/dev/
├── environments/staging/
└── environments/production/
terraform-modules-repo/ ← Platform team — used by everyone
├── vpc/
├── eks/
├── rds/
└── CHANGELOG.md
This provides enough isolation without excessive fragmentation.
Practices That Apply to Both Approaches #
There are several practices that matter regardless of monorepo or multirepo.
# 1. A single source of truth for the Terraform version
# Use .terraform-version or required_version in all configurations
terraform {
required_version = ">= 1.6, < 2.0"
}
# 2. Always commit the lock file
# .terraform.lock.hcl must be in .gitignore EXCEPTION
# (exclude everything but include the lock file)
# .gitignore:
# .terraform/
# !.terraform.lock.hcl ← this one is included
# 3. CODEOWNERS for review control
# .github/CODEOWNERS:
# /environments/production/ @infrastructure-team @senior-engineers
# /modules/ @platform-team
# 4. Consistent directory conventions
# Regardless of monorepo/multirepo, the internal structure must be uniform
# environments/
# dev/ staging/ production/
# (not: dev/ stg/ prod/ — inconsistency is confusing)
flowchart TD
A["Small team\nfew services"] --> B["Monorepo ✅"]
C["Large team\nmany services"] --> D["Multirepo ✅"]
E["Microservices\nstrong ownership"] --> D
F["Shared infra\nrefactoring needed"] --> B
style A fill:#3b82f6,stroke:#1e40af,color:#fff
style B fill:#10b981,stroke:#059669,color:#fff
style C fill:#f59e0b,stroke:#d97706,color:#fff
style D fill:#10b981,stroke:#059669,color:#fff
style E fill:#8b5cf6,stroke:#6d28d9,color:#fff
style F fill:#3b82f6,stroke:#1e40af,color:#fffHybrid Approach: Monorepo with Multi-State #
A hybrid approach combines the monorepo advantages (single source of truth) with multi-state (isolated blast radius).
# Monorepo structure with separate state per component
infrastructure/
├── modules/ # Shared modules (monorepo advantage)
│ ├── networking/
│ ├── compute/
│ └── database/
├── environments/
│ ├── dev/
│ │ ├── networking/ # Separate state
│ │ ├── compute/
│ │ └── database/
│ ├── staging/
│ └── production/
└── .github/workflows/ # Shared CI/CD config
flowchart TD
subgraph REPO["Monorepo"]
M["Shared Modules"]
E1["dev/networking"]
E2["dev/compute"]
E3["prod/networking"]
E4["prod/compute"]
end
subgraph STATE["Separate States"]
S1["state: dev-networking"]
S2["state: dev-compute"]
S3["state: prod-networking"]
S4["state: prod-compute"]
end
E1 --> S1
E2 --> S2
E3 --> S3
E4 --> S4
style REPO fill:#e3f2fd,stroke:#1565c0
style STATE fill:#fff3e0,stroke:#e65100Monorepo CI/CD Challenges #
# Problem: building all modules on a small change
# Solution: detect changed paths and only build what changed
# GitHub Actions: path filters
on:
push:
paths:
- 'infrastructure/networking/**'
# GitHub Actions: matrix strategy per component
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
networking: ${{ steps.changes.outputs.networking }}
compute: ${{ steps.changes.outputs.compute }}
steps:
- uses: dorny/paths-filter@v2
id: changes
with:
filters: |
networking:
- 'infrastructure/networking/**'
compute:
- 'infrastructure/compute/**'
plan-networking:
needs: detect-changes
if: needs.detect-changes.outputs.networking == 'true'
runs-on: ubuntu-latest
steps:
- run: cd infrastructure/networking && terraform plan
Shared Module Library #
# Monorepo: one repo for all modules and environments
infrastructure/
├── modules/ # SHARED: all environments use these
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── compute/
│ └── database/
├── environments/ # Per-environment config
│ ├── dev/
│ │ └── main.tf # → source = "../../modules/networking"
│ ├── staging/
│ └── production/
└── .github/workflows/ # SHARED CI/CD config
flowchart TD
subgraph REPO["Monorepo"]
MOD["modules/\n(SHARED)"]
DEV["environments/dev"]
STG["environments/staging"]
PRD["environments/production"]
end
MOD --> DEV
MOD --> STG
MOD --> PRD
style MOD fill:#e3f2fd,stroke:#1565c0
style PRD fill:#e8f5e9,stroke:#2e7d32Summary #
- There’s no universally better choice — a monorepo is better for small teams and tightly coupled infrastructure; multirepo is better for large teams with clear ownership.
- Start with a monorepo if you’re just beginning — it’s easier to split later as needed, but hard to merge existing multirepos.
- Multirepo requires module versioning — modules in separate repos must have explicit versioning so callers can upgrade gradually.
- A per-domain hybrid is the sweet spot for medium teams — one repo per domain (networking, platform, app) with a separate repo for shared modules.
- CODEOWNERS in GitHub/GitLab helps enforce who must review changes in specific directories — important for monorepos managed by many teams.
- .terraform.lock.hcl must be committed in both approaches — it ensures everyone and every pipeline uses the exact same provider versions.
← Previous: Common Security Mistakes Next: Lifecycle Management →