Opentofu provider iteration with `for_each`
2025-01-25 - a much anticipated feature
Tags: AWS OpenTofu
Introduction
The latest release of OpenTofu came with a much anticipated feature: provider
iteration with for_each!
My code was already no longer compatible with terraform since OpenTofu added the much needed variable interpolation in provider blocks feature, so I was more than ready to take the plunge.
Usage
A good example will be to rewrite the lengthy code from my Securing AWS default vpcs article a few months ago. It now looks like:
locals {
aws_regions = toset([
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
"ca-central-1",
"eu-central-1",
"eu-north-1",
"eu-west-1",
"eu-west-2",
"eu-west-3",
"sa-east-1",
"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2",
])
}
provider "aws" {
alias = "all"
default_tags { tags = { "managed-by" = "tofu" } }
for_each = concat(local.aws_regions)
profile = "common"
region = each.key
}
module "default" {
for_each = local.aws_regions
providers = { aws = aws.all[each.key] }
source = "../modules/defaults"
}
Note the use of the concat() function in the for_each definition of the
providers block. This is needed to silence a warning that tells you it is a bad
idea to iterate through your providers using the same expression in provider
definitions and module definitions.
Though I understand the reason (to allow for resources destructions when the list we are iterating on changes), it is not a bother for me in this case.
Modules limitations
The main limitation at the moment is the inability to pass down the whole
aws.all to a module. This leads to code that repeats itself a bit, but it is
still better than before.
For example, when creating resources for multiple aws accounts, a common pattern
is to have your DNS manged in a specific account (for me it is named core)
that you need to pass around. Let’s say you have another account named common
with for example monitoring stuff and here is how some module invocation can
look like:
module "base" {
providers = {
aws = aws.all["${var.environment}_${var.region}"]
aws.common = aws.all["common_us-east-1"]
aws.core = aws.all["core_us-east-1"]
}
source = "../modules/base"
...
}
It would be nice to be able to just pass down aws.all, but alas we cannot yet.
Cardinality limitation
Just be warned that you cannot go too crazy with this mechanism. I tried to iterate through a cross-product of all AWS regions and a dozen AWS accounts and it does not go well: OpenTofu slows down to a crawl and it starts taking a dozen minutes just to instantiate all providers in a folder, before planning any resources!
This is because providers are instantiated as separate processes that OpenTofu then talks to. This model does not scale that well (and consumes a fair bit of memory), as least for the time being.
Conclusion
I absolutely love this new feature!