Managing a single, massive Terraform state file often leads to slow plans, overlapping changes, and team frustrations. Recognizing the right moment to modularize your Terraform states can transform your infrastructure workflow—improving collaboration, speed, and stability. Instead of wrestling with a monolithic setup, breaking it into smaller, focused states aligns resources with ownership and lifecycle, making deployments more predictable and manageable.
Identify logical boundaries to streamline Terraform states
The first step in splitting a Terraform monolith is recognizing natural separations within your infrastructure. Group resources based on how they’re used, changed, and owned. Common divisions include:
- Networking – VPCs, subnets, route tables, and NAT gateways. These components change infrequently but serve as the foundation for everything else.
- DNS – Managed zones and records, typically owned by a platform team.
- Compute – Kubernetes clusters, VM scale sets, or container services. These evolve more often and depend heavily on networking.
- Application infrastructure – Databases, caches, queues, and storage accounts, usually managed by application teams.
- Monitoring – Dashboards, alerts, and log sinks. These change frequently but don’t depend on other layers.
A practical rule of thumb: if two resources are never modified together in the same pull request by the same person, they likely belong in separate states. This separation ensures clearer ownership and reduces deployment friction.
Map dependencies to maintain infrastructure integrity
Before relocating any resources, document how they interact. A dependency graph visualizes the flow of outputs from one state to another. For example:
Networking → DNS → Compute → Application → MonitoringKey outputs that bridge these states typically include:
- Networking → Compute:
vpc_id,private_subnet_ids - Compute → DNS:
load_balancer_ip - Compute → Application:
cluster_endpoint,cluster_ca_certificate - Application → Monitoring:
database_id,cache_name
Understanding these connections helps you preserve critical references during the transition.
Migrate resources safely using Terraform commands
Terraform’s state mv command enables moving resources between states without rebuilding them. Start by initializing the new state file and then relocate resources methodically:
cd modules/networking
terraform init
terraform state mv \
-state=../monolith/terraform.tfstate \
-state-out=./terraform.tfstate \
aws_vpc.main aws_vpc.main
tf state mv \
-state=../monolith/terraform.tfstate \
-state-out=./terraform.tfstate \
aws_subnet.private aws_subnet.privateAfter each move:
- Run
terraform planon the new state to confirm no changes occur. - Run
terraform planon the original state to verify the resources are removed.
This incremental approach minimizes risk and ensures consistency throughout the transition.
Replace direct references with modular inputs
In a monolithic setup, modules often reference resources directly, such as aws_vpc.main.id. After splitting, these references must adapt to use variables instead. For instance:
# Before (monolithic state)
resource "aws_eks_cluster" "main" {
vpc_config {
subnet_ids = aws_subnet.private[*].id
}
}
# After (separate compute state)
variable "private_subnet_ids" {
type = list(string)
}
resource "aws_eks_cluster" "main" {
vpc_config {
subnet_ids = var.private_subnet_ids
}
}Then, expose the subnet IDs from the networking state:
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}This shift ensures modular states remain independent while allowing necessary data exchange.
Connect modular states with dependency-aware tools
After splitting states, you need reliable ways to pass outputs between them. Several approaches exist, each with trade-offs:
- Terraform remote state data sources allow modules to read outputs from another state:
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "networking/terraform.tfstate"
region = "us-east-1"
}
}
resource "aws_eks_cluster" "main" {
vpc_config {
subnet_ids = data.terraform_remote_state.networking.outputs.private_subnet_ids
}
}However, this method requires manual backend configuration in every consuming module and lacks automatic dependency enforcement.
- Wrapper scripts and CI pipelines can automate the flow of outputs between states, but they often become brittle and opaque, with dependency logic embedded in scripts rather than code.
- Terragrunt introduces a dependency layer that enforces ordering and manages inputs declaratively:
# compute/terragrunt.hcl
dependency "networking" {
config_path = "../networking"
}
inputs = {
vpc_id = dependency.networking.outputs.vpc_id
private_subnet_ids = dependency.networking.outputs.private_subnet_ids
}While an improvement, Terragrunt operates locally and lacks persistent deployment tracking, approval workflows, and automatic change cascades.
- Snap CD provides a cloud-native solution designed for modular Terraform workflows. Each split state becomes a Module, with dependencies declared as code. Snap CD enforces apply order, runs Modules in parallel, and automatically triggers downstream updates when upstream outputs change. This approach eliminates manual orchestration and reduces operational overhead.
Best practices for a smooth transition
Modularizing Terraform states doesn’t have to be disruptive. Follow these guidelines to ensure a seamless shift:
- Split incrementally – Focus on one logical group at a time. Avoid attempting a full split in a single effort.
- Start with the most stable layer – Networking usually changes least and has the most downstream dependents, making it an ideal starting point.
- Keep shared modules lean – Avoid bundling unrelated resources into a single module, as this can reintroduce the problems you’re trying to solve.
By taking a structured, deliberate approach, you can transform a tangled Terraform monolith into a modular, maintainable system that scales with your team’s needs.
AI summary
Learn how to split monolithic Terraform states into modular, team-friendly states. Step-by-step guide covers boundaries, migration, and dependency management.