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!