Skip to content
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

Nested stacks template will not be republished when a reference is added/updated #7059

Closed
NetaNir opened this issue Mar 29, 2020 · 1 comment · Fixed by #7187
Closed

Nested stacks template will not be republished when a reference is added/updated #7059

NetaNir opened this issue Mar 29, 2020 · 1 comment · Fixed by #7187
Assignees
Labels
@aws-cdk/core Related to core CDK functionality bug This issue is a bug. in-progress This issue is being actively worked on. p1

Comments

@NetaNir
Copy link
Contributor

NetaNir commented Mar 29, 2020

When a user creates a NestedStack that reference a property in a sibling NestedStack, e.g:

                            +--------+                 
                       +----| Parent |-------+         
                       |    +--------+       |         
                       |                     |         
                       |                     |         
                       v                     v  
                +------------+      +------------------+
                |  Nested1   |      |     Nested2      |
                |            |      |                  |
                | Producer   |      |     Consumer     |
                | SNSTopic   |      |   Ref:SNSTopic   |
                +------------+      +------------------+

The CDK will create an output of the property, the Topic in the picture, on the producer stack. And the corresponding parameter on the consumer stack.
The outputs and parameters are created in the prepare phase of the CDK. In this phase, prepare is called on every construct in the tree, from leaves to root, ensuring that when a construct is being "prepared" it has all the knowledge it needs since all of its children have already been "prepared".
Since sibling nested stacks does not have a parent-child dependency it is not guaranteed that the consumer stack will be prepared before the producer stack, the order is dependent on the order in which the children array is processed.

Nested stack templates are uploaded to S3 using the CDK assets mechanism. In order to avoid uploading an unchanged assets, a hash is calculate for each asset and is used as the object key in S3. The hash is calculated differently for every type of assets. For CloudFormation templates, the hash is calculated based on all the template elements, including outputs and parameters, ensuring that every change to the template will result in a new file uploaded to S3.
The nested stack template file hash is calculated in the stack prepare method:

    if (this.nestedStackParent) {
      // add the nested stack template as an asset
      const cfn = JSON.stringify(this._toCloudFormation());
      const templateHash = crypto.createHash('sha256').update(cfn).digest('hex');
      const parent = this.nestedStackParent;
      const templateLocation = parent.addFileAsset({
        packaging: FileAssetPackaging.FILE,
        sourceHash: templateHash,
        fileName: this.templateFile
      });

full code

In the consumer producer example, the outputs and parameters on the respective stacks will be created when prepare is called on the consumer stack (the consumer stack is the one that needs the reference). This means that if the producer stack is prepared before the consumer stack, when the hash of the producer template file is calculated, it will not include the output, which means that the updated template will not be uploaded to S3, resulting in deployment failure.
Simply put, the hash is not calculated on the final template.

Why didn't our integration tests catch this?

you don't need to read the bellow to fix the described issue, it's just interesting :)

To traverse the tree from leaves to root in the prepare phase we create the topological sorting of the tree using:

root.findAll(ConstrcutOrder.Preorder).reverse()

code

Which means that the children in each level of the tree are processed from right to left.
Given the way the children are stored on the parent node and get children() implementation, the right to left order of the children array is opposite to the order in which the children were added to the node(1), which means that this code:

const producer = new ProducerNestedStack(this, 'Nested1');
const consumer =  new ConsumerNestedStack(this, 'Nested2', producer.topic);

Will result in the consumer stack being "prepared" before the producer stack.
Luckily, although not required, in most cases when referencing a property between resources, the producer resource will be created (and added to the children array) before the consumer resource. When the producer is created before the consumer the current logic works.

The producer resource must exist when the reference is added to the consumer resource but this does not implies anything on the creations order, for example:

consumer =  new ConsumerNestedStack(this, 'Nested2');
producer = new ProducerNestedStack(this, 'Nested1');
consumer.topic = producer.topic;

