in packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts [408:731]
private async _deploy(assembly: StackAssembly, action: 'deploy' | 'watch', options: ExtendedDeployOptions = {}): Promise<DeployResult> {
const ioHelper = asIoHelper(this.ioHost, action);
const selectStacks = options.stacks ?? ALL_STACKS;
const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks });
const stackCollection = await assembly.selectStacksV2(selectStacks);
await this.validateStacksMetadata(stackCollection, ioHelper);
const synthDuration = await synthSpan.end();
const ret: DeployResult = {
stacks: [],
};
if (stackCollection.stackCount === 0) {
await ioHelper.notify(IO.CDK_TOOLKIT_E5001.msg('This app contains no stacks'));
return ret;
}
const deployments = await this.deploymentsForAction('deploy');
const migrator = new ResourceMigrator({ deployments, ioHelper });
await migrator.tryMigrateResources(stackCollection, options);
const parameterMap = buildParameterMap(options.parameters?.parameters);
const hotswapMode = options.hotswap ?? HotswapMode.FULL_DEPLOYMENT;
if (hotswapMode !== HotswapMode.FULL_DEPLOYMENT) {
await ioHelper.notify(IO.CDK_TOOLKIT_W5400.msg([
'⚠️ The --hotswap and --hotswap-fallback flags deliberately introduce CloudFormation drift to speed up deployments',
'⚠️ They should only be used for development - never use them for your production Stacks!',
].join('\n')));
}
const stacks = stackCollection.stackArtifacts;
const stackOutputs: { [key: string]: any } = {};
const outputsFile = options.outputsFile;
const buildAsset = async (assetNode: AssetBuildNode) => {
const buildAssetSpan = await ioHelper.span(SPAN.BUILD_ASSET).begin({
asset: assetNode.asset,
});
await deployments.buildSingleAsset(
assetNode.assetManifestArtifact,
assetNode.assetManifest,
assetNode.asset,
{
stack: assetNode.parentStack,
roleArn: options.roleArn,
stackName: assetNode.parentStack.stackName,
},
);
await buildAssetSpan.end();
};
const publishAsset = async (assetNode: AssetPublishNode) => {
const publishAssetSpan = await ioHelper.span(SPAN.PUBLISH_ASSET).begin({
asset: assetNode.asset,
});
await deployments.publishSingleAsset(assetNode.assetManifest, assetNode.asset, {
stack: assetNode.parentStack,
roleArn: options.roleArn,
stackName: assetNode.parentStack.stackName,
forcePublish: options.forceAssetPublishing,
});
await publishAssetSpan.end();
};
const deployStack = async (stackNode: StackNode) => {
const stack = stackNode.stack;
if (stackCollection.stackCount !== 1) {
await ioHelper.notify(IO.DEFAULT_TOOLKIT_INFO.msg(chalk.bold(stack.displayName)));
}
if (!stack.environment) {
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.`,
);
}
// The generated stack has no resources
if (Object.keys(stack.template.Resources || {}).length === 0) {
// stack is empty and doesn't exist => do nothing
const stackExists = await deployments.stackExists({ stack });
if (!stackExists) {
return ioHelper.notify(IO.CDK_TOOLKIT_W5021.msg(`${chalk.bold(stack.displayName)}: stack has no resources, skipping deployment.`));
}
// stack is empty, but exists => delete
await ioHelper.notify(IO.CDK_TOOLKIT_W5022.msg(`${chalk.bold(stack.displayName)}: stack has no resources, deleting existing stack.`));
await this._destroy(assembly, 'deploy', {
stacks: { patterns: [stack.hierarchicalId], strategy: StackSelectionStrategy.PATTERN_MUST_MATCH_SINGLE },
roleArn: options.roleArn,
});
return;
}
const currentTemplate = await deployments.readCurrentTemplate(stack);
const formatter = new DiffFormatter({
ioHelper,
templateInfo: {
oldTemplate: currentTemplate,
newTemplate: stack,
},
});
const securityDiff = formatter.formatSecurityDiff();
const permissionChangeType = securityDiff.permissionChangeType;
const deployMotivation = '"--require-approval" is enabled and stack includes security-sensitive updates.';
const deployQuestion = `${deployMotivation}\nDo you wish to deploy these changes`;
const deployConfirmed = await ioHelper.requestResponse(IO.CDK_TOOLKIT_I5060.req(deployQuestion, {
motivation: deployMotivation,
concurrency,
permissionChangeType,
}));
if (!deployConfirmed) {
throw new ToolkitError('Aborted by user');
}
// 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;
const deploySpan = await ioHelper.span(SPAN.DEPLOY_STACK)
.begin(`${chalk.bold(stack.displayName)}: deploying... [${stackIndex}/${stackCollection.stackCount}]`, {
total: stackCollection.stackCount,
current: stackIndex,
stack,
});
let tags = options.tags;
if (!tags || tags.length === 0) {
tags = tagsForStack(stack);
}
let deployDuration;
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 deployments.deployStack({
stack,
deployName: stack.stackName,
roleArn: options.roleArn,
toolkitStackName: this.toolkitStackName,
reuseAssets: options.reuseAssets,
notificationArns,
tags,
deploymentMethod: options.deploymentMethod,
forceDeployment: options.forceDeployment,
parameters: Object.assign({}, parameterMap['*'], parameterMap[stack.stackName]),
usePreviousParameters: options.parameters?.keepExistingParameters,
rollback,
hotswap: hotswapMode,
extraUserAgent: options.extraUserAgent,
hotswapPropertyOverrides: options.hotswapProperties ? createHotswapPropertyOverrides(options.hotswapProperties) : undefined,
assetParallelism: options.assetParallelism,
});
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"`;
const question = `${motivation}. Perform a regular deployment`;
const confirmed = await ioHelper.requestResponse(IO.CDK_TOOLKIT_I5050.req(question, {
motivation,
concurrency,
}));
if (!confirmed) {
throw new ToolkitError('Aborted by user');
}
// Perform a rollback
await this._rollback(assembly, action, {
stacks: {
patterns: [stack.hierarchicalId],
strategy: StackSelectionStrategy.PATTERN_MUST_MATCH_SINGLE,
},
orphanFailedResources: options.orphanFailedResourcesDuringRollback,
});
// 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"';
const question = `${motivation}. Perform a regular deployment`;
const confirmed = await ioHelper.requestResponse(IO.CDK_TOOLKIT_I5050.req(question, {
motivation,
concurrency,
}));
if (!confirmed) {
throw new ToolkitError('Aborted by user');
}
// 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
? ` ✅ ${stack.displayName} (no changes)`
: ` ✅ ${stack.displayName}`;
await ioHelper.notify(IO.CDK_TOOLKIT_I5900.msg(chalk.green('\n' + message), deployResult));
deployDuration = await deploySpan.timing(IO.CDK_TOOLKIT_I5000);
if (Object.keys(deployResult.outputs).length > 0) {
const buffer = ['Outputs:'];
stackOutputs[stack.stackName] = deployResult.outputs;
for (const name of Object.keys(deployResult.outputs).sort()) {
const value = deployResult.outputs[name];
buffer.push(`${chalk.cyan(stack.id)}.${chalk.cyan(name)} = ${chalk.underline(chalk.cyan(value))}`);
}
await ioHelper.notify(IO.CDK_TOOLKIT_I5901.msg(buffer.join('\n')));
}
await ioHelper.notify(IO.CDK_TOOLKIT_I5901.msg(`Stack ARN:\n${deployResult.stackArn}`));
ret.stacks.push({
stackName: stack.stackName,
environment: {
account: stack.environment.account,
region: stack.environment.region,
},
stackArn: deployResult.stackArn,
outputs: deployResult.outputs,
hierarchicalId: stack.hierarchicalId,
});
} 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}:`] : []), e.message].join(' '),
);
} finally {
if (options.traceLogs) {
// deploy calls that originate from watch will come with their own cloudWatchLogMonitor
const cloudWatchLogMonitor = options.cloudWatchLogMonitor ?? new CloudWatchLogEventMonitor({ ioHelper });
const foundLogGroupsResult = await findCloudWatchLogGroups(await this.sdkProvider('deploy'), ioHelper, stack);
cloudWatchLogMonitor.addLogGroups(
foundLogGroupsResult.env,
foundLogGroupsResult.sdk,
foundLogGroupsResult.logGroupNames,
);
await ioHelper.notify(IO.CDK_TOOLKIT_I5031.msg(`The following log groups are added: ${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',
});
}
}
const duration = synthDuration.asMs + (deployDuration?.asMs ?? 0);
await deploySpan.end(`\n✨ Total time: ${formatTime(duration)}s\n`, { duration });
};
const assetBuildTime = options.assetBuildTime ?? AssetBuildTime.ALL_BEFORE_DEPLOY;
const prebuildAssets = assetBuildTime === AssetBuildTime.ALL_BEFORE_DEPLOY;
const concurrency = options.concurrency || 1;
const stacksAndTheirAssetManifests = stacks.flatMap((stack) => [
stack,
...stack.dependencies.filter(x => cxapi.AssetManifestArtifact.isAssetManifestArtifact(x)),
]);
const workGraph = new WorkGraphBuilder(ioHelper, prebuildAssets).build(stacksAndTheirAssetManifests);
// Unless we are running with '--force', skip already published assets
if (!options.forceAssetPublishing) {
await removePublishedAssetsFromWorkGraph(workGraph, deployments, 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,
});
return ret;
}