Fn::GetStackOutput: How CloudFormation and CDK Solved Cross-Region References Together
One String, Two Regions
Your ACM certificate must live in us-east-1. Your app runs in ap-northeast-1. You need the certificate ARN in your CloudFront distribution. How do you pass it?
Stack A (us-east-1) Stack B (ap-northeast-1)
┌─────────────────────┐ ┌─────────────────────┐
│ │ │ │
│ ACM Certificate │───── ? ───────▶│ CloudFront Dist │
│ (must be us-east-1)│ │ (needs cert ARN) │
│ │ │ │
└─────────────────────┘ └─────────────────────┘
Until recently, composing this relationship natively in a template required extra plumbing. Fn::ImportValue only works within the same account and region. Cross that boundary and you needed workarounds — custom resources, Lambda functions, SSM parameters, IAM roles, all to pass a single string.
This wasn't something CDK could solve alone. It needed a new CloudFormation primitive. And it mattered because AWS itself tells you to separate workloads by account and region. The Well-Architected Security Pillar (SEC01-BP01) says: "Separate workloads using accounts." But composing infrastructure across those boundaries natively required CloudFormation and CDK to evolve together. Enterprises were forced to choose between account isolation (security best practice) and infrastructure composability (developer productivity).
On June 6, 2018 — the first month of CDK's public release — issue #49 was opened: "Cross-region/account references." It was one of the most fundamental gaps in the CDK model, and solving it required changes at both the CloudFormation and CDK layers.
The Deadly Embrace
The missing primitive didn't just make things harder. It made things stuck.
When Stack A exports a value that Stack B imports, CloudFormation locks the export. You can't remove it while any stack references it. But you can't update the consumer to stop referencing it without first changing the producer. Neither stack can move.
┌─────────────┐ ┌─────────────┐
│ Stack A │──Export──X──▶│ Stack B │
│ (can't │ │ (can't │
│ delete │◀──Import─X───│ update │
│ export) │ │ import) │
└─────────────┘ └─────────────┘
💀 Deadly embrace. Neither stack can move.
Only fix: destroy and recreate.
For same-region stacks, Stack.exportValue() was a workaround. For cross-region? No workaround existed. You were stuck. (#7602, #34813)
This was just one symptom. The same root cause — no native cross-boundary reference — produced a long trail of issues:
| Issue | Problem |
|---|---|
| #5304 | Unable to update a stack that has exports |
| #25377 | CrossRegion references doesn't work when exporting to multiple regions |
| #22820 | crossRegionReference resources circumvent Aspects |
| #19881 | Custom Pipeline Role can't deploy cross-region support stacks |
| #17741 | Export changes prevent stack updates |
| #9556 | S3 + CloudFront cross-region references blocked |
| #9274 | ACM certificate cross-region usage pain |
| #17643 | Cannot deploy a cross-region pipeline from a Stage |
| #12741 | "cannot consume a cross reference from stack" in CDK Pipelines |
| #6242 | How to create resources across regions in one stack |
The Workarounds (and Why They Weren't Enough)
"All non-trivial abstractions, to some degree, are leaky."
— Joel Spolsky, The Law of Leaky Abstractions (2002)
CDK's promise was simple: pass a construct reference across stacks, and the framework figures out the wiring. But cross-region broke that abstraction wide open.
The community didn't wait. I built pahud/cdk-remote-stack back in 2021 — a construct library that used custom resources and SSM Parameter Store to bridge the gap:
┌─ Producer Stack (us-east-1) ─────────────────────────────────┐
│ │
│ ACM Certificate ──▶ CfnOutput │
│ │ │
│ ▼ │
│ Custom Resource (Lambda) │
│ └── Writes cert ARN to SSM Parameter Store │
│ │
└──────────────────────────────────────────────────────────────┘
│
SSM Parameter
(cross-region read)
│
▼
┌─ Consumer Stack (ap-northeast-1) ────────────────────────────┐
│ │
│ Custom Resource (Lambda) │
│ └── Reads cert ARN from SSM in us-east-1 │
│ │ │
│ ▼ │
│ CloudFront Distribution (uses cert ARN) │
│ │
└──────────────────────────────────────────────────────────────┘
It worked. But it was a hack — extra Lambda functions (cold starts, timeouts, runtime upgrades), extra IAM roles with cross-region permissions, SSM parameter pollution across regions, and fragile failure modes where a Lambda timeout could block your entire deployment.
I maintained this for years — 300+ releases. The most common question was always: "Is there a way to do this without a third-party construct?" And every time, the honest answer was: not yet.
In late 2022, the CDK team shipped crossRegionReferences: true as a stack property. Under the hood, it used the exact same pattern — Custom::CrossRegionExportWriter and Custom::CrossRegionExportReader backed by Lambda and SSM. Better than nothing, but it inherited all the same problems.
The Solution: Fn::GetStackOutput
CloudFormation now ships a native intrinsic function that resolves cross-region and cross-account references at deploy time:
{
"Fn::GetStackOutput": {
"StackName": "ProducerStack",
"OutputName": "CertificateArn",
"Region": "us-east-1",
"RoleArn": "arn:aws:iam::111111111111:role/cross-account-role"
}
}
No custom resources. No Lambda. No SSM. No intermediate state. The CloudFormation engine resolves it directly.
What it supports:
- ✅ Cross-region (us-east-1 → ap-northeast-1)
- ✅ Cross-account (account A → account B, via a role ARN)
- ✅ Cross-stack (Stack A → Stack B, same region)
- ✅ All combinations of the above
What it fixes:
| Problem | Before | After |
|---|---|---|
| Deadly embrace | Stuck — destroy and recreate | No Export locking with weak references |
| Custom resource failures | Lambda timeout = stuck deployment | No Lambda in the loop |
| SSM parameter pollution | Parameters scattered across regions | No SSM involved |
| Cross-account references | Manual, fragile, undocumented | First-class with RoleArn |
| Aspects circumvented (#22820) | Custom resources bypassed CDK Aspects | Native CFN — Aspects apply normally |
| Multi-region export (#25377) | Broken with multiple consumers | CFN handles it natively |
| Export update blocked (#5304, #17741) | Can't update stack with exports | No Exports to lock |
Cross-Account: Native at Last
Before Fn::GetStackOutput, if Account B needed a VPC ID from Account A, you had to manually export to SSM, set up cross-account IAM roles for SSM access, write custom resources to read the value, and maintain all of it yourself.
Now, Fn::GetStackOutput supports a RoleArn parameter. The consuming stack assumes a role in the producing account to read the output. CDK generates the necessary cross-account role and trust policy automatically.
┌─ Account A (shared-services) ────────────────────────────────┐
│ │
│ SharedVpc Stack │
│ Outputs: │
│ VpcId: vpc-abc123 │
│ │
│ Cross-Account Role (trusts Account B's CFN execution role) │
│ Allows: cloudformation:DescribeStacks │
│ │
└──────────────────────────────────────────────────────────────┘
│
Fn::GetStackOutput
(assumes role in Account A)
│
▼
┌─ Account B (workload) ───────────────────────────────────────┐
│ │
│ AppStack │
│ Fn::GetStackOutput: │
│ StackName: SharedVpc │
│ OutputName: VpcId │
│ Region: us-east-1 │
│ RoleArn: arn:aws:iam::ACCOUNT_A:role/cross-account-role │
│ │
└──────────────────────────────────────────────────────────────┘
This unlocks real multi-account architectures: shared services account exports VPC/subnet IDs, transit gateway attachments; security account exports KMS key ARNs; platform team manages producer stacks, application teams just reference. Zero custom plumbing.
Solving the Deadly Embrace
With Fn::GetStackOutput, references are "weak" — the producing stack has a plain Output (not an Export), so CloudFormation doesn't lock it. The consumer reads the output directly without creating a dependency lock. You can remove or update the producer's output without the deadly embrace.
Migration from existing strong references is a two-step deployment:
Step 1: "both" mode — keeps Export + adds plain Output; consumer switches to Fn::GetStackOutput
Step 2: "weak" mode — removes Export entirely. No more lock. No more deadly embrace.
Note: deleting or renaming the producer output still requires coordinating with consumers, but you're no longer locked by CloudFormation's export mechanism.
Real-World Use Cases
1. ACM Certificate + CloudFront
The single most common cross-region pain point in CDK.
BEFORE: AFTER:
┌─ us-east-1 ─────────────────┐ ┌─ us-east-1 ─────────────────┐
│ ACM Certificate │ │ ACM Certificate │
│ Custom::ExportWriter (λ) │ │ Outputs: │
│ IAM Role │ │ CertArn: arn:aws:acm:... │
│ SSM Parameter ──────────────┼──┐ └────────────────────────────────┘
└──────────────────────────────┘ │ ▲
│ ┌─ ap-northeast-1 ────────────┐
┌─ ap-northeast-1 ────────────┐ │ │ CloudFront Distribution │
│ Custom::ExportReader (λ) │◀─┘ │ Fn::GetStackOutput: │
│ IAM Role │ │ StackName: CertStack │
│ CloudFront Distribution │ │ OutputName: CertArn │
└──────────────────────────────┘ │ Region: us-east-1 │
└────────────────────────────────┘
Resources: 6 custom + 2 real Resources: 2 real. That's it.
const cert = new acm.Certificate(certStack, 'Cert', { domainName: 'example.com' });
new cloudfront.Distribution(appStack, 'Dist', { certificate: cert });
// Done. No custom resources. No Lambda. No SSM.
Full working example
import * as cdk from 'aws-cdk-lib';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
const app = new cdk.App();
// --- Producer: ACM Certificate in us-east-1 (required by CloudFront) ---
const certStack = new cdk.Stack(app, 'CertStack', {
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-1' },
});
const cert = new acm.Certificate(certStack, 'Cert', {
domainName: 'example.com',
validation: acm.CertificateValidation.fromDns(),
});
// --- Consumer: CloudFront in ap-northeast-1 ---
const appStack = new cdk.Stack(app, 'AppStack', {
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'ap-northeast-1' },
});
// Direct cross-region reference — CDK handles Fn::GetStackOutput automatically
new cloudfront.Distribution(appStack, 'Dist', {
defaultBehavior: { origin: new origins.HttpOrigin('example.com') },
certificate: cert, // cross-region reference — just works
domainNames: ['example.com'],
});
// With @aws-cdk/core:defaultCrossStackReferences set to "weak" in cdk.json,
// CDK automatically generates CfnOutput on the producer and Fn::GetStackOutput
// on the consumer. No manual wiring needed.
2. Cross-Account Shared Infrastructure
Enterprise multi-account pattern: security account owns KMS keys, workload accounts use them for encryption.
┌─ Account A (security) ────────────────┐ ┌─ Account B (workload) ──────────────┐
│ │ │ │
│ KMS Key │ │ S3 Bucket (encrypted) │
│ Outputs: │ │ Fn::GetStackOutput: │
│ KeyArn: arn:aws:kms:... │◀────│ StackName: SecurityStack │
│ │ │ OutputName: KeyArn │
│ Cross-Account Role (auto-generated) │ │ Region: us-east-1 │
│ (trusts Account B's CFN exec role) │ │ RoleArn: arn:...:role/... │
└────────────────────────────────────────┘ └──────────────────────────────────────┘
Full working example
import * as cdk from 'aws-cdk-lib';
import * as kms from 'aws-cdk-lib/aws-kms';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam';
const app = new cdk.App();
const ACCOUNT_A = '111111111111'; // security account
const ACCOUNT_B = '222222222222'; // workload account
// --- Account A: Security account owns the KMS key ---
const securityStack = new cdk.Stack(app, 'SecurityStack', {
env: { account: ACCOUNT_A, region: 'us-east-1' },
});
const key = new kms.Key(securityStack, 'SharedKey', {
description: 'Shared encryption key for workload accounts',
alias: 'alias/shared-encryption-key',
});
// Allow Account B to use the key.
// Key policies grant at the account level; IAM in Account B controls which principals can use it.
key.addToResourcePolicy(new iam.PolicyStatement({
actions: ['kms:Encrypt', 'kms:Decrypt', 'kms:GenerateDataKey*'],
principals: [new iam.AccountPrincipal(ACCOUNT_B)],
resources: ['*'],
}));
// --- Account B: Workload account uses the key for S3 encryption ---
const workloadStack = new cdk.Stack(app, 'WorkloadStack', {
env: { account: ACCOUNT_B, region: 'us-east-1' },
});
// Direct cross-account reference — CDK auto-generates role and Fn::GetStackOutput
new s3.Bucket(workloadStack, 'EncryptedBucket', {
encryption: s3.BucketEncryption.KMS,
encryptionKey: key, // cross-account reference — just works
bucketName: 'workload-encrypted-data',
});
> Note: Deploy SecurityStack in Account A first, then WorkloadStack in Account B. Both accounts must have CDK bootstrap completed. CDK automatically generates the cross-account role and trust policy. Fn::GetStackOutput only works in resource properties (not in the Outputs section of a template).
CDK Integration
PR #37724 by Otavio Macedo adds Fn.getStackOutput() as a first-class API, and PR #37800 / PR #37824 add automatic wiring — CDK handles the CfnOutput and Fn::GetStackOutput for you when you pass construct references across stacks.
Automatic Wiring
Set the context key in cdk.json:
{
"context": {
"@aws-cdk/core:defaultCrossStackReferences": "weak"
}
}
| Value | Behavior |
|---|---|
"strong" (default) |
Legacy — Fn::ImportValue for same-region, custom resources for cross-region |
"weak" |
Uses Fn::GetStackOutput for cross-stack references that require CloudFormation wiring |
"both" |
Transitional — keeps strong-side artifacts while consumers switch |
With "weak", the same CDK code you already have synthesizes to clean Fn::GetStackOutput instead of custom resources:
BEFORE (strong — custom resources): AFTER (weak — native):
┌────────────────────────────────┐ ┌────────────────────────────────┐
│ Producer Stack │ │ Producer Stack │
│ Certificate │ │ Certificate │
│ Custom::ExportWriter (λ) │ │ Outputs: │
│ IAM Role for Lambda │ │ CertArn: !GetAtt Cert.Arn │
│ SSM Parameter │ └────────────────────────────────┘
└────────────────────────────────┘
┌────────────────────────────────┐
┌────────────────────────────────┐ │ Consumer Stack │
│ Consumer Stack │ │ Fn::GetStackOutput: │
│ Custom::ExportReader (λ) │ │ StackName: ProducerStack │
│ IAM Role for Lambda │ │ OutputName: CertArn │
│ CloudFront Distribution │ │ Region: us-east-1 │
└────────────────────────────────┘ │ CloudFront Distribution │
└────────────────────────────────┘
Resources: 6 custom + 2 real Resources: 2 real. That's it.
And crossRegionReferences: true? With @aws-cdk/core:defaultCrossStackReferences set to "weak", it's no longer needed — cross-region references just work without it.
Explicit API
You can also call Fn.getStackOutput() directly for full control:
// Cross-region
const certArn = cdk.Fn.getStackOutput('CertStack', 'CertArn', 'us-east-1');
// Cross-account
const vpcId = cdk.Fn.getStackOutput('NetworkStack', 'VpcId', 'us-east-1',
'arn:aws:iam::111111111111:role/cfn-cross-account-read');
Limitation:
Fn::GetStackOutputcan only be used in resource properties — not in theOutputssection of a CloudFormation template.
Try It
For new projects, use "weak" from the start:
// cdk.json
{
"context": {
"@aws-cdk/core:defaultCrossStackReferences": "weak"
}
}
const cert = new acm.Certificate(certStack, 'Cert', { domainName: 'example.com' });
new cloudfront.Distribution(appStack, 'Dist', {
defaultBehavior: { origin: new origins.HttpOrigin('example.com') },
certificate: cert, // cross-region reference — just works
domainNames: ['example.com'],
});
No crossRegionReferences: true needed. No custom resources generated. No Lambda. No SSM.
For existing stacks with strong references, migrate in two deployments:
// Deployment 1: transitional — keeps strong artifacts, adds weak
{ "context": { "@aws-cdk/core:defaultCrossStackReferences": "both" } }
// Deployment 2: remove strong artifacts entirely
{ "context": { "@aws-cdk/core:defaultCrossStackReferences": "weak" } }
The Journey to the Right Primitive
CDK users needed cross-region and cross-account references.
│
▼
Community libraries and CDK features bridged the gap with custom resources.
│
▼
CloudFormation now provides Fn::GetStackOutput as the native primitive.
│
▼
CDK 2.254.0 wires that primitive into the construct reference model.
│
▼
┌─────────────────────────────────────────────────────────┐
│ Native cross-region + cross-account + cross-stack │
│ references. No custom resources. No extra plumbing. │
│ crossRegionReferences: no longer needed with weak refs.│
│ The platform and the framework, working together. │
└─────────────────────────────────────────────────────────┘
Final Thought
I built pahud/cdk-remote-stack because CDK users needed a bridge for cross-region references. It served its purpose, and I'm glad it helped people.
That bridge is no longer needed. CloudFormation added the primitive, CDK wired it up, and cdk-remote-stack can retire. 😂🎉
Werner Vogels has often described AWS in terms of small primitives developers can compose, rather than prescriptive frameworks. Fn::GetStackOutput is exactly that — a composable primitive that CloudFormation and CDK delivered together. A good primitive disappears into the framework. This one just did.
Pass a construct reference across regions. CDK and CloudFormation handle the rest. That's it.
Questions or feedback? Find me on X @pahudnet.
Fn::GetStackOutput CDK support was implemented by Otavio Macedo in aws-cdk#37724. The CloudFormation intrinsic function is available in all commercial AWS regions.
Top comments (0)