Sep 9, 2026

Terraform vs Terragrunt: the multi-market pattern that actually scales

Plain Terraform works for one environment. Here's what changes at four.

3 min read TerraformTerragruntIaCGitOps

Plain Terraform works fine for one environment. It stops working fine the moment you’re running the same stack across four markets, and it gets actively dangerous once you’re running it across six.

This is the pattern I’ve used to provision multi-market fintech infrastructure — Terragrunt on top of Terraform, with state isolation and drift prevention built in structurally, not enforced by discipline.

The problem with plain Terraform at scale

Say you’re running the same platform in Singapore, the Philippines, and Kenya. With plain Terraform, every market gets its own backend.tf and provider.tf — hand-written, copy-pasted, and slowly drifting apart as one engineer updates Singapore’s config and forgets Kenya’s.

markets/
├── singapore/
│   ├── main.tf
│   ├── backend.tf      # copy-pasted
│   └── provider.tf     # copy-pasted
├── philippines/
│   ├── main.tf
│   ├── backend.tf      # copy-pasted, easy to get wrong
│   └── provider.tf     # copy-pasted
└── kenya/
    ├── main.tf
    ├── backend.tf
    └── provider.tf

The failure mode here isn’t exotic. It’s someone updating the EKS version in Singapore, forgetting Kenya, and the two environments silently diverging for six months until an incident forces the comparison.

What Terragrunt actually changes

Terragrunt doesn’t replace Terraform — it’s a thin wrapper that assembles the right config before handing off to Terraform, which never knows Terragrunt was involved. The backend and provider blocks are generated at runtime from a single root definition, using a function called path_relative_to_include() that resolves to a unique path per market automatically.

infrastructure/
├── terragrunt.hcl              # root: backend + provider, defined once
├── modules/
│   └── market/                 # the actual resources
├── markets/
│   ├── _common/
│   │   └── market.hcl          # shared defaults: EKS version, node size
│   ├── singapore/
│   │   └── terragrunt.hcl      # 10 lines: region, CIDR, market name
│   ├── philippines/
│   │   └── terragrunt.hcl
│   └── kenya/
│       └── terragrunt.hcl

The root config, written once:

remote_state {
  backend = "s3"
  config = {
    bucket  = "platform-terraform-state"
    key     = "${path_relative_to_include()}/terraform.tfstate"
    region  = "ap-southeast-1"
    encrypt = true
  }
}

Every market inherits this. Singapore’s state lives at markets/singapore/terraform.tfstate, Kenya’s at markets/kenya/terraform.tfstate, generated automatically — never hand-written, never at risk of two markets accidentally pointing at the same state file.

Adding a market becomes a 10-line diff

include "root" {
  path = find_in_parent_folders()
}

include "common" {
  path = "../_common/market.hcl"
}

terraform {
  source = "../../modules//market"
}

inputs = {
  market_name = "ghana"
  aws_region  = "af-south-1"
  vpc_cidr    = "10.5.0.0/16"
}

That’s the entire file for a new market. Everything else — backend, provider, shared EKS version, shared tagging convention — is inherited. You raise this as a pull request, a CI tool like Digger posts the plan output as a PR comment, someone reviews it, merge triggers apply.

The fleet-wide upgrade problem, solved for free

The real payoff shows up when you need to upgrade EKS across every market at once. With plain Terraform, that’s N separate applies, each one a chance to get the version string wrong in one market. With Terragrunt:

terragrunt run-all apply --terragrunt-working-dir markets/

One command. It discovers every market from the folder structure, resolves dependency order if you’ve declared one, and applies consistently across all of them.

When plain Terraform is still fine

If you’re running one environment, or two that genuinely never need to stay in lockstep, Terragrunt is overhead you don’t need. The pattern earns its complexity specifically at the point where drift between environments becomes a real operational risk — which for most teams is somewhere between three and five environments, not one.

The lesson isn’t “always use Terragrunt.” It’s that state isolation and config inheritance should be structural, not something you’re trusting an engineer to remember to copy correctly at 11pm before a market launch.