← Back to list

Migrating a Terraform Monolith to Terragrunt: State Slicing Without Downtime

How I decomposed a monolithic Terraform state into isolated Terragrunt modules for a SaaS client — without touching live infrastructure.

Jakub Jakubowski · 2026-05-08 07:29 · 0 claps · 2.7 min read
#terraform #terraform-modules #terragrunt #terraform-vs-terragrunt #aws
Open on Medium ↗
Wiki topics: RAG · RAG & Retrieval ☁️ · DevOps & Cloud

Migrating a Terraform Monolith to Terragrunt: State Slicing Without Downtime

How I decomposed a monolithic Terraform state into isolated Terragrunt modules for a SaaS client — without touching live infrastructure.

What I Built

I decomposed a monolithic AWS infrastructure state into a Terragrunt monorepo with 13 isolated state files. This system provides per-component state locking and independent execution cycles for VPC, EKS, RDS, and 10 other logical infrastructure layers without modifying live resources.

System Architecture

Monolith State — A single S3-backed remote state containing all 19 infrastructure components.

Terragrunt Modules — 13 independent directories, each with its own state file and DynamoDB lock.

Dependency Graph — Explicit wiring between modules using Terragrunt dependency blocks with versioned, cacheable outputs.

Core Technical Behavior

The core of this system is the transformation of resource addresses during the migration. In the monolith, every resource lived under a top-level parent prefix, such as module.client_stage.module.database.

The execution flow for the migration required pulling the monolith state to a local file to serve as an immutable source. A script then enumerated every resource address and dynamically discovered child modules.

Address rewriting stripped the parent module prefix while preserving resource indices. For example, module.client_stage.module.database.module.rds.aws_db_instance.this[0] was moved to module.rds.aws_db_instance.this[0] within the new isolated RDS module state.

Capture a local snapshot of the monolith state

terraform state pull > monolith.tfstate

Enumerate all resources to validate the state list is not empty

STATE_LIST=$(terraform state list -state="$MONOLITH_STATE")

Extract child module names to discover logical boundaries

DIRECT_MODULES=$(echo "$STATE_LIST" | grep "^${MODULE_PREFIX}\.module\." | \
  sed "s|^${MODULE_PREFIX}\.module\.||" | \
  sed 's/^\([^.[]*\).*/\1/' | sort -u)

Execute the move from the immutable source to target module files

terraform state mv \
  -state="$MONOLITH_STATE" \
  -state-out="$TARGET_STATE" \
  "$resource" "$new_address"

The runtime behavior of the new system relies on Terragrunt dependency blocks. These blocks read output values from the completed state files of upstream modules, such as passing a VPC ID to a Kubernetes module.

Key Engineering Decisions

Script-driven slicing over manual commands ensures the operation is reproducible and reviewable in version control, preventing typos across hundreds of resource moves.

Immutable source state management using separate -state and -state-out files prevents the script from modifying the original monolith state, allowing for a clean restart if the process is interrupted.

Dynamic module discovery derives module names directly from the state list, providing a built-in completeness check that warns if a module exists in state but lacks a directory mapping.

Python-based address rewriting handles complex regex patterns for nested module prefixes and bracketed indices that are not safely expressible in bash.

Local backend initialization prior to S3 migration allows for verification of the sliced state against live infrastructure via a zero-diff plan before any state is pushed to the remote backend.

Materializing outputs via a no-op apply is required for Terragrunt’s DAG to resolve dependencies, as downstream modules cannot plan until the upstream module state contains valid outputs.

Trade-offs

Optimized for: blast radius reduction, per-module state locking, and independent execution cadence.

Sacrificed: operational simplicity during the migration window, requiring a change freeze while state existed in both monolithic and sliced forms.

Results / Cost Impact

The system now operates with 13 independent state files in S3, each with a dedicated DynamoDB lock.

Parallel workstreams are no longer blocked by a single global state lock.

Plan performance is restricted to the specific component being modified rather than the entire 19-component stack.

Conclusion

This migration turned a monolithic state into a modular system where management boundaries are explicitly defined. By treating state decomposition as a surgery problem rather than a re-creation problem, the infrastructure remained stable while the blast radius of changes was reduced.

Zero-diff plans after state slicing provide the final correctness guarantee that infrastructure remains unchanged.

Further Reading

For the full implementation details, see the complete article at jakops.cloud.

Need Help?

If you’re working on a similar state decomposition or evaluating Terragrunt adoption for a growing SaaS platform, feel free to reach out at hello@jakops.cloud.

https://jakops.cloud


메타데이터
post_id
ca2bcdde8de7
slug
migrating-a-terraform-monolith-to-terragrunt-state-slicing-without-downtime-ca2bcdde8de7
url
https://medium.com/@jakops/migrating-a-terraform-monolith-to-terragrunt-state-slicing-without-downtime-ca2bcdde8de7
canonical_url
https://medium.com/@jakops/migrating-a-terraform-monolith-to-terragrunt-state-slicing-without-downtime-ca2bcdde8de7
author_url
https://medium.com/@jakops
status
ok
fetched_at
2026-07-26 08:49:52