January 9, 2025·6 min read·engineeringterraformproviderslifecycle

OpenTofu 1.9 makes provider configurations iterable

OpenTofu 1.9 adds provider for_each for regional deployments. Its design preserves static dependencies and makes provider lifetime an explicit part of deprovisioning.

A regional deployment can repeat the same resource definitions while still requiring separately written provider configurations. That mismatch is the problem addressed by OpenTofu 1.9.0, released on January 9. An aliased provider block can now declare multiple instances with for_each, and resource instances can select among them. The change removes a restriction that previously made the provider configuration the awkward part of an otherwise reusable module.

I reach for Terraform by default because its language makes infrastructure APIs reusable, and OpenTofu interests me particularly because of its license. Provider iteration addresses a practical weakness in that abstraction. Repetition across regions belongs in configuration data when the infrastructure pattern is the same, but the provider relationship still has to remain explicit enough to plan changes and destroy existing objects.

The provider is part of the dependency graph

The provider-iteration RFC describes the previous pattern of repeated aliases, resources and module calls. Moving resource definitions into a child module reduced repetition inside each deployment. It did not remove the need to connect each deployment to its own configured provider. Adding another region could therefore require another structurally identical module call as well as another provider block.

The RFC deliberately stops short of treating providers as ordinary values. A resource block still identifies a particular provider configuration statically. The new freedom is the index selecting an instance of that configuration. This retains enough information to construct dependencies before expression evaluation. An indexed provider reference consequently looks like a collection lookup, but its enclosing reference has stricter meaning than an arbitrary expression.

That restriction is a reasonable compromise. Configuration authors get data-driven selection without requiring every provider relationship to become discoverable only during evaluation. It also explains why a conditional expression choosing between unrelated provider aliases is a different request. The release implements iteration within a known configuration, with dependencies that OpenTofu can recognize before it resolves the selected index.

The static-evaluation design provides the earlier architectural context. Some configuration must be understood during initialization, before ordinary planning can operate. Its approach evaluates variables and locals that do not depend on resources or data sources. Provider expansion builds on that distinction. An inventory supplied as input can determine the provider instances, while a discovery operation that itself requires a provider cannot supply the same early information.

A region inventory can configure multiple instances

The versioned provider documentation permits maps, objects and sets of strings as provider iteration inputs. With a set, each string is its own instance key. An alias is required because default provider configurations remain single instances that OpenTofu can select automatically. The alias identifies the configuration, and the bracketed key identifies one configured instance.

For example, these root-module declarations configure an AWS provider instance for each supplied region. They deliberately keep the inventory of configured regions separate from the inventory of enabled deployments. The example covers provider selection and module wiring, and assumes a child module already exists at ./regional.

variable "regions" {
  type = set(string)
}

variable "disabled_regions" {
  type    = set(string)
  default = []
}

provider "aws" {
  alias    = "by_region"
  for_each = var.regions
  region   = each.key
}

module "regional" {
  source   = "./regional"
  for_each = setsubtract(var.regions, var.disabled_regions)

  providers = {
    aws = aws.by_region[each.key]
  }
}

A reference such as aws.by_region["eu-west-1"] selects one instance. The static portion is aws.by_region, and the region string supplies the index. This distinction matters when reading the example because the provider block expands over all configured regions while the module expands over a filtered set. The difference is intentional and becomes necessary when removing infrastructure.

The 1.9 overview documents this separate-disabled-regions pattern. It also identifies limits on the first implementation. Provider iteration inputs must be obtainable statically from variables and locals. A whole collection of provider instances cannot be passed through a module interface. Each module instance receives a selected provider instance instead, which keeps the child module focused on one deployment.

The child module can keep its existing provider names

The module providers documentation describes the mapping as a connection between two namespaces. Its left side names the provider configuration as the child understands it. On the right, the reference names the configured instance in the parent. In the example, resources inside the child can continue using their ordinary default AWS configuration, even though each module instance receives a different regional instance from the root.

This is useful for module design. Regional selection can stay with the caller, which owns the deployment inventory, without adding a region-specific provider alias throughout the reusable module. A module that requires two provider configurations still needs both mappings. Iteration does not remove those explicit requirements or turn every child into a module that understands collections of providers.

Supplying a providers mapping also replaces the normal default-inheritance behavior. The mapping should therefore describe the configurations that the child actually needs. Treating it as an incidental override can obscure missing provider connections when a module later acquires another dependency. The interface remains small when the caller supplies one configured instance for each provider role the child declares.

Removing a deployment needs a surviving provider

The providers-within-modules documentation explains why provider configurations outlive resource declarations. State retains a reference to the provider configuration last used for each resource. When the resource declaration disappears, OpenTofu still needs that provider to refresh and destroy the remote object. Removing the configuration at the same time removes information needed to carry out the requested deletion.

In the example, disabling a region removes its module instance from the desired configuration while leaving the corresponding provider instance declared. OpenTofu can then use that provider to destroy the resources formerly managed by the module. Only after that apply has completed should the region leave the provider inventory. This gives removal a separate intermediate configuration instead of making creation and deletion look mechanically symmetrical.

I consider that explicit intermediate state a useful part of the design. A shorter configuration would be misleading if removing one map entry also removed the means to clean up the infrastructure it represented. The distinction between configured regions and enabled deployments communicates something operationally necessary about the desired transition.

The implementation history shows that this was a design concern during development. The tracking issue opened on October 31, 2024 links the static-evaluation proposal with implementation work and a warning for identical provider and resource iteration expressions. It also records destruction-related release-blocking tasks. Those details explain why the documentation spends time on removal even though creating another regional instance is the obvious demonstration of the feature.

Provider instances do not select plugin versions

The provider requirements documentation separates provider requirements from provider configurations. Requirements identify a provider source and constrain the acceptable plugin versions. Configurations supply settings for using that provider. Repeating configurations therefore does not provide a mechanism for choosing an unrelated plugin version for each region.

Each module declares its requirements, and OpenTofu selects a version satisfying the configuration as a whole. The dependency lock file records the selected provider version. A review of provider iteration should keep that distinction visible. Regional settings change the configured instances, while source addresses and version constraints continue to govern the executable dependency.

As someone who also writes providers, I find that separation helpful. Reusing a provider across regions should primarily change the configuration supplied to its implementation. The language can express that reuse without suggesting that every configured instance represents an independently versioned dependency.

The repository's provider-reference design notes distinguish the provider type from its local configuration name and its absolute configuration address. Those distinctions account for much of the apparent complexity. A name used inside a child module must resolve to the caller's configured provider, while the provider's global identity still identifies the implementation to install. Adding an instance key extends that addressing model. It does not make a region name a new provider type or change the identity of the plugin that implements the resources.

The release changelog also introduces -exclude, which skips selected objects and their dependants during planning. That controls which part of an existing graph participates in an operation. Provider iteration changes how the configuration declares repeated provider instances in the first place. Both affect multi-region work, but only the latter supplies the reusable declaration needed to connect regional module instances to their respective providers.