constructor()

in packages/@aws-cdk-containers/ecs-service-extensions/lib/service.ts [154:327]


  constructor(scope: Construct, id: string, props: ServiceProps) {
    super(scope, id);

    this.scope = scope;
    this.id = id;
    this.environment = props.environment;
    this.vpc = props.environment.vpc;
    this.cluster = props.environment.cluster;
    this.capacityType = props.environment.capacityType;
    this.serviceDescription = props.serviceDescription;

    // Check to make sure that the user has actually added a container
    const containerextension = this.serviceDescription.get('service-container');

    if (!containerextension) {
      throw new Error(`Service '${this.id}' must have a Container extension`);
    }

    // First set the scope for all the extensions
    for (const extensions in this.serviceDescription.extensions) {
      if (this.serviceDescription.extensions[extensions]) {
        this.serviceDescription.extensions[extensions].prehook(this, this.scope);
      }
    }

    // At the point of preparation all extensions have been defined on the service
    // so give each extension a chance to now add hooks to other extensions if
    // needed
    for (const extensions in this.serviceDescription.extensions) {
      if (this.serviceDescription.extensions[extensions]) {
        this.serviceDescription.extensions[extensions].addHooks();
      }
    }

    // Give each extension a chance to mutate the task def creation properties
    let taskDefProps = {
      // Default CPU and memory
      cpu: '256',
      memory: '512',

      // Allow user to pre-define the taskRole so that it can be used in resource policies that may
      // be defined before the ECS service exists in a CDK application
      taskRole: props.taskRole,

      // Ensure that the task definition supports both EC2 and Fargate
      compatibility: ecs.Compatibility.EC2_AND_FARGATE,
    } as ecs.TaskDefinitionProps;
    for (const extensions in this.serviceDescription.extensions) {
      if (this.serviceDescription.extensions[extensions]) {
        taskDefProps = this.serviceDescription.extensions[extensions].modifyTaskDefinitionProps(taskDefProps);
      }
    }

    // Now that the task definition properties are assembled, create it
    this.taskDefinition = new ecs.TaskDefinition(this.scope, `${this.id}-task-definition`, taskDefProps);

    // Now give each extension a chance to use the task definition
    for (const extensions in this.serviceDescription.extensions) {
      if (this.serviceDescription.extensions[extensions]) {
        this.serviceDescription.extensions[extensions].useTaskDefinition(this.taskDefinition);
      }
    }

    // Now that all containers are created, give each extension a chance
    // to bake its dependency graph
    for (const extensions in this.serviceDescription.extensions) {
      if (this.serviceDescription.extensions[extensions]) {
        this.serviceDescription.extensions[extensions].resolveContainerDependencies();
      }
    }

    // Give each extension a chance to mutate the service props before
    // service creation
    let serviceProps = {
      cluster: this.cluster,
      taskDefinition: this.taskDefinition,
      minHealthyPercent: 100,
      maxHealthyPercent: 200,
      desiredCount: props.desiredCount ?? 1,
    } as ServiceBuild;

    for (const extensions in this.serviceDescription.extensions) {
      if (this.serviceDescription.extensions[extensions]) {
        serviceProps = this.serviceDescription.extensions[extensions].modifyServiceProps(serviceProps);
      }
    }

    // If a maxHealthyPercent and desired count has been set while minHealthyPercent == 100% then we
    // need to do some failsafe checking to ensure that the maxHealthyPercent
    // actually allows a rolling deploy. Otherwise it is possible to end up with
    // blocked deploys that can take no action because minHealtyhPercent == 100%
    // prevents running, healthy tasks from being stopped, but a low maxHealthyPercent
    // can also prevents new parallel tasks from being started.
    if (serviceProps.maxHealthyPercent && serviceProps.desiredCount && serviceProps.minHealthyPercent && serviceProps.minHealthyPercent == 100) {
      if (serviceProps.desiredCount == 1) {
        // If there is one task then we must allow max percentage to be at
        // least 200% for another replacement task to be added
        serviceProps = {
          ...serviceProps,
          maxHealthyPercent: Math.max(200, serviceProps.maxHealthyPercent),
        };
      } else if (serviceProps.desiredCount <= 3) {
        // If task count is 2 or 3 then max percent must be at least 150% to
        // allow one replacement task to be launched at a time.
        serviceProps = {
          ...serviceProps,
          maxHealthyPercent: Math.max(150, serviceProps.maxHealthyPercent),
        };
      } else {
        // For anything higher than 3 tasks set max percent to at least 125%
        // For 4 tasks this will allow exactly one extra replacement task
        // at a time, for any higher task count it will allow 25% of the tasks
        // to be replaced at a time.
        serviceProps = {
          ...serviceProps,
          maxHealthyPercent: Math.max(125, serviceProps.maxHealthyPercent),
        };
      }
    }

    // Set desiredCount to `undefined` if auto scaling is configured for the service
    if (props.autoScaleTaskCount || this.autoScalingPoliciesEnabled) {
      serviceProps = {
        ...serviceProps,
        desiredCount: undefined,
      };
    }

    // Now that the service props are determined we can create
    // the service
    if (this.capacityType === EnvironmentCapacityType.EC2) {
      this.ecsService = new ecs.Ec2Service(this.scope, `${this.id}-service`, serviceProps);
    } else if (this.capacityType === EnvironmentCapacityType.FARGATE) {
      this.ecsService = new ecs.FargateService(this.scope, `${this.id}-service`, serviceProps);
    } else {
      throw new Error(`Unknown capacity type for service ${this.id}`);
    }

    // Create the auto scaling target and configure target tracking policies after the service is created
    if (props.autoScaleTaskCount) {
      this.scalableTaskCount = this.ecsService.autoScaleTaskCount({
        maxCapacity: props.autoScaleTaskCount.maxTaskCount,
        minCapacity: props.autoScaleTaskCount.minTaskCount,
      });

      if (props.autoScaleTaskCount.targetCpuUtilization) {
        const targetCpuUtilizationPercent = props.autoScaleTaskCount.targetCpuUtilization;
        this.scalableTaskCount.scaleOnCpuUtilization(`${this.id}-target-cpu-utilization-${targetCpuUtilizationPercent}`, {
          targetUtilizationPercent: targetCpuUtilizationPercent,
        });
        this.enableAutoScalingPolicy();
      }

      if (props.autoScaleTaskCount.targetMemoryUtilization) {
        const targetMemoryUtilizationPercent = props.autoScaleTaskCount.targetMemoryUtilization;
        this.scalableTaskCount.scaleOnMemoryUtilization(`${this.id}-target-memory-utilization-${targetMemoryUtilizationPercent}`, {
          targetUtilizationPercent: targetMemoryUtilizationPercent,
        });
        this.enableAutoScalingPolicy();
      }
    }

    // Now give all extensions a chance to use the service
    for (const extensions in this.serviceDescription.extensions) {
      if (this.serviceDescription.extensions[extensions]) {
        this.serviceDescription.extensions[extensions].useService(this.ecsService);
      }
    }

    // Error out if the auto scaling target is created but no scaling policies have been configured
    if (this.scalableTaskCount && !this.autoScalingPoliciesEnabled) {
      throw Error(`The auto scaling target for the service '${this.id}' has been created but no auto scaling policies have been configured.`);
    }
  }