skip to content

A shared Terraform child module declares its own `provider "aws"` block so it can pin a region. Why is that discouraged, and what should the module and its caller do instead?

level: seniorimportance: should knowfreq 44%

answer

  1. children inherit the root's provider already
  2. module keeps a decision that isn't its own
  3. count, for_each, depends_on all blocked
  4. destroy needs the config you just deleted
  5. declare requirements, pass configurations

basics

~20 s

A module owning its provider configuration cannot be used with count, for_each or depends_on, cannot be reused in another region, and becomes hard to remove — deleting the module block takes the provider config with it, leaving resources Terraform can no longer destroy. Declare requirements and let the caller pass configurations in.

solid answer

~50 s

Child modules already inherit the root's default provider configurations, so a `provider` block inside a shared module is a legacy pattern that costs you three things. First, the module can no longer be repeated or ordered — `count`, `for_each` and `depends_on` are all incompatible with a module that carries its own provider configuration. Second, it is not reusable: the region is baked in, so the caller cannot deploy it twice in different places. Third, and worst operationally, removing the module block removes the provider configuration along with it, and Terraform then cannot destroy the resources that configuration owned — you have to put the module back to tear it down. The right shape is a child that declares only what it needs in `required_providers`, adding `configuration_aliases` when it needs more than one configuration, and a root that passes them explicitly with the `providers = { ... }` argument on the module block.

code

hcl · 14 lines
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
}

go deeper

for a junior

Know that child modules automatically inherit the provider configuration declared in the root, so a shared module normally needs no provider block of its own.

for a middle

Be able to describe the two sides of the contract: required_providers with configuration_aliases in the child, and the providers = { ... } map on the calling module block, with the map's keys being the names used inside the module.

for a senior

Show the operational consequence — a module that owns its provider configuration cannot be repeated with for_each and cannot be deleted cleanly, because destroy needs the configuration that disappears with the module block — and describe how you recover.

for a principal

Own the standard for shared modules across the estate: providers configured only at the boundary, identity from assumed roles or CI-issued sessions, and a review gate that rejects modules carrying region or credentials so multi-account reuse stays possible.

## Inheritance is the default, and it is usually enough When the root configuration declares `provider "aws" { region = "eu-west-1" }`, every child module it calls uses that configuration automatically. A module does not need to declare anything to get it. That inheritance is why a `provider` block inside a shared module is almost always a mistake rather than a missing feature: the module is not gaining a capability, it is taking a decision away from its caller. ## Cost 1 — the module can no longer be repeated or ordered A module with its own provider configuration cannot be used with `count`, `for_each` or `depends_on`. The reason is structural: Terraform must resolve provider configurations before it can expand a module into instances or place it in the graph as a unit, so a module that carries its own cannot participate in any of those. In practice this surfaces the day someone tries to write `for_each = var.regions` on the module and gets an error they cannot fix without changing the module itself. ## Cost 2 — the module is not reusable A region pinned inside the module is a region every caller gets. You cannot call it twice for two regions, you cannot run it against a sandbox account, and you cannot test it anywhere other than where it was written. The whole point of a module is that the caller supplies the context. ## Cost 3 — you cannot remove it cleanly This is the failure that turns into an incident. Provider configurations are needed not only to create resources but to **destroy** them. If the provider configuration lives inside the module and you delete the `module` block, the configuration disappears at the same moment Terraform decides those resources should be destroyed — and it has nothing left to destroy them with. The plan errors out asking for a provider configuration that no longer exists in the configuration. The recovery is awkward: put the module block back, apply a change that empties it, or restructure so the provider lives in the root, then remove it. Doing that under time pressure, in a state file shared by a team, is exactly the situation you do not want to discover at decommissioning time. ## The correct shape The child declares its *requirements*, never its *configuration*: ```hcl # modules/bucket/versions.tf terraform { required_providers { aws = { source = "hashicorp/aws" version = ">= 5.0" configuration_aliases = [aws.replica] } } } ``` `configuration_aliases` is how a module says "I need a second AWS configuration, and I will refer to it as `aws.replica` internally" without deciding what it points at. Inside the module, a resource selects it with `provider = aws.replica`. The caller supplies the concrete configurations on the module block: ```hcl module "bucket" { source = "./modules/bucket" providers = { aws = aws.primary aws.replica = aws.eu_west_1 } } ``` The map's keys are the names *inside* the module; the values are configurations that exist *in the caller*. Note that passing `providers` explicitly replaces inheritance for that module — if you specify the map, it must cover every configuration the module needs, because the defaults are no longer inherited implicitly. ## Reading it in review A quick review heuristic for any module you are asked to reuse: open its `versions.tf`. `required_providers` with version constraints and possibly `configuration_aliases` is a module built to be composed. A `provider` block with a hardcoded region or, worse, credentials, is a module that was extracted from someone's root configuration and never finished — and it will fight `for_each` and destruction the first time you need either. ## What an interviewer is listening for A candidate who says only "providers should be in the root" has read the guideline. A candidate who explains that removal breaks destroy, that repetition meta-arguments are incompatible, and who can sketch `configuration_aliases` on one side and `providers = { ... }` on the other, has actually maintained shared modules.

  • What exactly goes wrong when you delete a module block whose provider configuration lived inside it?
    Terraform plans to destroy the module's resources but no longer has a provider configuration capable of destroying them, because it vanished with the module block. The plan fails asking for a configuration that is not in the code. You recover by restoring the module block, moving the provider configuration into the root, applying, and only then removing the module.
  • If a module block sets providers explicitly, does it still inherit the root's default provider?
    No — supplying the `providers` map replaces inheritance for that module, so the map has to cover every configuration the module uses, including the default one. That is why you usually see `aws = aws` in the map alongside the aliased entries. Omitting a needed key surfaces as a missing-provider-configuration error at plan.
  • Where should credentials live if not in the module?
    Outside the configuration entirely. The provider block in the root should get identity from the environment — an assumed role, an OIDC-issued session in CI, or the ambient credential chain — rather than from hardcoded keys anywhere in the code. A module should never contain credentials, and even a root provider block should carry configuration such as region and role, not secrets.

saying these in an interview costs you the question

  • Says every module should configure its own provider
  • Hardcodes region or credentials inside a shared module
  • Unaware count/for_each are blocked by a module provider block
  • Thinks removing the module is always a clean destroy
  • Confuses required_providers with a provider configuration

context