April 14, 2021·5 min read·engineeringterraformmodulesproviders

Terraform 0.15 makes module provider aliases explicit

Shared modules can declare provider configuration names through configuration_aliases. The change separates a module's requirements from the root configuration's connections to remote systems.

A shared Terraform module can need two configurations of the same provider without knowing which accounts or regions its caller will choose. Historically, an empty provider block with only an alias tried to express that requirement. Terraform 0.15, released on April 14, replaces those proxy configuration blocks with an explicit configuration_aliases argument inside required_providers.

The release retains some support for the old syntax, but adds errors where an empty provider block previously had an ambiguous meaning. That detail explains the change's purpose. An empty declaration could resemble a provider configuration even when its author intended only to reserve a name that a parent module would supply. The language now has a declaration specifically for the latter case.

I value Terraform most when its modules make an infrastructure abstraction reusable. A module that hardcodes its own connection settings limits that reuse. Declaring the names of the provider configurations it needs gives the caller a concrete interface while leaving operational settings in the root configuration.

Requirements and configurations answer different questions

The provider configuration documentation for 0.15 distinguishes installing a provider from configuring it. required_providers identifies a provider source and constrains its version. A provider block supplies settings such as a remote endpoint or cloud region. The source address identifies the plugin, while several configurations can use that same plugin with different settings.

The module provider documentation makes a further distinction. Child modules can inherit default provider configurations, but they do not inherit provider source and version requirements. Each module declares those requirements itself. An aliased provider configuration also does not pass automatically through the default inheritance mechanism.

Consider a child module that needs two AWS provider configurations. Its declaration can name the two roles without selecting regions. Those names describe the interface that each caller must satisfy, independently of its regional choices.

terraform {
  required_providers {
    aws = {
      source                = "hashicorp/aws"
      configuration_aliases = [aws.source, aws.destination]
    }
  }
}

The alias names are references, not quoted strings. They declare names the child can use when assigning a provider to a resource or data source. This block does not create two configured AWS clients by itself. The parent must supply the configurations that satisfy those names.

The root module can make that mapping explicit. Separate configurations select the regions, and the module call assigns each configuration to a name the child expects.

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

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

module "connection" {
  source = "./connection"

  providers = {
    aws.source      = aws.west
    aws.destination = aws.east
  }
}

This excerpt illustrates provider wiring. The child module still needs its resource declarations, and the root needs the remaining requirements and credentials appropriate to the actual configuration. The keys of providers use names expected by the child. Its values refer to configurations available in the caller. The names on either side need not match.

Provider configurations have a longer lifetime than resource blocks

The module documentation traces an older problem to configurations embedded in child modules. Terraform needs the provider configuration when destroying an object as well as when creating or updating it. Removing a module that owns both its resources and their provider configuration can remove the information needed to destroy those resources.

Terraform keeps a reference to the last provider configuration used for a resource in state. If its resource block disappears, that reference still lets Terraform find the configuration needed for the remaining operation. Removing the provider block prematurely breaks the relationship. The documentation says Terraform then reports an error during planning and asks for the provider configuration to be restored.

Root-level provider configurations make that lifetime easier to express. A module can disappear while the root configuration remains available to destroy the objects the module managed. Receiving configurations from a caller is therefore more than a convention about tidy files. It accommodates operations that continue after a resource declaration has been removed.

The new alias declaration fits that design. A child states which configuration names its resources use without constructing provider settings inside the module. Its caller owns the actual configurations and can retain them for the required lifetime. The language separates the dependency declaration from the connection settings that fulfill it.

I find that distinction particularly relevant when considering Terraform as both a configuration author and a provider author. The provider plugin implements operations against an API, but the module author decides which configured instance those operations use. An explicit alias requirement makes that decision visible at the module interface.

Static provider associations still constrain composition

The change does not make provider configurations ordinary dynamic values. The versioned documentation says that associations between resources and provider configurations are static. A module call using count or for_each cannot select a different provider configuration for each instance. Different sets of configurations require separate module blocks.

That matters for a common interpretation of reusability. A module can accept different configured providers in different calls, but it cannot treat a provider reference like an arbitrary map value selected by a loop index. configuration_aliases improves the declaration of the supported interface. It does not remove the language's static association rule.

The same documentation describes why legacy child modules with their own provider configurations conflict with module-level count, for_each, and depends_on, introduced in Terraform 0.13. Moving configurations to the root and passing them into a child addresses that structural incompatibility. Replacing proxy blocks with configuration_aliases then makes the remaining name declarations explicit.

The upgrade also removes old interpretations

Version 0.15 includes several removals of deprecated language and CLI forms. The release notes remove the old list and map functions in favor of collection syntax with conversions where needed. They also remove ignore_changes = ["*"] in favor of ignore_changes = all, and replace trailing directory arguments with the global -chdir option.

These changes make the release more demanding than a routine patch. For module authors, however, the provider-alias change has a specific benefit beyond cleanup. The module's required provider names now live beside its provider identity declaration. A reader can determine which configured providers the caller must pass without deciding whether an empty provider block is a real configuration or an old proxy declaration.