feat(core): per-resource cross-stack reference strength override#37840
feat(core): per-resource cross-stack reference strength override#37840otaviomacedo wants to merge 11 commits into
Conversation
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.
|
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 |
Yeah, that makes more sense. Updated. |
| // 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. |
There was a problem hiding this comment.
Interesting choice!
This needs to be in the public documentation of the members.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
referencesreceivesconsumes
For inbound:
isReferencedAsproduces
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Why not just this.scope? What's the advantage of automatically deferring to the default child?
There was a problem hiding this comment.
Because Node does not allow to set context after children have been added.
There was a problem hiding this comment.
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.
| // 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. |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Slightly shorter?
| export enum CrossStackReferenceStrength { | |
| export enum ReferenceStrength { |
| * Strong reference: uses CloudFormation Export/Import (same region) | ||
| * or ExportWriter/ExportReader custom resources (cross-region). | ||
| * | ||
| * The producing stack cannot be deleted while consumers exist. |
There was a problem hiding this comment.
| * 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.
There was a problem hiding this comment.
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:
- Strength of
B - Strength of
--> - Strength of
A - "STRONG"
or:
- Strength of
--> - Strength of
B - Strength of
A - "STRONG"
?
| * | ||
| * @param value - The reference strength to use. | ||
| */ | ||
| public strength(value: CrossStackReferenceStrength): void { |
There was a problem hiding this comment.
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>
✅ Updated pull request passes all PRLinter validations. Dismissing previous PRLinter review.
|
|
||||||||||||||
|
|
||||||||||||||
Allow individual resources to override the global cross-stack reference strength using
CrossStackReferences.of(construct).strength(...). This enables users to selectivelyweaken 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 resolveralready reads. When resolving a cross-stack reference, precedence is: per-resource
context (on the producer) > global context (on the consumer) > default ('strong').
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.