Skip to content

Commit 3507141

Browse files
authored
fix(cdk): Resolve cross stack and default parameters for hotswaps (#27195)
This PR solves two problems when doing hotswap deployments with nested stacks. 1. Adding capabilities to evaluate CFN parameters of `AWS::CloudFormation::Stack` type (i.e. a nested stack). In this PR, we are only resolving `Outputs` section of `AWS::CloudFormation::Stack` and it can be expanded to other fields in the future. See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-cloudformation.html#w2ab1c17c23c19b5 for CFN documentation around using Outputs ref parameters 2. If a template has parameters with default values and they are not provided (a value) by the parent stack when invoking, then resolve these parameters using the default values in the template. Partially helps #23533 and #25418 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 7df11f4 commit 3507141

6 files changed

+315
-18
lines changed

packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as AWS from 'aws-sdk';
22
import { ISDK } from './aws-auth';
3+
import { NestedStackNames } from './nested-stack-helpers';
34

45
export interface ListStackResources {
56
listStackResources(): Promise<AWS.CloudFormation.StackResourceSummary[]>;
@@ -42,27 +43,33 @@ export interface ResourceDefinition {
4243
}
4344

4445
export interface EvaluateCloudFormationTemplateProps {
46+
readonly stackName: string;
4547
readonly template: Template;
4648
readonly parameters: { [parameterName: string]: string };
4749
readonly account: string;
4850
readonly region: string;
4951
readonly partition: string;
5052
readonly urlSuffix: (region: string) => string;
51-
readonly listStackResources: ListStackResources;
53+
readonly sdk: ISDK;
54+
readonly nestedStackNames?: { [nestedStackLogicalId: string]: NestedStackNames };
5255
}
5356

5457
export class EvaluateCloudFormationTemplate {
55-
private readonly stackResources: ListStackResources;
58+
private readonly stackName: string;
5659
private readonly template: Template;
5760
private readonly context: { [k: string]: any };
5861
private readonly account: string;
5962
private readonly region: string;
6063
private readonly partition: string;
6164
private readonly urlSuffix: (region: string) => string;
65+
private readonly sdk: ISDK;
66+
private readonly nestedStackNames: { [nestedStackLogicalId: string]: NestedStackNames };
67+
private readonly stackResources: LazyListStackResources;
68+
6269
private cachedUrlSuffix: string | undefined;
6370

6471
constructor(props: EvaluateCloudFormationTemplateProps) {
65-
this.stackResources = props.listStackResources;
72+
this.stackName = props.stackName;
6673
this.template = props.template;
6774
this.context = {
6875
'AWS::AccountId': props.account,
@@ -74,22 +81,34 @@ export class EvaluateCloudFormationTemplate {
7481
this.region = props.region;
7582
this.partition = props.partition;
7683
this.urlSuffix = props.urlSuffix;
84+
this.sdk = props.sdk;
85+
86+
// We need names of nested stack so we can evaluate cross stack references
87+
this.nestedStackNames = props.nestedStackNames ?? {};
88+
89+
// The current resources of the Stack.
90+
// We need them to figure out the physical name of a resource in case it wasn't specified by the user.
91+
// We fetch it lazily, to save a service call, in case all hotswapped resources have their physical names set.
92+
this.stackResources = new LazyListStackResources(this.sdk, this.stackName);
7793
}
7894

7995
// clones current EvaluateCloudFormationTemplate object, but updates the stack name
80-
public createNestedEvaluateCloudFormationTemplate(
81-
listNestedStackResources: ListStackResources,
96+
public async createNestedEvaluateCloudFormationTemplate(
97+
stackName: string,
8298
nestedTemplate: Template,
8399
nestedStackParameters: { [parameterName: string]: any },
84100
) {
101+
const evaluatedParams = await this.evaluateCfnExpression(nestedStackParameters);
85102
return new EvaluateCloudFormationTemplate({
103+
stackName,
86104
template: nestedTemplate,
87-
parameters: nestedStackParameters,
105+
parameters: evaluatedParams,
88106
account: this.account,
89107
region: this.region,
90108
partition: this.partition,
91109
urlSuffix: this.urlSuffix,
92-
listStackResources: listNestedStackResources,
110+
sdk: this.sdk,
111+
nestedStackNames: this.nestedStackNames,
93112
});
94113
}
95114

@@ -262,20 +281,52 @@ export class EvaluateCloudFormationTemplate {
262281
return this.cachedUrlSuffix;
263282
}
264283

284+
// Try finding the ref in the passed in parameters
265285
const parameterTarget = this.context[logicalId];
266286
if (parameterTarget) {
267287
return parameterTarget;
268288
}
289+
290+
// If not in the passed in parameters, see if there is a default value in the template parameter that was not passed in
291+
const defaultParameterValue = this.template.Parameters?.[logicalId]?.Default;
292+
if (defaultParameterValue) {
293+
return defaultParameterValue;
294+
}
295+
269296
// if it's not a Parameter, we need to search in the current Stack resources
270297
return this.findGetAttTarget(logicalId);
271298
}
272299

273300
private async findGetAttTarget(logicalId: string, attribute?: string): Promise<string | undefined> {
301+
302+
// Handle case where the attribute is referencing a stack output (used in nested stacks to share parameters)
303+
// See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-cloudformation.html#w2ab1c17c23c19b5
304+
if (logicalId === 'Outputs' && attribute) {
305+
return this.evaluateCfnExpression(this.template.Outputs[attribute]?.Value);
306+
}
307+
274308
const stackResources = await this.stackResources.listStackResources();
275309
const foundResource = stackResources.find(sr => sr.LogicalResourceId === logicalId);
276310
if (!foundResource) {
277311
return undefined;
278312
}
313+
314+
if (foundResource.ResourceType == 'AWS::CloudFormation::Stack' && attribute?.startsWith('Outputs.')) {
315+
// need to resolve attributes from another stack's Output section
316+
const dependantStackName = this.nestedStackNames[logicalId]?.nestedStackPhysicalName;
317+
if (!dependantStackName) {
318+
//this is a newly created nested stack and cannot be hotswapped
319+
return undefined;
320+
}
321+
const dependantStackTemplate = this.template.Resources[logicalId];
322+
const evaluateCfnTemplate = await this.createNestedEvaluateCloudFormationTemplate(
323+
dependantStackName,
324+
dependantStackTemplate?.Properties?.NestedTemplate,
325+
dependantStackTemplate.newValue?.Properties?.Parameters);
326+
327+
// Split Outputs.<refName> into 'Outputs' and '<refName>' and recursively call evaluate
328+
return evaluateCfnTemplate.evaluateCfnExpression({ 'Fn::GetAtt': attribute.split(/\.(.*)/s) });
329+
}
279330
// now, we need to format the appropriate identifier depending on the resource type,
280331
// and the requested attribute name
281332
return this.formatResourceAttribute(foundResource, attribute);

packages/aws-cdk/lib/api/hotswap-deployments.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as cxapi from '@aws-cdk/cx-api';
33
import * as chalk from 'chalk';
44
import { ISDK, Mode, SdkProvider } from './aws-auth';
55
import { DeployStackResult } from './deploy-stack';
6-
import { EvaluateCloudFormationTemplate, LazyListStackResources } from './evaluate-cloudformation-template';
6+
import { EvaluateCloudFormationTemplate } from './evaluate-cloudformation-template';
77
import { isHotswappableAppSyncChange } from './hotswap/appsync-mapping-templates';
88
import { isHotswappableCodeBuildProjectChange } from './hotswap/code-build-projects';
99
import { ICON, ChangeHotswapResult, HotswapMode, HotswappableChange, NonHotswappableChange, HotswappableChangeCandidate, ClassifiedResourceChanges, reportNonHotswappableChange } from './hotswap/common';
@@ -24,6 +24,7 @@ const RESOURCE_DETECTORS: { [key:string]: HotswapDetector } = {
2424
'AWS::Lambda::Function': isHotswappableLambdaFunctionChange,
2525
'AWS::Lambda::Version': isHotswappableLambdaFunctionChange,
2626
'AWS::Lambda::Alias': isHotswappableLambdaFunctionChange,
27+
2728
// AppSync
2829
'AWS::AppSync::Resolver': isHotswappableAppSyncChange,
2930
'AWS::AppSync::FunctionConfiguration': isHotswappableAppSyncChange,
@@ -54,21 +55,21 @@ export async function tryHotswapDeployment(
5455
// create a new SDK using the CLI credentials, because the default one will not work for new-style synthesis -
5556
// it assumes the bootstrap deploy Role, which doesn't have permissions to update Lambda functions
5657
const sdk = (await sdkProvider.forEnvironment(resolvedEnv, Mode.ForWriting)).sdk;
57-
// The current resources of the Stack.
58-
// We need them to figure out the physical name of a resource in case it wasn't specified by the user.
59-
// We fetch it lazily, to save a service call, in case all hotswapped resources have their physical names set.
60-
const listStackResources = new LazyListStackResources(sdk, stackArtifact.stackName);
58+
59+
const currentTemplate = await loadCurrentTemplateWithNestedStacks(stackArtifact, sdk);
60+
6161
const evaluateCfnTemplate = new EvaluateCloudFormationTemplate({
62+
stackName: stackArtifact.stackName,
6263
template: stackArtifact.template,
6364
parameters: assetParams,
6465
account: resolvedEnv.account,
6566
region: resolvedEnv.region,
6667
partition: (await sdk.currentAccount()).partition,
6768
urlSuffix: (region) => sdk.getEndpointSuffix(region),
68-
listStackResources,
69+
sdk,
70+
nestedStackNames: currentTemplate.nestedStackNames,
6971
});
7072

71-
const currentTemplate = await loadCurrentTemplateWithNestedStacks(stackArtifact, sdk);
7273
const stackChanges = cfn_diff.diffTemplate(currentTemplate.deployedTemplate, stackArtifact.template);
7374
const { hotswappableChanges, nonHotswappableChanges } = await classifyResourceChanges(
7475
stackChanges, evaluateCfnTemplate, sdk, currentTemplate.nestedStackNames,
@@ -231,9 +232,8 @@ async function findNestedHotswappableChanges(
231232
};
232233
}
233234

234-
const nestedStackParameters = await evaluateCfnTemplate.evaluateCfnExpression(change.newValue?.Properties?.Parameters);
235-
const evaluateNestedCfnTemplate = evaluateCfnTemplate.createNestedEvaluateCloudFormationTemplate(
236-
new LazyListStackResources(sdk, nestedStackName), change.newValue?.Properties?.NestedTemplate, nestedStackParameters,
235+
const evaluateNestedCfnTemplate = await evaluateCfnTemplate.createNestedEvaluateCloudFormationTemplate(
236+
nestedStackName, change.newValue?.Properties?.NestedTemplate, change.newValue?.Properties?.Parameters,
237237
);
238238

239239
const nestedDiff = cfn_diff.diffTemplate(

packages/aws-cdk/lib/api/logs/find-cloudwatch-logs.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,14 @@ export async function findCloudWatchLogGroups(
5656

5757
const listStackResources = new LazyListStackResources(sdk, stackArtifact.stackName);
5858
const evaluateCfnTemplate = new EvaluateCloudFormationTemplate({
59+
stackName: stackArtifact.stackName,
5960
template: stackArtifact.template,
6061
parameters: {},
6162
account: resolvedEnv.account,
6263
region: resolvedEnv.region,
6364
partition: (await sdk.currentAccount()).partition,
6465
urlSuffix: (region) => sdk.getEndpointSuffix(region),
65-
listStackResources,
66+
sdk,
6667
});
6768

6869
const stackResources = await listStackResources.listStackResources();

0 commit comments

Comments
 (0)