Terraform Provider in Module: Inheritance, Configuration, and Best Practices

Provider configuration is one of the most misunderstood areas of Terraform modules. Providers are global to a Terraform configuration and the rules that govern how they are defined, inherited, and passed across module boundaries directly affect state correctness, multi-account deployments, and module reusability. Understanding where provider blocks are allowed, how inheritance works, and when to use explicit passing or configuration aliases prevents state loss, planning errors, and incompatible module patterns.

Provider Scope and Global Nature

Provider configurations are global to an entire Terraform configuration and can be shared across module boundaries. Unlike most other concepts in Terraform, a provider configuration is not scoped to the module in which it is written. Each resource in the configuration must be associated with one provider configuration.

Provider configurations can be defined only in a root Terraform module. Child modules do not define their own provider configurations; they receive them from their parent modules. Terraform retains, as part of its state, a reference to the provider configuration that was most recently used to apply changes to each resource. If Terraform finds a resource instance tracked in the state whose provider configuration block is no longer available then it will return an error during planning, prompting you to reintroduce the provider configuration.

Provider configurations are used for all operations on associated resources, including destroying remote objects and refreshing state. Because of this coupling, removing a provider block from the root after resources have been created will break the configuration and prevent safe planning and applies.

A module intended to be called by one or more other modules must not contain any provider blocks. A module containing its own provider configurations is not compatible with the foreach, count, and dependson arguments that were introduced in Terraform v0.13. For more information on legacy patterns, see Legacy Shared Modules with Provider Configurations.

Implicit Inheritance vs Explicit Passing

Providers can be passed down to descendant modules in two ways: either implicitly through inheritance, or explicitly via the providers argument within a module block.

By default, a child module inherits the default provider from its parent. If you configure an AWS provider in your root module, every child module automatically uses it.

```

Root module - providers.tf

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

This module automatically uses the aws provider from above

module "vpc" {
source = "./modules/vpc"
vpc_cidr = "10.0.0.0/16"
}
```

This works fine for simple setups where a single provider configuration is sufficient for all resources.

Implicit inheritance is the default behavior. When you define a provider in your root module, Terraform implicitly passes that provider configuration to any child modules to ensure all modules use the same configuration.

```
main.tf
provider "aws" {
region = "us-west-2"
}

module "vpc" {
source = "./modules/vpc"
}
```

The child module uses the provider configuration passed from the root module:

```
modules/vpc/main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}

resource "awsvpc" "main" {
cidr
block = "10.0.0.0/16"
tags = {
Name = "Main VPC"
}
}
```

The awsvpc resource inherits the same AWS provider configuration from the root module, and Terraform creates the awsvpc resource in the us-west-2 region.

Explicit passing via the providers meta-argument is required when you need more control, such as deploying to multiple AWS accounts, managing resources across regions, or working with multiple provider instances. Explicit passing allows a caller to select which provider configuration a child module will use.

Scenario Mechanism Example Use Case
Single default provider Implicit inheritance All resources in one region and account
Multiple provider configs for same provider Explicit providers argument + aliases Deploy to us-east-1 and us-west-2
Cross-account deployment Explicit providers argument with aliased providers Separate provider per AWS account
Module reuse with for_each No provider blocks in module Shared module called multiple times

Required Providers and Version Pinning

Although provider configurations are shared between modules, each module must declare its own provider requirements, so that Terraform can ensure that there is a single version of the provider that is compatible with all modules in the configuration and to specify the source address that serves as the global module-agnostic identifier for a provider.

To declare that a module requires particular versions of a specific provider, use a required_providers block inside a terraform block:

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

A provider requirement says, for example, "This module requires version v2.7.0 of the provider hashicorp/aws and will refer to it as aws." It doesn't, however, specify any of the configuration settings that determine what remote endpoints the provider will access, such as an AWS region; configuration settings come from provider configurations, and a particular overall Terraform configuration can potentially have several different configurations for the same provider.

Child modules do not inherit provider source or version requirements, so you must explicitly define those within a child module. This declaration is required for Terraform to validate compatibility across the configuration.

Required providers documentation is separate from provider configuration. The required_providers block documents dependencies and pins versions, while provider blocks in the root supply actual configuration arguments.

Configuration Aliases and Multi-Region Deployments

To declare multiple configuration names for a provider within a module, add the configuration_aliases argument:

terraform { required_providers { aws = { source = "hashicorp/aws" version = ">= 2.7.0" configuration_aliases = [aws.west] } } }

Without this declaration, Terraform raises an error when the module tries to reference aws.west in its resources.

```
modules/web-server/main.tf
terraform {
requiredproviders {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
configuration
aliases = [aws.west]
}
}
}

data "awsami" "amazonlinux" {
provider = aws.west
#...
}
```

