November 16, 2017·6 min read·engineeringterraformmodulesproviders

Terraform makes module versions and provider mappings explicit

Terraform 0.11 adds registry module constraints and explicit provider mappings. It also changes apply approval and stops suppressing errors in module outputs.

A reusable module has two dependencies that its source address alone cannot explain. It needs a particular implementation of the module, and its resources need provider configurations that select the intended infrastructure APIs. Terraform 0.11 gives both relationships explicit syntax. A module block can constrain a registry module's version and map provider configurations from its caller.

The November 16 release notes introduce version and providers as reserved module attributes. The release also changes provider inheritance, makes output interpolation errors fatal and asks for approval by default when applying without a saved plan. These changes affect the contracts around reusable configuration as well as interactive execution.

I value Terraform's general approach to reusable infrastructure definitions. Reuse is easier to judge when a caller states which module releases it accepts and which provider instance the child will use. Both choices can otherwise remain hidden behind a familiar module name.

Registry modules gain an enforceable selection policy

The module configuration reference defines version as a constraint for modules retrieved from registries. Terraform uses the newest version matching the constraint. The argument is not a general version selector for every supported source transport.

That qualification matters for Git-based modules. A registry constraint and a repository reference are different mechanisms. A configuration must use the mechanism associated with its source type, so adding version beside an arbitrary Git source does not establish a supported registry version policy.

The public registry already tracks semantic release versions. Terraform 0.11 adds the client-side configuration needed to restrict selection among those releases. Publication and consumption can now share a version vocabulary, while the permitted range remains the caller's decision.

A range can still admit several releases. The rule that Terraform selects the newest matching version explains what a constraint means without implying that it names one immutable artifact. An author seeking an exact selection must express a policy that reflects that intent.

The new reserved name also has a compatibility consequence. A child module that previously accepted an ordinary input variable named version now conflicts with Terraform's interpretation of that module argument. The release notes require such modules to change their interface. The same issue applies to providers.

Provider configuration is an execution dependency

The 0.11 upgrade guide describes the previous provider inheritance rules as problematic. An ancestor's provider settings could override settings in a descendant. There was also no well-defined mechanism for passing aliased provider configurations into a child that needed several instances.

Version 0.11 makes separately configured providers distinct. A child provider block no longer combines individual attributes with an ancestor's block. Default unaliased configurations can still be inherited, while aliased configurations must be passed explicitly.

This changes how a reader should interpret a partially specified provider inside a child module. It cannot rely on receiving arbitrary missing attributes from an ancestor through the former merge behavior. The upgrade guide directs existing configurations to copy necessary settings or adopt explicit provider passing.

A provider instance determines details such as a cloud region. Passing it into a child module therefore supplies an execution dependency, not merely a decorative name. Two module instances can use the same resource definitions while targeting different regions through different provider instances.

The mapping names the child's view and the caller's view

The new providers argument is a map. Its keys identify provider names expected by the child, and its values identify configurations in the caller. This lets a caller pass an aliased provider as the child's default provider.

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

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

  providers = {
    "aws" = "aws.east"
  }
}

This caller-side example follows the mapping syntax in the upgrade guide. The child module at ./network expects an AWS provider. The map associates that expectation with the caller's aws.east configuration. Authentication and the child's resource declarations are outside this snippet.

The direction of the mapping is important. A key belongs to the child's naming context, while a value belongs to the caller's. Reversing those names changes the meaning instead of simply restating the same association.

The guide recommends explicit provider blocks in the root module, with configurations passed to children. A child can then describe its resources without owning the concrete region configuration inside its own source. Callers retain control over where separate instances operate.

Provider lifetime has to outlast managed resources

Provider placement also affects destruction. The upgrade guide explains that removing a child module containing its own provider block removes the configuration Terraform needs to refresh and destroy that module's resources. Terraform no longer tries to substitute a same-named provider from an ancestor, because that can select the wrong region.

The consequence follows from the dependency. A resource can remain in state after its configuration block disappears, but Terraform still needs the correct provider to operate on the external object. Removing the only suitable provider at the same time leaves that operation without its required configuration.

The guide recommends moving provider blocks to the root and passing them into children. It also describes applying after the refactor so state records the new location of each resource's provider configuration. That synchronization should not change infrastructure when the resource configuration itself is unchanged.

The release notes say state now records the provider configuration most recently used to create or update a resource. This supports later destruction using the same association. The provider configuration still needs to be available, which explains why retaining it outside a removable child is useful.

I prefer a module contract that states these associations directly. Region selection belongs in a place where a caller can see it, and a resource's cleanup path needs the same API context as its creation path.

Output expressions now report their errors

Terraform 0.11 stops silently converting output-expression errors into empty strings. The upgrade guide says evaluation errors now stop execution and produce diagnostics, as they do elsewhere in configuration. An output that nobody consumes can still make the module invalid.

This matters because outputs form the boundary through which modules expose results. An empty string produced by error suppression can look like a legitimate value to a caller. Reporting the actual expression error keeps that failure attached to its source.

The guide gives conditional resources as a concrete example. A resource whose count can be zero has no instance to supply a direct attribute reference in that case. Existing output expressions that appeared to work only because errors were suppressed need to account for the absent instance.

The change does not mean every output must always contain a nonempty string. It means a successfully evaluated empty value and a failed expression are no longer silently treated as equivalent. That distinction makes module interfaces more informative.

Interactive apply keeps approval inside the locked operation

The upgrade guide also explains the new default for terraform apply without a saved plan. Terraform displays the generated plan and waits for confirmation. On a backend that supports locking, it holds the state lock through refresh, planning, confirmation and application.

That continuous lock is a meaningful difference from treating review as an unrelated preliminary step. Another Terraform apply using the same lock cannot invalidate the pending plan during the confirmation interval. The guarantee still depends on backend support for locking.

Applying an explicit saved plan keeps its existing behavior. Automation that previously ran an unsaved apply now needs a deliberate approval workflow or an explicit auto-approval choice. The guide recommends separated plan and apply for automation and limits its recommendation for unsaved auto-approval to unimportant environments.

Source spelling also becomes less ambiguous

The upgrade guide requires relative module paths to begin with ./ or ../. A bare string such as child is no longer interpreted as a relative path as registry source syntax becomes part of module resolution.

This is another interface clarification with an upgrade cost. A local module source should visibly describe a filesystem relationship, while a registry address identifies a published package. The explicit prefix prevents the resolver from having to infer the author's intent from an ambiguous string.

Taken together, these changes make the module block carry more of its actual contract. Its source identifies the implementation location, registry constraints bound release selection, and provider mappings state the API configurations supplied to the child. Output evaluation then reports failures in the values the module exposes to its callers.