skip to content

A child Terraform module has to create resources in a second AWS region. How does the module get that provider configuration, and what must the child module declare to accept it?

level: middleimportance: must knowfreq 58%

answer

  1. only default configurations are inherited
  2. the module block takes a map
  3. child name on the left, parent config on the right
  4. the child must declare the extra slot
  5. configuration_aliases inside required_providers

basics

~20 s

The calling module passes it with the module block's providers map, for example providers = { aws = aws.us, aws.replica = aws.eu }. The child declares the extra slot with configuration_aliases inside its required_providers entry, then selects it per resource.

solid answer

~50 s

Child modules automatically inherit only the *default*, unaliased provider configurations. Aliased ones must be handed over explicitly using the `providers` meta-argument on the `module` block: `providers = { aws = aws.us, aws.replica = aws.eu }`. The keys are names as the **child** sees them and the values are configurations as the **parent** sees them, so the child never learns the caller's alias names. For the child to have a second slot at all, it declares one in its own `required_providers` block via `configuration_aliases = [aws.replica]`, and its resources then say `provider = aws.replica`. That declaration is what makes the module's provider requirements part of its interface — a caller that forgets the map gets an error at plan time rather than silently creating everything in one region. Note that supplying `providers` explicitly replaces inheritance entirely: anything not listed is not available inside the child.

code

hcl · 20 lines
hcl
# modules/replicated-bucket/versions.tf
terraform {
  required_providers {
    aws = {
      source                = "hashicorp/aws"
      version               = ">= 5.0"
      configuration_aliases = [aws.replica]
    }
  }
}

# modules/replicated-bucket/main.tf
resource "aws_s3_bucket" "primary" {
  bucket = "example-primary-bucket"
}

resource "aws_s3_bucket" "replica" {
  provider = aws.replica
  bucket   = "example-replica-bucket"
}

go deeper

for a junior

Know that a module block can take a providers argument and that a child cannot see the caller's aliases automatically. Recognising the syntax when you meet it in a repository is enough at this level.

for a middle

Explain configuration_aliases as a declared requirement rather than a configuration, and get the direction of the providers map right: child name on the left, caller's configuration on the right.

for a senior

Point out that supplying the map disables inheritance entirely, that a missing entry fails at plan time by design, and that provider requirements belong in the module's documented interface next to its variables.

for a principal

Argue when a module should take two provider instances at all versus being invoked twice, once per region. Threading aliases through several module levels is a smell worth trading against a flatter, single-region module contract.

## Inheritance is only partial When you call a module without saying anything about providers, the child inherits the parent's **default** (unaliased) configurations. That covers the common case: a module full of `aws_*` resources works out of the box using whatever region the root's default `provider "aws"` block set. Aliased configurations are deliberately excluded from that inheritance. An alias is a local name, and letting child modules reach up into the caller's namespace for it would make the module depend on the caller's private naming. So the caller must pass it, and the child must declare that it expects one. ## The child side: configuration_aliases Inside the child module, the `required_providers` entry grows a `configuration_aliases` list. Each entry creates an additional named provider slot that the module's own resources can reference, but which the module itself never configures: ```hcl terraform { required_providers { aws = { source = "hashicorp/aws" version = ">= 5.0" configuration_aliases = [aws.replica] } } } resource "aws_s3_bucket" "replica" { provider = aws.replica bucket = var.replica_bucket_name } ``` This is the important design point: `configuration_aliases` declares a **requirement**, not a configuration. The module says "I need a second aws configuration and I will call it `replica`"; where that configuration points — which region, which account, which credentials — stays the caller's decision. `configuration_aliases` was introduced in Terraform 0.15; before that, modules with multiple provider instances were far clumsier. ## The parent side: the providers map The `module` block's `providers` argument is a map. Read it as *child name = parent configuration*: ```hcl module "replicated" { source = "./modules/replicated-bucket" providers = { aws = aws.us aws.replica = aws.eu } } ``` Here the child's default `aws` is bound to the parent's `aws.us`, and the child's `aws.replica` slot to the parent's `aws.eu`. The names on the two sides are independent — the caller can rearrange freely, which is exactly what makes the module reusable across a primary/secondary swap. Two behaviours surprise people: - **Supplying `providers` turns inheritance off.** Once the map is present, only what it lists exists inside the child. If the child also uses a `google_*` resource and you did not map `google`, that is an error, not a silent fallback. - **The map is not optional if the child declares `configuration_aliases`.** A missing entry fails at plan time with a message naming the module and the missing configuration. That early failure is the feature: without the declaration the module would just create everything in the default region and nobody would notice until a DR test. ## Nesting The same map is how you reach further down. A grandparent passes into a parent, and the parent passes on to its own children with another `providers` block. Nothing is implicit past the default configuration, so a deep tree that needs a second region carries the mapping at every level. That verbosity is a signal worth listening to: if three levels of module all need an explicit alias, the design may be better served by two invocations of a single-region module, one per region. ## Why not just declare a provider inside the child? Because a module that configures its own provider cannot be used with `count`, `for_each` or `depends_on`, cannot have its credentials or region chosen by the caller, and cannot be cleanly removed later — Terraform still needs that configuration to destroy what it created. The `configuration_aliases` + `providers` pair exists precisely so a module can *require* a provider instance without *owning* one. ## What good looks like A well-built multi-region module has no `provider` blocks at all, a `required_providers` entry that lists exactly the configurations it needs, resources that reference those names, and documentation that states the map a caller must supply. Its README shows the `providers = { ... }` snippet, because that map is as much part of the module's interface as its input variables.

  • In providers = { aws.replica = aws.eu }, which side is the child's name and which is the caller's?
    The key is the name inside the child module and the value is a configuration in the calling module. So `aws.replica = aws.eu` means "whatever the child calls `aws.replica`, bind it to my `aws.eu`". The two namespaces are independent, which is what lets one caller swap primary and secondary without editing the module.
  • What happens to inheritance once you specify the providers map explicitly?
    It stops. The map becomes the complete set of provider configurations visible inside the child, so any provider the child uses but the map omits is an error rather than a fallback to the parent's default. When you add a `providers` block you take responsibility for listing everything the child needs, including the default `aws` entry.
  • What error tells you a module's provider requirements were not satisfied?
    Terraform fails during plan with a message that the module requires a provider configuration it did not receive, naming the module and the missing alias. That is the point of `configuration_aliases`: without the declaration the module would quietly build everything against the inherited default configuration, and you would only find out during a failover test.

saying these in an interview costs you the question

  • Assumes aliased providers are inherited like default ones
  • Puts a provider block inside the child module instead
  • Thinks the map's keys are the caller's alias names
  • Believes configuration_aliases configures a provider
  • Passes the region as an input variable and expects it to switch providers

context