-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(cdk): transparent cross-stack references #1436
Conversation
Introduce context in resolve(), so that StackAwareToken can do something else if it's a Token from a different stack. Introduce a two-phase Freeze protocol which is a definite moment at which all lazy tokens must be resolved (so that the StackAwareTokens can build cross-Stack links). Change lock() into the freezing protocol, remove unlock as this was a speculative feature that is unused.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pseudo values: I agree that they should just be members of Stack
, and we should stop this little game to try and make stack (and tokens) cloudformation-agnostic. I think we've crossed that river long ago, and we should just accept it. When we do #798, so Construct
is also stack-aware, the usage will be really nice: this.stack.region
will just return the region (and can be even smart about whether we actually know the region as concrete value or use { Ref: AWS::Region }
if not.
const account = components.account == null | ||
? new AwsAccountId() | ||
: components.account; | ||
public static fromComponents(components: ArnComponents, anchor?: Construct): string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also make these ARN utils part of Stack
, so: this.stack.formatArn
and this.stack.parseArn
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would honestly prefer Arn.format and Arn.parse - that makes sense to me intuitively. Looking for string formatting capability on Stack sounds weird.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is that these methods now must be anchored within a stack, so it makes sense to have them as utilities of the stack in my mind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ArnUtils.parse
does not need the stack, only ArnUtils.fromComponents()
. To maintain symmetry, we should put both on Stack then, but I also don't know that it's the most discoverable or appropriate place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for symmetry. As for discoverability - I'd argue that associating those with the stack might make them more discoverable than they are now, which is basically a free floating class that people need to learn about without any way to discover through IDE content assist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh right. Symmetry makes it worth moving both to Stack
. I'm still somewhat wary of the namespace pollution, but every mitigation I can think of risks being immensely confusing, so... Good with that.
/** | ||
* Export a Token value for use in another stack | ||
*/ | ||
public exportValue(tokenValue: Token, consumingStack: Stack): Token { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this should be "The Way To Export" values from a stack, in which case we should probably expose the full surface area of "Export" such as type, export name, description, etc.
* Export a Token value for use in another stack | ||
*/ | ||
public exportValue(tokenValue: Token, consumingStack: Stack): Token { | ||
if (this.env.account !== consumingStack.env.account || this.env.region !== consumingStack.env.region) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My intuition is that this check should happen at the consuming side, not the exporting side and that export
should not need to know who is going to consume it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exporting cross-account/region mutates the exporting stack differently than exporting in-account/in-region, so the exporting stack still needs to know what is up.
In-env: Export
/Fn::ImportValue
Out-of-env: depending on how we choose to implement, either Output+IAM::Role
/CFN::CustomResource
(CFN native) or Output
/Parameter+Metadata
(toolkit assisted)
- stack.accountId - stack.region - stack.partition - stack.urlSuffix etc. BREAKING CHANGE: pseudo parameters have disappeared and should now be retrieved as attributes of Stack. Some account-granting functions now have an additional parameter, 'anchor', in which you should pass the current scope (typically 'this').
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still feel there's room for simplification/generalization here... I know this might be a bigger change, but now is our time to rethink about this...
Why do we even need resolve
as a free floating API at all besides for testing purposes?
I agree that we need two distinct phases, but I am wondering why we can't implement them generically without requiring each node to cooperate. Basically, it requires invoking toCloudFormation
twice on each stack element: in the first pass, just to find tokens in the result (without substitution), and let them hook into the "relationship" (i.e. add exports to the producing stack). In the second pass, we just synthesize and substitute all tokens with their resolved values.
-
Find all stack elements in the tree
-
Invoke
toCloudFormation
on each element -
Find all values with unresolved tokens in the template
-
If the value is "a pure token", resolve and replace the JSON node
-
If the value is a "string with tokens", resolve and produce an FnJoin statement (fail if the resolved value is not a string or an intrinsic function).
@@ -73,7 +73,7 @@ export class AlarmWidget extends ConcreteWidget { | |||
properties: { | |||
view: 'timeSeries', | |||
title: this.props.title, | |||
region: this.props.region || new AwsRegion(), | |||
region: this.props.region || new cdk.Token({ Ref: 'AWS::Region' }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason not to use stack.region
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should still provide something like Aws.region
and Aws.accountId
for people to be able to explicitly use these pseudo parameters? Or even Stack.region
and Stack.accountId
packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts
Outdated
Show resolved
Hide resolved
* construct tree, this class takes an anchor parameter; the pseudo parameter | ||
* values can be obtained as properties from an anchored object. | ||
*/ | ||
export class Aws { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need this as well as the stack.xxx
properties?
/** | ||
* Export a Token value for use in another stack | ||
*/ | ||
public exportValue(tokenValue: Token, consumingStack: Stack): Token { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we somehow hide this method so it won't confuse people. Technically IIUC people are not supposed to use it directly.
/** | ||
* In a consuming context, potentially substitute this Token with a different one | ||
*/ | ||
public substituteToken(consumingStack: Stack): Token { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would really love to explore the option to generalize this such that tokens can hook into their consumers (see #1433).
Here's my plan for generalizing this so that it can also subsume our current GIST
HOW DOES THIS HELP
CHANGES
|
Not sure I understand the motivation behind constructs notifying about which reference types they consume instead of notifying about concrete references. What I imagined is basically a virtual method on I am also not clear as to why we need the "double notification". If the construct is already notifying the framework that it's consuming a reference, why do we need the system to also notify the construct about the same thing? Wouldn't it be sufficient to just notify the reference that it's being consumed in some scope? Some alternative naming preferences:
|
I guess this confusion is caused by naming. One is:
The other one is:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow! Looks great.
// if hash components were added to the deployment, we use them to calculate | ||
// a logical ID for the deployment resource. | ||
if (this.hashComponents.length === 0) { | ||
this.lazyLogicalId = this.originalLogicalId; | ||
} else { | ||
const md5 = crypto.createHash('md5'); | ||
this.hashComponents | ||
.map(c => cdk.resolve(c)) | ||
.map(c => this.node.resolve(c)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am wondering if it would make sense to only expose resolve
during prepare and synthesis by passing in something like SynthesisContext
to the prepare
method? This will sure people don't abuse this capability before the tree is ready.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's interesting - /me likey.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we prepared to have unresolved tokens in error messages?
Not entirely clear how this should work if it can't resolve:
The lazy arguments to Token
will need to take the SynthesisContext
as an argument as well so they can resolve or JSONify inner structures:
All still in favor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rix0rrr thanks for the due diligence. It's so much nicer to have these discussions over actual examples.
Let's punt this change for now for the sake of progress, but I think we should still explore this. The examples you provided are interesting and actually expose some issues, which is precisely
-
Error messages: the practice of
JSON.stringify(resolve)
feels like a lucky hack anyway, and only works when specific construct authors are aware that a value might be a token. I think a more robust solution to this problem would be to improve the string representation of tokens of resource references (GetAtt/Refs) in strings so they will be less intimidating. For instance, one can argue that something like${{ "Fn::GetAtt": [ Logical, "Attribute" ]}}
can be used for GetAtt tokens (and this can be used as the map key to the token object. -
I'd argue that this use case should actually query
unresolved
and then just type-check. There is no need to actually resolve the values:
- As for lazy evaluation: totally agree. Lazy evaluation is basically a hook into the synthesis phase. If you recall, I proposed in the past to stop allowing "free floating" lazy tokens have have something like:
this.node.onSynthesize(synthesisContext => bla)
, so yes, passingsynthesisContext
to such a lazy handler makes total sense to me.
Hallelujah! |
It is now no longer necessary to use
export()
andimport()
whensharing constructs between two
Stacks
inside the same CDK app.Instead, objects defined in one stack can be used directly in another
stack.
The CDK will detect when an attribute (such as an ARN, ID or URL) of
such an object is used in a different stack, and will automatically
create the required
Export
in the producing stack and insert thecorresponding
Fn::ImportValue
in the consuming stack.BREAKING CHANGE: if you are using
export()
andimport()
to shareconstructs between stacks, you can stop doing that, instead of
FooImportProps
accept anIFoo
directly on the consuming stack,and use that object as usual.
ArnUtils.fromComponents()
andArnUtils.parse()
have been moved ontoStack
. All CloudFormationpseudo-parameter (such as
AWS::AccountId
etc) are now alsoaccessible via
Stack
, asstack.accountId
etc.resolve()
hasbeen moved to
this.node.resolve()
.CloudFormationJSON.stringify()
has been moved to
this.node.stringifyJson()
.validate()
nowshould be
protected
.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license.