Cross Provider Reference #
When you manage infrastructure with several providers at once, resources from different providers almost certainly need to know about each other. An AWS load balancer needs to know which Cloudflare domain will be pointed at it. A Kubernetes deployment needs to know the database endpoint just created on RDS. A secret in Vault needs to be filled with a password generated by AWS. Cross-provider references are how Terraform expresses these dependencies, and understanding them helps you build cohesive multi-provider configurations.
How Cross Provider References Work #
Cross-provider references work exactly like regular references — you simply use the resource address from the other provider. Terraform automatically builds a dependency graph that crosses provider boundaries.
provider "aws" {
region = "ap-southeast-1"
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
# A resource from the AWS provider
resource "aws_lb" "main" {
name = "production-lb"
internal = false
load_balancer_type = "application"
subnets = module.vpc.public_subnet_ids
}
# A resource from the Cloudflare provider referencing an AWS resource
resource "cloudflare_record" "app" {
zone_id = var.cloudflare_zone_id
name = "app"
value = aws_lb.main.dns_name # ← Cross-provider reference
type = "CNAME"
proxied = true
}
# The dependency graph formed:
# aws_lb.main → cloudflare_record.app
# (cloudflare_record.app can't be created before aws_lb.main finishes)
Common Cross Provider Reference Patterns #
AWS + Cloudflare: Automatic DNS #
# SSL certificate on AWS ACM
resource "aws_acm_certificate" "main" {
domain_name = "myapp.com"
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
}
# DNS record for certificate validation — on Cloudflare
resource "cloudflare_record" "acm_validation" {
for_each = {
for dvo in aws_acm_certificate.main.domain_validation_options :
dvo.domain_name => {
name = dvo.resource_record_name
value = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = var.cloudflare_zone_id
name = each.value.name
value = each.value.value
type = each.value.type
proxied = false # Not proxied for validation
ttl = 60
}
# Wait until the certificate is validated
resource "aws_acm_certificate_validation" "main" {
certificate_arn = aws_acm_certificate.main.arn
validation_record_fqdns = [for record in cloudflare_record.acm_validation : record.hostname]
}
AWS + Kubernetes: Deploying Applications to EKS #
provider "aws" {
region = "ap-southeast-1"
}
provider "kubernetes" {
host = aws_eks_cluster.main.endpoint
cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data)
exec {
api_version = "client.authentication.k8s.io/v1beta1"
command = "aws"
args = ["eks", "get-token", "--cluster-name", aws_eks_cluster.main.name]
}
}
# EKS cluster from the AWS provider
resource "aws_eks_cluster" "main" {
name = "production-eks"
role_arn = aws_iam_role.eks_cluster.arn
vpc_config {
subnet_ids = module.vpc.private_subnet_ids
}
}
# Kubernetes namespace from the Kubernetes provider
# (depends on the newly created EKS cluster)
resource "kubernetes_namespace" "app" {
metadata {
name = "production"
}
# Explicit depends_on because the kubernetes provider depends
# on the newly created EKS cluster
depends_on = [aws_eks_cluster.main]
}
# ConfigMap with the database endpoint from AWS RDS
resource "kubernetes_config_map" "app_config" {
metadata {
name = "app-config"
namespace = kubernetes_namespace.app.metadata[0].name
}
data = {
DB_HOST = aws_db_instance.main.address # Cross-provider reference
DB_PORT = aws_db_instance.main.port
DB_NAME = aws_db_instance.main.db_name
}
}
AWS + HashiCorp Vault: Secret Management #
provider "aws" {
region = "ap-southeast-1"
}
provider "vault" {
address = "https://vault.mycompany.com"
}
# Database password generated by Vault
resource "vault_database_secret_backend_role" "app" {
name = "app-role"
backend = vault_database_secret_backend.main.path
db_name = vault_database_secret_backend_connection.rds.name
}
# RDS using the endpoint that will be configured into Vault
resource "aws_db_instance" "main" {
identifier = "production-db"
engine = "postgres"
instance_class = "db.t3.medium"
}
# Vault configuration for the database connection
resource "vault_database_secret_backend_connection" "rds" {
backend = vault_database_secret_backend.main.path
name = "rds-postgres"
postgresql {
connection_url = "postgresql://{{username}}:{{password}}@${aws_db_instance.main.endpoint}/${aws_db_instance.main.db_name}"
# Cross-provider reference: use the RDS endpoint from AWS
}
}
Common Problems and Solutions #
# PROBLEM 1: The Kubernetes provider is configured before the cluster exists
# Error: "no such host" or connection refused
# ANTI-PATTERN: The kubernetes provider configured in a terraform block
# without depends_on — terraform tries to connect at init time
provider "kubernetes" {
host = aws_eks_cluster.main.endpoint # ✗ May not exist yet when the provider initializes
}
# CORRECT: Use lazy exec-based authentication (only connects when needed)
provider "kubernetes" {
host = aws_eks_cluster.main.endpoint
cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data)
exec {
api_version = "client.authentication.k8s.io/v1beta1"
command = "aws"
args = ["eks", "get-token", "--cluster-name", aws_eks_cluster.main.name]
}
# exec-based auth only runs when a kubernetes resource is actually created
}
# PROBLEM 2: Race conditions between providers
# Resource B from provider B is created before resource A from provider A finishes
# even though there's a reference
# CORRECT: Make sure the reference is enough, or add depends_on
resource "cloudflare_record" "app" {
value = aws_lb.main.dns_name # The reference is enough for create ordering
# For more complex cases (invisible side effects):
depends_on = [aws_lb_listener.https]
}
# PROBLEM 3: A "known after apply" value from one provider
# is needed to configure another provider
# This is a limitation that needs a workaround
# (usually split into two separate terraform applies,
# or use a data source to read after the resource is created)
The Cross-Provider Dependency Graph #
# Terraform builds a dependency graph that accounts for
# all cross-provider references
terraform graph | dot -Tsvg > graph.svg
# The graph will show:
# [aws provider] aws_lb.main
# │
# └──────────────────────────────────┐
# ▼
# [cloudflare provider] cloudflare_record.app
#
# Terraform understands that cloudflare_record.app must be created
# AFTER aws_lb.main — even though both are managed by different providers
Cross References Between AWS and Other Providers #
Terraform can combine resources from various providers in one configuration.
# Example: AWS EC2 + Cloudflare DNS
resource "aws_instance" "web" {
ami = "ami-12345"
instance_type = "t3.micro"
}
resource "cloudflare_record" "web" {
zone_id = var.cloudflare_zone_id
name = "app"
value = aws_instance.web.public_ip # Reference to AWS
type = "A"
ttl = 300
}
# Example: AWS + Datadog monitoring
resource "datadog_monitor" "cpu" {
name = "High CPU - ${aws_instance.web.id}"
type = "metric alert"
query = "avg(last_5m):avg:system.cpu.user{host:${aws_instance.web.id}} > 80"
message = "High CPU on ${aws_instance.web.public_ip}"
}
flowchart LR
A["AWS EC2\ninstance"] -->|"public_ip"| B["Cloudflare\nDNS record"]
A -->|"instance_id"| C["Datadog\nmonitor"]
A -->|"public_ip"| D["PagerDuty\nservice"]
style A fill:#fff3e0,stroke:#e65100
style B fill:#f3e5f5,stroke:#6a1b9a
style C fill:#e8f5e9,stroke:#2e7d32
style D fill:#fce4ec,stroke:#c62828Terraform Provider Dependency Graph #
When using multiple providers, Terraform must download and initialize all of them.
# Initialize all providers
terraform init
# Output:
# - Installing hashicorp/aws v5.30.0...
# - Installing cloudflare/cloudflare v4.20.0...
# - Installing datadog/datadog-aws v3.33.0...
# Terraform has been successfully initialized!
# Check the required providers
terraform providers
# ├── provider[registry.terraform.io/hashicorp/aws]
# ├── provider[registry.terraform.io/cloudflare/cloudflare]
# └── provider[registry.terraform.io/datadog/datadog-aws]
# Lock provider versions
terraform providers lock -platform=linux_amd64 -platform=darwin_arm64
# Generates .terraform.lock.hcl for cross-platform consistency
flowchart TD
A["terraform init"] --> B["Download AWS\nprovider"]
A --> C["Download Cloudflare\nprovider"]
A --> D["Download Datadog\nprovider"]
B --> E["Initialize\nprovider"]
C --> E
D --> E
E --> F["Ready to\nplan/apply"]
style A fill:#e3f2fd,stroke:#1565c0
style F fill:#e8f5e9,stroke:#2e7d32Provider Configuration Best Practices #
# BEST PRACTICE: Configure providers in one place
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
}
required_version = ">= 1.6.0"
}
# The default provider
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "terraform"
}
}
}
# A provider alias
provider "aws" {
alias = "us_east"
region = "us-east-1"
}
# A non-AWS provider
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
Provider Version Compatibility #
# When using multi-provider, make sure versions are compatible
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
datadog = {
source = "datadog/datadog"
version = "~> 3.0"
}
}
required_version = ">= 1.6.0"
}
# Check compatibility issues
terraform init 2>&1 | grep "incompatible"
# Lock provider versions for all platforms
terraform providers lock -platform=linux_amd64 -platform=darwin_arm64 -platform=windows_amd64
Summary #
- Cross-provider references work just like regular references — simply use the resource address from the other provider. Terraform automatically builds cross-provider dependencies.
- Three common patterns: AWS + Cloudflare (automatic DNS), AWS + Kubernetes (deploying to EKS), AWS + Vault (secret management).
- The Kubernetes provider needs special configuration — use exec-based auth so the connection is lazy and doesn’t fail while the cluster doesn’t exist yet.
depends_onfor side-effect dependencies invisible from direct references — for example, a Cloudflare record depending on a finished listener, not just the load balancer.- “Known after apply” values from provider A can’t be directly used to configure provider B — this is a limitation that sometimes requires two separate applies.
- The Terraform graph stays cohesive even with resources from various providers — execution order is calculated from all dependencies, not per-provider.