Configuration aliases enable a module to work with multiple configurations of the same provider, such as deploying to multiple AWS regions. By default, Terraform passes provider configurations from your root module down to any child modules you call. Use configuration aliases when a module needs multiple configurations of the same provider.

The provider block supports arguments for configuring a named provider, which is a plugin that lets Terraform interact with cloud providers, SaaS providers, and other APIs.

Provider Blocks in Root vs Child Modules

Define provider configurations in the root module of your Terraform configuration. Child modules receive their provider configurations from their parent modules, so we strongly recommend against defining provider blocks in child modules.

The provider block configures a named provider, which is a plugin that lets Terraform interact with cloud providers, SaaS providers, and other APIs. HashiCorp's public Terraform registry distributes providers separately from Terraform itself, and each provider has its own release cadence, documentation, and versions.

In practice, provider configuration is often dynamic, using variables and locals:

```
variable "aws_region" {
description = "The AWS region to deploy resources in"
type = string
default = "us-west-2"
}

locals {
common_tags = {
Environment = "production"
Project = "web-app"
}
}

provider "aws" {
region = var.awsregion
default
tags {
tags = local.common_tags
}
}
```

An empty configuration is also valid:

provider "random" { }

You can choose to omit this block entirely if you don't need to configure any provider-specific arguments. Terraform also assumes an empty default configuration for any provider that you do not explicitly configure with a provider block.

If a provider requires specific arguments, Terraform returns an error when resources try to use the default configuration because the default is not properly configured.

Variable Sharing and Provider Interaction

When building reusable Terraform modules, provider configuration and variable passing are separate concerns. Providers are automatically inherited by child modules in Terraform, so you don't need to declare provider blocks inside modules unless you need custom configuration. For variables, explicitly pass them from the root to each module - there's no automatic sharing.

TLDR: Providers are automatically inherited by child modules in Terraform, so you don't need to declare provider blocks inside modules unless you need custom configuration. For variables, explicitly pass them from the root to each module - there's no automatic sharing. Use required_providers in modules to document which providers they need, and use configuration aliases when a module needs multiple configurations of the same provider.

Common mistakes include defining provider blocks inside reusable modules, which breaks foreach and count usage and prevents proper provider passing. Another mistake is assuming provider version constraints are inherited; each module must declare its own requiredproviders.

Provider configuration in Terraform modules is one of those areas where things can get confusing quickly. Modules inherit providers from their callers, but sometimes you need more control - deploying to multiple AWS accounts, managing resources across regions, or working with multiple provider instances. This post explains how to handle all of these scenarios cleanly.

Common Pitfalls and Error Scenarios

Provider configuration errors typically surface at plan time. A missing provider configuration block for a resource already in state causes an error during planning, prompting you to reintroduce the provider configuration. This protects state integrity by ensuring resources are always operated on with the correct provider.

Another pitfall is using provider blocks inside modules intended for reuse. Such modules are not compatible with foreach, count, and dependson introduced in Terraform v0.13. The recommended pattern is to keep provider blocks only in the root and declare required_providers inside modules.

A third issue is mismatched provider names. When using aliases, the module must declare configurationaliases in requiredproviders, and the caller must pass the provider explicitly via the providers meta-argument. Without the alias declaration, Terraform will error when the module references an aliased provider.

Pitfall Symptom Remedy
Provider block in child module Error on for_each/count usage Remove provider block, move to root
Missing required_providers Version conflict or unknown provider Add required_providers with source and version
Missing configuration_aliases Unknown provider reference Add alias to required_providers and pass via providers argument
Removed provider config Plan error, state reference missing Reintroduce provider configuration with same name

Conclusion

Provider configuration in Terraform modules is governed by strict scope rules that prioritize state safety and reusability. Provider configurations are global, defined only in the root module, and shared across module boundaries through implicit inheritance or explicit passing. Modules must never contain provider blocks if they are intended for reuse, and they must declare required_providers to pin source and version independently of configuration.

Configuration aliases and the providers meta-argument provide the mechanism for advanced scenarios like multi-region and multi-account deployments. Variables are passed explicitly, while providers are inherited automatically, creating a clear separation between configuration settings and module inputs.

The correct pattern is root-owned provider blocks, module-declared requiredproviders, and explicit provider passing only when multiple configurations are needed. Following these rules avoids state errors, enables foreach and count, and keeps modules composable and portable across configurations.

Sources

  1. HashiCorp Terraform Modules Providers
  2. OneUptime Terraform Dynamic Provider Configuration
  3. DevOps Daily Terraform Provider Variable Sharing
  4. HashiCorp Terraform Provider Block

Related Posts