skip to content

In one AWS CDK app, stack B uses `bucket.bucketArn` from a bucket defined in stack A. What do the two synthesized CloudFormation templates contain, and why can removing that reference later break your deployment?

level: seniorimportance: should knowfreq 48%

answer

  1. a token, not a string
  2. Output plus Export on one side
  3. Fn::ImportValue on the other
  4. CloudFormation guards exports in use
  5. consumer first, producer second

basics

~20 s

The CDK adds an Output with an Export to stack A and an Fn::ImportValue to stack B. CloudFormation refuses to delete an export that is still imported, so dropping the reference fails unless the consumer is updated first or the export is pinned for one deploy.

solid answer

~50 s

Passing a construct's attribute across stacks looks like an ordinary variable, but it is a token. At synthesis the CDK turns it into a **CloudFormation Output with an `Export` name in stack A** and an **`Fn::ImportValue` in stack B**, and records a dependency so the CLI deploys A before B. That works fine until you try to unpick it: CloudFormation will not let a stack remove an export that another stack still imports, so the update to A fails with an error saying the export cannot be deleted because it is in use. Meanwhile B still has the import, so nothing can move. The fix is to deploy in two phases — first deploy B without the import, then deploy A to drop the export — and while doing that, call `stack.exportValue(bucket.bucketArn)` in A to keep the export alive even though no code references it any more.

code

typescript · 13 lines
typescript
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';

export class StorageStack extends cdk.Stack {
  public readonly bucket: s3.Bucket;
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);
    this.bucket = new s3.Bucket(this, 'Data');
    // Keep the export alive for one deploy while the consumer drops its import.
    this.exportValue(this.bucket.bucketArn);
  }
}

go deeper

for a junior

Know that passing a value between two CDK stacks is not just a code reference — it becomes a CloudFormation export in one template and an import in the other.

for a middle

Explain the generated Output/Export and Fn::ImportValue pair, and that the CDK adds a stack dependency so the producer deploys first.

for a senior

Walk the deadlock and the escape: consumer loses the import before the producer loses the export, with exportValue pinning it across the transitional deploy, and recognise cycles and cross-environment references as separate failures.

for a principal

Set the estate policy: which values are allowed to cross stack boundaries as tokens versus as stable agreed identifiers, and accept the loss of automatic ordering as the price of independently deployable stacks.

## What a cross-stack reference compiles to In CDK code the reference is unremarkable: ```typescript const storage = new StorageStack(app, 'Storage', { env }); new ServiceStack(app, 'Service', { env, dataBucket: storage.bucket }); ``` But `bucket.bucketArn` is not a string — it is a token, a placeholder the CDK resolves during synthesis. When the token is consumed inside a *different* stack of the same app, the CDK cannot inline a literal, so it wires the two templates together with the only mechanism CloudFormation offers between sibling stacks: - in **stack A**, an `Outputs` entry with an `Export` name (autogenerated, something like `ExportsOutputFnGetAttBucket...`), - in **stack B**, `Fn::ImportValue` of that export name where the value is used. The CDK also records a dependency between the two stacks, so `cdk deploy --all` deploys A first. You did not write either half. ## Why this is convenient and then painful The convenience is real: type-safe wiring, no manual export naming, and helper methods such as `grant*` work across stacks. The pain is that CloudFormation exports are *strongly* protected. Once an export is imported by another stack, CloudFormation refuses two things: renaming or removing the export, and modifying the exported value in a way that would change it. So the moment you refactor — move the bucket, stop passing it, change what B reads — the update to A fails with a message that the export cannot be deleted as it is in use by the consuming stack. The deploy of A rolls back. B is untouched, so the import is still there. You are stuck in a loop unless you understand the ordering. ## Breaking the deadlock The general procedure is: **remove the consumer's import first, then remove the producer's export.** 1. Change stack B so it no longer uses the value. Deploy B alone. Its `Fn::ImportValue` is gone. 2. Now deploy A. With nobody importing, the export can be removed. The trap in step 1 is that if you remove the reference in code, the CDK removes the export from A's template too — and if the CLI deploys A first (or you deploy `--all`), you hit the failure again. That is what `Stack.exportValue()` is for: calling `stack.exportValue(bucket.bucketArn)` in A forces the export to stay in the template even though no code consumes it. So the safe sequence is: 1. Add `exportValue` in A, remove the usage in B, deploy both. B drops the import, A keeps the export. 2. In a second change, remove the `exportValue` call and deploy A. The export goes away cleanly. This two-commit dance is the standard answer, and knowing it is most of what separates someone who has refactored a real CDK estate from someone who has only built one. ## Two related failure modes **Cycles.** If A references something from B *and* B references something from A, there is no valid deployment order and synthesis fails with a cyclic-reference error. You cannot break it with `addDependency`; you break it by moving the shared resource into a third stack, or by decoupling one direction — passing a plain string or reading the value from SSM Parameter Store rather than from the construct. **Different environments.** CloudFormation exports are scoped to one account and one region, so an `Fn::ImportValue` cannot cross either boundary. A reference between stacks in different environments therefore fails at synthesis. For cross-region within one account, `Stack` accepts `crossRegionReferences: true`, which the CDK implements with SSM parameters and custom resources instead of exports. Across accounts there is no automatic mechanism — you pass the value explicitly as a prop, through Parameter Store, or through your pipeline's configuration. ## Designing so it hurts less The token is what creates the coupling, so the way to avoid the coupling is to stop passing tokens across stack boundaries for anything you expect to change. Options, roughly in order of how much independence they buy: - keep tightly coupled resources in the same stack, so the reference never crosses a boundary; - pass **stable, human-chosen identifiers** (a bucket name you decided, an SSM parameter path) as plain strings, and re-import the resource in B with `Bucket.fromBucketName`; - publish values to SSM Parameter Store in A and read them in B, accepting an ordering constraint you manage yourself. Each step trades type safety and automatic ordering for the freedom to redeploy either stack alone. That is exactly the tradeoff an interviewer is probing.

  • What happens if stack A also references a value from stack B?
    Synthesis fails with a cyclic-reference error, because no deployment order satisfies both directions. `addDependency` cannot fix it. You break the cycle by moving the shared resource into a third stack, merging the two stacks, or decoupling one direction — passing a plain identifier or reading the value from SSM Parameter Store instead of from the construct.
  • Can a CDK cross-stack reference span two accounts or two regions?
    Not with exports — an `Fn::ImportValue` only resolves within one account and region, so a cross-environment reference fails at synthesis. In CDK v2, `crossRegionReferences: true` on the stacks handles same-account cross-region by using SSM parameters and custom resources. Across accounts you pass the value yourself, as a prop, a parameter or pipeline configuration.
  • How do you deliberately decouple two stacks that must stay separate?
    Stop passing the construct. Agree a stable identifier — a bucket name you chose or an SSM parameter path — publish it from the producer, and re-import in the consumer with something like `Bucket.fromBucketName`. You lose automatic ordering and some type safety, and gain the ability to deploy either stack without touching the other.
  • Why does `cdk deploy --all` sometimes hide this problem entirely?
    Because the CDK deploys stacks in dependency order, and while the reference exists that order is correct. The failure only appears when the reference is being removed, since the producer's update wants to drop an export the consumer has not yet stopped importing. Deploying stacks individually in the wrong order reproduces it just as well.

saying these in an interview costs you the question

  • Thinks the reference is only a TypeScript variable with no template effect
  • Says you can just delete the export and redeploy
  • Believes CloudFormation exports work across accounts or regions
  • Confuses the export deadlock with a state lock
  • Proposes deleting and recreating the producer stack as the normal fix

context