public async deploy()

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,
    });
  }