in packages/aws-cdk/lib/cli/cdk-toolkit.ts [342:676]
public async deploy(options: DeployOptions) {
if (options.watch) {
return this.watch(options);
}
// set progress from options, this includes user and app config
if (options.progress) {
this.ioHost.stackProgress = options.progress;
}
const startSynthTime = new Date().getTime();
const stackCollection = await this.selectStacksForDeploy(
options.selector,
options.exclusively,
options.cacheCloudAssembly,
options.ignoreNoStacks,
);
const elapsedSynthTime = new Date().getTime() - startSynthTime;
info(`\n✨ Synthesis time: ${formatTime(elapsedSynthTime)}s\n`);
if (stackCollection.stackCount === 0) {
error('This app contains no stacks');
return;
}
const migrator = new ResourceMigrator({
deployments: this.props.deployments,
ioHelper: asIoHelper(this.ioHost, 'deploy'),
});
await migrator.tryMigrateResources(stackCollection, {
toolkitStackName: this.toolkitStackName,
...options,
});
const requireApproval = options.requireApproval ?? RequireApproval.BROADENING;
const parameterMap = buildParameterMap(options.parameters);
if (options.hotswap !== HotswapMode.FULL_DEPLOYMENT) {
warning(
'⚠️ The --hotswap and --hotswap-fallback flags deliberately introduce CloudFormation drift to speed up deployments',
);
warning('⚠️ They should only be used for development - never use them for your production Stacks!\n');
}
let hotswapPropertiesFromSettings = this.props.configuration.settings.get(['hotswap']) || {};
let hotswapPropertyOverrides = new HotswapPropertyOverrides();
hotswapPropertyOverrides.ecsHotswapProperties = new EcsHotswapProperties(
hotswapPropertiesFromSettings.ecs?.minimumHealthyPercent,
hotswapPropertiesFromSettings.ecs?.maximumHealthyPercent,
);
const stacks = stackCollection.stackArtifacts;
const stackOutputs: { [key: string]: any } = {};
const outputsFile = options.outputsFile;
const buildAsset = async (assetNode: AssetBuildNode) => {
await this.props.deployments.buildSingleAsset(
assetNode.assetManifestArtifact,
assetNode.assetManifest,
assetNode.asset,
{
stack: assetNode.parentStack,
roleArn: options.roleArn,
stackName: assetNode.parentStack.stackName,
},
);
};
const publishAsset = async (assetNode: AssetPublishNode) => {
await this.props.deployments.publishSingleAsset(assetNode.assetManifest, assetNode.asset, {
stack: assetNode.parentStack,
roleArn: options.roleArn,
stackName: assetNode.parentStack.stackName,
forcePublish: options.force,
});
};
const deployStack = async (stackNode: StackNode) => {
const stack = stackNode.stack;
if (stackCollection.stackCount !== 1) {
highlight(stack.displayName);
}
if (!stack.environment) {
// eslint-disable-next-line max-len
throw new ToolkitError(
`Stack ${stack.displayName} does not define an environment, and AWS credentials could not be obtained from standard locations or no region was configured.`,
);
}
if (Object.keys(stack.template.Resources || {}).length === 0) {
// The generated stack has no resources
if (!(await this.props.deployments.stackExists({ stack }))) {
warning('%s: stack has no resources, skipping deployment.', chalk.bold(stack.displayName));
} else {
warning('%s: stack has no resources, deleting existing stack.', chalk.bold(stack.displayName));
await this.destroy({
selector: { patterns: [stack.hierarchicalId] },
exclusively: true,
force: true,
roleArn: options.roleArn,
fromDeploy: true,
});
}
return;
}
if (requireApproval !== RequireApproval.NEVER) {
const currentTemplate = await this.props.deployments.readCurrentTemplate(stack);
const formatter = new DiffFormatter({
ioHelper: asIoHelper(this.ioHost, 'deploy'),
templateInfo: {
oldTemplate: currentTemplate,
newTemplate: stack,
},
});
const securityDiff = formatter.formatSecurityDiff();
if (requiresApproval(requireApproval, securityDiff.permissionChangeType)) {
info(securityDiff.formattedDiff);
await askUserConfirmation(
this.ioHost,
concurrency,
'"--require-approval" is enabled and stack includes security-sensitive updates',
'Do you wish to deploy these changes',
);
}
}
// Following are the same semantics we apply with respect to Notification ARNs (dictated by the SDK)
//
// - undefined => cdk ignores it, as if it wasn't supported (allows external management).
// - []: => cdk manages it, and the user wants to wipe it out.
// - ['arn-1'] => cdk manages it, and the user wants to set it to ['arn-1'].
const notificationArns = (!!options.notificationArns || !!stack.notificationArns)
? (options.notificationArns ?? []).concat(stack.notificationArns ?? [])
: undefined;
for (const notificationArn of notificationArns ?? []) {
if (!validateSnsTopicArn(notificationArn)) {
throw new ToolkitError(`Notification arn ${notificationArn} is not a valid arn for an SNS topic`);
}
}
const stackIndex = stacks.indexOf(stack) + 1;
info(`${chalk.bold(stack.displayName)}: deploying... [${stackIndex}/${stackCollection.stackCount}]`);
const startDeployTime = new Date().getTime();
let tags = options.tags;
if (!tags || tags.length === 0) {
tags = tagsForStack(stack);
}
let elapsedDeployTime = 0;
try {
let deployResult: SuccessfulDeployStackResult | undefined;
let rollback = options.rollback;
let iteration = 0;
while (!deployResult) {
if (++iteration > 2) {
throw new ToolkitError('This loop should have stabilized in 2 iterations, but didn\'t. If you are seeing this error, please report it at https://github.com/aws/aws-cdk/issues/new/choose');
}
const r = await this.props.deployments.deployStack({
stack,
deployName: stack.stackName,
roleArn: options.roleArn,
toolkitStackName: options.toolkitStackName,
reuseAssets: options.reuseAssets,
notificationArns,
tags,
execute: options.execute,
changeSetName: options.changeSetName,
deploymentMethod: options.deploymentMethod,
forceDeployment: options.force,
parameters: Object.assign({}, parameterMap['*'], parameterMap[stack.stackName]),
usePreviousParameters: options.usePreviousParameters,
rollback,
hotswap: options.hotswap,
hotswapPropertyOverrides: hotswapPropertyOverrides,
extraUserAgent: options.extraUserAgent,
assetParallelism: options.assetParallelism,
ignoreNoStacks: options.ignoreNoStacks,
});
switch (r.type) {
case 'did-deploy-stack':
deployResult = r;
break;
case 'failpaused-need-rollback-first': {
const motivation = r.reason === 'replacement'
? `Stack is in a paused fail state (${r.status}) and change includes a replacement which cannot be deployed with "--no-rollback"`
: `Stack is in a paused fail state (${r.status}) and command line arguments do not include "--no-rollback"`;
if (options.force) {
warning(`${motivation}. Rolling back first (--force).`);
} else {
await askUserConfirmation(
this.ioHost,
concurrency,
motivation,
`${motivation}. Roll back first and then proceed with deployment`,
);
}
// Perform a rollback
await this.rollback({
selector: { patterns: [stack.hierarchicalId] },
toolkitStackName: options.toolkitStackName,
force: options.force,
});
// Go around through the 'while' loop again but switch rollback to true.
rollback = true;
break;
}
case 'replacement-requires-rollback': {
const motivation = 'Change includes a replacement which cannot be deployed with "--no-rollback"';
if (options.force) {
warning(`${motivation}. Proceeding with regular deployment (--force).`);
} else {
await askUserConfirmation(
this.ioHost,
concurrency,
motivation,
`${motivation}. Perform a regular deployment`,
);
}
// Go around through the 'while' loop again but switch rollback to true.
rollback = true;
break;
}
default:
throw new ToolkitError(`Unexpected result type from deployStack: ${JSON.stringify(r)}. If you are seeing this error, please report it at https://github.com/aws/aws-cdk/issues/new/choose`);
}
}
const message = deployResult.noOp
? ' ✅ %s (no changes)'
: ' ✅ %s';
success('\n' + message, stack.displayName);
elapsedDeployTime = new Date().getTime() - startDeployTime;
info(`\n✨ Deployment time: ${formatTime(elapsedDeployTime)}s\n`);
if (Object.keys(deployResult.outputs).length > 0) {
info('Outputs:');
stackOutputs[stack.stackName] = deployResult.outputs;
}
for (const name of Object.keys(deployResult.outputs).sort()) {
const value = deployResult.outputs[name];
info(`${chalk.cyan(stack.id)}.${chalk.cyan(name)} = ${chalk.underline(chalk.cyan(value))}`);
}
info('Stack ARN:');
logResult(deployResult.stackArn);
} catch (e: any) {
// It has to be exactly this string because an integration test tests for
// "bold(stackname) failed: ResourceNotReady: <error>"
throw new ToolkitError(
[`❌ ${chalk.bold(stack.stackName)} failed:`, ...(e.name ? [`${e.name}:`] : []), formatErrorMessage(e)].join(' '),
);
} finally {
if (options.cloudWatchLogMonitor) {
const foundLogGroupsResult = await findCloudWatchLogGroups(this.props.sdkProvider, asIoHelper(this.ioHost, 'deploy'), stack);
options.cloudWatchLogMonitor.addLogGroups(
foundLogGroupsResult.env,
foundLogGroupsResult.sdk,
foundLogGroupsResult.logGroupNames,
);
}
// If an outputs file has been specified, create the file path and write stack outputs to it once.
// Outputs are written after all stacks have been deployed. If a stack deployment fails,
// all of the outputs from successfully deployed stacks before the failure will still be written.
if (outputsFile) {
fs.ensureFileSync(outputsFile);
await fs.writeJson(outputsFile, stackOutputs, {
spaces: 2,
encoding: 'utf8',
});
}
}
info(`\n✨ Total time: ${formatTime(elapsedSynthTime + elapsedDeployTime)}s\n`);
};
const assetBuildTime = options.assetBuildTime ?? AssetBuildTime.ALL_BEFORE_DEPLOY;
const prebuildAssets = assetBuildTime === AssetBuildTime.ALL_BEFORE_DEPLOY;
const concurrency = options.concurrency || 1;
if (concurrency > 1) {
// always force "events" progress output when we have concurrency
this.ioHost.stackProgress = StackActivityProgress.EVENTS;
// ...but only warn if the user explicitly requested "bar" progress
if (options.progress && options.progress != StackActivityProgress.EVENTS) {
warning('⚠️ The --concurrency flag only supports --progress "events". Switching to "events".');
}
}
const stacksAndTheirAssetManifests = stacks.flatMap((stack) => [
stack,
...stack.dependencies.filter(x => cxapi.AssetManifestArtifact.isAssetManifestArtifact(x)),
]);
const workGraph = new WorkGraphBuilder(
asIoHelper(this.ioHost, 'deploy'),
prebuildAssets,
).build(stacksAndTheirAssetManifests);
// Unless we are running with '--force', skip already published assets
if (!options.force) {
await this.removePublishedAssets(workGraph, options);
}
const graphConcurrency: Concurrency = {
'stack': concurrency,
'asset-build': 1, // This will be CPU-bound/memory bound, mostly matters for Docker builds
'asset-publish': (options.assetParallelism ?? true) ? 8 : 1, // This will be I/O-bound, 8 in parallel seems reasonable
};
await workGraph.doParallel(graphConcurrency, {
deployStack,
buildAsset,
publishAsset,
});
}