Providers Within Modules
In a configuration with multiple modules, there are some special considerations for how resources are associated with provider configurations.
Each resource in the configuration must be associated with one provider configuration. Provider configurations, unlike most other concepts in OpenTF, are global to an entire OpenTF configuration and can be shared across module boundaries. Provider configurations can be defined only in a root module.
Providers can be passed down to descendent modules in two ways: either
implicitly through inheritance, or explicitly via the providers
argument
within a module
block. These two options are discussed in more detail in the
following sections.
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 for_each
, count
, and depends_on
arguments.
Provider configurations are used for all operations on associated resources,
including destroying remote objects and refreshing state. OpenTF retains, as
part of its state, a reference to the provider configuration that was most
recently used to apply changes to each resource. When a resource
block is
removed from the configuration, this record in the state will be used to locate
the appropriate configuration because the resource's provider
argument
(if any) will no longer be present in the configuration.
As a consequence, you must ensure that all resources that belong to a particular provider configuration are destroyed before you can remove that provider configuration's block from your configuration. If OpenTF 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 Version Constraints in Modules
Although provider configurations are shared between modules, each module must declare its own provider requirements, so that OpenTF 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 OpenTF
configuration can potentially have
several different configurations for the same provider.
Provider Aliases Within Modules
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.alternate ]
}
}
}
The above requirements are identical to the previous, with the addition of the
alias provider configuration name aws.alternate
, which can be referenced by
resources using the provider
argument.
If you are writing a shared module, constrain only the minimum
required provider version using a >=
constraint. This should specify the
minimum version containing the features your module relies on, and thus allow a
user of your module to potentially select a newer provider version if other
features are needed by other parts of their overall configuration.
Implicit Provider Inheritance
For convenience in simple configurations, a child module automatically inherits
default provider configurations from its parent. This means that
explicit provider
blocks appear only in the root module, and downstream
modules can simply declare resources for that provider and have them
automatically associated with the root provider configurations.
For example, the root module might contain only a provider
block and a
module
block to instantiate a child module:
provider "aws" {
region = "us-west-1"
}
module "child" {
source = "./child"
}
The child module can then use any resource from this provider with no further provider configuration required:
resource "aws_s3_bucket" "example" {
bucket = "provider-inherit-example"
}
We recommend using this approach when a single configuration for each provider is sufficient for an entire configuration.
~> Note: Only provider configurations are inherited by child modules, not provider source or version requirements. Each module must declare its own provider requirements. This is especially important for non-HashiCorp providers.
In more complex situations there may be multiple provider configurations, or a child module may need to use different provider settings than its parent. For such situations, you must pass providers explicitly.
Passing Providers Explicitly
When child modules each need a different configuration of a particular
provider, or where the child module requires a different provider configuration
than its parent, you can use the providers
argument within a module
block
to explicitly define which provider configurations are available to the
child module. For example:
# The default "aws" configuration is used for AWS resources in the root
# module where no explicit provider instance is selected.
provider "aws" {
region = "us-west-1"
}
# An alternate configuration is also defined for a different
# region, using the alias "usw2".
provider "aws" {
alias = "usw2"
region = "us-west-2"
}
# An example child module is instantiated with the alternate configuration,
# so any AWS resources it defines will use the us-west-2 region.
module "example" {
source = "./example"
providers = {
aws = aws.usw2
}
}
The providers
argument within a module
block is similar to
the provider
argument
within a resource, but is a map rather than a single string because a module may
contain resources from many different providers.
The keys of the providers
map are provider configuration names as expected by
the child module, and the values are the names of corresponding configurations
in the current module.
Once the providers
argument is used in a module
block, it overrides all of
the default inheritance behavior, so it is necessary to enumerate mappings
for all of the required providers. This is to avoid confusion and surprises
that may result when mixing both implicit and explicit provider passing.
Additional provider configurations (those with the alias
argument set) are
never inherited automatically by child modules, and so must always be passed
explicitly using the providers
map. For example, a module
that configures connectivity between networks in two AWS regions is likely
to need both a source and a destination region. In that case, the root module
may look something like this:
provider "aws" {
alias = "usw1"
region = "us-west-1"
}
provider "aws" {
alias = "usw2"
region = "us-west-2"
}
module "tunnel" {
source = "./tunnel"
providers = {
aws.src = aws.usw1
aws.dst = aws.usw2
}
}
The subdirectory ./tunnel
must then declare the configuration aliases for the
provider so the calling module can pass configurations with these names in its providers
argument:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 2.7.0"
configuration_aliases = [ aws.src, aws.dst ]
}
}
}
Each resource should then have its own provider
attribute set to either
aws.src
or aws.dst
to choose which of the two provider configurations to
use.