Nic Waller
Nic Waller

Nic Waller

Terraform every AWS Region

Nic Waller's photo
Nic Waller
ยทMay 28, 2022ยท

4 min read

Terraform every AWS Region

Photo by Brett Zeck on Unsplash

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:

  1. Identify all enabled regions, including opt-in regions.
  2. 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
ย 
Share this