export async function isHotswappableEcsServiceChange()

in packages/@aws-cdk/toolkit-lib/lib/api/hotswap/ecs-services.ts [16:164]


export async function isHotswappableEcsServiceChange(
  logicalId: string,
  change: ResourceChange,
  evaluateCfnTemplate: EvaluateCloudFormationTemplate,
  hotswapPropertyOverrides: HotswapPropertyOverrides,
): Promise<HotswapChange[]> {
  // the only resource change we can evaluate here is an ECS TaskDefinition
  if (change.newValue.Type !== 'AWS::ECS::TaskDefinition') {
    return [];
  }

  const ret: HotswapChange[] = [];

  // We only allow a change in the ContainerDefinitions of the TaskDefinition for now -
  // it contains the image and environment variables, so seems like a safe bet for now.
  // We might revisit this decision in the future though!
  const classifiedChanges = classifyChanges(change, ['ContainerDefinitions']);
  classifiedChanges.reportNonHotswappablePropertyChanges(ret);

  // find all ECS Services that reference the TaskDefinition that changed
  const resourcesReferencingTaskDef = evaluateCfnTemplate.findReferencesTo(logicalId);
  const ecsServiceResourcesReferencingTaskDef = resourcesReferencingTaskDef.filter(
    (r) => r.Type === ECS_SERVICE_RESOURCE_TYPE,
  );
  const ecsServicesReferencingTaskDef = new Array<EcsService>();
  for (const ecsServiceResource of ecsServiceResourcesReferencingTaskDef) {
    const serviceArn = await evaluateCfnTemplate.findPhysicalNameFor(ecsServiceResource.LogicalId);
    if (serviceArn) {
      ecsServicesReferencingTaskDef.push({
        logicalId: ecsServiceResource.LogicalId,
        serviceArn,
      });
    }
  }
  if (ecsServicesReferencingTaskDef.length === 0) {
    /**
     * ECS Services can have a task definition that doesn't refer to the task definition being updated.
     * We have to log this as a non-hotswappable change to the task definition, but when we do,
     * we wind up hotswapping the task definition and logging it as a non-hotswappable change.
     *
     * This logic prevents us from logging that change as non-hotswappable when we hotswap it.
     */
    ret.push(nonHotswappableChange(
      change,
      NonHotswappableReason.DEPENDENCY_UNSUPPORTED,
      'No ECS services reference the changed task definition',
      undefined,
      false,
    ));
  }
  if (resourcesReferencingTaskDef.length > ecsServicesReferencingTaskDef.length) {
    // if something besides an ECS Service is referencing the TaskDefinition,
    // hotswap is not possible in FALL_BACK mode
    const nonEcsServiceTaskDefRefs = resourcesReferencingTaskDef.filter((r) => r.Type !== ECS_SERVICE_RESOURCE_TYPE);
    for (const taskRef of nonEcsServiceTaskDefRefs) {
      ret.push(nonHotswappableChange(
        change,
        NonHotswappableReason.DEPENDENCY_UNSUPPORTED,
        `A resource '${taskRef.LogicalId}' with Type '${taskRef.Type}' that is not an ECS Service was found referencing the changed TaskDefinition '${logicalId}'`,
      ));
    }
  }

  const namesOfHotswappableChanges = Object.keys(classifiedChanges.hotswappableProps);
  if (namesOfHotswappableChanges.length > 0) {
    const taskDefinitionResource = await prepareTaskDefinitionChange(evaluateCfnTemplate, logicalId, change);
    ret.push({
      change: {
        cause: change,
        resources: [
          {
            logicalId,
            resourceType: change.newValue.Type,
            physicalName: await taskDefinitionResource.Family,
            metadata: evaluateCfnTemplate.metadataFor(logicalId),
          },
          ...ecsServicesReferencingTaskDef.map((ecsService) => ({
            resourceType: ECS_SERVICE_RESOURCE_TYPE,
            physicalName: ecsService.serviceArn.split('/')[2],
            logicalId: ecsService.logicalId,
            metadata: evaluateCfnTemplate.metadataFor(ecsService.logicalId),
          })),
        ],
      },
      hotswappable: true,
      service: 'ecs-service',
      apply: async (sdk: SDK) => {
        // Step 1 - update the changed TaskDefinition, creating a new TaskDefinition Revision
        // we need to lowercase the evaluated TaskDef from CloudFormation,
        // as the AWS SDK uses lowercase property names for these

        // The SDK requires more properties here than its worth doing explicit typing for
        // instead, just use all the old values in the diff to fill them in implicitly
        const lowercasedTaskDef = transformObjectKeys(taskDefinitionResource, lowerCaseFirstCharacter, {
          // All the properties that take arbitrary string as keys i.e. { "string" : "string" }
          // https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_RegisterTaskDefinition.html#API_RegisterTaskDefinition_RequestSyntax
          ContainerDefinitions: {
            DockerLabels: true,
            FirelensConfiguration: {
              Options: true,
            },
            LogConfiguration: {
              Options: true,
            },
          },
          Volumes: {
            DockerVolumeConfiguration: {
              DriverOpts: true,
              Labels: true,
            },
          },
        });
        const registerTaskDefResponse = await sdk.ecs().registerTaskDefinition(lowercasedTaskDef);
        const taskDefRevArn = registerTaskDefResponse.taskDefinition?.taskDefinitionArn;

        let ecsHotswapProperties = hotswapPropertyOverrides.ecsHotswapProperties;
        let minimumHealthyPercent = ecsHotswapProperties?.minimumHealthyPercent;
        let maximumHealthyPercent = ecsHotswapProperties?.maximumHealthyPercent;

        // Step 2 - update the services using that TaskDefinition to point to the new TaskDefinition Revision
        // Forcing New Deployment and setting Minimum Healthy Percent to 0.
        // As CDK HotSwap is development only, this seems the most efficient way to ensure all tasks are replaced immediately, regardless of original amount
        // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism
        await Promise.all(
          ecsServicesReferencingTaskDef.map(async (service) => {
            const cluster = service.serviceArn.split('/')[1];
            const update = await sdk.ecs().updateService({
              service: service.serviceArn,
              taskDefinition: taskDefRevArn,
              cluster,
              forceNewDeployment: true,
              deploymentConfiguration: {
                minimumHealthyPercent: minimumHealthyPercent !== undefined ? minimumHealthyPercent : 0,
                maximumPercent: maximumHealthyPercent !== undefined ? maximumHealthyPercent : undefined,
              },
            });

            await sdk.ecs().waitUntilServicesStable({
              cluster: update.service?.clusterArn,
              services: [service.serviceArn],
            });
          }),
        );
      },
    });
  }

  return ret;
}