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): faster "no-op" deployments #6346

Merged
merged 4 commits into from
Feb 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/aws-cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ bootstrapped (using `cdk bootstrap`), only stacks that are not using assets and
$ cdk deploy --app='node bin/main.js' MyStackName
```

Before creating a change set, `cdk deploy` will compare the template of the
currently deployed stack to the template that is about to be deployed and will
skip deployment if they are identical. Use `--force` to override this behavior
and always deploy the stack.

#### `cdk destroy`
Deletes a stack from it's environment. This will cause the resources in the stack to be destroyed (unless they were
configured with a `DeletionPolicy` of `Retain`). During the stack destruction, the command will output progress
Expand Down
6 changes: 4 additions & 2 deletions packages/aws-cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ async function parseCommandLineArguments() {
.option('ci', { type: 'boolean', desc: 'Force CI detection (deprecated)', default: process.env.CI !== undefined })
.option('notification-arns', {type: 'array', desc: 'ARNs of SNS topics that CloudFormation will notify with stack related events', nargs: 1, requiresArg: true})
.option('tags', { type: 'array', alias: 't', desc: 'Tags to add to the stack (KEY=VALUE)', nargs: 1, requiresArg: true })
.option('execute', {type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true})
.option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true })
.option('force', { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false })
)
.command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', yargs => yargs
.option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only destroy requested stacks, don\'t include dependees' })
Expand Down Expand Up @@ -224,7 +225,8 @@ async function initCommandLine() {
reuseAssets: args['build-exclude'],
tags: configuration.settings.get(['tags']),
sdk: aws,
execute: args.execute
execute: args.execute,
force: args.force,
});

case 'destroy':
Expand Down
76 changes: 72 additions & 4 deletions packages/aws-cdk/lib/api/deploy-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as uuid from 'uuid';
import { Tag } from "../api/cxapp/stacks";
import { prepareAssets } from '../assets';
import { debug, error, print } from '../logging';
import { toYAML } from '../serialize';
import { deserializeStructure, toYAML } from '../serialize';
import { Mode } from './aws-auth/credentials';
import { ToolkitInfo } from './toolkit-info';
import { changeSetHasNoChanges, describeStack, stackExists, stackFailedCreating, waitForChangeSet, waitForStack } from './util/cloudformation';
Expand Down Expand Up @@ -54,6 +54,12 @@ export interface DeployStackOptions {
* @default - no additional parameters will be passed to the template
*/
parameters?: { [name: string]: string | undefined };

/**
* Deploy even if the deployed template is identical to the one we are about to deploy.
* @default false
*/
force?: boolean;
}

const LARGE_TEMPLATE_SIZE_KB = 50;
Expand All @@ -64,6 +70,28 @@ export async function deployStack(options: DeployStackOptions): Promise<DeploySt
throw new Error(`The stack ${options.stack.displayName} does not have an environment`);
}

const cfn = await options.sdk.cloudFormation(options.stack.environment.account, options.stack.environment.region, Mode.ForWriting);
const deployName = options.deployName || options.stack.stackName;

if (!options.force) {
debug(`checking if we can skip this stack based on the currently deployed template (use --force to override)`);
const deployed = await getDeployedTemplate(cfn, deployName);
if (deployed && JSON.stringify(options.stack.template) === JSON.stringify(deployed.template)) {
debug(`${deployName}: no change in template, skipping (use --force to override)`);
shivlaks marked this conversation as resolved.
Show resolved Hide resolved
return {
noOp: true,
outputs: await getStackOutputs(cfn, deployName),
stackArn: deployed.stackId,
stackArtifact: options.stack
};
} else {
debug(`${deployName}: template changed, deploying...`);
}
}

// bail out if the current template is exactly the same as the one we are about to deploy
// in cdk-land, this means nothing changed because assets (and therefore nested stacks) are immutable.

const params = await prepareAssets(options.stack, options.toolkitInfo, options.reuseAssets);

// add passed CloudFormation parameters
Expand All @@ -76,11 +104,8 @@ export async function deployStack(options: DeployStackOptions): Promise<DeploySt
}
}

const deployName = options.deployName || options.stack.stackName;

const executionId = uuid.v4();

const cfn = await options.sdk.cloudFormation(options.stack.environment.account, options.stack.environment.region, Mode.ForWriting);
const bodyParameter = await makeBodyParameter(options.stack, options.toolkitInfo);

if (await stackFailedCreating(cfn, deployName)) {
Expand Down Expand Up @@ -212,3 +237,46 @@ export async function destroyStack(options: DestroyStackOptions) {
}
return;
}

async function getDeployedTemplate(cfn: aws.CloudFormation, stackName: string): Promise<{ template: any, stackId: string } | undefined> {
const stackId = await getStackId(cfn, stackName);
if (!stackId) {
return undefined;
}

const template = await readCurrentTemplate(cfn, stackName);
return { stackId, template };
}

export async function readCurrentTemplate(cfn: aws.CloudFormation, stackName: string) {
try {
const response = await cfn.getTemplate({ StackName: stackName, TemplateStage: 'Original' }).promise();
return (response.TemplateBody && deserializeStructure(response.TemplateBody)) || {};
} catch (e) {
if (e.code === 'ValidationError' && e.message === `Stack with id ${stackName} does not exist`) {
return {};
} else {
throw e;
}
}
}

async function getStackId(cfn: aws.CloudFormation, stackName: string): Promise<string | undefined> {
try {
const stacks = await cfn.describeStacks({ StackName: stackName }).promise();
if (!stacks.Stacks) {
return undefined;
}
if (stacks.Stacks.length !== 1) {
return undefined;
}
eladb marked this conversation as resolved.
Show resolved Hide resolved

return stacks.Stacks[0].StackId!;

} catch (e) {
if (e.message.includes('does not exist')) {
return undefined;
}
throw e;
}
}
24 changes: 10 additions & 14 deletions packages/aws-cdk/lib/api/deployment-target.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { CloudFormationStackArtifact } from '@aws-cdk/cx-api';
import { Tag } from "../api/cxapp/stacks";
import { debug } from '../logging';
import { deserializeStructure } from '../serialize';
import { Mode } from './aws-auth/credentials';
import { deployStack, DeployStackResult } from './deploy-stack';
import { deployStack, DeployStackResult, readCurrentTemplate } from './deploy-stack';
import { loadToolkitInfo } from './toolkit-info';
import { ISDK } from './util/sdk';

Expand Down Expand Up @@ -31,6 +30,12 @@ export interface DeployStackOptions {
reuseAssets?: string[];
tags?: Tag[];
execute?: boolean;

/**
* 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
*/
force?: boolean;
}

export interface ProvisionerProps {
Expand All @@ -49,18 +54,8 @@ export class CloudFormationDeploymentTarget implements IDeploymentTarget {

public async readCurrentTemplate(stack: CloudFormationStackArtifact): Promise<Template> {
debug(`Reading existing template for stack ${stack.displayName}.`);

const cfn = await this.aws.cloudFormation(stack.environment.account, stack.environment.region, Mode.ForReading);
try {
const response = await cfn.getTemplate({ StackName: stack.stackName }).promise();
return (response.TemplateBody && deserializeStructure(response.TemplateBody)) || {};
} catch (e) {
if (e.code === 'ValidationError' && e.message === `Stack with id ${stack.stackName} does not exist`) {
return {};
} else {
throw e;
}
}
return readCurrentTemplate(cfn, stack.stackName);
}

public async deployStack(options: DeployStackOptions): Promise<DeployStackResult> {
Expand All @@ -75,7 +70,8 @@ export class CloudFormationDeploymentTarget implements IDeploymentTarget {
reuseAssets: options.reuseAssets,
toolkitInfo,
tags: options.tags,
execute: options.execute
execute: options.execute,
force: options.force
});
}
}
9 changes: 8 additions & 1 deletion packages/aws-cdk/lib/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ export class CdkToolkit {
reuseAssets: options.reuseAssets,
notificationArns: options.notificationArns,
tags,
execute: options.execute
execute: options.execute,
force: options.force
});

const message = result.noOp
Expand Down Expand Up @@ -308,6 +309,12 @@ export interface DeployOptions {
* @default true
*/
execute?: boolean;

/**
* Always deploy, even if templates are identical.
* @default false
*/
force?: boolean;
}

export interface DestroyOptions {
Expand Down
Loading