TerraformVI · Providers and the Provider EcosystemProduction Terraform
Provider Aliases for Multi-Region and Multi-Account
What you'll learn
- Add an `alias` to a `provider` block and explain what it does
- Reference an aliased provider from a resource using `provider = aws.west`
- Recognise when aliases are appropriate: multi-region, multi-account, multi-role
- Apply the per-resource provider pattern to keep the binding explicit
- Diagnose the common alias failure modes
Prerequisites
None — start here.
Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13
A single Terraform configuration sometimes needs to talk to
more than one region, more than one AWS account, or more than
one role. The mechanism is the provider alias: two or more
instances of the same provider, each with its own
configuration. The lesson teaches the alias syntax, when to
reach for it, and the discipline that keeps the bindings
explicit.
The default provider
A configuration with one provider block has one default
provider:
provider "aws" {
region = "eu-west-2"
}
resource "aws_s3_bucket" "logs" {
bucket = "acme-prod-logs"
}
The resource uses the default provider implicitly. There is nothing to bind. This is the common case for single-region, single-account estates.
Adding an aliased provider
The moment a second region or a second account enters the
configuration, you declare a second provider with an
alias:
provider "aws" {
region = "eu-west-2"
alias = "primary"
}
provider "aws" {
region = "us-west-2"
alias = "west"
}
provider "aws" {
region = "us-west-2"
alias = "west_readonly"
assume_role {
role_arn = "arn:aws:iam::222222222222:role/TerraformReadOnly"
}
}
resource "aws_s3_bucket" "logs" {
provider = aws.primary
bucket = "acme-prod-logs-eu"
}
resource "aws_cloudwatch_log_group" "us_audit" {
provider = aws.west
name = "/acme/audit"
}
data "aws_caller_identity" "west_readonly" {
provider = aws.west_readonly
}
Three things to notice:
aliasnames the instance.primary,west,west_readonlyare local labels. They are referenced from resources via<TYPE>.<ALIAS>.aliasis required on every additional block. You can leave one block unaliased and that one is the default. Or you can alias every block and reference each by name. The latter is clearer in code review.provider = aws.westbinds the resource. The meta-argumentproviderselects which instance the resource uses.
When aliases are appropriate
Four production patterns justify aliases:
1. Multi-region. The estate spans eu-west-2 and
us-west-2. An S3 bucket lives in eu-west-2; a
CloudWatch log group lives in us-west-2. Two providers,
two aliases, two resources, explicit binding.
2. Multi-account. Production lives in account
111111111111; staging lives in 222222222222. Each
account has its own role, its own credentials, its own
state. Two providers, one per account, with the
assume_role block on each.
3. Multi-role. A read-only role for data sources and a read-write role for resources. Two providers, distinct alias names, explicit binding on every resource. The read-only role can never accidentally create a resource because no resource references it.
4. Multi-tenant SaaS. The provider is one, but each customer has a separate configuration. Inside a module, an alias lets the module target a specific tenant without spreading provider state across the rest of the configuration.
A pattern that does not justify aliases: changing the region of a single resource in-place. If only one resource needs a different region, an alias is appropriate. If every resource should be in a single region, set the default provider’s region and stop.
The per-resource binding discipline
There are two ways to bind a resource to a provider:
# Approach 1: parent module block (legacy).
module "network" {
source = "./modules/network"
providers = {
aws = aws.west
}
}
# Approach 2: per-resource `provider` meta-argument.
resource "aws_vpc" "this" {
provider = aws.west
cidr_block = "10.0.0.0/16"
}
Approach 2 is the production default. It keeps the binding visible at the resource site, which is where the reader is looking. Approach 1 hides the binding inside a module call and forces the reader to remember which provider the module inherited.
A common pattern in a multi-region estate:
provider "aws" {
alias = "primary"
region = "eu-west-2"
}
provider "aws" {
alias = "west"
region = "us-west-2"
}
# Most resources use the primary (default).
resource "aws_s3_bucket" "primary" {
bucket = "acme-primary"
}
# A few resources target the west region explicitly.
resource "aws_s3_bucket" "replica" {
provider = aws.west
bucket = "acme-west"
}
Production failure modes
Five failure modes recur:
1. Resource does not declare a provider and no default
exists. Two provider "aws" blocks, both with alias,
and a resource with no provider = line. Terraform errors:
“Multiple provider instances for aws”. Fix: declare an
unaliased provider "aws" { ... } for the default, or add
provider = aws.<alias> to every resource.
2. Two providers with the same alias. Two provider "aws" blocks both with alias = "west". Terraform errors:
“Duplicate provider alias”. Fix: pick distinct alias names.
3. Resource references an alias that does not exist. A
resource uses provider = aws.south but no provider has
alias = "south". Terraform errors: “Invalid provider
reference”. Fix: declare the aliased provider or correct the
typo.
4. Wrong account on assume_role. The aliased provider
has the right region but the wrong role ARN. The plan
succeeds, the apply fails with AccessDenied. Fix: confirm
the role ARN and the trust policy.
5. Mixing unaliased and aliased without intent. A
configuration has an unaliased provider "aws" for us-east-1
and an aliased aws.west for us-west-2. Most resources use
the default. A new resource is added without provider =,
inherits the default, and lands in us-east-1. The reviewer
expects us-west-2. Fix: alias every provider and reference
each explicitly.
Operational guidance
For a production estate:
- Alias every provider, or none. If you have more than one instance of a provider, alias every instance and bind every resource explicitly. It is more typing but the binding is visible.
- Keep the alias names descriptive.
west,staging,readonly,breakglassbeataws1,aws_secondary,r. - Put the aliased providers in their own file.
providers.tfwith allproviderblocks; the rest of the configuration has resources, data sources, and modules. Easier to audit. - Pair aliases with separate state. A multi-region or multi-account estate almost always wants separate state files. The provider alias is the in-config signal; the state file is the runtime boundary.
What comes next
The next lesson is on provider authentication — how the provider actually authenticates, and how to keep credentials out of Git.
Verification
Knowledge check · 6 questions
Q1. What does the `alias` argument on a `provider` block do?
Q2. How is an aliased provider bound to a resource?
Q3. A configuration can mix one unaliased provider and one aliased provider in the same file.
Q4. A configuration has two `provider "aws"` blocks, both with `alias`. A resource references neither. What happens?
Q5. Which of the following are appropriate uses of provider aliases? (Select all that apply.)
Q6. A team operates in `eu-west-2` and `us-west-2`. A new resource is added without a `provider =` argument and lands in `eu-west-2`. The team expected `us-west-2`. What is the production-default mitigation?
Passing score: 75%. Answers are checked in this browser.