Terraform mono-repos work well when you are small. One repository, one state file, one team. But as infrastructure grows and more teams contribute, the mono-repo becomes a bottleneck: long plan times, merge conflicts on shared modules, blast radius concerns, and the inability to enforce team-level access controls.
At a compliance technology company, I led the migration from a single Terraform repository managing all cloud infrastructure to a multi-repo architecture with modular, reusable components. Here is how we did it without breaking production.
Why the Mono-Repo Had to Go
The original setup had everything in one repository:
- Networking (VPCs, subnets, security groups)
- Compute (EC2 instances, ASGs, Lambda functions)
- Databases (RDS, ElastiCache)
- Security (IAM roles, policies, KMS keys)
- Monitoring (CloudWatch, SNS, alarms)
A single terraform plan took over 10 minutes because it evaluated every resource in the account. A change to a security group required reviewing the entire infrastructure diff. And critically, there was no way to restrict who could modify what — anyone with repo access could change networking, IAM, or database configurations.
For SOC2 and PCI compliance, this lack of access control was a finding that needed remediation.
The Target Architecture
We designed a multi-repo structure organized by domain:
terraform-modules/ # Shared, versioned modules (published to private registry)
terraform-networking/ # VPCs, subnets, route tables, NAT gateways
terraform-compute/ # EC2, ASGs, Lambda, ECS
terraform-data/ # RDS, ElastiCache, S3, DynamoDB
terraform-security/ # IAM, KMS, security groups, WAF
terraform-monitoring/ # CloudWatch, alarms, dashboards, SNS
Each repository had:
- Its own Terraform state file (stored in S3 with DynamoDB locking)
- Team-level access controls via GitHub CODEOWNERS
- Independent CI/CD pipeline (Jenkins) for plan/apply
- Cross-repo references via Terraform remote state data sources
The Migration Process
Step 1: Extract shared modules
Before splitting the mono-repo, we identified reusable patterns and extracted them into a dedicated terraform-modules repository. Each module was versioned using Git tags (e.g., v1.2.0) and referenced via source URLs with version constraints.
This was the most time-consuming step but paid dividends later — teams could consume well-tested modules without copying code.
Step 2: Split state files
The hardest part of the migration was splitting the monolithic state file. We used terraform state mv to relocate resources from the mono-state to domain-specific state files. The process for each domain:
- Create the new repository with the extracted Terraform code
- Initialize a new S3 backend for the domain-specific state
- Run
terraform state mvfor each resource from old state to new state - Verify with
terraform plan— a clean plan (no changes) confirmed the migration was correct - Remove the migrated code from the mono-repo
We did this domain by domain, starting with the lowest-risk area (monitoring) and ending with the highest-risk (networking and security).
Step 3: Set up cross-repo references
Domains need to reference each other. For example, compute resources need VPC and subnet IDs from the networking domain. We used Terraform remote state data sources:
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "company-terraform-state"
key = "networking/terraform.tfstate"
region = "us-east-1"
}
}
resource "aws_instance" "app" {
subnet_id = data.terraform_remote_state.networking.outputs.private_subnet_ids[0]
}
We defined clear output contracts for each domain — networking exports subnet IDs, CIDR blocks, and VPC IDs. Security exports IAM role ARNs and KMS key IDs. These outputs became the API surface between domains.
Step 4: Secrets management with git-crypt
The mono-repo had stored some sensitive values (not ideal, but reality). During the migration, we implemented git-crypt for each repository to encrypt sensitive files at rest. Combined with AWS Secrets Manager for runtime secrets, this eliminated plaintext credentials from the codebase.
Step 5: CI/CD per repository
Each repository got its own Jenkins pipeline with:
- On PR:
terraform fmt -check,terraform validate,terraform plan(posted as PR comment) - On merge to main:
terraform apply -auto-approvewith manual approval gate for production - Scheduled: nightly
terraform planto detect drift
Lessons Learned
Module versioning is critical
Without version pinning, a module change could break multiple downstream repositories simultaneously. We enforced semantic versioning and required explicit version bumps in consuming repos.
State migration is the riskiest step
A botched terraform state mv can orphan resources or cause Terraform to try recreating existing infrastructure. We always:
- Backed up state files before any migration
- Ran
terraform planafter every state move to verify zero changes - Performed migrations during maintenance windows
Documentation of outputs is the contract
Remote state references create implicit dependencies between repositories. We documented every output with its type, description, and which repos consume it. Changing or removing an output required checking all downstream consumers first.
Start with low-risk domains
Migrating monitoring first let us validate the process with minimal blast radius. By the time we got to networking and security, the process was well-rehearsed.
Key Takeaways
- Extract shared modules first — they form the foundation of the multi-repo architecture
- Split state files carefully, one domain at a time, with verification after each move
- Define clear output contracts between domains to manage cross-repo dependencies
- Use git-crypt or similar tools for secrets — the migration is a good time to fix this
- Independent CI/CD per repo enables team autonomy and reduces blast radius
- Migrate low-risk domains first to validate the process
The multi-repo architecture requires more upfront coordination, but the benefits — faster plans, team-level access control, reduced blast radius, and compliance auditability — make it worthwhile for any organization beyond the single-team stage.