Terraform Multi-Region and Multi-Account AWS with Provider Aliases
One configuration, several regions or accounts #
A provider block configures one region and one set of credentials. To work with more than one, define several configurations of the same provider and give the additional ones an alias. Typical reasons:
- A certificate for CloudFront must be in
us-east-1(static website). - Disaster recovery in a second region (replicated buckets, backup copies).
- Resources in several accounts: shared services, multi-account organizations.
Aliases #
provider "aws" {
region = "eu-west-1" # default provider
}
provider "aws" {
alias = "virginia"
region = "us-east-1"
}
provider "aws" {
alias = "dr"
region = "eu-central-1"
}Resources use the default provider unless you set provider:
resource "aws_acm_certificate" "cdn" {
provider = aws.virginia
domain_name = "www.example.com"
validation_method = "DNS"
}
resource "aws_s3_bucket" "primary" {
bucket = "ditwl-data-primary"
}
resource "aws_s3_bucket" "replica" {
provider = aws.dr
bucket = "ditwl-data-replica"
}Data sources accept provider too, for example data "aws_region" "dr" { provider = aws.dr }.
Another account with assume_role #
provider "aws" {
alias = "shared"
region = "eu-west-1"
assume_role {
role_arn = "arn:aws:iam::222222222222:role/terraform"
session_name = "terraform"
}
}The identity that runs Terraform needs permission to assume that role, and the role's trust policy must allow it (IAM). In a pipeline the base credentials come from OIDC.
Passing providers to modules #
A module uses the default provider of the caller. To give it another one, or two, use providers:
module "bucket_with_replica" {
source = "./modules/replicated-bucket"
providers = {
aws = aws
aws.replica = aws.dr
}
}Inside the module, declare which configurations it expects:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
configuration_aliases = [aws.replica]
}
}
}Resources in the module then use provider = aws.replica. Do not define provider blocks inside reusable modules: let the caller pass them. See modules.
Many regions: the limitation and the solution #
Provider configurations cannot be created in a loop with for_each in Terraform, so deploying to ten regions means ten provider blocks and ten module calls. Options:
- OpenTofu 1.9 and later supports
for_eachon provider configurations, which removes the repetition:
locals {
regions = toset(["eu-west-1", "eu-central-1", "us-east-1"])
}
provider "aws" {
alias = "by_region"
for_each = local.regions
region = each.key
}
module "regional" {
source = "./modules/regional"
for_each = local.regions
providers = {
aws = aws.by_region[each.key]
}
}- Terraform needs generated code, one configuration per region (with Terragrunt or directories, see project structure), or the newer approaches that HashiCorp has introduced for multi-region. Check the documentation of your version.
State and locking #
All resources of one configuration go to a single state and backend, whatever region or account they are in. Splitting by account and environment into separate states reduces the blast radius (workspaces vs directories).
Good practices #
- Use the default provider for the main region and aliases for exceptions, with names that describe the purpose (
aws.virginia,aws.dr). - Keep global resources (IAM, Route 53, CloudFront) in a known region.
- Tag resources with their region and account for cost tracking.
- Test that all aliased providers authenticate before the first
applywithterraform plan. - Avoid destroying a provider's resources in the same run that removes the provider: Terraform needs the provider configuration to destroy them.