Skip to content

Commit

Permalink
feat(cli): support --no-rollback flag (#16293)
Browse files Browse the repository at this point in the history
CloudFormation recently released a feature that makes it easier
to iterate quickly on development stacks, where the rollback step
of a deployment is skipped if a deployment fails.

https://aws.amazon.com/de/blogs/aws/new-for-aws-cloudformation-quickly-retry-stack-operations-from-the-point-of-failure/

This allows a developer to fix their code and retry quickly without
losing time waiting for rollbacks.

Fixes #16289.


----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
rix0rrr committed Aug 31, 2021
1 parent ffdcd94 commit d763d90
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 4 deletions.
20 changes: 19 additions & 1 deletion packages/aws-cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ $ cdk diff --app='node bin/main.js' MyStackName --template=path/to/template.yml

### `cdk deploy`

Deploys a stack of your CDK app to it's environment. During the deployment, the toolkit will output progress
Deploys a stack of your CDK app to its environment. During the deployment, the toolkit will output progress
indications, similar to what can be observed in the AWS CloudFormation Console. If the environment was never
bootstrapped (using `cdk bootstrap`), only stacks that are not using assets and synthesize to a template that is under
51,200 bytes will successfully deploy.
Expand All @@ -154,6 +154,24 @@ currently deployed stack to the template and tags that are about to be deployed
will skip deployment if they are identical. Use `--force` to override this behavior
and always deploy the stack.

#### Disabling Rollback

If a resource fails to be created or updated, the deployment will *roll back* before the CLI returns. All changes made
up to that point will be undone (resources that were created will be deleted, updates that were made will be changed
back) in order to leave the stack in a consistent state at the end of the operation. If you are using the CDK CLI
to iterate on a development stack in your personal account, you might not require CloudFormation to leave your
stack in a consistent state, but instead would prefer to update your CDK application and try again.

To disable the rollback feature, specify `--no-rollback` (`-R` for short):

```console
$ cdk deploy --no-rollback
$ cdk deploy -R
```

NOTE: you cannot use `--no-rollback` for any updates that would cause a resource replacement, only for updates
and creations of new resources.

#### Deploying multiple stacks

You can have multiple stacks in a cdk app. An example can be found in [how to create multiple stacks](https://docs.aws.amazon.com/cdk/latest/guide/stack_how_to_create_multiple_stacks.html).
Expand Down
16 changes: 15 additions & 1 deletion packages/aws-cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,11 @@ async function parseCommandLineArguments() {
.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 })
.option('previous-parameters', { type: 'boolean', default: true, desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)' })
.option('progress', { type: 'string', choices: [StackActivityProgress.BAR, StackActivityProgress.EVENTS], desc: 'Display mode for stack activity events' }),
.option('progress', { type: 'string', choices: [StackActivityProgress.BAR, StackActivityProgress.EVENTS], desc: 'Display mode for stack activity events' })
.option('rollback', { type: 'boolean', default: true, desc: 'Rollback stack to stable state on failure (iterate more rapidly with --no-rollback or -R)' })
// Hack to get '-R' as an alias for '--no-rollback', suggested by: https://github.com/yargs/yargs/issues/1729
.option('R', { type: 'boolean', hidden: true })
.middleware(yargsNegativeAlias('R', 'rollback'), true),
)
.command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', yargs => yargs
.option('all', { type: 'boolean', default: false, desc: 'Destroy all available stacks' })
Expand Down Expand Up @@ -319,6 +323,7 @@ async function initCommandLine() {
outputsFile: configuration.settings.get(['outputsFile']),
progress: configuration.settings.get(['progress']),
ci: args.ci,
rollback: configuration.settings.get(['rollback']),
});

case 'destroy':
Expand Down Expand Up @@ -421,6 +426,15 @@ function arrayFromYargs(xs: string[]): string[] | undefined {
return xs.filter(x => x !== '');
}

function yargsNegativeAlias<T extends { [x in S | L ]: boolean | undefined }, S extends string, L extends string>(shortName: S, longName: L) {
return (argv: T) => {
if (shortName in argv && argv[shortName]) {
(argv as any)[longName] = false;
}
return argv;
};
}

