What is Multi Provider? #
Most Terraform tutorials start with a single provider — usually AWS or GCP. But real infrastructure is rarely that simple. You might need to create resources in two AWS regions at once for disaster recovery, manage DNS on Cloudflare while the servers are on AWS, or create a Kubernetes cluster on GCP while storing secrets in HashiCorp Vault. Multi-provider is Terraform’s ability to manage all of this in a single configuration, with proper coordination between resources from different sources.
flowchart TD
A["Terraform\nConfig"] --> B["AWS\nap-southeast-1"]
A --> C["AWS\nus-east-1"]
A --> D["Cloudflare"]
A --> E["Vault"]
B --> F["EC2, RDS, S3"]
C --> G["DR Resources"]
D --> H["DNS Records"]
E --> I["Secrets"]
style A fill:#3b82f6,stroke:#1e40af,color:#fff
style B fill:#f59e0b,stroke:#d97706,color:#fff
style C fill:#f59e0b,stroke:#d97706,color:#fff
style D fill:#ef4444,stroke:#dc2626,color:#fff
style E fill:#8b5cf6,stroke:#6d28d9,color:#fffBasic Provider Configuration #
By default, one provider block is enough for a single deployment. The default provider doesn’t need an alias.
# Single provider — the most common
provider "aws" {
region = "ap-southeast-1"
}
# All resources in .tf files use this provider automatically
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
Provider Aliases: The Foundation of Multi-Provider #
An alias is a mechanism that gives an extra name to a provider configuration. With aliases, you can have multiple instances of the same provider — for example, AWS in two different regions — or several different providers in one configuration.
# AWS provider for the primary region (no alias = the default provider)
provider "aws" {
region = "ap-southeast-1"
}
# AWS provider for the disaster recovery region (with an alias)
provider "aws" {
alias = "us_east"
region = "us-east-1"
}
# A provider for DNS (a completely different provider)
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
# A resource using the default provider (no need to mention the provider)
resource "aws_vpc" "primary" {
provider = aws # Optional — this is the default
cidr_block = "10.0.0.0/16"
}
# A resource using a provider with an alias
resource "aws_vpc" "dr" {
provider = aws.us_east # Format: <type>.<alias>
cidr_block = "10.1.0.0/16"
}
# A resource from a different provider
resource "cloudflare_record" "app" {
zone_id = var.cloudflare_zone_id
name = "app"
value = aws_lb.main.dns_name
type = "CNAME"
}
Common Multi-Provider Use Cases #
1. MULTI-REGION (AWS)
Reason: Disaster recovery, latency reduction, data residency
Pattern: A provider alias per region
2. MULTI-ACCOUNT (AWS)
Reason: Environment isolation, billing separation
Pattern: A provider alias per account with different assume_role
3. MULTI-CLOUD
Reason: Best-of-breed services, vendor lock-in mitigation
Pattern: Different providers (aws + google + azurerm)
4. CLOUD + SaaS
Reason: DNS on Cloudflare, monitoring on Datadog, secrets in Vault
Pattern: A cloud provider + SaaS/tool providers
5. KUBERNETES
Reason: Deploying to k8s while provisioning its infrastructure
Pattern: The aws/gcp provider + kubernetes/helm providers
Multi-Region: Disaster Recovery #
One of the most common multi-provider use cases is replicating resources to a second region for disaster recovery.
provider "aws" {
region = "ap-southeast-1" # Primary region — Singapore
}
provider "aws" {
alias = "dr"
region = "ap-northeast-1" # DR region — Tokyo
}
# Primary database in Singapore
resource "aws_db_instance" "primary" {
identifier = "production-db-primary"
engine = "postgres"
instance_class = "db.r5.large"
# ...
backup_retention_period = 7
}
# Read replica in Tokyo for DR
resource "aws_db_instance" "replica" {
provider = aws.dr # This resource is created in the Tokyo region
identifier = "production-db-replica"
replicate_source_db = aws_db_instance.primary.arn
instance_class = "db.r5.large"
}
# S3 bucket in Tokyo for backup storage
resource "aws_s3_bucket" "dr_backup" {
provider = aws.dr
bucket = "production-dr-backup-ap-northeast-1"
}
Multi-Account: Environment Isolation #
With multi-account, every environment lives in a separate AWS account. The provider alias determines which account a resource is created in.
# Provider for the production account
provider "aws" {
alias = "production"
region = "ap-southeast-1"
assume_role {
role_arn = "arn:aws:iam::333333333:role/TerraformDeployRole"
}
}
# Provider for the shared services account (DNS, artifact registry, etc.)
provider "aws" {
alias = "shared"
region = "ap-southeast-1"
assume_role {
role_arn = "arn:aws:iam::444444444:role/TerraformReadRole"
}
}
# Resource in the production account
resource "aws_instance" "app" {
provider = aws.production
ami = data.aws_ami.ubuntu.id
instance_type = "t3.medium"
}
# Read the hosted zone from the shared account
data "aws_route53_zone" "main" {
provider = aws.shared
name = "myapp.com"
}
# Create a DNS record in the shared account pointing at a production resource
resource "aws_route53_record" "app" {
provider = aws.shared
zone_id = data.aws_route53_zone.main.zone_id
name = "app.myapp.com"
type = "A"
alias {
name = aws_lb.main.dns_name
zone_id = aws_lb.main.zone_id
evaluate_target_health = true
}
}
Multi-Cloud: AWS + Cloudflare #
provider "aws" {
region = "ap-southeast-1"
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
# Infrastructure on AWS
resource "aws_lb" "main" {
name = "production-lb"
internal = false
load_balancer_type = "application"
subnets = module.vpc.public_subnet_ids
}
# DNS on Cloudflare pointing at the AWS load balancer
resource "cloudflare_record" "app" {
zone_id = var.cloudflare_zone_id
name = "app"
value = aws_lb.main.dns_name # Reference to an AWS resource
type = "CNAME"
proxied = true # Enable the Cloudflare proxy (WAF, DDoS protection)
ttl = 1 # Auto TTL when proxied
}
flowchart LR
A["AWS LB"] -->|"dns_name"| B["Cloudflare\nCNAME Record"]
C["AWS RDS\nPrimary"] -->|"arn"| D["AWS RDS\nReplica"]
style A fill:#f59e0b,stroke:#d97706,color:#fff
style B fill:#ef4444,stroke:#dc2626,color:#fff
style C fill:#10b981,stroke:#059669,color:#fff
style D fill:#10b981,stroke:#059669,color:#fffProviders Inside Modules #
When a module needs a specific provider (not the default), there are two ways to pass it: implicitly (the module inherits the default provider) or explicitly (the root module passes a provider alias to the module).
# The explicit way: the root module passes provider aliases to a child module
# modules/replication/main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
configuration_aliases = [aws.primary, aws.replica]
# This module needs TWO aws configurations: primary and replica
}
}
}
# The root module passes provider aliases to the module
module "replication" {
source = "./modules/replication"
providers = {
aws.primary = aws # The default provider becomes aws.primary in the module
aws.replica = aws.us_east # The alias provider becomes aws.replica in the module
}
}
Provider Aliasing for Multi-Region #
Aliasing lets one configuration use several instances of the same provider with different configurations.
# The default provider
provider "aws" {
region = "ap-southeast-1"
}
# A provider alias for another region
provider "aws" {
alias = "us_east"
region = "us-east-1"
}
# A provider alias for another account
provider "aws" {
alias = "shared_services"
region = "ap-southeast-1"
assume_role {
role_arn = "arn:aws:iam::999888777666:role/CrossAccountRole"
}
}
# Using a specific provider
resource "aws_s3_bucket" "logs" {
provider = aws.us_east
bucket = "my-logs-us-east"
}
resource "aws_s3_bucket" "shared" {
provider = aws.shared_services
bucket = "shared-artifacts"
}
flowchart TD
A["Root Config"] --> B["aws (default)\nap-southeast-1"]
A --> C["aws.us_east\nus-east-1"]
A --> D["aws.shared_services\n999888777666"]
B --> E["Primary\nresources"]
C --> F["Cross-region\nreplication"]
D --> G["Shared\nservices"]
style B fill:#e8f5e9,stroke:#2e7d32
style C fill:#e3f2fd,stroke:#1565c0
style D fill:#fff3e0,stroke:#e65100Provider Version Constraints #
Each provider has a version that should be locked for consistency.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
datadog = {
source = "datadog/datadog-aws"
version = "~> 3.0"
}
}
required_version = ">= 1.6.0"
}
# Lock file for cross-platform consistency
# .terraform.lock.hcl is committed to Git
terraform providers lock -platform=linux_amd64 -platform=darwin_arm64 -platform=windows_amd64
# Update the provider to the latest version
terraform init -upgrade
# Review the changes in .terraform.lock.hcl before committing
Multi-Provider Use Cases #
COMMON MULTI-PROVIDER SCENARIOS:
1. AWS + Cloudflare: DNS management
- AWS: compute, storage, networking
- Cloudflare: DNS records, CDN, WAF
2. AWS + Datadog: monitoring
- AWS: infrastructure
- Datadog: dashboards, alerts, APM
3. AWS + PagerDuty: incident management
- AWS: cloud resources
- PagerDuty: escalation policies, schedules
4. AWS + GitHub: CI/CD + infrastructure
- AWS: cloud resources
- GitHub: repositories, teams, webhooks
5. AWS + Kubernetes: container orchestration
- AWS: EKS cluster, networking
- Kubernetes: deployments, services, ingress
Provider Inheritance #
# Provider configuration in the root module
# Child modules automatically inherit the provider from the root
# Root module:
provider "aws" {
region = "ap-southeast-1"
default_tags {
tags = {
Project = "my-project"
ManagedBy = "terraform"
}
}
}
# The child module automatically uses the provider above
# NO provider configuration needed in the child module
# For alias providers, you must pass them explicitly:
module "us_east_resources" {
source = "./modules/regional"
providers = {
aws = aws.us_east
}
}
Summary #
- Provider aliases are the basic mechanism of multi-provider — giving extra names to provider configurations so resources can use them explicitly.
- Alias usage format:
provider = <type>.<alias>inside a resource block.- A provider without an alias is the default — resources that don’t mention
providerexplicitly use the default provider.- Four main use cases: multi-region (DR), multi-account (isolation), multi-cloud, and cloud + SaaS combinations (Cloudflare, Datadog, Vault).
- Resources from different providers can reference each other —
cloudflare_record.app.value = aws_lb.main.dns_nameis a normal and valid pattern.- Modules needing specific provider aliases must define
configuration_aliasesinrequired_providersand receive the provider via aprovidersblock when called.
← Previous: Directory Based Next: Cross Provider Reference →