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