CDK Finally Has a Way Out of the Deadly Embrace

Export Stack1:ExportsOutputFnGetAtt-****** cannot be deleted as it is in use by Stack2

I've hit that error on every multi-stack CDK app I've built. The aws-cdk-lib README has a name for it now, the deadly embrace, and the same section documents the way out. It's one line in cdk.json.

Props were exports all along

My credit-card side project has four stacks per stage. The Data stack owns a DynamoDB table and a Cognito user pool, and it hands both to the Api stack as ordinary props. I thought of that as passing objects around, which is the part of CDK I like.

CloudFormation saw something else. For every value that crossed a stack boundary, CDK wrote an Export on the producer and an Fn::ImportValue on the consumer; my app had five. CloudFormation refuses to change or remove an export while any stack imports it, so the producer can't drop the resource until the consumer stops using it, and the consumer can't deploy first because the value it wants is gone. Neither stack can move.

The flag that removes the lock

@aws-cdk/core:defaultCrossStackReferences takes one of three values (I'm on aws-cdk-lib 2.270.0).

{
  "context": {
    "@aws-cdk/core:defaultCrossStackReferences": "both"
  }
}

A new app can start on weak. For stacks that are already deployed:

  1. Set the flag to both.
  2. Deploy every stage, so that each consumer stops importing before any export goes away.
  3. Set the flag to weak and deploy again.

Skipping both recreates the embrace, because the producer tries to delete an export the deployed consumer still imports.

What changed in my templates

I set both and diffed the synth. All five Fn::ImportValue references became Fn::GetStackOutput, the five exports stayed, and no construct code changed. One reference crosses regions (a web ACL in us-east-1, read from us-west-2), and under strong CDK had built a custom resource with its own Lambda to carry it. That reader is gone; the Web stack went from 43 resources to 39 and my deploy pipeline lost a job.

Four tests failed, three of them snapshots. The fourth counted that cross-region reader, and it sat next to an assertion I trusted more than I should have. That assertion walks each template for Fn::ImportValue strings to prove references only flow one way; with no imports left it passes on any wiring at all. I rewrote it to follow both mechanisms.

The synth also stopped printing a warning I'd scrolled past more than once, "No cross-stack-reference strength configured, defaulting to strong".

Then weak

After the both deploy I changed the value to weak and diffed again. Every export is gone and no template contains Fn::ImportValue. The us-east-1 stack went from five resources to one, because the export writer that carried the web ACL's ARN across regions (a custom resource, its Lambda, a role and a policy) has nothing left to write. The pipeline lost a second job with it.

One more test failed. It required at least two of those cross-region Lambdas to exist, so that a log-retention rule had something to cover; it now asserts there are none.

Both deploys went through on my development stage, both first and weak after it. I haven't taken a production stage through the two steps yet.

When I'd keep strong

weak gives up a guard. CloudFormation will now let me delete a resource another stack still reads, and I find out when the consumer's next deploy fails. My table and user pool are retained and deletion-protected. The lock was buying me nothing there. A shared KMS key or a VPC other teams build on is different, and CrossStackReferences.of(resource).produce() keeps one resource strong without touching the app default.

Everything else in that app is on weak now.