Skip to content

feat(core): per-resource cross-stack reference strength override#37840

Open
otaviomacedo wants to merge 11 commits into
mainfrom
otaviom/per-scope-weak-refs
Open

feat(core): per-resource cross-stack reference strength override#37840
otaviomacedo wants to merge 11 commits into
mainfrom
otaviom/per-scope-weak-refs

Conversation

@otaviomacedo
Copy link
Copy Markdown
Contributor

@otaviomacedo otaviomacedo commented May 12, 2026

Allow individual resources to override the global cross-stack reference strength using CrossStackReferences.of(construct).strength(...). This enables users to selectively
weaken specific references (avoiding the deadly embrace problem) without changing
the app-wide default.

The API uses setContext() on the target construct's node, which the reference resolver
already reads. When resolving a cross-stack reference, precedence is: per-resource
context (on the producer) > global context (on the consumer) > default ('strong').

const bucket = new s3.Bucket(producer, 'SharedBucket');
CrossStackReferences.of(bucket).strength(CrossStackReferenceStrength.WEAK);

When called on an L2 construct, the context is set on its default child (the underlying
CfnResource), which is the actual target of cross-stack references.

Also updates the "Cross-stack reference strength" README section to document the
per-resource override and recommend weakening references instead of the legacy
exportValue() workaround.

Allow individual resources to override the global cross-stack reference strength
via `applyCrossStackReferenceStrength()`. This enables users to selectively
weaken specific references (avoiding the deadly embrace problem) without
changing the app-wide default.

The method is available on `Resource` and `CfnResource`, following the same
pattern as `applyRemovalPolicy()`. Per-resource strength takes precedence over
the global `@aws-cdk/core:defaultCrossStackReferences` context key.

Also updates the "Removing automatic cross-stack references" README section to
recommend weakening references instead of the legacy `exportValue()` workaround.
@otaviomacedo otaviomacedo requested a review from a team as a code owner May 12, 2026 09:38
@github-actions github-actions Bot added the p2 label May 12, 2026
@mergify mergify Bot added the contribution/core This is a PR that came from AWS. label May 12, 2026
@mergify mergify Bot temporarily deployed to automation May 12, 2026 09:39 Inactive
@mergify mergify Bot temporarily deployed to automation May 12, 2026 09:39 Inactive
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 12, 2026

⚠️ This pull request description does not follow the correct template structure.

PRs without a linked issue will receive lower priority for review and merging. Please update the description to follow the PR template and include a line like Closes #123 in the Issue section. If no existing issue matches your change, create one first.

Copy link
Copy Markdown
Collaborator

@aws-cdk-automation aws-cdk-automation left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This review is outdated)

Copy link
Copy Markdown
Contributor

@rix0rrr rix0rrr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why methods specifically on CfnResource, and not just helpers to set the (already existing) feature flag on a construct-node level into context?

Perhaps using a CrossStackReferences.of(...) style API?

@otaviomacedo
Copy link
Copy Markdown
Contributor Author

Why methods specifically on CfnResource, and not just helpers to set the (already existing) feature flag on a construct-node level into context?

Perhaps using a CrossStackReferences.of(...) style API?

Yeah, that makes more sense. Updated.

@otaviomacedo otaviomacedo requested a review from rix0rrr May 12, 2026 13:48
Comment on lines +80 to +82
// Per-resource strength is read from the producer via context on reference.target
// ("how should I be referenced?"). Global strength is read from the consumer
// ("how do I receive references?"). Per-resource wins when set.
Copy link
Copy Markdown
Contributor

@rix0rrr rix0rrr May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting choice!

This needs to be in the public documentation of the members.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait.

This makes it impossible to reference using one strength, but be referenced using another, doesn't it?

Should we not have 2 methods on the CrossStackReferences.of() class?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. The previous iteration didn't suffer from this, but lacked an API sugar to set the strength for a given context. So perhaps we can get the best of both worlds? Something like:

CrossStackReferences.of(scope).inbound(ReferenceStrength.STRONG);
// ☝️sugar for scope.applyReferenceStrength(ReferenceStrenght.STRONG)

CrossStackReferences.of(scope).outbound(ReferenceStrength.WEAK);
// ☝️sugar forscope.node.setContext(DEFAULT_CROSS_STACK_REFERENCES, ReferenceStrenght.WEAK)

The inbound/outbound terminology here is just a placeholder. Possible names (none of which I find self-explanatory):

For outbound:

  • references
  • receives
  • consumes

For inbound:

  • isReferencedAs
  • produces

