Skip to content

Commit

Permalink
feat(cli): Configurable --change-set-name CLI flag (#13024)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
Wenzil authored Feb 23, 2021
1 parent e628a73 commit 18184df
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 17 deletions.
7 changes: 4 additions & 3 deletions packages/aws-cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,13 +288,14 @@ The `progress` key can also be specified as a user setting (`~/.cdk.json`)
#### Externally Executable CloudFormation Change Sets

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

```console
$ cdk deploy --no-execute
$ cdk deploy --no-execute --change-set-name MyChangeSetName
```

### `cdk destroy`
Expand Down
2 changes: 2 additions & 0 deletions packages/aws-cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ async function parseCommandLineArguments() {
// @deprecated(v2) -- tags are part of the Cloud Assembly and tags specified here will be overwritten on the next deployment
.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 })
.option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true })
.option('change-set-name', { type: 'string', desc: 'Name of the CloudFormation change set to create' })
.option('force', { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false })
.option('parameters', { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', nargs: 1, requiresArg: true, default: {} })
.option('outputs-file', { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true })
Expand Down Expand Up @@ -316,6 +317,7 @@ async function initCommandLine() {
reuseAssets: args['build-exclude'],
tags: configuration.settings.get(['tags']),
execute: args.execute,
changeSetName: args.changeSetName,
force: args.force,
parameters: parameterMap,
usePreviousParameters: args['previous-parameters'],
Expand Down
7 changes: 7 additions & 0 deletions packages/aws-cdk/lib/api/cloudformation-deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ export interface DeployStackOptions {
*/
execute?: boolean;

/**
* Optional name to use for the CloudFormation change set.
* If not provided, a name will be generated automatically.
*/
changeSetName?: string;

/**
* Force deployment, even if the deployed template is identical to the one we are about to deploy.
* @default false deployment will be skipped if the template is identical
Expand Down Expand Up @@ -173,6 +179,7 @@ export class CloudFormationDeployments {
toolkitInfo,
tags: options.tags,
execute: options.execute,
changeSetName: options.changeSetName,
force: options.force,
parameters: options.parameters,
usePreviousParameters: options.usePreviousParameters,
Expand Down
28 changes: 17 additions & 11 deletions packages/aws-cdk/lib/api/deploy-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ export interface DeployStackOptions {
*/
execute?: boolean;

/**
* Optional name to use for the CloudFormation change set.
* If not provided, a name will be generated automatically.
*/
changeSetName?: string;

/**
* The collection of extra parameters
* (in addition to those used for assets)
Expand Down Expand Up @@ -174,7 +180,6 @@ export interface DeployStackOptions {
}

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

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

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

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

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

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

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

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

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

return { noOp: false, outputs: cloudFormationStack.outputs, stackArn: changeSet.StackId!, stackArtifact };
Expand Down
7 changes: 7 additions & 0 deletions packages/aws-cdk/lib/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export class CdkToolkit {
notificationArns: options.notificationArns,
tags,
execute: options.execute,
changeSetName: options.changeSetName,
force: options.force,
parameters: Object.assign({}, parameterMap['*'], parameterMap[stack.stackName]),
usePreviousParameters: options.usePreviousParameters,
Expand Down Expand Up @@ -554,6 +555,12 @@ export interface DeployOptions {
*/
execute?: boolean;

/**
* Optional name to use for the CloudFormation change set.
* If not provided, a name will be generated automatically.
*/
changeSetName?: string;

/**
* Always deploy, even if templates are identical.
* @default false
Expand Down
15 changes: 12 additions & 3 deletions packages/aws-cdk/test/integ/cli/cli.integtest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,10 @@ integTest('nested stack with parameters', withDefaultFixture(async (fixture) =>
expect(response.StackResources?.length).toEqual(1);
}));

integTest('deploy without execute', withDefaultFixture(async (fixture) => {
integTest('deploy without execute a named change set', withDefaultFixture(async (fixture) => {
const changeSetName = 'custom-change-set-name';
const stackArn = await fixture.cdkDeploy('test-2', {
options: ['--no-execute'],
options: ['--no-execute', '--change-set-name', changeSetName],
captureStderr: false,
});
// verify that we only deployed a single stack (there's a single ARN in the output)
Expand All @@ -150,8 +151,16 @@ integTest('deploy without execute', withDefaultFixture(async (fixture) => {
const response = await fixture.aws.cloudFormation('describeStacks', {
StackName: stackArn,
});

expect(response.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS');

//verify a change set was created with the provided name
const changeSetResponse = await fixture.aws.cloudFormation('listChangeSets', {
StackName: stackArn,
});
const changeSets = changeSetResponse.Summaries || [];
expect(changeSets.length).toEqual(1);
expect(changeSets[0].ChangeSetName).toEqual(changeSetName);
expect(changeSets[0].Status).toEqual('CREATE_COMPLETE');
}));

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

0 comments on commit 18184df

Please sign in to comment.