Passing Aliased Providers Into Terraform Modules the Right Way
Implicit provider inheritance breaks the moment a module needs two regions or accounts. Here's how to wire aliased providers explicitly with configuration_aliases and provider maps.
- #terraform
- #ai
- #providers
- #modules
- #multi-region
Single-region Terraform modules feel effortless because of a convenience you probably never thought about: child modules silently inherit the default provider from the root. You never wire anything; it just works. Then someone asks for a cross-region replica, or a DNS record in a separate account, and that convenience turns into an error you can’t make sense of — because a child module has no way to ask for a specific aliased provider unless you tell it how.
The fix is to make provider plumbing explicit. It’s more verbose, but it’s the only thing that scales past one region or one account.
Why inheritance stops being enough
A root module can define multiple aliased providers:
provider "aws" {
region = "us-east-1"
}
provider "aws" {
alias = "replica"
region = "eu-west-1"
}
The default aws provider is inherited by child modules automatically. The aws.replica provider is not — aliased providers are never inherited implicitly. So the moment a module needs to create something in eu-west-1, you have to pass that provider in, and the module has to declare that it expects it.
Declaring the contract with configuration_aliases
Inside the child module, you declare which providers it consumes using configuration_aliases:
# modules/replicated-bucket/versions.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
configuration_aliases = [aws.primary, aws.replica]
}
}
}
This turns “this module happens to use whatever it inherited” into “this module requires exactly two AWS providers, named primary and replica.” That’s a contract a reviewer — and Terraform itself — can check. The module then references them explicitly:
resource "aws_s3_bucket" "source" {
provider = aws.primary
bucket = "${var.name}-source"
}
resource "aws_s3_bucket" "replica" {
provider = aws.replica
bucket = "${var.name}-replica"
}
Wiring the call site with a providers map
At the module block, you supply a providers = {} map. The names on the left are the module’s internal expectations; the values on the right are your configured providers:
module "bucket" {
source = "./modules/replicated-bucket"
name = "app-data"
providers = {
aws.primary = aws
aws.replica = aws.replica
}
}
Read that map as “the module’s aws.primary is fed by my default aws; its aws.replica is fed by my aws.replica.” Once you pass any provider explicitly, there’s no automatic inheritance for that module — you’re now responsible for every provider it needs.
The nested hop everyone forgets
The part that catches people is forwarding aliases through more than one level. If module A calls module B, and B also needs both providers, A has to pass them down again:
# inside modules/replicated-bucket/main.tf (module A)
module "lifecycle_rules" { # module B
source = "./modules/lifecycle"
providers = {
aws.primary = aws.primary
aws.replica = aws.replica
}
}
There’s no re-inheritance once you’ve gone explicit. Every hop in the tree has to forward the aliases, and B must declare its own configuration_aliases. Miss a hop and terraform validate fails with “provider configuration not present” — annoying, but at least it fails loudly at plan time rather than silently creating resources in the wrong place.
Let AI map the flow, then validate
Tracing which alias each module level needs across a deep tree is exactly the kind of bookkeeping AI is good at — provided you check its work with terraform validate. A useful prompt:
Here’s my module hierarchy: root calls
network, which callspeering. The root hasaws(us-east-1) andaws.peer(us-west-2). Thepeeringmodule needs both. Show theconfiguration_aliasesfor each module and theproviders = {}map at every call site, including the nested hop fromnetworktopeering.
A good response gives you:
peeringdeclaresconfiguration_aliases = [aws.this, aws.peer]. The root passes{ aws.this = aws, aws.peer = aws.peer }tonetwork;networkforwards{ aws.this = aws.this, aws.peer = aws.peer }topeering. Runterraform validateto confirm every provider resolves. Note: moving a resource onto a different aliased provider can re-target its region and force replacement — check the plan.
That last note is why you verify rather than trust. Our aliased provider passing prompt is structured to produce the flow map, the contracts, and the call-site maps together, and our module passed-provider config prompt covers the single-level case in more depth.
A few rules that keep it sane
- Declare every alias a module uses. If a module references
aws.replica, it must list it inconfiguration_aliases. No exceptions. - Never rely on inheritance for aliased providers. Only the default provider is inherited; aliases must always be passed.
- Forward through every hop. Deep trees need the map repeated at each
moduleblock. - Watch the plan when re-pointing resources. Changing which provider a resource uses can move it to a new region or account and recreate it.
Explicit provider wiring is more typing, but it makes a module’s dependencies legible and lets the same module serve one region or five without rewrites. For the wider set of provider, region, and account patterns, browse our Terraform category.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.