Describe the bug
When computing the mangled diff for nested stacks, formatStackDiffHelper references this.oldTemplate (the root stack's deployed template) instead of the oldTemplate parameter (the nested stack's deployed template).
This causes cdk diff to produce incorrect output for nested stacks under certain conditions.
Regression Issue
Last Known Working CDK Version
2.1006.0
Expected Behavior
cdk diff for a nested stack should compute the difference between the nested stack's deployed template and its generated template.
Current Behavior
During the cdk diff calculation, formatStackDiffHelper calculates the mangledDiff from the deployed template and the new template to compute the difference:
https://github.com/aws/aws-cdk-cli/blob/main/packages/@aws-cdk/toolkit-lib/lib/api/diff/diff-formatter.ts#L268-L276
// detect and filter out mangled characters from the diff
if (diff.differenceCount && !options.strict) {
const mangledNewTemplate = JSON.parse(mangleLikeCloudFormation(JSON.stringify(this.newTemplate.template)));
const mangledDiff = fullDiff(this.oldTemplate, mangledNewTemplate, this.changeSet);
filteredChangesCount = Math.max(0, diff.differenceCount - mangledDiff.differenceCount);
if (filteredChangesCount > 0) {
diff = mangledDiff;
}
}
For root stack
For the root stack, it correctly evaluates the diff because this.oldTemplate is the root stack's deployed template.
|
oldTemplate: currentTemplate, |
Retrieving the root template.
const currentTemplate = templateWithNestedStacks.deployedRootTemplate;
...
templateInfos.push({
oldTemplate: currentTemplate,
});
Creating the DiffFormatter instance
|
const formatter = new DiffFormatter({ templateInfo }); |
The constructor saves the root deployed template to this.oldTemplate
|
constructor(props: DiffFormatterProps) { |
Root stack diff
public formatStackDiff(options: FormatStackDiffOptions = {}): FormatStackDiffOutput {
return this.formatStackDiffHelper(
this.oldTemplate, // ← Passes the root deployed template as a parameter
this.stackName,
this.nestedStacks,
options,
this.mappings,
);
}
For nested stacks
However, for nested stacks, formatStackDiffHelper is called recursively
https://github.com/aws/aws-cdk-cli/blob/main/packages/%40aws-cdk/toolkit-lib/lib/api/diff/diff-formatter.ts#L309-L321
const nestedStack = nestedStackTemplates[nestedStackLogicalId];
(this.newTemplate as any)._template = nestedStack.generatedTemplate;
const nextDiff = this.formatStackDiffHelper(
nestedStack.deployedTemplate,
nestedStack.physicalName ?? nestedStackLogicalId,
nestedStack.nestedStackTemplates,
options,
this.mappings,
);
numStacksWithChanges += nextDiff.numStacksWithChanges;
formattedDiff += nextDiff.formattedDiff;
}
The oldTemplate parameter correctly receives the nested stack's deployed template. But the class property this.oldTemplate still retains the root stack's template set in the constructor.
Since mangledDiff is calculated using this.oldTemplate, it incorrectly uses the root stack's deployed template instead of the nested stack's.
If filteredChangesCount > 0, the assignment diff = mangledDiff overwrites the correct diff (nested deployed vs. nested generated template) with a cross-comparison between the root parent template and the nested template.
As a result, it outputs a meaningless diff between completely different templates, rather than the actual changes in the nested stack.
If filteredChangesCount <= 0, it is not overwritten, so there is no impact.
Additionally, when the --strict option is passed to cdk diff, the mangled filter process mentioned above is skipped, so it is expected to return the correct diff.
Reproduction Steps
This issue occurs when the condition filteredChangesCount > 0 is met during the cdk diff calculation.
I have confirmed that the issue can be reproduced using the code below:
- Create a CDK project using
npx cdk init app --language typescript with the latest CDK version.
- Generate about 20
CfnOutputs to increase the template differences.
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
class StackA extends cdk.NestedStack {
constructor(scope: Construct, id: string) {
super(scope, id);
new cdk.aws_s3.Bucket(this, 'MyBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
for (let i = 0; i < 20; i++) {
new cdk.CfnOutput(this, `Out${i}`, { value: `value-${i}` });
}
}
}
export class TestStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new StackA(this, 'A');
}
}
npx cdk deploy
- Remove the
for loop (all 20 outputs).
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
class StackA extends cdk.NestedStack {
constructor(scope: Construct, id: string) {
super(scope, id);
new cdk.aws_s3.Bucket(this, 'MyBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// for (let i = 0; i < 20; i++) {
// new cdk.CfnOutput(this, `Out${i}`, { value: `value-${i}` });
// }
}
}
export class TestStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new StackA(this, 'A');
}
}
- Run
npx cdk diff — shows incorrect diff. (This output actually shows the difference between the deployed root stack template and the nested stack's generated template).
$ npx cdk diff
Stack TestStack
Resources
[~] AWS::CloudFormation::Stack A.NestedStack/A.NestedStackResource ANestedStackANestedStackResource5CBB2250
└─ [~] TemplateURL
└─ [~] .Fn::Join:
└─ @@ -13,6 +13,6 @@
[ ] {
[ ] "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}"
[ ] },
[-] "/bab1cb19ccfd92073d1509d7ea04b79a4519332cdb70d63effb0b0c1448fd0e6.json"
[+] "/83504affc354b5989d9c1e29a92ce3a0b86741cc3b8574f255b504cc9b1ee955.json"
[ ] ]
[ ] ]
Stack TestStack-ANestedStackANestedStackResource5CBB2250-I07ST41MA3BF
Parameters
[-] Parameter BootstrapVersion BootstrapVersion: {"Type":"AWS::SSM::Parameter::Value<String>","Default":"/cdk-bootstrap/hnb659fds/version","Description":"Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"}
Resources
[-] AWS::CloudFormation::Stack A.NestedStack/A.NestedStackResource ANestedStackANestedStackResource5CBB2250 destroy
[+] AWS::S3::Bucket A/MyBucket MyBucketF68F3FF0
Omitted 16 changes because they are likely mangled non-ASCII characters. Use --strict to print them.
- Run
npx cdk diff --strict — shows correct diff.
Possible Solution
The calculation of mangledDiff in formatStackDiff was refactored in PR #283 (f9f6d1d).
In this refactor, the oldTemplate was incorrectly replaced with this.oldTemplate.
Before the refactor in #278, oldTemplate was used in this part.
const mangledDiff = fullDiff(oldTemplate, mangledNewTemplate, changeSet);
After refactor (#283, bug):
const mangledDiff = fullDiff(this.oldTemplate, mangledNewTemplate, options.changeSet);
This remains unfixed in the current main branch:
const mangledDiff = fullDiff(this.oldTemplate, mangledNewTemplate, this.changeSet);
so the fix would likely be replacing this.oldTemplate with the oldTemplate parameter in formatStackDiffHelper:
Additional Information/Context
This bug appears to have been introduced in CDK CLI version 2.1007.0, after PR #283 was merged.
Versions 2.1006.0 and earlier return the correct cdk diff results.
CDK CLI Version
2.1111.0
Framework Version
No response
Node.js Version
v22.12.0
OS
AmazonLinux 2023, Ubuntu
Language
TypeScript
Language Version
No response
Other information
No response
Describe the bug
When computing the mangled diff for nested stacks,
formatStackDiffHelperreferencesthis.oldTemplate(the root stack's deployed template) instead of theoldTemplateparameter (the nested stack's deployed template).This causes
cdk diffto produce incorrect output for nested stacks under certain conditions.Regression Issue
Last Known Working CDK Version
2.1006.0
Expected Behavior
cdk difffor a nested stack should compute the difference between the nested stack's deployed template and its generated template.Current Behavior
During the
cdk diffcalculation,formatStackDiffHelpercalculates themangledDifffrom the deployed template and the new template to compute the difference:https://github.com/aws/aws-cdk-cli/blob/main/packages/@aws-cdk/toolkit-lib/lib/api/diff/diff-formatter.ts#L268-L276
For root stack
For the root stack, it correctly evaluates the diff because
this.oldTemplateis the root stack's deployed template.aws-cdk-cli/packages/@aws-cdk/toolkit-lib/lib/actions/diff/private/helpers.ts
Line 106 in 403fe8e
Retrieving the root template.
Creating the DiffFormatter instance
aws-cdk-cli/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts
Line 380 in 403fe8e
The constructor saves the root deployed template to
this.oldTemplateaws-cdk-cli/packages/@aws-cdk/toolkit-lib/lib/api/diff/diff-formatter.ts
Line 154 in 403fe8e
Root stack diff
For nested stacks
However, for nested stacks, formatStackDiffHelper is called recursively
https://github.com/aws/aws-cdk-cli/blob/main/packages/%40aws-cdk/toolkit-lib/lib/api/diff/diff-formatter.ts#L309-L321
The
oldTemplateparameter correctly receives the nested stack's deployed template. But the class propertythis.oldTemplatestill retains the root stack's template set in the constructor.Since
mangledDiffis calculated usingthis.oldTemplate, it incorrectly uses the root stack's deployed template instead of the nested stack's.If
filteredChangesCount > 0, the assignmentdiff = mangledDiffoverwrites the correct diff (nested deployed vs. nested generated template) with a cross-comparison between the root parent template and the nested template.As a result, it outputs a meaningless diff between completely different templates, rather than the actual changes in the nested stack.
If
filteredChangesCount <= 0, it is not overwritten, so there is no impact.Additionally, when the
--strictoption is passed tocdk diff, the mangled filter process mentioned above is skipped, so it is expected to return the correct diff.Reproduction Steps
This issue occurs when the condition
filteredChangesCount > 0is met during the cdk diff calculation.I have confirmed that the issue can be reproduced using the code below:
npx cdk init app --language typescriptwith the latest CDK version.CfnOutputsto increase the template differences.npx cdk deployforloop (all 20 outputs).npx cdk diff— shows incorrect diff. (This output actually shows the difference between the deployed root stack template and the nested stack's generated template).npx cdk diff --strict— shows correct diff.Possible Solution
The calculation of
mangledDiffin formatStackDiff was refactored in PR #283 (f9f6d1d).In this refactor, the
oldTemplatewas incorrectly replaced withthis.oldTemplate.Before the refactor in #278, oldTemplate was used in this part.
After refactor (#283, bug):
This remains unfixed in the current main branch:
so the fix would likely be replacing
this.oldTemplatewith theoldTemplateparameter in formatStackDiffHelper:Additional Information/Context
This bug appears to have been introduced in CDK CLI version 2.1007.0, after PR #283 was merged.
Versions 2.1006.0 and earlier return the correct cdk diff results.
CDK CLI Version
2.1111.0
Framework Version
No response
Node.js Version
v22.12.0
OS
AmazonLinux 2023, Ubuntu
Language
TypeScript
Language Version
No response
Other information
No response