When managing your AWS infrastructure with Terraform, it is surprisingly hard to create resources in all enabled regions, including opt-in regions like eu-south-1.
With Terraform 1.2.1, it is not possible to do this in a single apply
phase:
- Identify all enabled regions, including opt-in regions.
- Create a resource in each enabled region.
Use Case
It's a good idea to put a few guardrails in place when setting up new AWS accounts. For example, AWS has an option to enable default encryption for all new EBS volumes. Terraform can manage this setting with the aws_ebs_encryption_by_default resource, but the setting only applies to a single region, so it must be enabled one region at a time.
Get All Regions
It is deceptively simple to identify all enabled regions. This works great.
data "aws_regions" "enabled" {
all_regions = true
filter {
name = "opt-in-status"
values = ["opt-in-not-required", "not-opted-in"]
}
}
Approaches
Looping with meta-arguments
๐โโ๏ธ Oh, that's easy. I'll just repeat the resource with for_each.
resource "aws_ebs_encryption_by_default" "example" {
for_each = toset(data.aws_regions.enabled.names)
provider = "aws.${each.value}"
enabled = true
}
๐ฅ A wild error appears! We can't use string interpolation to pick the provider.
Error: Invalid provider configuration reference
on regions.tf line 3, in resource "aws_ebs_encryption_by_default" "example":
3: provider = "aws.${each.value}"
A provider configuration reference must not be given in quotes.
Note: it's also impossible to declare providers with for_each
.
Provider inside Module
๐คจ Okay, I'll just declare the provider inside a module, then loop over the module.
I'll create a new module
directory with provider.tf
like this:
provider "aws" {
region = var.region
assume_role {
role_arn = "arn:aws:iam::${var.account_id}:role/OrganizationAccountAccessRole"
}
}
And then I'll use for_each
with that module.
module "regions" {
for_each = toset(data.aws_regions.enabled.names)
source = "./region"
}
๐ฅ Another error! Modules can either have internal providers, or be used with for_each
, but not both.
โ Error: Module is incompatible with count, for_each, and depends_on
โ
โ on regions.tf line 19, in module "regions":
โ 19: for_each = toset(data.aws_regions.enabled.names)
โ
โ The module at module.regions is a legacy module which contains its own local provider configurations, and so calls to it may not use the count, for_each, or depends_on arguments.
โ
โ If you also control the module "./region", consider updating this module to instead expect provider configurations to be passed by its caller.
Copy/paste provider blocks
๐ Fine, I'll just copy/paste a provider block for all possible regions.
provider "aws" {
alias = "us-east-1"
region = "us-east-1"
assume_role {
role_arn = "arn:aws:iam::${var.account_id}:role/OrganizationAccountAccessRole"
}
}
provider "aws" {
alias = "af-south-1"
region = "af-south-1"
assume_role {
role_arn = "arn:aws:iam::${var.account_id}:role/OrganizationAccountAccessRole"
}
}
# {etc.}
๐ฅ No. If you declare a provider block for a region that is not-opted-in
then you'll be blocked from using STS in that region, so Terraform won't be able to get a token and it will error out.
Error: error configuring Terraform AWS Provider: IAM Role (arn:aws:iam::12345556789:role/OrganizationAccountAccessRole) cannot be assumed.
There are a number of possible causes of this - the most common are:
* The credentials used in order to assume the role are invalid
* The credentials do not have appropriate permission to assume the role
* The role ARN is not valid
Error: operation error STS: AssumeRole, https response error StatusCode: 403, RequestID: d089d6a7-377e-4633-b36a-037108f32cb4, api error InvalidClientTokenId: The security token included in the request is invalid.
with provider["registry.terraform.io/hashicorp/aws"].af-south-1,
on test.tf line 9, in provider "aws":
9: provider "aws" {
Bash script
๐ Ugh fine, I'll just write a bash script.
% aws ec2 describe-regions --all-regions \
--filters "Name=opt-in-status,Values=not-opted-in,opt-in-not-required" \
--query 'Regions[].RegionName[]' --output text |
xargs -n1 -I% terraform apply -var="region=%"
Or just ignore opt-in regions
If you don't need to include opt-in regions, this problem actually disappears. Just create provider
blocks for all of the standard regions.
- 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