One Block to Force a Default AWS Region Across Every Terragrunt Module
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.
Comments
No comments yet. Start the discussion.