Back to blog
Terraform

From Mono-Repo to Multi-Repo: Refactoring Terraform at Scale

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:

  1. Create the new repository with the extracted Terraform code
  2. Initialize a new S3 backend for the domain-specific state
  3. Run terraform state mv for each resource from old state to new state
  4. Verify with terraform plan — a clean plan (no changes) confirmed the migration was correct
  5. 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-approve with manual approval gate for production
  • Scheduled: nightly terraform plan to 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 plan after 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.

Need help refactoring your Terraform?

Book a 30-minute architecture review and get actionable recommendations.

Book an architecture review