Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { DescribeStackResourcesCommand } from '@aws-sdk/client-cloudformation';
import { UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda';
import { integTest, withDefaultFixture } from '../../../lib';
import { waitForLambdaUpdateComplete } from '../drift/drift_helpers';

jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime

integTest(
'deploy with revert-drift true',
withDefaultFixture(async (fixture) => {
await fixture.cdkDeploy('driftable', {});

// Get the Lambda, we want to now make it drift
const response = await fixture.aws.cloudFormation.send(
new DescribeStackResourcesCommand({
StackName: fixture.fullStackName('driftable'),
}),
);
const lambdaResource = response.StackResources?.find(
resource => resource.ResourceType === 'AWS::Lambda::Function',
);
if (!lambdaResource || !lambdaResource.PhysicalResourceId) {
throw new Error('Could not find Lambda function in stack resources');
}
const functionName = lambdaResource.PhysicalResourceId;

// Update the Lambda function, introducing drift
await fixture.aws.lambda.send(
new UpdateFunctionConfigurationCommand({
FunctionName: functionName,
Description: 'I\'m slowly drifting (drifting away)',
}),
);

// Wait for the stack update to complete
await waitForLambdaUpdateComplete(fixture, functionName);

const drifted = await fixture.cdk(['drift', fixture.fullStackName('driftable')], { verbose: false });

expect(drifted).toMatch(/Stack.*driftable/);
expect(drifted).toContain('1 resource has drifted');

// Update the Stack with drift-aware
await fixture.cdkDeploy('driftable', {
options: ['--revert-drift'],
captureStderr: false,
});

// After performing a drift-aware deployment, verify that no drift has occurred.
const noDrifted = await fixture.cdk(['drift', fixture.fullStackName('driftable')], { verbose: false });

expect(noDrifted).toMatch(/Stack.*driftable/); // can't just .toContain because of formatting
expect(noDrifted).toContain('No drift detected');
}),
);
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DescribeStackResourcesCommand } from '@aws-sdk/client-cloudformation';
import { GetFunctionCommand, UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda';
import { integTest, sleep, withDefaultFixture } from '../../../lib';
import { UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda';
import { waitForLambdaUpdateComplete } from './drift_helpers';
import { integTest, withDefaultFixture } from '../../../lib';

jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime

Expand Down Expand Up @@ -44,34 +45,3 @@ integTest(
).rejects.toThrow('exited with error');
}),
);

async function waitForLambdaUpdateComplete(fixture: any, functionName: string): Promise<void> {
const delaySeconds = 5;
const timeout = 30_000; // timeout after 30s
const deadline = Date.now() + timeout;

while (true) {
const response = await fixture.aws.lambda.send(
new GetFunctionCommand({
FunctionName: functionName,
}),
);

const lastUpdateStatus = response.Configuration?.LastUpdateStatus;

if (lastUpdateStatus === 'Successful') {
return; // Update completed successfully
}

if (lastUpdateStatus === 'Failed') {
throw new Error('Lambda function update failed');
}

if (Date.now() > deadline) {
throw new Error(`Timed out after ${timeout / 1000} seconds.`);
}

// Wait before checking again
await sleep(delaySeconds * 1000);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DescribeStackResourcesCommand } from '@aws-sdk/client-cloudformation';
import { GetFunctionCommand, UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda';
import { integTest, sleep, withDefaultFixture } from '../../../lib';
import { UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda';
import { waitForLambdaUpdateComplete } from './drift_helpers';
import { integTest, withDefaultFixture } from '../../../lib';

jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime

Expand Down Expand Up @@ -63,34 +64,3 @@ integTest(
}
}),
);

