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

feat(cli): change set name is now a constant, and --no-execute will always produce one (even if empty) #12683

12 changes: 12 additions & 0 deletions packages/aws-cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,18 @@ When `cdk deploy` is executed, deployment events will include the complete histo

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
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.

```console
$ cdk deploy --no-execute
```

### `cdk destroy`

Deletes a stack from it's environment. This will cause the resources in the stack to be destroyed (unless they were
Expand Down
34 changes: 25 additions & 9 deletions packages/aws-cdk/lib/api/deploy-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ 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 @@ -228,14 +229,20 @@ export async function deployStack(options: DeployStackOptions): Promise<DeploySt

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

const changeSetName = `CDK-${executionId}`;
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();
}

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

debug(`Attempting to create ChangeSet ${changeSetName} to ${update ? 'update' : 'create'} stack ${deployName}`);
debug(`Attempting to create ChangeSet ${CDK_CHANGE_SET_NAME} to ${update ? 'update' : 'create'} stack ${deployName}`);
print('%s: creating CloudFormation changeset...', colors.bold(deployName));
const changeSet = await cfn.createChangeSet({
StackName: deployName,
ChangeSetName: changeSetName,
ChangeSetName: CDK_CHANGE_SET_NAME,
ChangeSetType: update ? 'UPDATE' : 'CREATE',
Description: `CDK Changeset for execution ${executionId}`,
TemplateBody: bodyParameter.TemplateBody,
Expand All @@ -247,7 +254,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, changeSetName);
const changeSetDescription = await waitForChangeSet(cfn, deployName, CDK_CHANGE_SET_NAME);

// Update termination protection only if it has changed.
const terminationProtection = stackArtifact.terminationProtection ?? false;
Expand All @@ -262,21 +269,24 @@ export async function deployStack(options: DeployStackOptions): Promise<DeploySt

if (changeSetHasNoChanges(changeSetDescription)) {
debug('No changes are to be performed on %s.', deployName);
await cfn.deleteChangeSet({ StackName: deployName, ChangeSetName: changeSetName }).promise();
if (options.execute) {
debug('Deleting empty change set %s', changeSet.Id);
await cfn.deleteChangeSet({ StackName: deployName, ChangeSetName: CDK_CHANGE_SET_NAME }).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', changeSetName, deployName);
await cfn.executeChangeSet({ StackName: deployName, ChangeSetName: changeSetName }).promise();
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();
// 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...', changeSetName, deployName);
debug('Execution of changeset %s on stack %s has started; waiting for the update to complete...', CDK_CHANGE_SET_NAME, deployName);
try {
const finalStack = await waitForStackDeploy(cfn, deployName);

Expand All @@ -288,7 +298,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)', changeSetName);
print('Changeset %s created and waiting in review for manual execution (--no-execute)', CDK_CHANGE_SET_NAME);
}

return { noOp: false, outputs: cloudFormationStack.outputs, stackArn: changeSet.StackId!, stackArtifact };
Expand Down Expand Up @@ -408,6 +418,12 @@ async function canSkipDeploy(
return false;
}

// Creating changeset only (default true), never skip
if (deployStackOptions.execute === false) {
debug(`${deployName}: --no-execute, always creating change set`);
return false;
}

// No existing stack
if (!cloudFormationStack.exists) {
debug(`${deployName}: no existing stack`);
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/test/api/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ beforeEach(() => {
executed = true;
return {};
}),
deleteChangeSet: jest.fn(),
getTemplate: jest.fn(() => {
executed = true;
return {};
Expand Down
47 changes: 47 additions & 0 deletions packages/aws-cdk/test/api/deploy-stack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,53 @@ test('not executed and no error if --no-execute is given', async () => {
expect(cfnMocks.executeChangeSet).not.toHaveBeenCalled();
});

test('empty change set is deleted if --execute is given', async () => {
cfnMocks.describeChangeSet?.mockImplementation(() => ({
Status: 'FAILED',
StatusReason: 'No updates are to be performed.',
}));

// GIVEN
givenStackExists();

// WHEN
await deployStack({
...standardDeployStackArguments(),
execute: true,
force: true, // Necessary to bypass "skip deploy"
});

// THEN
expect(cfnMocks.createChangeSet).toHaveBeenCalled();
expect(cfnMocks.executeChangeSet).not.toHaveBeenCalled();

//the first deletion is for any existing cdk change sets, the second is for the deleting the new empty change set
expect(cfnMocks.deleteChangeSet).toHaveBeenCalledTimes(2);
});

test('empty change set is not deleted if --no-execute is given', async () => {
cfnMocks.describeChangeSet?.mockImplementation(() => ({
Status: 'FAILED',
StatusReason: 'No updates are to be performed.',
}));

// GIVEN
givenStackExists();

// WHEN
await deployStack({
...standardDeployStackArguments(),
execute: false,
});

// THEN
expect(cfnMocks.createChangeSet).toHaveBeenCalled();
expect(cfnMocks.executeChangeSet).not.toHaveBeenCalled();

//the first deletion is for any existing cdk change sets
expect(cfnMocks.deleteChangeSet).toHaveBeenCalledTimes(1);
});

test('use S3 url for stack deployment if present in Stack Artifact', async () => {
// WHEN
await deployStack({
Expand Down