Skip to content
SRE & DevOpsDeep Dive Published Updated 8 min readViews unavailable

Terraform Provider Aliases: Explicit Multi-Region and Multi-Account Boundaries

Route Terraform resources and child modules to explicit provider configurations, avoiding cross-account mistakes from implicit defaults or dynamic assumptions.

One Terraform root configuration can use more than one configuration of the same provider. A team may deploy an application in two regions, manage shared DNS from a separate account, or read inventory from a central platform account while creating resources in workload accounts. Provider aliases give those configurations distinct names, and the provider meta-argument binds a resource or module call to one of them.

An alias is not a second provider binary, an independent version selection, or a dynamic account switch. It is a statically declared runtime configuration for a provider source. Terraform needs that association during planning, refresh, creation, update, and destruction. A correct design makes the intended account and region visible in the configuration and keeps the provider configuration available for as long as state still contains objects managed through it.

Separate requirements from configurations

Every module declares its provider requirements in a terraform block. That identifies the source address, local name, and acceptable versions. Provider configuration blocks supply runtime settings such as region, endpoint, and authentication. Only the root module should normally define those configurations; reusable child modules receive them from the caller.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.40, < 6.0"
    }
  }
}

provider "aws" {
  region = var.default_region
}

provider "aws" {
  alias  = "production"
  region = var.production_region
}

The unaliased block is the default configuration. Resources and data sources that omit the provider argument use the default configuration associated with their provider type. An aliased block is selected explicitly as aws.production. If a configuration declares only aliased blocks, Terraform can synthesize an empty default configuration for unqualified resources. That implicit empty configuration may fail later if the provider needs required settings, so define a deliberate default or ensure every relevant object is explicitly assigned.

Keep version constraints in required_providers, not in a provider block. The provider-block version argument is deprecated. All aliases for the same source use the version selected by the root configuration’s dependency lock process; an alias cannot pin a different plugin version.

Bind resources deliberately

Use the provider meta-argument on a resource or data source when it must use an alternate configuration. In a multi-region setup, make the choice obvious in resource addresses and code review rather than relying on the current default:

resource "aws_s3_bucket" "primary_logs" {
  provider = aws.production
  bucket   = var.production_log_bucket
}

data "aws_caller_identity" "production" {
  provider = aws.production
}

output "production_account_id" {
  value = data.aws_caller_identity.production.account_id
}

For separate accounts, prefer short-lived workload identity or provider-supported role assumption rather than long-lived access keys in configuration. The precise authentication arguments depend on the provider and execution environment. Keep credentials outside versioned .tf files, avoid printing secret values, and make the selected role, account, and region inspectable through non-secret identity data where the provider supports it.

Treat the account boundary as an invariant, not a naming convention. A resource called production can still be created in the wrong account if its provider binding is wrong. Before an apply, verify the effective caller identity and region, review the plan, and use policy controls or explicit preconditions where available to reject an unexpected target. Human-readable alias names help reviewers but do not themselves enforce an account ID.

Pass provider configurations into child modules

Child modules inherit a matching default provider configuration when the caller does not provide a mapping. An explicit providers argument is clearer when a module must use a particular account or region:

module "production_app" {
  source = "./modules/application"

  providers = {
    aws = aws.production
  }

  environment = "production"
}

Inside that child module, ordinary aws_* resources use the provider configuration received as its local aws. The child still declares its own required_providers entry, but should not define an aws provider block with embedded credentials or a fixed region. That keeps the module reusable and lets the root own account selection.

A module that genuinely needs two configurations of one provider must declare the additional local alias as a provider requirement. It then refers to the alias only for resources that belong to that other boundary:

terraform {
  required_providers {
    aws = {
      source                = "hashicorp/aws"
      version               = ">= 5.40"
      configuration_aliases = [aws.replica]
    }
  }
}

resource "aws_s3_bucket" "destination" {
  provider = aws.replica
  bucket   = var.destination_bucket_name
}

The example makes the destination bucket belong to the destination-side configuration. Do not infer provider ownership from a resource name when configuring a cross-account API: an S3 replication configuration attaches to a source bucket, so that operation belongs to the source-side provider even though it sends objects to a destination account. Check the provider documentation for the exact API boundary.

The caller maps the child module’s expected local names to root configurations:

module "replication" {
  source = "./modules/replication"

  providers = {
    aws         = aws.production
    aws.replica = aws.audit
  }
}

Provider map keys are the child module’s provider names; the values are configurations declared by the root. Document that contract in the module README and test it with a plan that demonstrates which resources bind to each side. Do not add a provider alias to a child module unless its resources really need that distinct configuration.

Aliases are static, not a loop variable

Provider associations are resolved statically. You cannot compute a provider argument from a resource attribute or a for_each key, and a module’s providers mapping cannot select a different provider configuration for each module instance. A module using for_each can receive one fixed provider mapping for all instances in that block, not a map that changes the AWS account per key.

When two deployments require distinct accounts, use separate module blocks with explicit mappings, or split the deployments into separate root configurations and state boundaries. If the number of accounts is large and generated from inventory, consider whether a higher-level orchestration system should invoke a reviewed root configuration once per account instead of trying to make provider selection dynamic inside one Terraform graph. The static restriction is a safety property: Terraform can associate each managed object with the configuration it needs for refresh and teardown.

Aliases also do not create separate state. Two aliases in one root configuration still participate in the same plan and state snapshot. That may be appropriate for tightly coupled cross-account resources, but it means state access and apply authority span both environments. Separate roots and state when ownership, credentials, approval paths, or blast radius must be isolated. Avoid using workspaces as a substitute for a security boundary when the account credentials and state access are still shared.

Keep configurations through object teardown

Terraform records which provider configuration was most recently used for each object. It needs that configuration not only to create an object but also to refresh, update, and destroy it. Removing an alias while objects still depend on it can make planning fail because the state refers to a configuration that no longer exists.

For an account migration, do not delete the old alias immediately after changing the module mapping. First plan and apply the migration using a configuration that still provides both old and new provider aliases. Confirm that old objects have been destroyed or intentionally detached from management, then remove the obsolete provider configuration in a later change. For a region or account move that requires replacement, make the expected destruction and creation explicit in the plan and verify data retention, DNS, and service cutover separately.

Do not assume that renaming an alias moves an object to a different account. Alias names are configuration addresses; changing a mapping may cause Terraform to refresh or manage an object with a different identity than intended. Review provider changes alongside resource addresses, state bindings, and the plan. A gradual migration with a saved state backup and one authoritative apply owner is safer than editing aliases and state at the same time.

Review checklist for multi-target plans

Before applying, check that every resource and module has the intended provider mapping, that default-provider behavior is explicit, and that provider aliases are defined in the root where their authentication is controlled. Verify the account identity, region, API endpoint, and role through the provider’s supported identity mechanisms. Confirm that no secret appears in the diff or plan output, then inspect planned creates, replacements, and destroys against the target environment.

In code review, ask whether aliases represent durable operational boundaries or merely duplicate declarations. Prefer a small number of meaningful, documented names such as production, shared_dns, or replica, and avoid ambiguous names like provider2. Test reusable modules with explicit provider mappings, validate from a clean checkout, and preserve the old configuration until state no longer needs it. This keeps cross-region and cross-account Terraform understandable at the exact point when an incorrect target would be expensive.

Related:

Sources:

Comments