async function waitForLambdaUpdateComplete(fixture: any, functionName: string): Promise<void> {
const delaySeconds = 5;
const timeout = 30_000; // timeout after 30s
const deadline = Date.now() + timeout;

while (true) {
const response = await fixture.aws.lambda.send(
new GetFunctionCommand({
FunctionName: functionName,
}),
);

const lastUpdateStatus = response.Configuration?.LastUpdateStatus;

if (lastUpdateStatus === 'Successful') {
return; // Update completed successfully
}

if (lastUpdateStatus === 'Failed') {
throw new Error('Lambda function update failed');
}

if (Date.now() > deadline) {
throw new Error(`Timed out after ${timeout / 1000} seconds.`);
}

// Wait before checking again
await sleep(delaySeconds * 1000);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { GetFunctionCommand } from '@aws-sdk/client-lambda';
import { sleep } from '../../../lib';

export async function waitForLambdaUpdateComplete(fixture: any, functionName: string): Promise<void> {
const delaySeconds = 5;
const timeout = 30_000; // timeout after 30s
const deadline = Date.now() + timeout;

while (true) {
const response = await fixture.aws.lambda.send(
new GetFunctionCommand({
FunctionName: functionName,
}),
);

const lastUpdateStatus = response.Configuration?.LastUpdateStatus;

if (lastUpdateStatus === 'Successful') {
return; // Update completed successfully
}

if (lastUpdateStatus === 'Failed') {
throw new Error('Lambda function update failed');
}

if (Date.now() > deadline) {
throw new Error(`Timed out after ${timeout / 1000} seconds.`);
}

// Wait before checking again
await sleep(delaySeconds * 1000);
}
}
7 changes: 7 additions & 0 deletions packages/@aws-cdk/toolkit-lib/lib/actions/deploy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ export interface ChangeSetDeployment {
* @default false
*/
readonly importExistingResources?: boolean;

/**
* Creates a drift-aware change set that brings actual resource states in line with template definitions.
*
* @default false
*/
readonly revertDrift?: boolean;
}

/**
Expand Down
15 changes: 13 additions & 2 deletions packages/@aws-cdk/toolkit-lib/lib/api/deployments/deploy-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,8 @@ class FullCloudFormationDeployment {
const changeSetName = deploymentMethod.changeSetName ?? 'cdk-deploy-change-set';
const execute = deploymentMethod.execute ?? true;
const importExistingResources = deploymentMethod.importExistingResources ?? false;
const changeSetDescription = await this.createChangeSet(changeSetName, execute, importExistingResources);
const revertDrift = deploymentMethod.revertDrift ?? false;
const changeSetDescription = await this.createChangeSet(changeSetName, execute, importExistingResources, revertDrift);
await this.updateTerminationProtection();

if (changeSetHasNoChanges(changeSetDescription)) {
Expand Down Expand Up @@ -495,7 +496,7 @@ class FullCloudFormationDeployment {
return this.executeChangeSet(changeSetDescription);
}

private async createChangeSet(changeSetName: string, willExecute: boolean, importExistingResources: boolean) {
private async createChangeSet(changeSetName: string, willExecute: boolean, importExistingResources: boolean, revertDrift: boolean) {
await this.cleanupOldChangeset(changeSetName);

await this.ioHelper.defaults.debug(`Attempting to create ChangeSet with name ${changeSetName} to ${this.verb} stack ${this.stackName}`);
Expand All @@ -508,6 +509,7 @@ class FullCloudFormationDeployment {
Description: `CDK Changeset for execution ${this.uuid}`,
ClientToken: `create${this.uuid}`,
ImportExistingResources: importExistingResources,
DeploymentMode: revertDrift ? 'REVERT_DRIFT' : undefined,
...this.commonPrepareOptions(),
});

Expand Down Expand Up @@ -774,6 +776,15 @@ async function canSkipDeploy(
return false;
}

// Drift-aware
if (
deployStackOptions.deploymentMethod?.method === 'change-set' &&
deployStackOptions.deploymentMethod.revertDrift
) {
await ioHelper.defaults.debug(`${deployName}: --revert-drift, always creating change set`);
return false;
}

// No existing stack
if (!cloudFormationStack.exists) {
await ioHelper.defaults.debug(`${deployName}: no existing stack`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,41 @@ describe('import-existing-resources', () => {
});
});

describe('revert-drift', () => {
test('is disabled by default', async () => {
// WHEN
await testDeployStack({
...standardDeployStackArguments(),
deploymentMethod: {
method: 'change-set',
},
});

// THEN
expect(mockCloudFormationClient).toHaveReceivedCommandWith(CreateChangeSetCommand, {
...expect.anything,
DeploymentMode: undefined,
});
});

test('is added to the CreateChangeSetCommandInput', async () => {
// WHEN
await testDeployStack({
...standardDeployStackArguments(),
deploymentMethod: {
method: 'change-set',
revertDrift: true,
},
});

// THEN
expect(mockCloudFormationClient).toHaveReceivedCommandWith(CreateChangeSetCommand, {
...expect.anything,
DeploymentMode: 'REVERT_DRIFT',
});
});
});

test.each([
// From a failed state, a --no-rollback is possible as long as there is not a replacement
[StackStatus.UPDATE_FAILED, 'no-rollback', 'no-replacement', 'did-deploy-stack'],
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/lib/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ export async function makeConfig(): Promise<CliConfig> {
'asset-parallelism': { type: 'boolean', desc: 'Whether to build/publish assets in parallel' },
'asset-prebuild': { type: 'boolean', desc: 'Whether to build all assets before deploying the first stack (useful for failing Docker builds)', default: true },
'ignore-no-stacks': { type: 'boolean', desc: 'Whether to deploy if the app contains no stacks', default: false },
'revert-drift': { type: 'boolean', desc: 'Create a drift-aware change set that brings actual resource states in line with template definitions', default: false }
},
arg: {
name: 'STACKS',
Expand Down
5 changes: 5 additions & 0 deletions packages/aws-cdk/lib/cli/cli-type-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,11 @@
"type": "boolean",
"desc": "Whether to deploy if the app contains no stacks",
"default": false
},
"revert-drift": {
"type": "boolean",
"desc": "Create a drift-aware change set that brings actual resource states in line with template definitions",
"default": false
}
},
"arg": {
Expand Down
6 changes: 6 additions & 0 deletions packages/aws-cdk/lib/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,9 @@ function determineDeploymentMethod(args: any, configuration: Configuration, watc
if (args.importExistingResources) {
throw new ToolkitError('--import-existing-resources cannot be enabled with method=direct');
}
if (args.revertDrift) {
throw new ToolkitError('--revert-drift cannot be used with method=direct');
}
deploymentMethod = { method: 'direct' };
break;
case 'change-set':
Expand All @@ -653,6 +656,7 @@ function determineDeploymentMethod(args: any, configuration: Configuration, watc
execute: true,
changeSetName: args.changeSetName,
importExistingResources: args.importExistingResources,
revertDrift: args.revertDrift,
};
break;
case 'prepare-change-set':
Expand All @@ -661,6 +665,7 @@ function determineDeploymentMethod(args: any, configuration: Configuration, watc
execute: false,
changeSetName: args.changeSetName,
importExistingResources: args.importExistingResources,
revertDrift: args.revertDrift,
};
break;
case undefined:
Expand All @@ -670,6 +675,7 @@ function determineDeploymentMethod(args: any, configuration: Configuration, watc
execute: watch ? true : args.execute ?? true,
changeSetName: args.changeSetName,
importExistingResources: args.importExistingResources,
revertDrift: args.revertDrift,
};
break;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/aws-cdk/lib/cli/convert-to-user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export function convertYargsToUserInput(args: any): UserInput {
assetParallelism: args.assetParallelism,
assetPrebuild: args.assetPrebuild,
ignoreNoStacks: args.ignoreNoStacks,
revertDrift: args.revertDrift,
STACKS: args.STACKS,
};
break;
Expand Down Expand Up @@ -431,6 +432,7 @@ export function convertConfigToUserInput(config: any): UserInput {
assetParallelism: config.deploy?.assetParallelism,
assetPrebuild: config.deploy?.assetPrebuild,
ignoreNoStacks: config.deploy?.ignoreNoStacks,
revertDrift: config.deploy?.revertDrift,
};
const rollbackOptions = {
all: config.rollback?.all,
Expand Down
5 changes: 5 additions & 0 deletions packages/aws-cdk/lib/cli/parse-command-line-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,11 @@ export function parseCommandLineArguments(args: Array<string>): any {
default: false,
type: 'boolean',
desc: 'Whether to deploy if the app contains no stacks',
})
.option('revert-drift', {
default: false,
type: 'boolean',
desc: 'Create a drift-aware change set that brings actual resource states in line with template definitions',
}),
)
.command('rollback [STACKS..]', 'Rolls back the stack(s) named STACKS to their last stable state', (yargs: Argv) =>
Expand Down
7 changes: 7 additions & 0 deletions packages/aws-cdk/lib/cli/user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,13 @@ export interface DeployOptions {
*/
readonly ignoreNoStacks?: boolean;

/**
* Create a drift-aware change set that brings actual resource states in line with template definitions
*
* @default - false
*/
readonly revertDrift?: boolean;

/**
* Positional argument for deploy
*/
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/test/cli/cli-arguments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ describe('yargs', () => {
previousParameters: true,
progress: undefined,
requireApproval: undefined,
revertDrift: undefined,
rollback: false,
tags: undefined,
toolkitStackName: undefined,
Expand Down