Skip to main content
RunBook Academy

TerraformVI · Providers and the Provider EcosystemProduction Terraform

Provider Aliases for Multi-Region and Multi-Account

Intermediate⏱ ~12 minbash

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

Not yet marked complete on this device.

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:

  • alias names the instance. primary, west, west_readonly are local labels. They are referenced from resources via <TYPE>.<ALIAS>.
  • alias is 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.west binds the resource. The meta-argument provider selects 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, breakglass beat aws1, aws_secondary, r.
  • Put the aliased providers in their own file. providers.tf with all provider blocks; 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

  1. Q1. What does the `alias` argument on a `provider` block do?

  2. Q2. How is an aliased provider bound to a resource?

  3. Q3. A configuration can mix one unaliased provider and one aliased provider in the same file.

  4. Q4. A configuration has two `provider "aws"` blocks, both with `alias`. A resource references neither. What happens?

  5. Q5. Which of the following are appropriate uses of provider aliases? (Select all that apply.)

  6. 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.