DEV Community

Cover image for Fn::GetStackOutput: How CloudFormation and CDK Solved Cross-Region References Together
Pahud Hsieh
Pahud Hsieh

Posted on

Fn::GetStackOutput: How CloudFormation and CDK Solved Cross-Region References Together

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)   │
│                     │                │                     │
└─────────────────────┘                └─────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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)                     │
│                                                              │
└──────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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 │
│                                                              │
└──────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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

(#9274, #9556, #3464, #15689)

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.
Enter fullscreen mode Exit fullscreen mode
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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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/...       │
└────────────────────────────────────────┘     └──────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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',
});
Enter fullscreen mode Exit fullscreen mode

> 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"
  }
}
Enter fullscreen mode Exit fullscreen mode
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.
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

Limitation: Fn::GetStackOutput can only be used in resource properties — not in the Outputs section of a CloudFormation template.

Try It

For new projects, use "weak" from the start:

// cdk.json
{
  "context": {
    "@aws-cdk/core:defaultCrossStackReferences": "weak"
  }
}
Enter fullscreen mode Exit fullscreen mode
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'],
});
Enter fullscreen mode Exit fullscreen mode

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" } }
Enter fullscreen mode Exit fullscreen mode

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.      │
    └─────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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)