initCommandLine()
.then(value => {
if (value == null) { return; }
Expand Down
8 changes: 8 additions & 0 deletions packages/aws-cdk/lib/api/cloudformation-deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ export interface DeployStackOptions {
* @default false
*/
readonly ci?: boolean;

/**
* Rollback failed deployments
*
* @default true
*/
readonly rollback?: boolean;
}

export interface DestroyStackOptions {
Expand Down Expand Up @@ -204,6 +211,7 @@ export class CloudFormationDeployments {
usePreviousParameters: options.usePreviousParameters,
progress: options.progress,
ci: options.ci,
rollback: options.rollback,
});
}

Expand Down
14 changes: 13 additions & 1 deletion packages/aws-cdk/lib/api/deploy-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,13 @@ export interface DeployStackOptions {
* @default false
*/
readonly ci?: boolean;

/**
* Rollback failed deployments
*
* @default true
*/
readonly rollback?: boolean;
}

const LARGE_TEMPLATE_SIZE_KB = 50;
Expand Down Expand Up @@ -282,7 +289,12 @@ export async function deployStack(options: DeployStackOptions): Promise<DeploySt
const execute = options.execute === undefined ? true : options.execute;
if (execute) {
debug('Initiating execution of changeset %s on stack %s', changeSet.Id, deployName);
await cfn.executeChangeSet({ StackName: deployName, ChangeSetName: changeSetName }).promise();

// Do a bit of contortions to only pass the `DisableRollback` flag if it's true. That way,
// CloudFormation won't balk at the unrecognized option in regions where the feature is not available yet.
const disableRollback = options.rollback === false ? { DisableRollback: true } : undefined;
await cfn.executeChangeSet({ StackName: deployName, ChangeSetName: changeSetName, ...disableRollback }).promise();

// eslint-disable-next-line max-len
const monitor = options.quiet ? undefined : StackActivityMonitor.withDefaultPrinter(cfn, deployName, stackArtifact, {
resourcesTotal: (changeSetDescription.Changes ?? []).length,
Expand Down
8 changes: 8 additions & 0 deletions packages/aws-cdk/lib/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ export class CdkToolkit {
usePreviousParameters: options.usePreviousParameters,
progress: options.progress,
ci: options.ci,
rollback: options.rollback,
});

const message = result.noOp
Expand Down Expand Up @@ -625,6 +626,13 @@ export interface DeployOptions {
* @default false
*/
readonly ci?: boolean;

/**
* Rollback failed deployments
*
* @default true
*/
readonly rollback?: boolean;
}

export interface DestroyOptions {
Expand Down
9 changes: 9 additions & 0 deletions packages/aws-cdk/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ export class Settings {

/**
* Parse Settings out of CLI arguments.
*
* CLI arguments in must be accessed in the CLI code via
* `configuration.settings.get(['argName'])` instead of via `args.argName`.
*
* The advantage is that they can be configured via `cdk.json` and
* `$HOME/.cdk.json`. Arguments not listed below and accessed via this object
* can only be specified on the command line.
*
* @param argv the received CLI arguments.
* @returns a new Settings object.
*/
Expand Down Expand Up @@ -272,6 +280,7 @@ export class Settings {
progress: argv.progress,
bundlingStacks,
lookups: argv.lookups,
rollback: argv.rollback,
});
}

Expand Down
2 changes: 1 addition & 1 deletion packages/aws-cdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"@aws-cdk/region-info": "0.0.0",
"@jsii/check-node": "1.33.0",
"archiver": "^5.3.0",
"aws-sdk": "^2.848.0",
"aws-sdk": "^2.979.0",
"camelcase": "^6.2.0",
"cdk-assets": "0.0.0",
"colors": "^1.4.0",
Expand Down
29 changes: 29 additions & 0 deletions packages/aws-cdk/test/api/deploy-stack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,35 @@ test('updateTerminationProtection called when termination protection is undefine
}));
});

describe('disable rollback', () => {
test('by default, we do not disable rollback (and also do not pass the flag)', async () => {
// WHEN
await deployStack({
...standardDeployStackArguments(),
});

// THEN
expect(cfnMocks.executeChangeSet).toHaveBeenCalledTimes(1);
expect(cfnMocks.executeChangeSet).not.toHaveBeenCalledWith(expect.objectContaining({
DisableRollback: expect.anything(),
}));
});

test('rollback can be disabled by setting rollback: false', async () => {
// WHEN
await deployStack({
...standardDeployStackArguments(),
rollback: false,
});

// THEN
expect(cfnMocks.executeChangeSet).toHaveBeenCalledWith(expect.objectContaining({
DisableRollback: true,
}));
});

});

/**
* Set up the mocks so that it looks like the stack exists to start with
*
Expand Down
15 changes: 15 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2319,6 +2319,21 @@ aws-sdk@^2.848.0, aws-sdk@^2.928.0:
uuid "3.3.2"
xml2js "0.4.19"

aws-sdk@^2.979.0:
version "2.979.0"
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.979.0.tgz#d0104fec763cc3eafb123e709f94866790109da4"
integrity sha512-pKKhpYZwmihCvuH3757WHY8JQI9g2wvtF3s0aiyH2xCUmX/6uekhExz/utD4uqZP3m3PwKZPGQkQkH30DtHrPw==
dependencies:
buffer "4.9.2"
events "1.1.1"
ieee754 "1.1.13"
jmespath "0.15.0"
querystring "0.2.0"
sax "1.2.1"
url "0.10.3"
uuid "3.3.2"
xml2js "0.4.19"

aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
Expand Down

0 comments on commit d763d90

Please sign in to comment.