in packages/@aws-cdk/toolkit-lib/lib/api/deployments/deployments.ts [447:572]
public async rollbackStack(options: RollbackStackOptions): Promise<RollbackStackResult> {
let resourcesToSkip: string[] = options.orphanLogicalIds ?? [];
if (options.orphanFailedResources && resourcesToSkip.length > 0) {
throw new ToolkitError('Cannot combine --force with --orphan');
}
const env = await this.envs.accessStackForMutableStackOperations(options.stack);
if (options.validateBootstrapStackVersion ?? true) {
// Do a verification of the bootstrap stack version
await this.validateBootstrapStackVersion(
options.stack.stackName,
BOOTSTRAP_STACK_VERSION_FOR_ROLLBACK,
options.stack.bootstrapStackVersionSsmParameter,
env.resources);
}
const cfn = env.sdk.cloudFormation();
const deployName = options.stack.stackName;
// We loop in case of `--force` and the stack ends up in `CONTINUE_UPDATE_ROLLBACK`.
let maxLoops = 10;
while (maxLoops--) {
const cloudFormationStack = await CloudFormationStack.lookup(cfn, deployName);
const stackArn = cloudFormationStack.stackId;
const executionRoleArn = await env.replacePlaceholders(options.roleArn ?? options.stack.cloudFormationExecutionRoleArn);
switch (cloudFormationStack.stackStatus.rollbackChoice) {
case RollbackChoice.NONE:
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_WARN.msg(`Stack ${deployName} does not need a rollback: ${cloudFormationStack.stackStatus}`));
return { stackArn: cloudFormationStack.stackId, notInRollbackableState: true };
case RollbackChoice.START_ROLLBACK:
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`Initiating rollback of stack ${deployName}`));
await cfn.rollbackStack({
StackName: deployName,
RoleARN: executionRoleArn,
ClientRequestToken: randomUUID(),
// Enabling this is just the better overall default, the only reason it isn't the upstream default is backwards compatibility
RetainExceptOnCreate: true,
});
break;
case RollbackChoice.CONTINUE_UPDATE_ROLLBACK:
if (options.orphanFailedResources) {
// Find the failed resources from the deployment and automatically skip them
// (Using deployment log because we definitely have `DescribeStackEvents` permissions, and we might not have
// `DescribeStackResources` permissions).
const poller = new StackEventPoller(cfn, {
stackName: deployName,
stackStatuses: ['ROLLBACK_IN_PROGRESS', 'UPDATE_ROLLBACK_IN_PROGRESS'],
});
await poller.poll();
resourcesToSkip = poller.resourceErrors
.filter((r) => !r.isStackEvent && r.parentStackLogicalIds.length === 0)
.map((r) => r.event.LogicalResourceId ?? '');
}
const skipDescription = resourcesToSkip.length > 0 ? ` (orphaning: ${resourcesToSkip.join(', ')})` : '';
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_WARN.msg(`Continuing rollback of stack ${deployName}${skipDescription}`));
await cfn.continueUpdateRollback({
StackName: deployName,
ClientRequestToken: randomUUID(),
RoleARN: executionRoleArn,
ResourcesToSkip: resourcesToSkip,
});
break;
case RollbackChoice.ROLLBACK_FAILED:
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_WARN.msg(
`Stack ${deployName} failed creation and rollback. This state cannot be rolled back. You can recreate this stack by running 'cdk deploy'.`,
));
return { stackArn, notInRollbackableState: true };
default:
throw new ToolkitError(`Unexpected rollback choice: ${cloudFormationStack.stackStatus.rollbackChoice}`);
}
const monitor = new StackActivityMonitor({
cfn,
stack: options.stack,
stackName: deployName,
ioHelper: this.ioHelper,
});
await monitor.start();
let stackErrorMessage: string | undefined = undefined;
let finalStackState = cloudFormationStack;
try {
const successStack = await stabilizeStack(cfn, this.ioHelper, deployName);
// This shouldn't really happen, but catch it anyway. You never know.
if (!successStack) {
throw new ToolkitError('Stack deploy failed (the stack disappeared while we were rolling it back)');
}
finalStackState = successStack;
const errors = monitor.errors.join(', ');
if (errors) {
stackErrorMessage = errors;
}
} catch (e: any) {
stackErrorMessage = suffixWithErrors(formatErrorMessage(e), monitor.errors);
} finally {
await monitor.stop();
}
if (finalStackState.stackStatus.isRollbackSuccess || !stackErrorMessage) {
return { stackArn, success: true };
}
// Either we need to ignore some resources to continue the rollback, or something went wrong
if (finalStackState.stackStatus.rollbackChoice === RollbackChoice.CONTINUE_UPDATE_ROLLBACK && options.orphanFailedResources) {
// Do another loop-de-loop
continue;
}
throw new ToolkitError(
`${stackErrorMessage} (fix problem and retry, or orphan these resources using --orphan or --force)`,
);
}
throw new ToolkitError(
"Rollback did not finish after a large number of iterations; stopping because it looks like we're not making progress anymore. You can retry if rollback was progressing as expected.",
);
}