Multiple Providers & Aliases
A single provider block configures one instance of a provider with one set of settings — one AWS region, one Azure subscription, one GCP project. Real infrastructure rarely fits in that single instance: you replicate buckets across regions, attach a CloudFront distribution to an ACM certificate that must live in us-east-1, or manage resources in a separate audit account. Provider aliases let you declare several configurations of the same provider in one module and point each resource (or module call) at the configuration it needs. This works identically in Terraform 1.5+ and OpenTofu.
The default provider configuration
Every provider block without an alias argument is the default configuration for that provider. Resources of that type use it automatically unless told otherwise.
provider "aws" {
region = "us-east-1"
}
resource "aws_s3_bucket" "primary" {
bucket = "devcraftly-primary"
}
The bucket above implicitly uses the default aws provider, so it is created in us-east-1.
Adding aliased configurations
To configure the same provider a second time, add a block with the same name plus an alias. The alias gives that configuration an addressable name in the form <provider>.<alias>.
provider "aws" {
region = "us-east-1" # default
}
provider "aws" {
alias = "west"
region = "us-west-2"
}
provider "aws" {
alias = "eu"
region = "eu-central-1"
}
You now have three AWS configurations: the default plus aws.west and aws.eu.
Selecting a configuration on a resource
A resource opts into a non-default configuration with the meta-argument provider, set to the <provider>.<alias> reference (no quotes — it is an expression, not a string).
resource "aws_s3_bucket" "us_east" {
bucket = "devcraftly-use1"
# default provider -> us-east-1
}
resource "aws_s3_bucket" "us_west" {
provider = aws.west
bucket = "devcraftly-usw2"
}
resource "aws_s3_bucket" "europe" {
provider = aws.eu
bucket = "devcraftly-euc1"
}
A terraform apply creates each bucket in its respective region:
Output:
aws_s3_bucket.us_east: Creating...
aws_s3_bucket.us_west: Creating...
aws_s3_bucket.europe: Creating...
aws_s3_bucket.us_east: Creation complete after 2s [id=devcraftly-use1]
aws_s3_bucket.us_west: Creation complete after 3s [id=devcraftly-usw2]
aws_s3_bucket.europe: Creation complete after 3s [id=devcraftly-euc1]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Tip: Data sources accept the same
providerargument. Use it to read state from a different region or account — e.g.data "aws_availability_zones" "west" { provider = aws.west }.
A worked example: CloudFront + ACM
A classic case where aliases are mandatory: AWS CloudFront only accepts ACM certificates issued in us-east-1, even when the rest of your stack lives elsewhere. Declare a default provider for your primary region and an aliased provider pinned to us-east-1 for the certificate.
provider "aws" {
region = "eu-west-1" # primary region for app resources
}
provider "aws" {
alias = "use1"
region = "us-east-1" # required for CloudFront/ACM
}
resource "aws_acm_certificate" "cdn" {
provider = aws.use1
domain_name = "cdn.devcraftly.com"
validation_method = "DNS"
}
resource "aws_cloudfront_distribution" "site" {
enabled = true
default_root_object = "index.html"
origin {
domain_name = "origin.devcraftly.com"
origin_id = "primary"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
}
}
default_cache_behavior {
target_origin_id = "primary"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.cdn.arn
ssl_support_method = "sni-only"
}
}
The certificate is created in us-east-1 via aws.use1, while the (global) CloudFront distribution references its ARN — no second root module required.
Passing aliased providers into modules
Child modules don’t inherit aliased configurations automatically; you pass them explicitly through a providers map in the module call. Inside the module, declare which aliases it expects with configuration_aliases.
# root main.tf
module "replica" {
source = "./modules/bucket"
providers = {
aws = aws.west # map the module's default "aws" to our west alias
}
}
# modules/bucket/versions.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
configuration_aliases = [aws.replica]
}
}
}
| Argument | Where it lives | Purpose |
|---|---|---|
alias | provider block | Names an additional provider configuration |
provider | resource / data source | Selects which configuration to use |
providers | module call | Maps parent configs onto a child module’s slots |
configuration_aliases | module’s required_providers | Declares extra configs the module requires |
Warning: Never set credentials or
regiondirectly inside a reusable module’sproviderblock. Pass configured providers in from the root instead — this keeps modules portable and avoids hidden, hard-to-override defaults.
Best practices
- Keep one clearly-named default provider and reserve aliases for genuine multi-region or multi-account needs.
- Name aliases for meaning, not coincidence (
aws.audit,aws.dr) rather thanaws.alias2. - Declare
configuration_aliasesin modules so callers get an explicit error when a required provider is missing, instead of a silent fallback. - Pin every provider with
required_providers(see below) so aliased configs all resolve to the same version. - Avoid passing the default provider implicitly into modules that also need aliases — be explicit in the
providersmap to prevent surprises. - Remember that destroying an aliased resource still requires its provider configuration to exist; don’t delete the alias block before the resources it manages.