| Tested on | Ubuntu 26.04 LTS (Resolute Raccoon) |
|---|---|
| Package | terraform 1.15.8-1hashicorp/local 2.9.0 |
| Applies to | Any host with Terraform installed |
| Lab environment | Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu |
| Privilege | Normal user |
| Scope | Passing default and aliased provider configurations into child modules — provider requirements versus configurations, automatic default inheritance, providers meta-argument mapping, configuration_aliases, explicit mapping replacing implicit inheritance, count and for_each on module blocks, reproduced alias errors, and safe provider refactor. Does not cover provider installation, authentication depth, full version constraint theory, AWS multi-account architecture, module variable and output tutorials, or manual state JSON editing. |
| Related guides | Terraform module inputs and outputs Terraform providers Provider configuration not present Terraform modules Terraform Associate certification course |
The Terraform module inputs and outputs lesson showed how variables and outputs cross a module boundary. This lesson answers the companion question: how does a parent module pass default or aliased provider configurations into a child module?
Root module
│
├── provider "local" {}
│
└── module "child"
│
└── resource using local providerCloud examples often use provider "aws" with a region and an alias such as west for a second region. The lab uses hashicorp/local with a west alias so you can reproduce every mapping and error without cloud credentials.
~/terraform-labs/terraform-provider-alias-module/. Success demos live in demos/; reproduced failures live in errors/.
Provider requirements versus provider configurations
Terraform splits provider wiring into two layers:
- Provider requirements — which plugin a module needs (
required_providerswithsourceand optional version constraints) - Provider configurations — concrete settings such as
region, credentials, or feature flags (providerblocks, usually in the root module)
| Layer | Declared in | Inherited by child? |
|---|---|---|
| Requirements | Each module that uses the provider | No — each reusable child declares its own required_providers |
| Configurations | Root module (typical pattern) | Default configuration yes, when you omit providers; aliases only when explicitly mapped |
Reusable child modules should declare required_providers for every provider they use. They should not define normal provider configuration blocks — the caller configures providers and passes them in.
Shared lab setup
Create the shared child modules and lab root. Every demo below references paths under ~/terraform-labs/terraform-provider-alias-module/.
mkdir -p ~/terraform-labs/terraform-provider-alias-module/modules/simple
mkdir -p ~/terraform-labs/terraform-provider-alias-module/modules/mapped-default
mkdir -p ~/terraform-labs/terraform-provider-alias-module/modules/dual-config
cd ~/terraform-labs/terraform-provider-alias-moduleWrite the simple child module that uses the default local provider implicitly:
cat > modules/simple/versions.tf <<'EOF'
terraform {
required_providers {
local = {
source = "hashicorp/local"
}
}
}
EOFAdd the resource block in main.tf:
cat > modules/simple/main.tf <<'EOF'
resource "local_file" "simple" {
filename = "${path.module}/simple-out.txt"
content = "written by simple module default local provider"
}
EOFWrite the mapped-default child module — it also refers to the default local name only:
cat > modules/mapped-default/versions.tf <<'EOF'
terraform {
required_providers {
local = {
source = "hashicorp/local"
}
}
}
EOFWrite the mapped-default main.tf:
cat > modules/mapped-default/main.tf <<'EOF'
resource "local_file" "mapped" {
filename = "${path.module}/mapped-out.txt"
content = "mapped-default module uses provider name local only"
}
EOFWrite the dual-config child module that declares configuration_aliases and uses both provider slots:
cat > modules/dual-config/versions.tf <<'EOF'
terraform {
required_providers {
local = {
source = "hashicorp/local"
configuration_aliases = [local.west]
}
}
}
EOFWrite resources that use both provider slots:
cat > modules/dual-config/main.tf <<'EOF'
resource "local_file" "default_slot" {
filename = "${path.module}/dual-default.txt"
content = "dual-config default local provider"
}
resource "local_file" "west_slot" {
provider = local.west
filename = "${path.module}/dual-west.txt"
content = "dual-config local.west alias"
}
EOFDefault provider inheritance
When the root defines a default provider and the child only needs that default, you can omit the providers meta-argument entirely. The child does not inherit the parent's required_providers declaration — it carries its own. It does inherit the parent's default local configuration automatically.
mkdir -p ~/terraform-labs/terraform-provider-alias-module/demos/default-inheritance
cd ~/terraform-labs/terraform-provider-alias-module/demos/default-inheritanceCreate the root module that calls the simple child without a providers map:
cat > versions.tf <<'EOF'
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
EOFDefine the root default local provider:
cat > providers.tf <<'EOF'
provider "local" {}
EOFCall the simple child without a providers map:
cat > main.tf <<'EOF'
module "simple" {
source = "../../modules/simple"
}
EOFInitialize and validate the default-inheritance demo:
terraform init -input=falseTerraform has been successfully initialized!Run validate on the initialized root module:
terraform validateSuccess! The configuration is valid.Plan shows the child resource without any providers map on the module block:
terraform plan -no-color -input=false# module.simple.local_file.simple will be created
Plan: 1 to add, 0 to change, 0 to destroy.Apply and read the file the module created:
terraform apply -auto-approve -input=false -no-colorApply complete! Resources: 1 added, 0 changed, 0 destroyed.Display the module output file:
cat ~/terraform-labs/terraform-provider-alias-module/modules/simple/simple-out.txtwritten by simple module default local providerThe successful plan and apply confirm that the child received a usable local provider configuration through automatic default inheritance. Because the lab's default and aliased local configurations have no behavioral settings that distinguish them, the file content itself is not evidence of a particular alias.
Destroy when you finish this section so the shared module path stays clean for later demos:
terraform destroy -auto-approve -input=false -no-colorDestroy complete! Resources: 1 destroyed.Pass an aliased provider as the child default
A common pattern: the root owns two configurations of the same provider, but the child should treat one alias as its default local name.
In AWS terms the root might look like this (conceptual only — the lab uses local below):
provider "aws" {
region = "us-east-1"
}
provider "aws" {
alias = "west"
region = "us-west-2"
}
module "application" {
source = "./modules/application"
providers = {
aws = aws.west
}
}Inside the child module, resources still refer to aws — not aws.west. The parent's providers map connects the child's local name aws to the root's aws.west configuration.
Lab equivalent: the root defines default and west aliases, then maps local.west into the child's default local slot.
mkdir -p ~/terraform-labs/terraform-provider-alias-module/demos/map-alias-to-default
cd ~/terraform-labs/terraform-provider-alias-module/demos/map-alias-to-defaultCreate the root module with both provider configurations and an explicit providers map:
cat > versions.tf <<'EOF'
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
EOFDefine default and west provider blocks in the root:
cat > providers.tf <<'EOF'
provider "local" {}
provider "local" {
alias = "west"
}
EOFMap local.west into the child's default local slot:
cat > main.tf <<'EOF'
module "mapped_default" {
source = "../../modules/mapped-default"
providers = {
local = local.west
}
}
EOFThe child modules/mapped-default/ declares only required_providers for local and writes a file without a provider argument — it expects the default local slot, which the parent fills with local.west.
Initialize and plan from the map-alias demo directory:
terraform init -input=falsePlan the mapped-default module call:
terraform plan -no-color -input=false# module.mapped_default.local_file.mapped will be created
Plan: 1 to add, 0 to change, 0 to destroy.Apply and verify the mapped file:
terraform apply -auto-approve -input=false -no-colorApply complete! Resources: 1 added, 0 changed, 0 destroyed.Read the mapped-default output path:
cat ~/terraform-labs/terraform-provider-alias-module/modules/mapped-default/mapped-out.txtmapped-default module uses provider name local onlySuccessful initialization and planning confirm that Terraform accepted the explicit mapping local = local.west. The local provider has no region or account setting to make the two configurations visibly different at runtime; the purpose of this lab is to verify provider wiring without cloud credentials. The child source never mentions west — only the parent's providers map selects which root configuration backs the child's default provider name.
Destroy when you finish this section:
terraform destroy -auto-approve -input=false -no-colorUse configuration_aliases when the child needs multiple configurations
When the child must use more than one configuration of the same provider — for example one file on the default local and another on local.west — declare the extra names with configuration_aliases in the child's required_providers:
terraform {
required_providers {
local = {
source = "hashicorp/local"
configuration_aliases = [local.west]
}
}
}configuration_aliases does not create a provider configuration. It only documents which alternate configuration names the child is allowed to receive. The root still owns every provider block. The child modules/dual-config/ module (created in the shared lab setup) uses both slots — one resource on the default local provider and one on local.west.
AWS-shaped equivalent:
module "application" {
source = "./modules/application"
providers = {
aws = aws
aws.west = aws.west
}
}Create the configuration-aliases demo directory:
mkdir -p ~/terraform-labs/terraform-provider-alias-module/demos/configuration-aliases
cd ~/terraform-labs/terraform-provider-alias-module/demos/configuration-aliasesCreate the root module that maps both provider configurations into the dual-config child:
cat > versions.tf <<'EOF'
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
EOFDefine both root provider configurations:
cat > providers.tf <<'EOF'
provider "local" {}
provider "local" {
alias = "west"
}
EOFPass both configurations through the module providers map:
cat > main.tf <<'EOF'
module "dual_config" {
source = "../../modules/dual-config"
providers = {
local = local
local.west = local.west
}
}
EOFInitialize the configuration-aliases demo and inspect the plan:
terraform init -input=falsePlan shows both child resources:
terraform plan -no-color -input=false# module.dual_config.local_file.default_slot will be created
# module.dual_config.local_file.west_slot will be created
Plan: 2 to add, 0 to change, 0 to destroy.Apply and confirm both files:
terraform apply -auto-approve -input=false -no-colorApply complete! Resources: 2 added, 0 changed, 0 destroyed.Cat both dual-config output files:
cat ~/terraform-labs/terraform-provider-alias-module/modules/dual-config/dual-default.txt ~/terraform-labs/terraform-provider-alias-module/modules/dual-config/dual-west.txtdual-config default local provider
dual-config local.west aliasThe child configuration explicitly assigns west_slot to local.west, while default_slot uses the default local configuration. Successful plan and apply confirm Terraform accepted both provider mappings. Because both local configurations behave identically in this lab, the generated file contents are only convenient markers for the two resources, not independent evidence of which provider configuration executed them.
Destroy when you finish this section:
terraform destroy -auto-approve -input=false -no-colorProvider inheritance versus explicit providers mapping
| Situation | Recommended behavior |
|---|---|
| Child uses only the parent's default provider | Omit providers — automatic inheritance |
| Child should use a different root configuration as its default | providers = { local = local.west } (or aws = aws.west) |
| Child needs multiple configurations of the same provider | configuration_aliases in the child plus full providers map in the parent |
Important current behavior: when you supply a providers map on a module block, Terraform passes only the configurations you list. Do not assume every other root provider configuration still flows into that module call automatically. If the child needs both default and alias, map both keys — as in the configuration_aliases example above.
Modules with count or for_each
Module instances created with count or for_each share the same providers map on the module block. You cannot generate provider configurations dynamically — provider blocks do not support count or for_each.
mkdir -p ~/terraform-labs/terraform-provider-alias-module/demos/count-instances
cd ~/terraform-labs/terraform-provider-alias-module/demos/count-instancesCreate a root module that calls the dual-config child twice with one shared providers map:
cat > versions.tf <<'EOF'
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
EOFDefine both root provider configurations:
cat > providers.tf <<'EOF'
provider "local" {}
provider "local" {
alias = "west"
}
EOFCall the dual-config child with count = 2:
cat > main.tf <<'EOF'
module "dual_config" {
count = 2
source = "../../modules/dual-config"
providers = {
local = local
local.west = local.west
}
}
EOFInitialize and plan the count demo:
terraform init -input=falsePlan shows indexed module addresses, each inheriting the same provider wiring:
terraform plan -no-color -input=false# module.dual_config[0].local_file.default_slot will be created
# module.dual_config[0].local_file.west_slot will be created
# module.dual_config[1].local_file.default_slot will be created
# module.dual_config[1].local_file.west_slot will be created
Plan: 4 to add, 0 to change, 0 to destroy.module.dual_config[0] and module.dual_config[1] both use the same local and local.west configurations you passed once on the block. This section is not a full count versus for_each tutorial — it only shows that provider mapping is per module call, not per instance index.
Fix common provider alias errors
Each failure below is a separate directory under errors/. Create the complete root configuration, reproduce the failure, then compare the fix.
Missing required provider configuration
The child declares configuration_aliases = [local.west] but the parent omits the providers map.
mkdir -p ~/terraform-labs/terraform-provider-alias-module/errors/missing-alias-mapping
cd ~/terraform-labs/terraform-provider-alias-module/errors/missing-alias-mappingCreate the root module without a providers map:
cat > versions.tf <<'EOF'
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
EOFDefine both root provider blocks even though the module call omits the map:
cat > providers.tf <<'EOF'
provider "local" {}
provider "local" {
alias = "west"
}
EOFCall dual-config without a providers map:
cat > main.tf <<'EOF'
module "dual_config" {
source = "../../modules/dual-config"
}
EOFInitialize to surface the missing-alias error:
terraform init -input=falseError: Missing required provider configuration
on main.tf line 1:
1: module "dual_config" {
The child module requires an additional configuration for provider
hashicorp/local, with the local name "local.west".Fix: add the map the child expects:
module "dual_config" {
source = "../../modules/dual-config"
providers = {
local = local
local.west = local.west
}
}Reference to undefined provider in the map key
A typo in the providers map key leaves the real alias unmapped.
mkdir -p ~/terraform-labs/terraform-provider-alias-module/errors/wrong-provider-key
cd ~/terraform-labs/terraform-provider-alias-module/errors/wrong-provider-keyCreate the root module with a typo in the map key (local.westt):
cat > versions.tf <<'EOF'
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
EOFDefine both root provider configurations:
cat > providers.tf <<'EOF'
provider "local" {}
provider "local" {
alias = "west"
}
EOFPass a typo key in the providers map:
cat > main.tf <<'EOF'
module "dual_config" {
source = "../../modules/dual-config"
providers = {
local = local
local.westt = local.west
}
}
EOFInitialize to surface the warning and follow-on error:
terraform init -input=falseWarning: Reference to undefined provider
on main.tf line 6, in module "dual_config":
6: local.westt = local.west
There is no explicit declaration for local provider name "local.westt" in
module.dual_config, so Terraform is assuming you mean to pass a configuration
for "hashicorp/local".
Error: Missing required provider configuration
...
with the local name "local.west".Fix: match the key exactly to the name declared in the child's configuration_aliases — local.west, not local.westt.
Reference to undefined provider configuration in the root
The map points at a configuration that does not exist in the root module.
mkdir -p ~/terraform-labs/terraform-provider-alias-module/errors/undefined-provider-ref
cd ~/terraform-labs/terraform-provider-alias-module/errors/undefined-provider-refCreate the root module with a map target that was never declared (local.east):
cat > versions.tf <<'EOF'
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
EOFDefine only the default and west root configurations:
cat > providers.tf <<'EOF'
provider "local" {}
provider "local" {
alias = "west"
}
EOFMap local.west to the nonexistent local.east configuration:
cat > main.tf <<'EOF'
module "dual_config" {
source = "../../modules/dual-config"
providers = {
local = local
local.west = local.east
}
}
EOFInitialize and validate the broken map:
terraform init -input=falseRun validate on the initialized root:
terraform validateError: missing provider provider["registry.terraform.io/hashicorp/local"].eastFix: reference a provider block that actually exists — local.west in this lab — or add the missing root configuration before mapping it.
Provider configuration removed while state still depends on it
Apply the dual-config module successfully, then delete the provider "local" { alias = "west" } block from the root while the module mapping and resources still need local.west.
mkdir -p ~/terraform-labs/terraform-provider-alias-module/errors/provider-removed
cd ~/terraform-labs/terraform-provider-alias-module/errors/provider-removedCreate the root module with both provider configurations and a full providers map:
cat > versions.tf <<'EOF'
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
EOFDefine both root provider configurations:
cat > providers.tf <<'EOF'
provider "local" {}
provider "local" {
alias = "west"
}
EOFMap both configurations into the dual-config child:
cat > main.tf <<'EOF'
module "dual_config" {
source = "../../modules/dual-config"
providers = {
local = local
local.west = local.west
}
}
EOFInitialize and apply so state records both provider configurations:
terraform init -input=falseApply the dual-config module to populate state:
terraform apply -auto-approve -input=false -no-colorApply complete! Resources: 2 added, 0 changed, 0 destroyed.Remove the west alias block while the module mapping still references local.west:
cat > providers.tf <<'EOF'
provider "local" {}
EOFPlan after removing the alias:
terraform plan -no-color -input=falseError: missing provider provider["registry.terraform.io/hashicorp/local"].westOn Terraform 1.15.8 the message uses missing provider rather than the older phrase "Provider configuration not present." The underlying problem is the same: configuration disappeared while state still references it. See Provider configuration not present for the full restore-and-migrate walkthrough.
Restore the west block and destroy when you finish this section:
cat > providers.tf <<'EOF'
provider "local" {}
provider "local" {
alias = "west"
}
EOFDestroy the applied resources:
terraform destroy -auto-approve -input=false -no-colorRefactor provider aliases without breaking state
Renaming or removing provider configurations is a state-sensitive change. A safe sequence:
- Inspect which resources and module calls use each provider configuration (
terraform state list, provider addresses in plan output). - Keep the old provider configuration in the root while any resource or module mapping still references it.
- Update module
providersmaps or resourceproviderarguments to the target configuration. - Run
terraform planand confirm no unexpected destroys — only provider metadata changes or explicit moves you intended. - Complete resource migration or destruction under the new configuration.
- Remove the obsolete provider block only when nothing in configuration or state still references it.
Do not edit terraform.tfstate JSON by hand. Do not delete provider blocks before state stops referencing them.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Missing required provider configuration on terraform init |
Child declares configuration_aliases but parent omitted providers |
Map every alias name the child expects |
| Warning: Reference to undefined provider | Typo in providers map key |
Match keys to child configuration_aliases names exactly |
missing provider … .east (or similar) |
Map references a root alias that was never declared | Add the root provider block or fix the map target |
| Child resource uses wrong environment or region | Default inheritance when you meant an alias | Add providers = { aws = aws.west } (or local = local.west) |
| Plan fails after deleting alias block | State still bound to removed configuration | Restore provider block; migrate or destroy; then remove |
Explicit providers map but child still missing a config |
Only some configurations were mapped | Map every name the child uses — explicit map replaces implicit pass-through |
References
- Module providers meta-argument —
providers,configuration_aliases, inheritance rules - Provider configuration — default and aliased
providerblocks - Provider requirements —
required_providersin child modules
Summary
Provider wiring across modules comes down to requirements versus configurations. Every reusable child declares its own required_providers; the root owns provider blocks and passes configurations inward. Omit providers when the child only needs the default — Terraform inherits that configuration automatically. Map providers = { local = local.west } when the child should treat a root alias as its default name without mentioning the alias inside the module.
When the child itself needs multiple configurations, configuration_aliases declares the extra names the module accepts, and the parent must map each one explicitly. Supplying a providers map replaces automatic pass-through for that module call — list every configuration the child needs, not only the alias.
The reproduced errors in errors/ show the usual mistakes: missing maps, typos in map keys, references to undefined root aliases, and deleting provider blocks before state releases them. Refactor provider aliases by keeping old configurations alive until plan shows a clean handoff. For the dedicated troubleshooting chapter on stale provider references, read Provider configuration not present.