Having said this, is all this sugaring worth the complexity? The main use case I see for this feature is: for safety, I want all my references to be strong, but occasionally, I need to remove a reference or a consumed resource, and I know what I'm doing, but the deadly embrace will not let me do it. So migrate only the producer with WEAK, and then do the removal, while maintaining the safety of the rest of the app.

So most of the time, on the consumer side, users will be setting the strength on a wide context, such as the whole app, or a stack. And, on the producer side, it's the opposite: users only need it for a narrow scope, such as a resource. So, just a applyReferenceStrength (open to better naming suggestion) would suffice.

Copy link
Copy Markdown
Contributor

@rix0rrr rix0rrr May 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think

CrossStackReferences.of(this).produce(STRONG);
CrossStackReferences.of(this).consume(WEAK);

Makes sense to me?

}

private targetNode(): IConstruct {
const defaultChild = this.scope.node.defaultChild;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just this.scope? What's the advantage of automatically deferring to the default child?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because Node does not allow to set context after children have been added.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooohhhh

Copy link
Copy Markdown
Contributor

@rix0rrr rix0rrr May 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's slightly ass. Then maybe we can't use context after all. Because for example the structure of an iam.Role looks like:

Role
  +-- Resource (AWS::IAM::Role)
  +-- Policy1 (AWS::IAM::Policy)
  +-- Policy2 (AWS::IAM::Policy)

And then:

CrossStackReferences.of(role).consume(some value);

Would configure the AWS::IAM::Role, where for example, which barely references anything. Instead, the Policies usually reference resources.

So while it did sound nice and regular to use the context value for everything, with the restriction of not being able to change context after the fact makes it a bad choice after all.

Sorry.

Comment on lines +80 to +82
// Per-resource strength is read from the producer via context on reference.target
// ("how should I be referenced?"). Global strength is read from the consumer
// ("how do I receive references?"). Per-resource wins when set.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait.

This makes it impossible to reference using one strength, but be referenced using another, doesn't it?

Should we not have 2 methods on the CrossStackReferences.of() class?

/**
* Controls how cross-stack references to a resource are resolved.
*/
export enum CrossStackReferenceStrength {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly shorter?

Suggested change
export enum CrossStackReferenceStrength {
export enum ReferenceStrength {

Comment thread packages/aws-cdk-lib/core/lib/cross-stack-reference-strength.ts Outdated
* Strong reference: uses CloudFormation Export/Import (same region)
* or ExportWriter/ExportReader custom resources (cross-region).
*
* The producing stack cannot be deleted while consumers exist.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* The producing stack cannot be deleted while consumers exist.
* The producing stack or resource cannot be deleted while consumers exist.
*
* If you deploy your stacks using a pipeline, you must remove the consumer first
* while keeping the producer intact, before removing the producer.
*
* Weak references make it so you can remove both producer and consumer in
* a single deployment, at the cost of a small time window of inconsistency.

The removal of a strong reference is still a missing feature. I wonder if we need something like:

Stack.of(this).consumeReference(bucket.bucketArn);

Which should lead to the given value being BOTH referenced in an inconsequential place.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the only question is: should it override the constraint imposed by the producer? In other words, for a given reference A --> B, would the precedence be:

  1. Strength of B
  2. Strength of -->
  3. Strength of A
  4. "STRONG"

or:

  1. Strength of -->
  2. Strength of B
  3. Strength of A
  4. "STRONG"

?

*
* @param value - The reference strength to use.
*/
public strength(value: CrossStackReferenceStrength): void {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not super happy with the name but I don't think I have a better one for you :/

Co-authored-by: Rico Hermans <rix0rrr@gmail.com>
@aws-cdk-automation aws-cdk-automation dismissed their stale review May 13, 2026 15:13

✅ Updated pull request passes all PRLinter validations. Dismissing previous PRLinter review.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 13, 2026

⚠️ Experimental Feature: This security report is currently in experimental phase. Results may include false positives and the rules are being actively refined.
This security report is NOT a review blocker. Please try merge from main to avoid findings unrelated to the PR.
To suppress a specific rule, see Suppressing Rules.


TestsPassed ❌️SkippedFailed
Security Guardian Results
TestResult
No test annotations available

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 13, 2026

⚠️ Experimental Feature: This security report is currently in experimental phase. Results may include false positives and the rules are being actively refined.
This security report is NOT a review blocker. Please try merge from main to avoid findings unrelated to the PR.
To suppress a specific rule, see Suppressing Rules.


TestsPassed ❌️SkippedFailed
Security Guardian Results with resolved templates
TestResult
No test annotations available

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contribution/core This is a PR that came from AWS. p2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants