A quiet failure mode in multi-module Terragrunt setups: someone copies an existing module to bootstrap a new one, forgets to check the region in the provider config, and a resource lands in us-east-1 instead of eu-west-1 — three weeks later, someone's debugging why a Lambda can't reach a VPC that "should" be right there.
This isn't a Terraform bug. It's a consequence of region being just another string that every module has to get right, independently, forever.
The default pattern (and why it's fragile)
Most Terragrunt setups declare region per-project:
# projects/some-service/project.hcl
locals {
aws_region = "eu-west-1"
}
Then the provider generation block in root.hcl reads it:
# root.hcl
locals {
project = read_terragrunt_config(find_in_parent_folders("project.hcl"))
}
generate "provider" {
path = "provider.tf"
if_exists = "overwrite"
contents = <<EOF
provider "aws" {
region = "${local.project.locals.aws_region}"
}
EOF
}
Nothing wrong with this in isolation. The problem shows up at scale: with 20+ modules, aws_region is set 20+ times, by hand, by whoever created each module — usually by copy-pasting an existing project.hcl and hoping they remembered to check every field. New modules silently inherit whatever the copy-paste source happened to have, correct or not.
The pattern: a default that wins unless explicitly overridden
Flip the default. root.hcl computes the region with a fallback, so a project only needs to declare aws_region when it's an actual, deliberate exception:
# root.hcl
locals {
project = read_terragrunt_config(find_in_parent_folders("project.hcl"))
# Falls back to the org-wide default if the project doesn't override it.
aws_region = try(local.project.locals.aws_region, "eu-west-1")
}
generate "provider" {
path = "provider.tf"
if_exists = "overwrite"
contents = <<EOF
provider "aws" {
region = "${local.aws_region}"
}
EOF
}
Now a brand-new project.hcl can be empty on the region question and still deploy to the right place by default:
# projects/brand-new-service/project.hcl
locals {
legacy_prefix = "brand-new-service"
# no aws_region — inherits eu-west-1 from root.hcl automatically
}
An exception is now a visible, deliberate line, not a silent inheritance from whatever was copy-pasted:
# projects/legacy-email-sender/project.hcl
locals {
legacy_prefix = "legacy-email-sender"
aws_region = "eu-north-1" # locked to this region — legacy third-party integration, see runbook §3
}
The difference is where the burden of proof sits. Before: every module has to actively get the region right. After: every module gets it right by doing nothing, and getting it "wrong" (i.e., different from the default) requires writing a line down and, ideally, a comment saying why.
Where this generalizes
Region is the clearest example because a wrong region is easy to notice, but the same shape applies to any setting that should be consistent across a growing set of modules and currently isn't enforced by anything except habit: default tags, default VPC, default log retention. Anywhere you find yourself saying "just copy an existing module's config and change what's different," that's a signal the common part belongs in the root config with a try() fallback, and only the actual differences belong in each project's own file.
It costs one extra local in root.hcl. It saves the debugging session three weeks from now.
Top comments (0)