Using Multiple Terraform Providers in One CDKTF Stack
Deploying to two AWS regions, two cloud accounts, or two providers from a single CDKTF stack requires provider aliases and explicit per-resource provider routing. This guide shows the typed Python patterns for declaring aliased providers and binding each resource to the correct one. It builds on Terraform Provider Bridging, part of the broader CDKTF Workflows & Terraform Synthesis workflow.
The moment your infrastructure spans more than one region or account, a single default provider is no longer enough. Terraform models this with provider aliases, and CDKTF exposes the same mechanism through the generated Python provider classes. Get the routing wrong and resources silently land in the default region, or synthesis fails because a resource cannot resolve its provider.
Prerequisites
- Python 3.9+ with type annotations (
mypy --strictrecommended for the patterns below). - CDKTF CLI 0.20+ and the AWS provider bindings: pin
"hashicorp/aws@~> 6.0"incdktf.json, then runcdktf get. - IAM permissions for every account and region you target. For cross-account work, a role you can assume in the secondary account (for example
arn:aws:iam::<secondary-account-id>:role/cdktf-deployer). - Credentials supplied via environment variables or assumed roles only — never embedded in Python source.
- A configured remote state backend; a multi-provider stack still writes to one state file, so backend isolation per environment matters more, not less.
Concept: provider aliases and resource routing
A CDKTF stack synthesizes to a single Terraform configuration with one state file. Within that configuration you may declare one default provider per provider type and any number of aliased providers. Each resource either inherits the default provider or is explicitly bound to an alias.
In Python, every generated provider class (for example AwsProvider) accepts an alias argument. You capture the returned provider object and pass it to each resource through the provider= argument. There is no implicit matching by region — the binding is explicit and by object reference.
Provider note: All providers and all aliased resources live in the same synthesized stack and therefore the same state file. This is different from running separate stacks per region; choose multiple providers in one stack when the resources are tightly coupled (for example a primary bucket and its cross-region replica), and separate stacks when they have independent lifecycles.
Implementation
1. Declare a default provider and aliased providers
Create one provider without an alias (the default) and one aliased provider per additional region or account. Capture each in a typed field so resources can reference it.
# Run: cdktf get && cdktf synth
# Provider note: the no-alias AwsProvider is the stack default; aliased ones are opt-in per resource.
from dataclasses import dataclass
from constructs import Construct
from cdktf import App, TerraformStack
from cdktf_cdktf_provider_aws.provider import AwsProvider
@dataclass(frozen=True)
class RegionConfig:
primary_region: str = "us-east-1"
replica_region: str = "eu-west-1"
class MultiRegionStack(TerraformStack):
def __init__(self, scope: Construct, ns: str, config: RegionConfig) -> None:
super().__init__(scope, ns)
# Default provider: resources with no provider= argument use this one.
self.primary: AwsProvider = AwsProvider(
self,
"aws_primary",
region=config.primary_region,
)
# Aliased provider: only resources that pass provider=self.replica use it.
self.replica: AwsProvider = AwsProvider(
self,
"aws_replica",
region=config.replica_region,
alias="replica",
)
2. Bind resources to a specific provider
Pass the captured provider object via provider=. Resources omitting provider= use the default; resources passing the alias deploy to the second region.
# Run: cdktf synth (inspect cdktf.out/stacks/<name>/cdk.tf.json to confirm provider keys)
# State implication: both buckets are tracked in the SAME state file for this stack.
from cdktf_cdktf_provider_aws.s3_bucket import S3Bucket
from cdktf_cdktf_provider_aws.s3_bucket_versioning import (
S3BucketVersioningA,
S3BucketVersioningVersioningConfiguration,
)
# Inside MultiRegionStack.__init__, after the providers above:
primary_bucket = S3Bucket(
self,
"primary_data",
bucket="acme-data-primary",
# No provider= -> uses the default (primary) provider.
)
S3BucketVersioningA(
self,
"primary_versioning",
bucket=primary_bucket.id,
versioning_configuration=S3BucketVersioningVersioningConfiguration(
status="Enabled",
),
)
replica_bucket = S3Bucket(
self,
"replica_data",
bucket="acme-data-replica",
provider=self.replica, # Routes this resource to eu-west-1.
)
S3BucketVersioningA(
self,
"replica_versioning",
bucket=replica_bucket.id,
provider=self.replica, # The dependent resource MUST also pin the alias.
versioning_configuration=S3BucketVersioningVersioningConfiguration(
status="Enabled",
),
)
3. Use aliases for multi-account via assume-role
The same alias mechanism handles a second AWS account. Give the aliased provider an assume_role block instead of (or in addition to) a different region.
# Run: cdktf synth && cdktf deploy --stack multi-account
# Provider note: the assume_role chain must be permitted by the secondary account's trust policy.
from typing import Optional
from cdktf_cdktf_provider_aws.provider import AwsProvider, AwsProviderAssumeRole
def add_secondary_account_provider(
stack: TerraformStack,
secondary_account_id: str,
region: str = "us-east-1",
role_name: str = "cdktf-deployer",
) -> AwsProvider:
role_arn: str = f"arn:aws:iam::{secondary_account_id}:role/{role_name}"
return AwsProvider(
stack,
"aws_secondary",
region=region,
alias="secondary",
assume_role=[
AwsProviderAssumeRole(
role_arn=role_arn,
session_name="cdktf-multi-account",
)
],
)
Bind any resource that must live in the secondary account by passing provider= the object returned from add_secondary_account_provider, exactly as in step 2.
4. Synthesize and deploy
Run synthesis, inspect the generated JSON to confirm both provider entries appear, then deploy the stack as a unit.
# Generate bindings, synthesize, and review the provider block before deploying.
cdktf get
cdktf synth
# Both default and aliased providers should appear under "provider" -> "aws".
cat cdktf.out/stacks/multi-region/cdk.tf.json | python -m json.tool | grep -A2 '"alias"'
cdktf deploy --stack multi-region
Verification
Confirm the synthesized configuration contains both the default and aliased providers, and that the replica resource carries the alias reference. A pytest assertion against the synthesized JSON catches routing mistakes before deploy.
# Run: pytest tests/test_multi_provider.py
import json
from cdktf import Testing
from my_stack import MultiRegionStack, RegionConfig
def test_both_providers_and_alias_routing() -> None:
app = Testing.app()
stack = MultiRegionStack(app, "multi-region", RegionConfig())
synthesized = json.loads(Testing.synth(stack))
providers = synthesized["provider"]["aws"]
assert isinstance(providers, list) and len(providers) == 2
regions = {p["region"] for p in providers}
assert regions == {"us-east-1", "eu-west-1"}
# The replica bucket must reference the aliased provider as "aws.replica".
replica = synthesized["resource"]["aws_s3_bucket"]["replica_data"]
assert replica["provider"] == "aws.replica"
A passing test plus a cdktf diff that shows resources targeted at the expected regions confirms the routing is correct.
Gotchas & Edge Cases
Dependent resources need the alias too. Binding the parent resource (the bucket) to an alias does not propagate to dependent resources (the versioning config, bucket policy, replication rule). Each resource that should live in the aliased region must pass
provider=itself. Omitting it on a child resource silently deploys that child to the default region, splitting one logical resource across two regions.
assume_roleis a list, not a dict. The CDKTF AWS provider modelsassume_roleas a repeatable block, so the Python argument expects[AwsProviderAssumeRole(...)]— a list with a single element. Passing a bare object raises a synthesis-time type error.
One state file, larger blast radius. Because every provider in the stack shares one state file, a corrupted apply can affect resources across all regions and accounts at once. For loosely coupled environments prefer separate stacks (and separate state) over many aliases; reserve multi-provider stacks for genuinely co-dependent resources such as cross-region replication pairs.
Frequently Asked Questions
How many providers can I declare in a single CDKTF stack? One default plus any number of aliased providers per provider type, and you may mix provider types (for example AWS and Cloudflare) in the same stack. The practical limit is operational: every provider shares one state file, so a very large multi-provider stack concentrates risk. Split into multiple stacks when resources have independent lifecycles.
Do I have to set an alias on the default provider?
No. Declare exactly one provider without an alias argument — that becomes the implicit default for any resource that omits provider=. Adding an alias to every provider and never declaring a default works too, but then every resource must explicitly pass provider=, which is more verbose.
Can I reference outputs from a resource in one provider when creating a resource in another?
Yes. Resource attributes (for example primary_bucket.arn) are plain token references within the stack and cross provider boundaries freely during synthesis. This is the main advantage of one multi-provider stack over separate stacks, where you would need remote state data sources to share values.
Why did my resource deploy to the wrong region despite passing a region to the provider?
The most common cause is a dependent resource missing its provider= binding, so it fell through to the default provider's region. Inspect cdk.tf.json and confirm every related resource carries the same "provider": "aws.<alias>" value. The verification test above catches this.
Related
- Terraform Provider Bridging — the parent guide on translating provider schemas into typed Python classes.
- Converting Existing Terraform HCL to CDKTF Python — replicate aliased and multi-region providers when migrating from HCL.
- State Backend Configuration for CDKTF — isolate and lock the shared state file a multi-provider stack writes to.