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.`);
}
}