August 10, 2020·6 min read·engineeringterraformmodulesproviders

Terraform 0.13 makes module instances part of the language

Terraform 0.13 adds module iteration and dependencies alongside namespaced provider requirements. The design makes module identity and provider lifetime explicit during reuse and upgrades.

Repeating a reusable infrastructure module should not require repeating the entire call block. Terraform 0.13 adds count and for_each to module calls, extending an existing resource-language mechanism to groups of resources. It also makes explicit dependencies available at the module boundary.

The August 10 release pairs those additions with namespaced provider installation. These changes meet where a module becomes a reusable unit. Each instance needs a stable address, and its resources need an unambiguous provider identity and a provider configuration that remains available for their full lifecycle.

I consider module iteration a useful improvement to Terraform's central abstraction. It lets a caller describe repeated infrastructure without requiring the child module to anticipate every repetition pattern. The restrictions around providers explain why that improvement involves more than accepting an extra argument.

A call block can describe multiple module instances

The versioned module documentation gives module iteration the same syntax and type constraints as resource iteration. A for_each value is a map with string keys or a set of strings. Terraform makes the each object available inside that call.

A reduced example can demonstrate the language without requiring a provider. Suppose a local child module declares one string input and exports it unchanged.

variable "name" {
  type = string
}

output "name" {
  value = var.name
}

With that configuration in ./named, a caller can instantiate it for two names.

module "named" {
  source   = "./named"
  for_each = toset(["assets", "media"])
  name     = each.key
}

output "assets_name" {
  value = module.named["assets"].name
}

The keyed reference matters. A single module call previously exposed an output through a reference such as module.named.name. Once the call creates multiple instances, the expression must identify which instance supplies the output. In this example, the key is part of that identity.

The same rule appears in resource addresses beneath an iterated module. The documentation's bucket example produces addresses containing module.bucket["assets"] and module.bucket["media"]. Each module instance key identifies a separate collection of child resources, even when both instances come from the same source directory.

Iteration does not erase resource identity

The module documentation explains that moving resources across module boundaries changes how Terraform identifies them. State movement can inform Terraform that a resource moved instead of asking it to treat the new address as a different object. With iterated modules, the instance index or key also becomes part of that address.

That means a refactoring which introduces iteration needs more consideration than a text-level comparison. The infrastructure declaration may look equivalent to a human while its addresses differ. The relevant question is whether the existing state objects retain an intended correspondence to the new configuration addresses.

This is also why output references provide a useful boundary between modules. A caller consumes declared outputs instead of reaching directly into a child's internal resources. Iteration expands the set of module instances, while that output interface can remain the way callers obtain results.

The source argument remains a literal string. Iteration supplies different inputs to instances of the selected module. It does not select a different source expression for every key. That preserves a clear distinction between choosing reusable code and choosing the data with which each instance uses it.

Provider configurations must outlive their resources

The documentation describes a legacy pattern in which child modules declare their own provider configurations. Terraform retains that pattern for compatible old module calls, but it cannot combine it with module count, for_each or depends_on.

The lifecycle explanation is practical. Terraform needs a provider configuration to destroy an object as well as to create or update it. If removing a module also removes the only provider configuration for its resources, Terraform loses information it still needs to destroy those resources.

Passing provider configurations from the caller separates their lifetime from the child resources. A module can receive default provider configuration through inheritance or receive an explicit mapping through its providers argument. The versioned documentation also describes proxy blocks for declaring aliased configurations expected by a child.

For this release, those proxy declarations use the documented provider-block convention. They should not be confused with configuring an independent provider inside the child. Their purpose is to name a configuration supplied by the caller.

Provider assignment also remains static. A module using iteration cannot choose a different provider configuration for every instance. When instances require distinct provider configurations, the documentation calls for separate module blocks for the distinct sets. Iteration therefore does not provide an automatic mechanism for selecting a provider alias from a region name.

Module dependencies affect evaluation

The new depends_on argument describes an explicit dependency involving the entire module. The module documentation says it delays final evaluation of the module and its submodules until dependencies have been applied. That is a broader statement than waiting for one input expression to become known.

An ordinary reference already expresses a relationship where one value supplies another argument. An explicit module dependency is useful when the intended ordering cannot be expressed adequately through that data reference. Its module-wide scope should remain part of the explanation, since it can affect more than one resource.

The upgrade guide connects dependency support to changes in data-resource timing. Terraform 0.13 reads data resources during planning so dependencies on managed resources can be respected. Disabling refresh no longer disables those data-resource reads.

This separates managed-resource synchronization from data reads needed to produce a plan. A workflow depending on -refresh=false to suppress both behaviors changes under this release. That consequence follows from the lifecycle work supporting dependency ordering, even though it is easy to miss in a summary focused only on new module arguments.

Provider source addresses distinguish independent publishers

The upgrade guide also explains the new provider namespace. A requirement identifies the provider's source, while a provider configuration supplies settings for using it. These are related declarations with different responsibilities.

terraform {
  required_providers {
    newrelic = {
      source  = "newrelic/newrelic"
      version = "~> 2.1.1"
    }
  }
}

The source address disambiguates the publisher and provider within the registry namespace. The guide requires explicit sources outside the HashiCorp namespace, while recommending explicitness for HashiCorp providers too. Automatic installation can consequently support independent provider publishers without assuming one shared namespace.

Each module declares its own requirements. Supplying a provider configuration from a parent does not remove the child's responsibility to identify the provider it requires. That distinction is especially important for reusable modules distributed separately from their callers.

The terraform 0.13upgrade command updates the current module's configuration and can consult the existing lookup information for legacy provider names. The guide instructs authors to run the process for each module separately. One invocation at the root is not a promise that every child module has been rewritten.

Local provider distribution follows the namespace

The upgrade guide describes a corresponding change to manually installed provider directories. The directory hierarchy now includes the registry hostname, namespace, provider name and version before the target platform. This lets a local installation preserve the same identity as a provider requirement.

For an in-house provider, the hostname can establish a virtual registry namespace even before an actual registry service exists there. Terraform can find a package in the appropriate local hierarchy. Hosting a registry later can preserve the source address already used by modules.

That arrangement separates identity from the immediate download mechanism. An organization can name its provider consistently while supplying the executable through local distribution. The guide also documents terraform providers mirror for populating a local directory from the current configuration's requirements.

Existing state also contains provider references

Updating configuration is only part of the migration because state records provider associations too. The guide describes automatic reference updates during the first apply, using configuration to determine the proper provider. Removing resource blocks before that step can deprive Terraform of information needed for the mapping.

In-house providers may require an explicit state mapping with terraform state replace-provider. The guide distinguishes their legacy addresses from fully qualified addresses under a domain the organization controls. It also warns that the resulting state uses syntax Terraform 0.12 cannot parse.

I find the separation between provider requirement, configuration and state association useful when reading this release. They answer which implementation is needed, how it is configured and which existing objects it manages. Module reuse becomes more predictable when those answers remain explicit.

Terraform 0.13 expands how callers instantiate modules while preserving constraints imposed by infrastructure lifecycle. Keys identify instances, provider declarations identify implementations, and retained provider configurations allow later destruction. The upgrade work follows those same relationships in both configuration and existing state.