Skip to content

Provider Configuration Across Modules


Modules separate infrastructure implementation, but providers introduce another question: which provider configuration should a child module use?

This is one of the areas where Terraform’s behavior can feel surprising at first. The key is to separate two ideas: a module describes infrastructure, while a provider configuration tells Terraform where and how to manage that infrastructure.

Provider Configuration Lives In The Root

A common pattern is to configure the provider in the root module:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

Then a child module can contain AWS resources without defining another provider configuration:

resource "aws_vpc" "this" {
  cidr_block = var.cidr
}

The child module can use the provider configuration inherited from its caller.

The conceptual flow is:

    flowchart TD
    R[Root Module]
    P[AWS Provider Configuration]
    M[Child Module]
    R --> P
    R --> M
    P --> M
  

The child module does not need to repeat the root provider configuration merely because it creates AWS resources.

This is one of the reasons reusable modules should generally avoid embedding their own provider configurations.

Provider Inheritance

Suppose the root module has:

provider "aws" {
  region = "us-east-1"
}

and calls:

module "network" {
  source = "./modules/network"
}

The child module can use AWS resources:

resource "aws_vpc" "this" {
  cidr_block = var.cidr
}

Terraform can associate those resources with the inherited AWS provider configuration.

You can think of it as:

Root
 β”‚
 β”‚ provider configuration
 β–Ό
Child Module
 β”‚
 β”‚ inherited provider
 β–Ό
AWS Resources

The module does not need to know how the caller authenticated or which region the caller selected unless its interface specifically needs that information.

Why Reusable Modules Usually Should Not Configure Providers

Imagine a reusable module containing:

provider "aws" {
  region = "us-east-1"
}

That module now contains a provider configuration decision that really belongs to the caller.

What happens if another caller needs the same module in us-west-2?

Or in another AWS account?

The module becomes harder to reuse because its implementation has taken ownership of something that should normally be supplied by its environment.

A more reusable module contains the resources:

resource "aws_vpc" "this" {
  cidr_block = var.cidr
}

while the root module controls the provider configuration.

Note

A reusable child module generally declares which providers it requires, but the provider configurations themselves are normally supplied by the calling configuration.

This distinction becomes particularly important when aliases enter the picture.

Multiple Provider Configurations

Sometimes one provider configuration is not enough.

For AWS, you might need:

Root Module
    β”‚
    β”œβ”€β”€ AWS us-east-1
    β”‚
    └── AWS us-west-2

You can create an aliased provider:

provider "aws" {
  region = "us-east-1"
}

provider "aws" {
  alias  = "west"
  region = "us-west-2"
}

Now there are two AWS provider configurations.

The first is the default configuration.

The second is identified by the alias west.

This lets the same root configuration manage resources in different provider configurations.

Pass A Provider Explicitly To A Module

Suppose a module should use the west configuration.

The root module can pass the provider:

module "secondary" {
  source = "./modules/secondary"

  providers = {
    aws = aws.west
  }
}

The child module’s AWS resources can then use the provider configuration supplied to the module.

The flow is:

Root Module
    β”‚
    β”œβ”€β”€ aws
    β”‚     └── us-east-1
    β”‚
    └── aws.west
          └── us-west-2
                β”‚
                β–Ό
        Secondary Module

The child module does not need to contain:

provider "aws" {
  region = "us-west-2"
}

The caller decides which provider configuration the module receives.

Declaring Required Provider Names

A module should declare the providers it expects through required_providers.

For example, a child module can have:

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
  }
}

This tells Terraform which provider the module expects.

It does not configure the region or account.

That remains a provider configuration concern.

Think of the distinction as:

required_providers
        β”‚
        β–Ό
Which provider does the module require?

provider configuration
        β”‚
        β–Ό
Which configured instance should manage these resources?

Those are different questions.

Provider Aliases In A Child Module

Provider aliases become useful when a child module is designed to work with more than one provider configuration.

For example, a module might have resources that need two AWS configurations.

The module can declare the expected provider configurations and the caller can supply them.

A simplified structure is:

                 Root Module
                /           \
       aws.primary         aws.secondary
             β”‚                  β”‚
             β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      β–Ό
                Child Module

The important point is that aliases are part of the provider wiring between the caller and the module.

They are not simply arbitrary strings passed as ordinary module variables.

A Two-Region Example

Consider this root configuration:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

provider "aws" {
  alias  = "west"
  region = "us-west-2"
}

module "primary" {
  source = "./modules/region-resource"
}

module "secondary" {
  source = "./modules/region-resource"

  providers = {
    aws = aws.west
  }
}

The same child module implementation can be used twice.

The first instance uses the default provider configuration.

The second instance explicitly receives the west configuration.

Conceptually:

                 Root Module
                 /          \
                β–Ό            β–Ό
          Primary Module  Secondary Module
                β”‚               β”‚
          aws us-east-1    aws us-west-2

This is a powerful reuse pattern: the module implementation remains the same while the provider context changes.

Why Explicit Passing Is Useful

Explicit provider passing makes an important architectural decision visible.

Compare:

module "secondary" {
  source = "./modules/region-resource"
}

with:

module "secondary" {
  source = "./modules/region-resource"

  providers = {
    aws = aws.west
  }
}

The second version tells the reader immediately:

This module intentionally operates through the west provider configuration.

That is valuable in configurations that manage multiple regions or accounts.

Multiple Accounts

The same pattern can be used with multiple AWS accounts.

For example:

provider "aws" {
  region = "us-east-1"
}

provider "aws" {
  alias  = "shared"
  region = "us-east-1"
}

The exact authentication and account-selection mechanism depends on your AWS setup, but the Terraform architecture remains:

Root
 β”œβ”€β”€ default AWS configuration
 └── aliased AWS configuration
          β”‚
          β–Ό
       Module

A module can therefore be reused against different provider configurations without embedding account-specific assumptions in its implementation.

This is one reason provider configuration belongs outside a reusable module.

Common Confusion: Provider And Module Inputs

A region could technically be represented as an ordinary module variable:

variable "region" {
  type = string
}

but that is not the same thing as configuring the AWS provider.

For example:

region = var.region

inside a resource does not replace provider configuration.

The provider is responsible for the context in which the resource is managed.

A module should generally receive the provider configuration through Terraform’s provider mechanism rather than pretending provider configuration is just another resource argument.

Provider Configuration And Module Configuration Are Different Concerns

Keep these concepts separate:

Module configuration
    β”‚
    β”œβ”€β”€ Inputs
    β”œβ”€β”€ Outputs
    └── Resources

Provider configuration
    β”‚
    β”œβ”€β”€ Region
    β”œβ”€β”€ Account context
    β”œβ”€β”€ Authentication
    └── Other provider settings

The module describes infrastructure.

The provider configuration describes the context in which Terraform manages that infrastructure.

Separating these concerns makes modules more reusable.

The Design Pattern To Remember

For ordinary reusable modules:

Root Module
    β”‚
    β”œβ”€β”€ provider configuration
    β”‚
    β–Ό
Child Module
    β”‚
    β–Ό
Resources

For multiple provider configurations:

                 Root Module
                /           \
        Provider A       Provider B
             β”‚                β”‚
             β–Ό                β–Ό
        Module A          Module B

For a reusable module that must explicitly receive an aliased provider:

module "secondary" {
  source = "./modules/secondary"

  providers = {
    aws = aws.west
  }
}

The root module decides the provider context.

The child module implements infrastructure within that context.

That separation is what allows the same module to work across regions, accounts, and other provider configurations without embedding environment-specific provider configuration inside the module.

Last updated on