Mastering OpenTofu provider iteration with for_each

Master OpenTofu's for_each to iterate providers, reduce duplication, and manage multi-cloud resources with clean, reusable code.

OpenTofu 1.9 introduced provider iteration with for_each, a powerful feature that dramatically reduces code duplication when managing infrastructure across multiple regions, accounts, or cloud providers. Unlike earlier approaches that required repetitive provider blocks, for_each enables dynamic creation of provider configurations from a single definition, streamlining complex infrastructure management.

How provider for_each works in OpenTofu

Provider for_each enables creating multiple provider configurations from a single provider block, with each instance receiving a unique key that can be referenced in resources and modules. This feature was introduced in OpenTofu 1.9 (January 2025) and requires specific syntax elements:

provider "aws" {
  alias    = "by_region"  # Alias is required when using for_each
  for_each = var.regions  # Takes a map, set of strings, or object
  region   = each.key     # Accessing the current iteration key
}

The provider block creates multiple configurations, one for each element in the for_each expression. The each object provides access to the current iteration's key and value, similar to resource for_each.

When using these provider instances with resources, you must explicitly reference them:

resource "aws_vpc" "regional" {
  for_each   = var.regions
  provider   = aws.by_region[each.key]
  cidr_block = "10.0.0.0/16"
  
  tags = {
    Name = "vpc-${each.key}"
  }
}

This pattern allows different resource instances to use different provider configurations based on the same iteration key.

Best practices for provider iteration

When implementing provider iteration in OpenTofu, follow these key practices to avoid common issues:

Use separate variables for providers and resources to properly manage resource lifecycles:

variable "regions" {
  description = "Regions for provider configurations"
  type        = set(string)
  default     = ["us-east-1", "us-west-2", "eu-central-1"]
}

variable "disabled_regions" {
  description = "Regions to be disabled (resources removed)"
  type        = set(string)
  default     = []
}

Apply the setsubtract pattern to ensure resources are properly removed before providers:

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

module "network" {
  source    = "./modules/network"
  providers = {
    aws = aws.by_region[each.key]
  }
  # This ensures we can safely remove regions
  for_each = setsubtract(var.regions, var.disabled_regions)
}

Implement a two-phase deprovisioning strategy when removing regions or accounts:

  1. First add the region to disabled_regions and apply (destroys resources)
  2. Only then remove the region from regions variable (removes provider)

Always use provider aliases with for_each, as the current implementation doesn't support for_each with default providers.

Use static variables and locals for provider for_each expressions, as they can't use values from data sources or resources.

Provider iteration patterns with examples

Multi-region deployment

The most common use case is deploying resources across multiple AWS regions:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.0.0"
    }
  }
}

variable "regions" {
  type    = set(string)
  default = ["us-east-1", "us-west-2", "eu-central-1"]
}

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

# Deploy VPCs in each region
resource "aws_vpc" "regional" {
  for_each   = var.regions
  provider   = aws.by_region[each.key]
  cidr_block = "10.0.0.0/16"
  
  tags = {
    Name   = "vpc-${each.key}"
    Region = each.value
  }
}

Multi-account deployment

For organizations with separate AWS accounts for different environments:

locals {
  accounts = {
    dev  = { id = "123456789012", region = "us-east-1" }
    test = { id = "234567890123", region = "us-east-1" }
    prod = { id = "345678901234", region = "us-east-1" }
  }
}

provider "aws" {
  alias    = "account"
  for_each = local.accounts
  region   = each.value.region
  
  assume_role {
    role_arn     = "arn:aws:iam::${each.value.id}:role/TerraformRole"
    session_name = "OpenTofuSession"
  }
}