So how did this suddenly break?

As apart of the fix in aws/constructs#23, the topological sorting was changed from:

root.findAll(ConstrcutOrder.Preorder).reverse()

To:

root.findAll(ConstrcutOrder.Postorder)

Resulting in the children array processing order to change from right->left to left->right, causing the producer to be prepared before the consumer, breaking(2) the nested stacks integ test.
Since this change in the constructs module broke the CDK integ test we reverted the change and kept the original order.

(1) We store the children in a map based on their id (string), and retrieve the actual constructs using Object.values(children) which returns string keys in the order in which they were added to the object (see reference)
(2) The integration test only fail because it was run as part of an integration test suite. The test suite that includes a test which produces the same template as the one produced by the test that fail, minus the outputs element. This resulted in the template with the outputs not being uploaded, failing the deployment. If the test was run independently, it would have passed :/ (AI, more extensive integ test for assets?)

@NetaNir NetaNir added the needs-triage This issue or PR still needs to be triaged. label Mar 29, 2020
@NetaNir NetaNir added @aws-cdk/core Related to core CDK functionality and removed needs-triage This issue or PR still needs to be triaged. labels Mar 29, 2020
@NetaNir NetaNir changed the title Nested Stacks template will not be republished when a reference is added/updated Nested stacks template will not be republished when a reference is added/updated Mar 29, 2020
@eladb
Copy link
Contributor

eladb commented Mar 29, 2020

Nice catch!

@SomayaB SomayaB assigned NetaNir and eladb and unassigned NetaNir Mar 30, 2020
@eladb eladb added p1 bug This issue is a bug. labels Mar 31, 2020
@SomayaB SomayaB added the in-progress This issue is being actively worked on. label Apr 6, 2020
eladb pushed a commit that referenced this issue Apr 10, 2020
#7187)

Fixes #6473 by centralizing all the logic to resolve cross-references into `App.prepare()`. This allows reasoning about the entire application once from a single code flow, dramatically simplified the algorithm and solved the issue at hand (and probably a handful of other issues).

This logic is implemented in a function called `prepareApp` which is normally called from `App.prepare()` but if a `Stack` is created as a root (normally in unit tests, we invoke this logic from there to retain current behavior).

This algorithm takes care of both resolving cross references and add template assets for nested stacks. This is because assets are currently addressed using CFN parameters, which means that when we adding them to the parent of a nested stack, the parent is mutated, so we need to rectify references again. To make sure this is done correctly, we always create assets in DFS order.


Fixes #7059
Fixes #5888
@mergify mergify bot closed this as completed in #7187 Apr 16, 2020
mergify bot pushed a commit that referenced this issue Apr 16, 2020
#7187)

Fixes #6473 by centralizing the logic to resolve cross-references and prepare nested stack template assets in `App.prepare`, which has a global view of the app and is the last prepare to execute before synthesis. This dramatically simplified reference resolution and allows dealing with nested stack assets only after all cross references have been resolved (the root cause for #7059).

This logic is implemented in a function called `prepareApp` which is normally called from `App.prepare()` but if a `Stack` is created as a root (normally in unit tests, we invoke this logic from there to retain current behavior).

This algorithm takes care of both resolving cross references and add template assets for nested stacks. This is because assets are currently addressed using CFN parameters, which means that when we adding them to the parent of a nested stack, the parent is mutated, so we need to rectify references again. To make sure this is done correctly, we always create assets in DFS order.

All changes to the test snapshots stem from new asset IDs of nested stack templates, not the template themselves. The change is a result of the fact that the refactor caused the "Parameters" section to appear in a different place in the template, but the template itself is identical.

Fixes #7059 by first resolving all references in the app and only then calculating the hash of the nested stack templates for their assets.

Fixes #5888 but this was not verified.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/core Related to core CDK functionality bug This issue is a bug. in-progress This issue is being actively worked on. p1
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants