Skip to content
Infrastructure as Code iac providers 4 min read

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 provider argument. 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]
    }
  }
}
ArgumentWhere it livesPurpose
aliasprovider blockNames an additional provider configuration
providerresource / data sourceSelects which configuration to use
providersmodule callMaps parent configs onto a child module’s slots
configuration_aliasesmodule’s required_providersDeclares extra configs the module requires

Warning: Never set credentials or region directly inside a reusable module’s provider block. 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 than aws.alias2.
  • Declare configuration_aliases in 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 providers map 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.
Last updated June 14, 2026
Was this helpful?