From 3606deb5b519365d846e6e66406c835889827055 Mon Sep 17 00:00:00 2001 From: Marrick Lip Date: Wed, 8 Jan 2025 06:24:11 +1100 Subject: [PATCH] feat(ecs): warning when creating a service with the default minHealthyPercent (#31738) ### Issue Closes #31705 ### Reason for this change CDK overrides the default value of 100% used by CloudFormation's `AWS::ECS::Service` and the `CreateService` API. This allows the number of running tasks to drop by up to 50% during deployments and [Fargate maintenance](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-maintenance.html) etc. This is an unsafe default for services which must support a consistent load e.g. handle web traffic via an ALB. ### Description of changes A warning appears when the default value is implicitly used. CloudFormation's default is overridden in three different places so multiple warnings are added: * `BaseService` emits `@aws-cdk/aws-ecs:minHealthyPercent` when `minHealthyPercent` is `undefined` * `Ec2Service` emits `@aws-cdk/aws-ecs:minHealthyPercentDaemon` when `daemon` is `true` and `minHealthyPercent` is `undefined` * `ExternalService` emits `@aws-cdk/aws-ecs:minHealthyPercentExternal` when `minHealthyPercent` is `undefined` At most one warning appears due to the way the CloudFormation's default is overidden. `README.md` has been updated for `aws_ecs` and `aws_ecs_patterns` to ensure these examples don't trigger this warning. ### Description of how you validated changes Unit tests have been added to ensure the warnings appear are there when they should be and not when they shouldn't. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-cdk-lib/aws-ecs-patterns/README.md | 34 +++++++ packages/aws-cdk-lib/aws-ecs/README.md | 30 ++++-- .../aws-ecs/lib/base/base-service.ts | 4 + .../aws-ecs/lib/ec2/ec2-service.ts | 6 +- .../aws-ecs/lib/external/external-service.ts | 7 +- .../aws-ecs/test/ec2/ec2-service.test.ts | 94 +++++++++++++++++++ .../test/external/external-service.test.ts | 51 +++++++++- .../test/fargate/fargate-service.test.ts | 42 +++++++++ 8 files changed, 259 insertions(+), 9 deletions(-) diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/README.md b/packages/aws-cdk-lib/aws-ecs-patterns/README.md index 1ea629d90fe8c..c61643ab1aab8 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/README.md +++ b/packages/aws-cdk-lib/aws-ecs-patterns/README.md @@ -30,6 +30,7 @@ const loadBalancedEcsService = new ecsPatterns.ApplicationLoadBalancedEc2Service entryPoint: ['entry', 'point'], }, desiredCount: 2, + minHealthyPercent: 100, }); ``` @@ -46,6 +47,7 @@ const loadBalancedFargateService = new ecsPatterns.ApplicationLoadBalancedFargat command: ['command'], entryPoint: ['entry', 'point'], }, + minHealthyPercent: 100, }); loadBalancedFargateService.targetGroup.configureHealthCheck({ @@ -142,6 +144,7 @@ const loadBalancedEcsService = new ecsPatterns.NetworkLoadBalancedEc2Service(thi }, }, desiredCount: 2, + minHealthyPercent: 100, }); ``` @@ -156,6 +159,7 @@ const loadBalancedFargateService = new ecsPatterns.NetworkLoadBalancedFargateSer taskImageOptions: { image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), }, + minHealthyPercent: 100, }); ``` @@ -272,6 +276,7 @@ const queueProcessingEc2Service = new ecsPatterns.QueueProcessingEc2Service(this }, maxScalingCapacity: 5, containerName: 'test', + minHealthyPercent: 100, }); ``` @@ -292,6 +297,7 @@ const queueProcessingFargateService = new ecsPatterns.QueueProcessingFargateServ }, maxScalingCapacity: 5, containerName: 'test', + minHealthyPercent: 100, }); ``` @@ -314,6 +320,7 @@ const queueProcessingFargateService = new ecsPatterns.QueueProcessingFargateServ }, maxScalingCapacity: 5, containerName: 'test', + minHealthyPercent: 100, disableCpuBasedScaling: true, }); ``` @@ -332,6 +339,7 @@ const queueProcessingFargateService = new ecsPatterns.QueueProcessingFargateServ environment: {}, maxScalingCapacity: 5, containerName: 'test', + minHealthyPercent: 100, cpuTargetUtilizationPercent: 90, }); ``` @@ -392,6 +400,7 @@ declare const cluster: ecs.Cluster; const loadBalancedFargateService = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'Service', { vpc, cluster, + minHealthyPercent: 100, certificate, sslPolicy: SslPolicy.RECOMMENDED, domainName: 'api.example.com', @@ -414,6 +423,7 @@ const loadBalancedFargateService = new ecsPatterns.ApplicationLoadBalancedFargat taskImageOptions: { image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), }, + minHealthyPercent: 100, capacityProviderStrategies: [ { capacityProvider: 'FARGATE_SPOT', @@ -441,6 +451,7 @@ const loadBalancedFargateService = new ecsPatterns.ApplicationLoadBalancedFargat taskImageOptions: { image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), }, + minHealthyPercent: 100, }); const scalableTarget = loadBalancedFargateService.service.autoScaleTaskCount({ @@ -471,6 +482,7 @@ const loadBalancedFargateService = new ecsPatterns.ApplicationLoadBalancedFargat taskImageOptions: { image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), }, + minHealthyPercent: 100, }); const scalableTarget = loadBalancedFargateService.service.autoScaleTaskCount({ @@ -499,6 +511,7 @@ const loadBalancedFargateService = new ecsPatterns.ApplicationLoadBalancedFargat taskImageOptions: { image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), }, + minHealthyPercent: 100, deploymentController: { type: ecs.DeploymentControllerType.CODE_DEPLOY, }, @@ -522,6 +535,7 @@ const service = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'Ser taskImageOptions: { image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), }, + minHealthyPercent: 100, circuitBreaker: { rollback: true }, }); ``` @@ -553,6 +567,7 @@ const queueProcessingFargateService = new ecsPatterns.QueueProcessingFargateServ vpc, memoryLimitMiB: 512, image: ecs.ContainerImage.fromRegistry('test'), + minHealthyPercent: 100, securityGroups: [securityGroup], taskSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED }, }); @@ -566,6 +581,7 @@ const queueProcessingFargateService = new ecsPatterns.QueueProcessingFargateServ vpc, memoryLimitMiB: 512, image: ecs.ContainerImage.fromRegistry('test'), + minHealthyPercent: 100, assignPublicIp: true, }); ``` @@ -578,6 +594,7 @@ const queueProcessingFargateService = new ecsPatterns.QueueProcessingFargateServ vpc, memoryLimitMiB: 512, image: ecs.ContainerImage.fromRegistry('test'), + minHealthyPercent: 100, maxReceiveCount: 42, retentionPeriod: Duration.days(7), visibilityTimeout: Duration.minutes(5), @@ -595,6 +612,7 @@ const queueProcessingFargateService = new ecsPatterns.QueueProcessingFargateServ vpc, memoryLimitMiB: 512, image: ecs.ContainerImage.fromRegistry('test'), + minHealthyPercent: 100, assignPublicIp: true, cooldown: Duration.seconds(500), }); @@ -610,6 +628,7 @@ const queueProcessingFargateService = new ecsPatterns.QueueProcessingFargateServ cluster, memoryLimitMiB: 512, image: ecs.ContainerImage.fromRegistry('test'), + minHealthyPercent: 100, capacityProviderStrategies: [ { capacityProvider: 'FARGATE_SPOT', @@ -632,6 +651,7 @@ const queueProcessingFargateService = new ecsPatterns.QueueProcessingFargateServ vpc, memoryLimitMiB: 512, image: ecs.ContainerImage.fromRegistry('test'), + minHealthyPercent: 100, healthCheck: { command: [ "CMD-SHELL", "curl -f http://localhost/ || exit 1" ], // the properties below are optional @@ -664,6 +684,7 @@ const queueProcessingEc2Service = new ecsPatterns.QueueProcessingEc2Service(this cluster, memoryLimitMiB: 512, image: ecs.ContainerImage.fromRegistry('test'), + minHealthyPercent: 100, capacityProviderStrategies: [ { capacityProvider: capacityProvider.capacityProviderName, @@ -684,6 +705,7 @@ const loadBalancedFargateService = new ecsPatterns.ApplicationLoadBalancedFargat taskImageOptions: { image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), }, + minHealthyPercent: 100, taskSubnets: { subnets: [ec2.Subnet.fromSubnetId(this, 'subnet', 'VpcISOLATEDSubnet1Subnet80F07FA0')], }, @@ -702,6 +724,7 @@ const loadBalancedFargateService = new ecsPatterns.ApplicationLoadBalancedFargat taskImageOptions: { image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), }, + minHealthyPercent: 100, idleTimeout: Duration.seconds(120), }); ``` @@ -881,6 +904,7 @@ const applicationLoadBalancedFargateService = new ecsPatterns.ApplicationLoadBal taskImageOptions: { image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), }, + minHealthyPercent: 100, runtimePlatform: { cpuArchitecture: ecs.CpuArchitecture.ARM64, operatingSystemFamily: ecs.OperatingSystemFamily.LINUX, @@ -966,6 +990,7 @@ const service = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'Ser cluster, vpc, desiredCount: 1, + minHealthyPercent: 100, taskImageOptions: { image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), dockerLabels: { @@ -992,6 +1017,7 @@ const loadBalancedFargateService = new ecsPatterns.ApplicationLoadBalancedFargat taskImageOptions: { image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), }, + minHealthyPercent: 100, taskSubnets: { subnets: [ec2.Subnet.fromSubnetId(this, 'subnet', 'VpcISOLATEDSubnet1Subnet80F07FA0')], }, @@ -1020,6 +1046,7 @@ const loadBalancedFargateService = new ecsPatterns.ApplicationLoadBalancedFargat taskImageOptions: { image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), }, + minHealthyPercent: 100, enableExecuteCommand: true }); ``` @@ -1095,6 +1122,7 @@ const applicationLoadBalancedFargateService = new ecsPatterns.ApplicationLoadBal taskImageOptions: { image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), }, + minHealthyPercent: 100, }); const networkLoadBalancedFargateService = new ecsPatterns.NetworkLoadBalancedFargateService(this, 'NLBFargateServiceWithCustomEphemeralStorage', { @@ -1105,6 +1133,7 @@ const networkLoadBalancedFargateService = new ecsPatterns.NetworkLoadBalancedFar taskImageOptions: { image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), }, + minHealthyPercent: 100, }); ``` @@ -1119,6 +1148,7 @@ const queueProcessingFargateService = new ecsPatterns.NetworkLoadBalancedFargate taskImageOptions: { image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), }, + minHealthyPercent: 100, securityGroups: [securityGroup], }); ``` @@ -1195,6 +1225,7 @@ const service = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'myS taskImageOptions: { image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), }, + minHealthyPercent: 100, ipAddressType: elbv2.IpAddressType.DUAL_STACK, }); @@ -1203,6 +1234,7 @@ const applicationLoadBalancedEc2Service = new ecsPatterns.ApplicationLoadBalance taskImageOptions: { image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), }, + minHealthyPercent: 100, ipAddressType: elbv2.IpAddressType.DUAL_STACK, }); ``` @@ -1225,6 +1257,7 @@ const networkLoadbalancedFargateService = new ecsPatterns.NetworkLoadBalancedFar taskImageOptions: { image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), }, + minHealthyPercent: 100, ipAddressType: elbv2.IpAddressType.DUAL_STACK, }); @@ -1233,6 +1266,7 @@ const networkLoadbalancedEc2Service = new ecsPatterns.NetworkLoadBalancedEc2Serv taskImageOptions: { image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), }, + minHealthyPercent: 100, ipAddressType: elbv2.IpAddressType.DUAL_STACK, }); ``` diff --git a/packages/aws-cdk-lib/aws-ecs/README.md b/packages/aws-cdk-lib/aws-ecs/README.md index dce4c0c88c817..76a0e49414600 100644 --- a/packages/aws-cdk-lib/aws-ecs/README.md +++ b/packages/aws-cdk-lib/aws-ecs/README.md @@ -35,6 +35,7 @@ taskDefinition.addContainer('DefaultContainer', { const ecsService = new ecs.Ec2Service(this, 'Service', { cluster, taskDefinition, + minHealthyPercent: 100, }); ``` @@ -761,6 +762,7 @@ const service = new ecs.FargateService(this, 'Service', { cluster, taskDefinition, desiredCount: 5, + minHealthyPercent: 100, }); ``` @@ -774,6 +776,7 @@ const service = new ecs.ExternalService(this, 'Service', { cluster, taskDefinition, desiredCount: 5, + minHealthyPercent: 100, }); ``` @@ -794,14 +797,16 @@ new ecs.ExternalService(this, 'Service', { cluster, taskDefinition, desiredCount: 5, - taskDefinitionRevision: ecs.TaskDefinitionRevision.of(1) + minHealthyPercent: 100, + taskDefinitionRevision: ecs.TaskDefinitionRevision.of(1), }); new ecs.ExternalService(this, 'Service', { cluster, taskDefinition, desiredCount: 5, - taskDefinitionRevision: ecs.TaskDefinitionRevision.LATEST + minHealthyPercent: 100, + taskDefinitionRevision: ecs.TaskDefinitionRevision.LATEST, }); ``` @@ -822,6 +827,7 @@ declare const taskDefinition: ecs.TaskDefinition; const service = new ecs.FargateService(this, 'Service', { cluster, taskDefinition, + minHealthyPercent: 100, circuitBreaker: { enable: true, rollback: true @@ -858,6 +864,7 @@ declare const elbAlarm: cw.Alarm; const service = new ecs.FargateService(this, 'Service', { cluster, taskDefinition, + minHealthyPercent: 100, deploymentAlarms: { alarmNames: [elbAlarm.alarmName], behavior: ecs.AlarmBehavior.ROLLBACK_ON_ALARM, @@ -944,6 +951,7 @@ const service = new ecs.FargateService(this, 'Service', { serviceName, cluster, taskDefinition, + minHealthyPercent: 100, }); const cpuMetric = new cw.Metric({ @@ -981,7 +989,7 @@ on the service, there will be no restrictions on the alarm name. declare const vpc: ec2.Vpc; declare const cluster: ecs.Cluster; declare const taskDefinition: ecs.TaskDefinition; -const service = new ecs.FargateService(this, 'Service', { cluster, taskDefinition }); +const service = new ecs.FargateService(this, 'Service', { cluster, taskDefinition, minHealthyPercent: 100 }); const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { vpc, internetFacing: true }); const listener = lb.addListener('Listener', { port: 80 }); @@ -1008,7 +1016,7 @@ Alternatively, you can also create all load balancer targets to be registered in declare const cluster: ecs.Cluster; declare const taskDefinition: ecs.TaskDefinition; declare const vpc: ec2.Vpc; -const service = new ecs.FargateService(this, 'Service', { cluster, taskDefinition }); +const service = new ecs.FargateService(this, 'Service', { cluster, taskDefinition, minHealthyPercent: 100 }); const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { vpc, internetFacing: true }); const listener = lb.addListener('Listener', { port: 80 }); @@ -1047,7 +1055,7 @@ for the alternatives. declare const cluster: ecs.Cluster; declare const taskDefinition: ecs.TaskDefinition; declare const vpc: ec2.Vpc; -const service = new ecs.Ec2Service(this, 'Service', { cluster, taskDefinition }); +const service = new ecs.Ec2Service(this, 'Service', { cluster, taskDefinition, minHealthyPercent: 100 }); const lb = new elb.LoadBalancer(this, 'LB', { vpc }); lb.addListener({ externalPort: 80 }); @@ -1060,7 +1068,7 @@ Similarly, if you want to have more control over load balancer targeting: declare const cluster: ecs.Cluster; declare const taskDefinition: ecs.TaskDefinition; declare const vpc: ec2.Vpc; -const service = new ecs.Ec2Service(this, 'Service', { cluster, taskDefinition }); +const service = new ecs.Ec2Service(this, 'Service', { cluster, taskDefinition, minHealthyPercent: 100 }); const lb = new elb.LoadBalancer(this, 'LB', { vpc }); lb.addListener({ externalPort: 80 }); @@ -1396,6 +1404,7 @@ specificContainer.addPortMappings({ new ecs.Ec2Service(this, 'Service', { cluster, taskDefinition, + minHealthyPercent: 100, cloudMapOptions: { // Create SRV records - useful for bridge networking dnsRecordType: cloudmap.DnsRecordType.SRV, @@ -1453,6 +1462,7 @@ taskDefinition.addContainer('web', { new ecs.FargateService(this, 'FargateService', { cluster, taskDefinition, + minHealthyPercent: 100, capacityProviderStrategies: [ { capacityProvider: 'FARGATE_SPOT', @@ -1530,6 +1540,7 @@ taskDefinition.addContainer('web', { new ecs.Ec2Service(this, 'EC2Service', { cluster, taskDefinition, + minHealthyPercent: 100, capacityProviderStrategies: [ { capacityProvider: capacityProvider.capacityProviderName, @@ -1626,6 +1637,7 @@ declare const taskDefinition: ecs.TaskDefinition; const service = new ecs.Ec2Service(this, 'Service', { cluster, taskDefinition, + minHealthyPercent: 100, enableExecuteCommand: true, }); ``` @@ -1699,6 +1711,7 @@ cluster.addDefaultCloudMapNamespace({ const service = new ecs.FargateService(this, 'Service', { cluster, taskDefinition, + minHealthyPercent: 100, serviceConnectConfiguration: { services: [ { @@ -1723,6 +1736,7 @@ declare const taskDefinition: ecs.TaskDefinition; const service = new ecs.FargateService(this, 'Service', { cluster, taskDefinition, + minHealthyPercent: 100, }); service.enableServiceConnect(); ``` @@ -1736,6 +1750,7 @@ declare const taskDefinition: ecs.TaskDefinition; const customService = new ecs.FargateService(this, 'CustomizedService', { cluster, taskDefinition, + minHealthyPercent: 100, serviceConnectConfiguration: { logDriver: ecs.LogDrivers.awsLogs({ streamPrefix: 'sc-traffic', @@ -1765,6 +1780,7 @@ declare const taskDefinition: ecs.TaskDefinition; const service = new ecs.FargateService(this, 'Service', { cluster, taskDefinition, + minHealthyPercent: 100, serviceConnectConfiguration: { services: [ { @@ -1828,6 +1844,7 @@ taskDefinition.addVolume(volume); const service = new ecs.FargateService(this, 'FargateService', { cluster, taskDefinition, + minHealthyPercent: 100, }); service.addVolume(volume); @@ -1857,6 +1874,7 @@ taskDefinition.addVolume(volumeFromSnapshot); const service = new ecs.FargateService(this, 'FargateService', { cluster, taskDefinition, + minHealthyPercent: 100, }); service.addVolume(volumeFromSnapshot); diff --git a/packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts b/packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts index 9f5891ba78e55..9c627fe7d0baf 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts @@ -708,6 +708,10 @@ export abstract class BaseService extends Resource throw new Error('CODE_DEPLOY deploymentController can only be used with the `latest` task definition revision'); } + if (props.minHealthyPercent === undefined) { + Annotations.of(this).addWarningV2('@aws-cdk/aws-ecs:minHealthyPercent', 'minHealthyPercent has not been configured so the default value of 50% is used. The number of running tasks will decrease below the desired count during deployments etc. See https://github.com/aws/aws-cdk/issues/31705'); + } + if (props.deploymentController?.type === DeploymentControllerType.CODE_DEPLOY) { // Strip the revision ID from the service's task definition property to // prevent new task def revisions in the stack from triggering updates diff --git a/packages/aws-cdk-lib/aws-ecs/lib/ec2/ec2-service.ts b/packages/aws-cdk-lib/aws-ecs/lib/ec2/ec2-service.ts index 09c731f40517f..0b5eeae081948 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/ec2/ec2-service.ts @@ -1,6 +1,6 @@ import { Construct } from 'constructs'; import * as ec2 from '../../../aws-ec2'; -import { Lazy, Resource, Stack } from '../../../core'; +import { Lazy, Resource, Stack, Annotations } from '../../../core'; import { BaseService, BaseServiceOptions, DeploymentControllerType, IBaseService, IService, LaunchType } from '../base/base-service'; import { fromServiceAttributes, extractServiceNameFromArn } from '../base/from-service-attributes'; import { NetworkMode, TaskDefinition } from '../base/task-definition'; @@ -220,6 +220,10 @@ export class Ec2Service extends BaseService implements IEc2Service { }); this.node.addValidation({ validate: this.validateEc2Service.bind(this) }); + + if (props.minHealthyPercent === undefined && props.daemon) { + Annotations.of(this).addWarningV2('@aws-cdk/aws-ecs:minHealthyPercentDaemon', 'minHealthyPercent has not been configured so the default value of 0% for a daemon service is used. See https://github.com/aws/aws-cdk/issues/31705'); + } } /** diff --git a/packages/aws-cdk-lib/aws-ecs/lib/external/external-service.ts b/packages/aws-cdk-lib/aws-ecs/lib/external/external-service.ts index 89b52f908554a..4c79f5b917e03 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/external/external-service.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/external/external-service.ts @@ -3,7 +3,7 @@ import * as appscaling from '../../../aws-applicationautoscaling'; import * as ec2 from '../../../aws-ec2'; import * as elbv2 from '../../../aws-elasticloadbalancingv2'; import * as cloudmap from '../../../aws-servicediscovery'; -import { ArnFormat, Resource, Stack } from '../../../core'; +import { ArnFormat, Resource, Stack, Annotations } from '../../../core'; import { AssociateCloudMapServiceOptions, BaseService, BaseServiceOptions, CloudMapOptions, DeploymentControllerType, EcsTarget, IBaseService, IEcsLoadBalancerTarget, IService, LaunchType, PropagatedTagSource } from '../base/base-service'; import { fromServiceAttributes } from '../base/from-service-attributes'; import { ScalableTaskCount } from '../base/scalable-task-count'; @@ -132,6 +132,11 @@ export class ExternalService extends BaseService implements IExternalService { this.node.addValidation({ validate: () => this.networkConfiguration !== undefined ? ['Network configurations not supported for an external service'] : [], }); + + if (props.minHealthyPercent === undefined) { + Annotations.of(this).addWarningV2('@aws-cdk/aws-ecs:minHealthyPercentExternal', 'minHealthyPercent has not been configured so the default value of 0% for an external service is used. The number of running tasks will decrease below the desired count during deployments etc. See https://github.com/aws/aws-cdk/issues/31705'); + } + } /** diff --git a/packages/aws-cdk-lib/aws-ecs/test/ec2/ec2-service.test.ts b/packages/aws-cdk-lib/aws-ecs/test/ec2/ec2-service.test.ts index b6aff9a744f7a..b40634812a452 100644 --- a/packages/aws-cdk-lib/aws-ecs/test/ec2/ec2-service.test.ts +++ b/packages/aws-cdk-lib/aws-ecs/test/ec2/ec2-service.test.ts @@ -1630,6 +1630,100 @@ describe('ec2 service', () => { }); + test('warning if minHealthyPercent not set', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + addDefaultCapacityProvider(cluster, stack, vpc); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + new ecs.Ec2Service(stack, 'Ec2Service', { + cluster, + taskDefinition, + }); + + // THEN + Annotations.fromStack(stack).hasWarning('/Default/Ec2Service', 'minHealthyPercent has not been configured so the default value of 50% is used. The number of running tasks will decrease below the desired count during deployments etc. See https://github.com/aws/aws-cdk/issues/31705 [ack: @aws-cdk/aws-ecs:minHealthyPercent]'); + }); + + test('no warning if minHealthyPercent set', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + addDefaultCapacityProvider(cluster, stack, vpc); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + new ecs.Ec2Service(stack, 'Ec2Service', { + cluster, + taskDefinition, + minHealthyPercent: 50, + }); + + // THEN + Annotations.fromStack(stack).hasNoWarning('/Default/Ec2Service', 'minHealthyPercent has not been configured so the default value of 50% is used. The number of running tasks will decrease below the desired count during deployments etc. See https://github.com/aws/aws-cdk/issues/31705 [ack: @aws-cdk/aws-ecs:minHealthyPercent]'); + }); + + test('warning if minHealthyPercent not set for a daemon service', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + addDefaultCapacityProvider(cluster, stack, vpc); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + new ecs.Ec2Service(stack, 'Ec2Service', { + cluster, + taskDefinition, + daemon: true, + }); + + // THEN + Annotations.fromStack(stack).hasWarning('/Default/Ec2Service', 'minHealthyPercent has not been configured so the default value of 0% for a daemon service is used. See https://github.com/aws/aws-cdk/issues/31705 [ack: @aws-cdk/aws-ecs:minHealthyPercentDaemon]'); + Annotations.fromStack(stack).hasNoWarning('/Default/Ec2Service', 'minHealthyPercent has not been configured so the default value of 50% is used. The number of running tasks will decrease below the desired count during deployments etc. See https://github.com/aws/aws-cdk/issues/31705 [ack: @aws-cdk/aws-ecs:minHealthyPercent]'); + }); + + test('no warning if minHealthyPercent set for a daemon service', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + addDefaultCapacityProvider(cluster, stack, vpc); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + new ecs.Ec2Service(stack, 'Ec2Service', { + cluster, + taskDefinition, + minHealthyPercent: 50, + daemon: true, + }); + + // THEN + Annotations.fromStack(stack).hasNoWarning('/Default/Ec2Service', 'minHealthyPercent has not been configured so the default value of 0% for a daemon service is used. See https://github.com/aws/aws-cdk/issues/31705 [ack: @aws-cdk/aws-ecs:minHealthyPercentDaemon]'); + Annotations.fromStack(stack).hasNoWarning('/Default/Ec2Service', 'minHealthyPercent has not been configured so the default value of 50% is used. The number of running tasks will decrease below the desired count during deployments etc. See https://github.com/aws/aws-cdk/issues/31705 [ack: @aws-cdk/aws-ecs:minHealthyPercent]'); + }); + describe('with a TaskDefinition with Bridge network mode', () => { test('it errors if vpcSubnets is specified', () => { // GIVEN diff --git a/packages/aws-cdk-lib/aws-ecs/test/external/external-service.test.ts b/packages/aws-cdk-lib/aws-ecs/test/external/external-service.test.ts index d77246d693959..9eeb40aafee2e 100644 --- a/packages/aws-cdk-lib/aws-ecs/test/external/external-service.test.ts +++ b/packages/aws-cdk-lib/aws-ecs/test/external/external-service.test.ts @@ -1,4 +1,4 @@ -import { Template } from '../../../assertions'; +import { Template, Annotations } from '../../../assertions'; import * as autoscaling from '../../../aws-autoscaling'; import * as cloudwatch from '../../../aws-cloudwatch'; import * as ec2 from '../../../aws-ec2'; @@ -581,6 +581,7 @@ describe('external service', () => { type: DeploymentControllerType.EXTERNAL, }, circuitBreaker: { rollback: true }, + minHealthyPercent: 100, // required to prevent test failure due to warning }); app.synth(); @@ -590,4 +591,52 @@ describe('external service', () => { 'Deployment circuit breaker requires the ECS deployment controller.', ]); }); + + test('warning if minHealthyPercent not set for an external service', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + addDefaultCapacityProvider(cluster, stack, vpc); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + // THEN + Annotations.fromStack(stack).hasWarning('/Default/ExternalService', 'minHealthyPercent has not been configured so the default value of 0% for an external service is used. The number of running tasks will decrease below the desired count during deployments etc. See https://github.com/aws/aws-cdk/issues/31705 [ack: @aws-cdk/aws-ecs:minHealthyPercentExternal]'); + Annotations.fromStack(stack).hasNoWarning('/Default/ExternalService', 'minHealthyPercent has not been configured so the default value of 50% is used. The number of running tasks will decrease below the desired count during deployments etc. See https://github.com/aws/aws-cdk/issues/31705 [ack: @aws-cdk/aws-ecs:minHealthyPercent]'); + }); + + test('no warning if minHealthyPercent set for an external service', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + addDefaultCapacityProvider(cluster, stack, vpc); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + minHealthyPercent: 100, + }); + + // THEN + Annotations.fromStack(stack).hasNoWarning('/Default/ExternalService', 'minHealthyPercent has not been configured so the default value of 0% for an external service is used. The number of running tasks will decrease below the desired count during deployments etc. See https://github.com/aws/aws-cdk/issues/31705 [ack: @aws-cdk/aws-ecs:minHealthyPercentExternal]'); + Annotations.fromStack(stack).hasNoWarning('/Default/ExternalService', 'minHealthyPercent has not been configured so the default value of 50% is used. The number of running tasks will decrease below the desired count during deployments etc. See https://github.com/aws/aws-cdk/issues/31705 [ack: @aws-cdk/aws-ecs:minHealthyPercent]'); + }); + }); diff --git a/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-service.test.ts b/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-service.test.ts index e8659057ef90b..59d353ba5a611 100644 --- a/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-service.test.ts +++ b/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-service.test.ts @@ -1343,6 +1343,47 @@ describe('fargate service', () => { }, }); }); + + test('warning if minHealthyPercent not set', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + }); + + const service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + + // THEN + Annotations.fromStack(stack).hasWarning('/Default/FargateService', 'minHealthyPercent has not been configured so the default value of 50% is used. The number of running tasks will decrease below the desired count during deployments etc. See https://github.com/aws/aws-cdk/issues/31705 [ack: @aws-cdk/aws-ecs:minHealthyPercent]'); + }); + + test('no warning if minHealthyPercent set', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + }); + + const service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + minHealthyPercent: 50, + }); + + // THEN + Annotations.fromStack(stack).hasNoWarning('/Default/FargateService', 'minHealthyPercent has not been configured so the default value of 50% is used. The number of running tasks will decrease below the desired count during deployments etc. See https://github.com/aws/aws-cdk/issues/31705 [ack: @aws-cdk/aws-ecs:minHealthyPercent]'); + }); }); describe('when enabling service connect', () => { @@ -3502,6 +3543,7 @@ describe('fargate service', () => { const service = new ecs.FargateService(stack, 'Service', { cluster, taskDefinition, + minHealthyPercent: 50, // must be set to avoid warning causing test failure }); // WHEN