# Deploy security groups in each account
resource "aws_security_group" "example" {
  for_each = local.accounts
  provider = aws.account[each.key]
  name     = "example-sg-${each.key}"
  
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Advanced multi-dimension pattern

For complex scenarios requiring combinations of accounts, regions, and roles:

locals {
  # Define dimensions
  accounts = {
    dev  = "123456789012"
    prod = "987654321098"
  }
  
  regions = {
    us_east = "us-east-1"
    eu_west = "eu-west-1"
  }
  
  # Create all combinations
  combinations = {
    for pair in setproduct(keys(local.accounts), keys(local.regions)) :
    "${pair[0]}_${pair[1]}" => {
      account_id = local.accounts[pair[0]]
      region     = local.regions[pair[1]]
    }
  }
}

provider "aws" {
  alias    = "configured"
  for_each = local.combinations
  
  region = each.value.region
  
  assume_role {
    role_arn     = "arn:aws:iam::${each.value.account_id}:role/AdminRole"
    session_name = "OpenTofuSession"
  }
}

# Deploy resources using specific provider combinations
resource "aws_s3_bucket" "logs" {
  for_each = local.combinations
  provider = aws.configured[each.key]
  bucket   = "logs-${each.key}-${each.value.account_id}"
}

When provider iteration is most beneficial

Provider iteration with for_each is particularly valuable in these scenarios:

Multi-region deployments for improving application performance, meeting regional compliance requirements, or implementing disaster recovery strategies. This pattern simplifies maintaining consistent infrastructure across regions while allowing region-specific customizations.

Multi-account AWS environments that separate development, staging, and production for better security isolation. Provider iteration makes managing resources across these accounts more maintainable with less code duplication.

Cross-cloud implementations that deploy similar resources across different cloud providers (AWS, Azure, GCP). The for_each pattern creates a consistent approach across providers while respecting their unique configurations.

Dynamic infrastructure provisioning where the number of regions or accounts may change frequently. Provider iteration makes adding or removing regions/accounts more manageable through simple variable updates.

Progressive rollouts when deploying changes gradually across regions or accounts to minimize risk. The pattern enables controlled, systematic deployment strategies.

For_each vs count with providers

When deciding between for_each and count for provider iteration, consider these key differences:

Identification method: For_each uses string keys (like region names) while count uses numeric indices. For_each's named keys make configurations more readable and stable when elements are added or removed.

# Using for_each (preferred)
provider "aws" {
  alias    = "by_region"
  for_each = var.regions
  region   = each.value
}

# Using count (less flexible)
provider "aws" {
  alias  = "region_${count.index}"
  count  = length(var.regions)
  region = var.regions[count.index]
}

Stability under changes: When removing an element with for_each, only that specific provider instance is affected. With count, removing an element shifts all subsequent indices, potentially causing unnecessary resource recreation and state churn.

Expressiveness: For_each offers better expressiveness with maps and sets, making configurations more maintainable. Count is limited to simple numeric iterations.

Reference pattern: Resources reference for_each providers using the map key, which is more intuitive than count's numeric index:

# With for_each (clear reference)
resource "aws_vpc" "example" {
  provider = aws.by_region["us-east-1"]
}

# With count (less intuitive)
resource "aws_vpc" "example" {
  provider = aws.region_0  # Which region is index 0?
}

Potential pitfalls and troubleshooting

When implementing provider iteration, be aware of these common issues:

Resource lifecycle management problems occur when removing a provider that still has resources. OpenTofu requires providers to outlive their resources by at least one plan/apply cycle. You'll see warnings like:

Warning: Provider configuration for_each matches resource

OpenTofu relies on a provider instance to destroy resource instances that are 
associated with it, and so the provider instance must outlive all of its 
resource instances by at least one plan/apply round.

Solution: Use the two-phase removal strategy described in best practices.

Static evaluation limitation restricts for_each expressions to variables and locals that can be determined before apply. Expressions depending on data sources or resources aren't supported.

Alias requirement means each provider using for_each must have an alias defined. The default provider configuration cannot use for_each.

Performance impact can be significant with many provider configurations. Each provider instance runs as a separate process, so having dozens or hundreds of combinations (e.g., accounts × regions) can cause slowdowns and memory issues.

Troubleshooting tips:

  • Separate provider and resource variables to properly manage lifecycles
  • Use targeted destroy operations before removing providers
  • Limit the number of provider instances to those actually needed
  • Consider using -refresh=false for planning when state is already current
  • Break very large infrastructures into separate state files

Advanced provider iteration techniques

For complex infrastructure requirements, consider these advanced patterns:

Chaining for_each between providers and resources:

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

resource "aws_vpc" "regional" {
  for_each   = var.vpcs
  provider   = aws.by_region[each.value.region]
  cidr_block = each.value.cidr_block
}

resource "aws_subnet" "public" {
  # Chain from the previous resource
  for_each          = aws_vpc.regional
  provider          = aws.by_region[var.vpcs[each.key].region]
  vpc_id            = each.value.id
  cidr_block        = "10.0.1.0/24"
  availability_zone = "${var.vpcs[each.key].region}a"
}

Conditional provider selection to dynamically choose providers based on deployment requirements:

module "app_deployment" {
  source = "./modules/app"
  
  for_each = var.deployment_targets
  
  providers = {
    aws = each.value.type == "primary" ? 
          aws.by_region["us-east-1"] : 
          aws.by_region["us-west-2"]
  }
  
  environment = each.key
  config      = each.value.config
}

Null provider pattern for transitional states:

locals {
  active_regions = {
    for k, v in var.regions :
    k => v if !contains(var.disabled_regions, k)
  }
  
  disabled_regions = {
    for k, v in var.regions :
    k => v if contains(var.disabled_regions, k)
  }
}

# Resources only in active regions
resource "aws_vpc" "example" {
  for_each   = local.active_regions
  provider   = aws.by_region[each.key]
  cidr_block = "10.0.0.0/16"
}

OpenTofu vs Terraform comparison

Provider iteration with for_each has notable differences between OpenTofu and Terraform:

Feature availability: Provider for_each is available in OpenTofu 1.9.0 (released in January 2025) but is not yet implemented in Terraform open-source as of May 2025. This is a key differentiator between the two tools.

Commercial alternatives: Terraform has introduced a similar functionality called "Terraform Stacks" in their commercial HashiCorp Cloud Platform (HCP) offering, but not in the open-source product.

Provider compatibility: Both OpenTofu and Terraform use the same provider ecosystem. OpenTofu works with all existing Terraform providers without modification, though it uses its own separate registry.

Implementation approach: OpenTofu implemented provider for_each as part of its commitment to prioritizing community-requested features after forking from Terraform in 2023. The implementation required significant work on the core engine to support dynamic provider instances while maintaining backward compatibility.

Future development: Given the demand for this feature, Terraform may eventually implement something similar in their open-source version, but currently, this capability is exclusive to OpenTofu or Terraform's commercial offerings.

Conclusion

Provider iteration with for_each in OpenTofu represents a significant advancement in infrastructure as code capabilities, enabling more maintainable and DRY configurations for complex deployments. While powerful, successful implementation requires understanding the limitations and following best practices, particularly around resource lifecycle management.

The feature demonstrates OpenTofu's commitment to implementing community-requested features and provides a compelling reason for organizations managing complex multi-region or multi-account infrastructure to consider OpenTofu. As infrastructure requirements grow increasingly complex, tools like provider for_each become essential for maintaining clean, manageable code while supporting sophisticated deployment patterns.