Skip to content

Commit 18184df

Browse files
authored
feat(cli): Configurable --change-set-name CLI flag (#13024)
closes #11075 This PR is based on @swar8080's work in #12683. Adds the following CLI flag: `--change-set-name`: Optional name of the CloudFormation change set to create, instead of using the default one. An external script or the CodePipeline CloudFormation action can use this name to later deploy the changes. Motivation: see #12683 (comment)
1 parent e628a73 commit 18184df

File tree

6 files changed

+49
-17
lines changed

6 files changed

+49
-17
lines changed

packages/aws-cdk/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,13 +288,14 @@ The `progress` key can also be specified as a user setting (`~/.cdk.json`)
288288
#### Externally Executable CloudFormation Change Sets
289289

290290
For more control over when stack changes are deployed, the CDK can generate a
291-
CloudFormation change set but not execute it. The name of the generated
291+
CloudFormation change set but not execute it. The default name of the generated
292292
change set is *cdk-deploy-change-set*, and a previous change set with that
293293
name will be overwritten. The change set will always be created, even if it
294-
is empty.
294+
is empty. A name can also be given to the change set to make it easier to later
295+
execute.
295296

296297
```console
297-
$ cdk deploy --no-execute
298+
$ cdk deploy --no-execute --change-set-name MyChangeSetName
298299
```
299300

300301
### `cdk destroy`

packages/aws-cdk/bin/cdk.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ async function parseCommandLineArguments() {
9494
// @deprecated(v2) -- tags are part of the Cloud Assembly and tags specified here will be overwritten on the next deployment
9595
.option('tags', { type: 'array', alias: 't', desc: 'Tags to add to the stack (KEY=VALUE), overrides tags from Cloud Assembly (deprecated)', nargs: 1, requiresArg: true })
9696
.option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true })
97+
.option('change-set-name', { type: 'string', desc: 'Name of the CloudFormation change set to create' })
9798
.option('force', { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false })
9899
.option('parameters', { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', nargs: 1, requiresArg: true, default: {} })
99100
.option('outputs-file', { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true })
@@ -316,6 +317,7 @@ async function initCommandLine() {
316317
reuseAssets: args['build-exclude'],
317318
tags: configuration.settings.get(['tags']),
318319
execute: args.execute,
320+
changeSetName: args.changeSetName,
319321
force: args.force,
320322
parameters: parameterMap,
321323
usePreviousParameters: args['previous-parameters'],

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ export interface DeployStackOptions {
6969
*/
7070
execute?: boolean;
7171

72+
/**
73+
* Optional name to use for the CloudFormation change set.
74+
* If not provided, a name will be generated automatically.
75+
*/
76+
changeSetName?: string;
77+
7278
/**
7379
* Force deployment, even if the deployed template is identical to the one we are about to deploy.
7480
* @default false deployment will be skipped if the template is identical
@@ -173,6 +179,7 @@ export class CloudFormationDeployments {
173179
toolkitInfo,
174180
tags: options.tags,
175181
execute: options.execute,
182+
changeSetName: options.changeSetName,
176183
force: options.force,
177184
parameters: options.parameters,
178185
usePreviousParameters: options.usePreviousParameters,

packages/aws-cdk/lib/api/deploy-stack.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ export interface DeployStackOptions {
131131
*/
132132
execute?: boolean;
133133

134+
/**
135+
* Optional name to use for the CloudFormation change set.
136+
* If not provided, a name will be generated automatically.
137+
*/
138+
changeSetName?: string;
139+
134140
/**
135141
* The collection of extra parameters
136142
* (in addition to those used for assets)
@@ -174,7 +180,6 @@ export interface DeployStackOptions {
174180
}
175181

176182
const LARGE_TEMPLATE_SIZE_KB = 50;
177-
const CDK_CHANGE_SET_NAME = 'cdk-deploy-change-set';
178183

179184
/** @experimental */
180185
export async function deployStack(options: DeployStackOptions): Promise<DeployStackResult> {
@@ -229,20 +234,21 @@ export async function deployStack(options: DeployStackOptions): Promise<DeploySt
229234

230235
await publishAssets(legacyAssets.toManifest(stackArtifact.assembly.directory), options.sdkProvider, stackEnv);
231236

237+
const changeSetName = options.changeSetName || 'cdk-deploy-change-set';
232238
if (cloudFormationStack.exists) {
233239
//Delete any existing change sets generated by CDK since change set names must be unique.
234240
//The delete request is successful as long as the stack exists (even if the change set does not exist).
235-
debug(`Removing existing change set with name ${CDK_CHANGE_SET_NAME} if it exists`);
236-
await cfn.deleteChangeSet({ StackName: deployName, ChangeSetName: CDK_CHANGE_SET_NAME }).promise();
241+
debug(`Removing existing change set with name ${changeSetName} if it exists`);
242+
await cfn.deleteChangeSet({ StackName: deployName, ChangeSetName: changeSetName }).promise();
237243
}
238244

239245
const update = cloudFormationStack.exists && cloudFormationStack.stackStatus.name !== 'REVIEW_IN_PROGRESS';
240246

241-
debug(`Attempting to create ChangeSet ${CDK_CHANGE_SET_NAME} to ${update ? 'update' : 'create'} stack ${deployName}`);
247+
debug(`Attempting to create ChangeSet with name ${changeSetName} to ${update ? 'update' : 'create'} stack ${deployName}`);
242248
print('%s: creating CloudFormation changeset...', colors.bold(deployName));
243249
const changeSet = await cfn.createChangeSet({
244250
StackName: deployName,
245-
ChangeSetName: CDK_CHANGE_SET_NAME,
251+
ChangeSetName: changeSetName,
246252
ChangeSetType: update ? 'UPDATE' : 'CREATE',
247253
Description: `CDK Changeset for execution ${executionId}`,
248254
TemplateBody: bodyParameter.TemplateBody,
@@ -254,7 +260,7 @@ export async function deployStack(options: DeployStackOptions): Promise<DeploySt
254260
Tags: options.tags,
255261
}).promise();
256262
debug('Initiated creation of changeset: %s; waiting for it to finish creating...', changeSet.Id);
257-
const changeSetDescription = await waitForChangeSet(cfn, deployName, CDK_CHANGE_SET_NAME);
263+
const changeSetDescription = await waitForChangeSet(cfn, deployName, changeSetName);
258264

259265
// Update termination protection only if it has changed.
260266
const terminationProtection = stackArtifact.terminationProtection ?? false;
@@ -271,22 +277,22 @@ export async function deployStack(options: DeployStackOptions): Promise<DeploySt
271277
debug('No changes are to be performed on %s.', deployName);
272278
if (options.execute) {
273279
debug('Deleting empty change set %s', changeSet.Id);
274-
await cfn.deleteChangeSet({ StackName: deployName, ChangeSetName: CDK_CHANGE_SET_NAME }).promise();
280+
await cfn.deleteChangeSet({ StackName: deployName, ChangeSetName: changeSetName }).promise();
275281
}
276282
return { noOp: true, outputs: cloudFormationStack.outputs, stackArn: changeSet.StackId!, stackArtifact };
277283
}
278284

279285
const execute = options.execute === undefined ? true : options.execute;
280286
if (execute) {
281-
debug('Initiating execution of changeset %s on stack %s', CDK_CHANGE_SET_NAME, deployName);
282-
await cfn.executeChangeSet({ StackName: deployName, ChangeSetName: CDK_CHANGE_SET_NAME }).promise();
287+
debug('Initiating execution of changeset %s on stack %s', changeSet.Id, deployName);
288+
await cfn.executeChangeSet({ StackName: deployName, ChangeSetName: changeSetName }).promise();
283289
// eslint-disable-next-line max-len
284290
const monitor = options.quiet ? undefined : StackActivityMonitor.withDefaultPrinter(cfn, deployName, stackArtifact, {
285291
resourcesTotal: (changeSetDescription.Changes ?? []).length,
286292
progress: options.progress,
287293
changeSetCreationTime: changeSetDescription.CreationTime,
288294
}).start();
289-
debug('Execution of changeset %s on stack %s has started; waiting for the update to complete...', CDK_CHANGE_SET_NAME, deployName);
295+
debug('Execution of changeset %s on stack %s has started; waiting for the update to complete...', changeSet.Id, deployName);
290296
try {
291297
const finalStack = await waitForStackDeploy(cfn, deployName);
292298

@@ -298,7 +304,7 @@ export async function deployStack(options: DeployStackOptions): Promise<DeploySt
298304
}
299305
debug('Stack %s has completed updating', deployName);
300306
} else {
301-
print('Changeset %s created and waiting in review for manual execution (--no-execute)', CDK_CHANGE_SET_NAME);
307+
print('Changeset %s created and waiting in review for manual execution (--no-execute)', changeSet.Id);
302308
}
303309

304310
return { noOp: false, outputs: cloudFormationStack.outputs, stackArn: changeSet.StackId!, stackArtifact };

packages/aws-cdk/lib/cdk-toolkit.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ export class CdkToolkit {
186186
notificationArns: options.notificationArns,
187187
tags,
188188
execute: options.execute,
189+
changeSetName: options.changeSetName,
189190
force: options.force,
190191
parameters: Object.assign({}, parameterMap['*'], parameterMap[stack.stackName]),
191192
usePreviousParameters: options.usePreviousParameters,
@@ -554,6 +555,12 @@ export interface DeployOptions {
554555
*/
555556
execute?: boolean;
556557

558+
/**
559+
* Optional name to use for the CloudFormation change set.
560+
* If not provided, a name will be generated automatically.
561+
*/
562+
changeSetName?: string;
563+
557564
/**
558565
* Always deploy, even if templates are identical.
559566
* @default false

packages/aws-cdk/test/integ/cli/cli.integtest.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,10 @@ integTest('nested stack with parameters', withDefaultFixture(async (fixture) =>
139139
expect(response.StackResources?.length).toEqual(1);
140140
}));
141141

142-
integTest('deploy without execute', withDefaultFixture(async (fixture) => {
142+
integTest('deploy without execute a named change set', withDefaultFixture(async (fixture) => {
143+
const changeSetName = 'custom-change-set-name';
143144
const stackArn = await fixture.cdkDeploy('test-2', {
144-
options: ['--no-execute'],
145+
options: ['--no-execute', '--change-set-name', changeSetName],
145146
captureStderr: false,
146147
});
147148
// verify that we only deployed a single stack (there's a single ARN in the output)
@@ -150,8 +151,16 @@ integTest('deploy without execute', withDefaultFixture(async (fixture) => {
150151
const response = await fixture.aws.cloudFormation('describeStacks', {
151152
StackName: stackArn,
152153
});
153-
154154
expect(response.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS');
155+
156+
//verify a change set was created with the provided name
157+
const changeSetResponse = await fixture.aws.cloudFormation('listChangeSets', {
158+
StackName: stackArn,
159+
});
160+
const changeSets = changeSetResponse.Summaries || [];
161+
expect(changeSets.length).toEqual(1);
162+
expect(changeSets[0].ChangeSetName).toEqual(changeSetName);
163+
expect(changeSets[0].Status).toEqual('CREATE_COMPLETE');
155164
}));
156165

157166
integTest('security related changes without a CLI are expected to fail', withDefaultFixture(async (fixture) => {

0 commit comments

Comments
 (0)