Terraform Provider Aliases with Modules

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
hashicorp/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?

text
Root module
   ├── provider "local" {}
   └── module "child"
            └── resource using local provider

Cloud 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.

NOTE
Run terraform init in each lab directory under ~/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_providers with source and optional version constraints)
  • Provider configurations — concrete settings such as region, credentials, or feature flags (provider blocks, 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/.

bash
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-module

Write the simple child module that uses the default local provider implicitly:

bash
cat > modules/simple/versions.tf <<'EOF'
terraform {
  required_providers {
    local = {
      source = "hashicorp/local"
    }
  }
}
EOF

Add the resource block in main.tf:

bash
cat > modules/simple/main.tf <<'EOF'
resource "local_file" "simple" {
  filename = "${path.module}/simple-out.txt"
  content  = "written by simple module default local provider"
}
EOF

Write the mapped-default child module — it also refers to the default local name only:

bash
cat > modules/mapped-default/versions.tf <<'EOF'
terraform {
  required_providers {
    local = {
      source = "hashicorp/local"
    }
  }
}
EOF

Write the mapped-default main.tf:

bash
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"
}
EOF

Write the dual-config child module that declares configuration_aliases and uses both provider slots:

bash
cat > modules/dual-config/versions.tf <<'EOF'
terraform {
  required_providers {
    local = {
      source                = "hashicorp/local"
      configuration_aliases = [local.west]
    }
  }
}
EOF

Write resources that use both provider slots:

bash
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"
}
EOF

Default 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.

bash
mkdir -p ~/terraform-labs/terraform-provider-alias-module/demos/default-inheritance
cd ~/terraform-labs/terraform-provider-alias-module/demos/default-inheritance

Create the root module that calls the simple child without a providers map:

bash
cat > versions.tf <<'EOF'
terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}
EOF

Define the root default local provider:

bash
cat > providers.tf <<'EOF'
provider "local" {}
EOF

Call the simple child without a providers map:

bash
cat > main.tf <<'EOF'
module "simple" {
  source = "../../modules/simple"
}
EOF

Initialize and validate the default-inheritance demo:

bash
terraform init -input=false
output
Terraform has been successfully initialized!

Run validate on the initialized root module:

bash
terraform validate
output
Success! The configuration is valid.

Plan shows the child resource without any providers map on the module block:

bash
terraform plan -no-color -input=false
output
# 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:

bash
terraform apply -auto-approve -input=false -no-color
output
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Display the module output file:

bash
cat ~/terraform-labs/terraform-provider-alias-module/modules/simple/simple-out.txt
output
written by simple module default local provider

The 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:

bash
terraform destroy -auto-approve -input=false -no-color
output
Destroy 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):

hcl
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.

bash
mkdir -p ~/terraform-labs/terraform-provider-alias-module/demos/map-alias-to-default
cd ~/terraform-labs/terraform-provider-alias-module/demos/map-alias-to-default

Create the root module with both provider configurations and an explicit providers map:

bash
cat > versions.tf <<'EOF'
terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}
EOF

Define default and west provider blocks in the root:

bash
cat > providers.tf <<'EOF'
provider "local" {}

provider "local" {
  alias = "west"
}
EOF

Map local.west into the child's default local slot:

bash
cat > main.tf <<'EOF'
module "mapped_default" {
  source = "../../modules/mapped-default"

  providers = {
    local = local.west
  }
}
EOF

The 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:

bash
terraform init -input=false

Plan the mapped-default module call:

bash
terraform plan -no-color -input=false
output
# 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:

bash
terraform apply -auto-approve -input=false -no-color
output
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Read the mapped-default output path:

bash
cat ~/terraform-labs/terraform-provider-alias-module/modules/mapped-default/mapped-out.txt
output
mapped-default module uses provider name local only

Successful 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:

bash
terraform destroy -auto-approve -input=false -no-color

Use 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:

hcl
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:

hcl
module "application" {
  source = "./modules/application"

  providers = {
    aws      = aws
    aws.west = aws.west
  }
}

Create the configuration-aliases demo directory:

bash
mkdir -p ~/terraform-labs/terraform-provider-alias-module/demos/configuration-aliases
cd ~/terraform-labs/terraform-provider-alias-module/demos/configuration-aliases

Create the root module that maps both provider configurations into the dual-config child:

bash
cat > versions.tf <<'EOF'
terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}
EOF

Define both root provider configurations:

bash
cat > providers.tf <<'EOF'
provider "local" {}

provider "local" {
  alias = "west"
}
EOF

Pass both configurations through the module providers map:

bash
cat > main.tf <<'EOF'
module "dual_config" {
  source = "../../modules/dual-config"

  providers = {
    local      = local
    local.west = local.west
  }
}
EOF

Initialize the configuration-aliases demo and inspect the plan:

bash
terraform init -input=false

Plan shows both child resources:

bash
terraform plan -no-color -input=false
output
# 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:

bash
terraform apply -auto-approve -input=false -no-color
output
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Cat both dual-config output files:

bash
cat ~/terraform-labs/terraform-provider-alias-module/modules/dual-config/dual-default.txt ~/terraform-labs/terraform-provider-alias-module/modules/dual-config/dual-west.txt
output
dual-config default local provider
dual-config local.west alias

The 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:

bash
terraform destroy -auto-approve -input=false -no-color

Provider 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.

bash
mkdir -p ~/terraform-labs/terraform-provider-alias-module/demos/count-instances
cd ~/terraform-labs/terraform-provider-alias-module/demos/count-instances

Create a root module that calls the dual-config child twice with one shared providers map:

bash
cat > versions.tf <<'EOF'
terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}
EOF

Define both root provider configurations:

bash
cat > providers.tf <<'EOF'
provider "local" {}

provider "local" {
  alias = "west"
}
EOF

Call the dual-config child with count = 2:

bash
cat > main.tf <<'EOF'
module "dual_config" {
  count  = 2
  source = "../../modules/dual-config"

  providers = {
    local      = local
    local.west = local.west
  }
}
EOF

Initialize and plan the count demo:

bash
terraform init -input=false

Plan shows indexed module addresses, each inheriting the same provider wiring:

bash
terraform plan -no-color -input=false
output
# 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.

bash
mkdir -p ~/terraform-labs/terraform-provider-alias-module/errors/missing-alias-mapping
cd ~/terraform-labs/terraform-provider-alias-module/errors/missing-alias-mapping

Create the root module without a providers map:

bash
cat > versions.tf <<'EOF'
terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}
EOF

Define both root provider blocks even though the module call omits the map:

bash
cat > providers.tf <<'EOF'
provider "local" {}

provider "local" {
  alias = "west"
}
EOF

Call dual-config without a providers map:

bash
cat > main.tf <<'EOF'
module "dual_config" {
  source = "../../modules/dual-config"
}
EOF

Initialize to surface the missing-alias error:

bash
terraform init -input=false
output
Error: 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:

hcl
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.

bash
mkdir -p ~/terraform-labs/terraform-provider-alias-module/errors/wrong-provider-key
cd ~/terraform-labs/terraform-provider-alias-module/errors/wrong-provider-key

Create the root module with a typo in the map key (local.westt):

bash
cat > versions.tf <<'EOF'
terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}
EOF

Define both root provider configurations:

bash
cat > providers.tf <<'EOF'
provider "local" {}

provider "local" {
  alias = "west"
}
EOF

Pass a typo key in the providers map:

bash
cat > main.tf <<'EOF'
module "dual_config" {
  source = "../../modules/dual-config"

  providers = {
    local       = local
    local.westt = local.west
  }
}
EOF

Initialize to surface the warning and follow-on error:

bash
terraform init -input=false
output
Warning: 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_aliaseslocal.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.

bash
mkdir -p ~/terraform-labs/terraform-provider-alias-module/errors/undefined-provider-ref
cd ~/terraform-labs/terraform-provider-alias-module/errors/undefined-provider-ref

Create the root module with a map target that was never declared (local.east):

bash
cat > versions.tf <<'EOF'
terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}
EOF

Define only the default and west root configurations:

bash
cat > providers.tf <<'EOF'
provider "local" {}

provider "local" {
  alias = "west"
}
EOF

Map local.west to the nonexistent local.east configuration:

bash
cat > main.tf <<'EOF'
module "dual_config" {
  source = "../../modules/dual-config"

  providers = {
    local      = local
    local.west = local.east
  }
}
EOF

Initialize and validate the broken map:

bash
terraform init -input=false

Run validate on the initialized root:

bash
terraform validate
output
Error: missing provider provider["registry.terraform.io/hashicorp/local"].east

Fix: 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.

bash
mkdir -p ~/terraform-labs/terraform-provider-alias-module/errors/provider-removed
cd ~/terraform-labs/terraform-provider-alias-module/errors/provider-removed

Create the root module with both provider configurations and a full providers map:

bash
cat > versions.tf <<'EOF'
terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}
EOF

Define both root provider configurations:

bash
cat > providers.tf <<'EOF'
provider "local" {}

provider "local" {
  alias = "west"
}
EOF

Map both configurations into the dual-config child:

bash
cat > main.tf <<'EOF'
module "dual_config" {
  source = "../../modules/dual-config"

  providers = {
    local      = local
    local.west = local.west
  }
}
EOF

Initialize and apply so state records both provider configurations:

bash
terraform init -input=false

Apply the dual-config module to populate state:

bash
terraform apply -auto-approve -input=false -no-color
output
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Remove the west alias block while the module mapping still references local.west:

bash
cat > providers.tf <<'EOF'
provider "local" {}
EOF

Plan after removing the alias:

bash
terraform plan -no-color -input=false
output
Error: missing provider provider["registry.terraform.io/hashicorp/local"].west

On 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:

bash
cat > providers.tf <<'EOF'
provider "local" {}

provider "local" {
  alias = "west"
}
EOF

Destroy the applied resources:

bash
terraform destroy -auto-approve -input=false -no-color

Refactor provider aliases without breaking state

Renaming or removing provider configurations is a state-sensitive change. A safe sequence:

  1. Inspect which resources and module calls use each provider configuration (terraform state list, provider addresses in plan output).
  2. Keep the old provider configuration in the root while any resource or module mapping still references it.
  3. Update module providers maps or resource provider arguments to the target configuration.
  4. Run terraform plan and confirm no unexpected destroys — only provider metadata changes or explicit moves you intended.
  5. Complete resource migration or destruction under the new configuration.
  6. 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


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.


NOTE
If you have not built a local child module yet, start with Terraform modules. For variable and output boundaries, see Terraform module inputs and outputs.

Frequently Asked Questions

1. Do child modules inherit provider configurations from the root module?

Yes for the default configuration when you omit the providers meta-argument on the module block. Aliased configurations never inherit automatically. When you supply an explicit providers map, Terraform passes only the configurations you list — it does not implicitly pass every other root provider configuration to that module call.

2. What does configuration_aliases do in a child module?

It declares alternate provider configuration names the child module expects to receive from the parent, such as local.west. It does not create a provider configuration. The root module still owns every provider block; the parent maps real configurations into those names with the providers meta-argument.

3. When should I map an aliased provider to the child default provider name?

Set providers = { local = local.west } on the module block when the child refers to the default provider name local but should run through a root alias such as local.west. The child keeps omitting provider or writing provider local; the parent chooses which root configuration fills that slot.

4. Can I create provider configurations with count or for_each?

No. Provider blocks cannot use count, for_each, or depends_on. Module blocks can use count or for_each, and every instance shares the same providers map you pass on that module block.

5. Why does terraform plan fail after I delete a provider alias block?

State still records which provider configuration managed each resource. Removing the alias from configuration while resources or module mappings still reference it leaves Terraform unable to resolve that configuration. Restore the provider block, finish migration or destroy, then remove the obsolete configuration.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)