Skip to content

Commit a9caa45

Browse files
authored
feat(ecs-patterns): remove default desiredCount to align with cfn behaviour (under feature flag) (#13130)
AWS CloudFormation previously required a desiredCount to be specified to create an Amazon ECS Service. They have recently removed that required field, and now provide the following default behaviour, if the desired count is not specified. * when creating a service, the service is created and starts up with a desired count of 1. * when updating a service, the service is updated and starts up with the same desired count as it had prior to the deployment. The AWS-CDK currently adds a default desiredCount of 1 when creating or updating an Amazon ECS service, and this could lead to issues when a service has scaled up due to autoscaling. When updating a service using the AWS-CDK, the service would then be scaled down to the original desiredCount, leading to potential outages. This change introduces a new feature flag `REMOVE_DEFAULT_DESIRED_COUNT` to flip the default behaviour of setting the desiredCount for any service created using any of the following ECS Patterns constructs. Without the feature flag set, the desiredCount of any service created is automatically defaulted to 1 (which is the current behaviour). With the feature flag set (to true), the default will be removed and the desiredCount will only be passed to the service if it is passed in as input. * ApplicationLoadBalancedEc2Service * ApplicationLoadBalancedFargateService * NetworkLoadBalancedEc2Service * NetworkLoadBalancedFargateService * QueueProcessingEc2Service * QueueProcessingFargateService **BREAKING CHANGE:** the desiredCount property stored on the above constructs will be optional, allowing them to be undefined. This is enabled through the `@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount` feature flag. We would recommend all aws-cdk users to set the `REMOVE_DEFAULT_DESIRED_COUNT` flag to true for all of their existing applications. Fixes: #12990 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 5d71d8e commit a9caa45

File tree

34 files changed

+436
-45
lines changed

34 files changed

+436
-45
lines changed

packages/@aws-cdk/aws-ecs-patterns/README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,3 +466,41 @@ const scheduledFargateTask = new ScheduledFargateTask(stack, 'ScheduledFargateTa
466466
platformVersion: ecs.FargatePlatformVersion.VERSION1_4,
467467
});
468468
```
469+
470+
### Use the REMOVE_DEFAULT_DESIRED_COUNT feature flag
471+
472+
The REMOVE_DEFAULT_DESIRED_COUNT feature flag is used to override the default desiredCount that is autogenerated by the CDK. This will set the desiredCount of any service created by any of the following constructs to be undefined.
473+
474+
* ApplicationLoadBalancedEc2Service
475+
* ApplicationLoadBalancedFargateService
476+
* NetworkLoadBalancedEc2Service
477+
* NetworkLoadBalancedFargateService
478+
* QueueProcessingEc2Service
479+
* QueueProcessingFargateService
480+
481+
If a desiredCount is not passed in as input to the above constructs, CloudFormation will either create a new service to start up with a desiredCount of 1, or update an existing service to start up with the same desiredCount as prior to the update.
482+
483+
To enable the feature flag, ensure that the REMOVE_DEFAULT_DESIRED_COUNT flag within an application stack context is set to true, like so:
484+
485+
```ts
486+
stack.node.setContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT, true);
487+
```
488+
489+
The following is an example of an application with the REMOVE_DEFAULT_DESIRED_COUNT feature flag enabled:
490+
491+
```ts
492+
const app = new App();
493+
494+
const stack = new Stack(app, 'aws-ecs-patterns-queue');
495+
stack.node.setContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT, true);
496+
497+
const vpc = new ec2.Vpc(stack, 'VPC', {
498+
maxAzs: 2,
499+
});
500+
501+
new QueueProcessingFargateService(stack, 'QueueProcessingService', {
502+
vpc,
503+
memoryLimitMiB: 512,
504+
image: new ecs.AssetImage(path.join(__dirname, '..', 'sqs-reader')),
505+
});
506+
```

packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ export interface ApplicationLoadBalancedServiceBaseProps {
7878
* The desired number of instantiations of the task definition to keep running on the service.
7979
* The minimum value is 1
8080
*
81-
* @default 1
81+
* @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is 1;
82+
* if true, the default is 1 for all new services and uses the existing services desired count
83+
* when updating an existing service.
8284
*/
8385
readonly desiredCount?: number;
8486

@@ -311,12 +313,19 @@ export interface ApplicationLoadBalancedTaskImageOptions {
311313
* The base class for ApplicationLoadBalancedEc2Service and ApplicationLoadBalancedFargateService services.
312314
*/
313315
export abstract class ApplicationLoadBalancedServiceBase extends CoreConstruct {
314-
315316
/**
316317
* The desired number of instantiations of the task definition to keep running on the service.
318+
* @deprecated - Use `internalDesiredCount` instead.
317319
*/
318320
public readonly desiredCount: number;
319321

322+
/**
323+
* The desired number of instantiations of the task definition to keep running on the service.
324+
* The default is 1 for all new services and uses the existing services desired count
325+
* when updating an existing service if one is not provided.
326+
*/
327+
public readonly internalDesiredCount?: number;
328+
320329
/**
321330
* The Application Load Balancer for the service.
322331
*/
@@ -368,7 +377,9 @@ export abstract class ApplicationLoadBalancedServiceBase extends CoreConstruct {
368377
if (props.desiredCount !== undefined && props.desiredCount < 1) {
369378
throw new Error('You must specify a desiredCount greater than 0');
370379
}
380+
371381
this.desiredCount = props.desiredCount || 1;
382+
this.internalDesiredCount = props.desiredCount;
372383

373384
const internetFacing = props.publicLoadBalancer ?? true;
374385

packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ export interface ApplicationMultipleTargetGroupsServiceBaseProps {
4747
/**
4848
* The desired number of instantiations of the task definition to keep running on the service.
4949
*
50-
* @default 1
50+
* @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is 1;
51+
* if true, the default is 1 for all new services and uses the existing services desired count
52+
* when updating an existing service.
5153
*/
5254
readonly desiredCount?: number;
5355

@@ -329,12 +331,19 @@ export interface ApplicationListenerProps {
329331
* The base class for ApplicationMultipleTargetGroupsEc2Service and ApplicationMultipleTargetGroupsFargateService classes.
330332
*/
331333
export abstract class ApplicationMultipleTargetGroupsServiceBase extends CoreConstruct {
332-
333334
/**
334335
* The desired number of instantiations of the task definition to keep running on the service.
336+
* @deprecated - Use `internalDesiredCount` instead.
335337
*/
336338
public readonly desiredCount: number;
337339

340+
/**
341+
* The desired number of instantiations of the task definition to keep running on the service.
342+
* The default is 1 for all new services and uses the existing services desired count
343+
* when updating an existing service, if one is not provided.
344+
*/
345+
public readonly internalDesiredCount?: number;
346+
338347
/**
339348
* The default Application Load Balancer for the service (first added load balancer).
340349
*/
@@ -365,7 +374,10 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends CoreCon
365374
this.validateInput(props);
366375

367376
this.cluster = props.cluster || this.getDefaultCluster(this, props.vpc);
377+
368378
this.desiredCount = props.desiredCount || 1;
379+
this.internalDesiredCount = props.desiredCount;
380+
369381
if (props.taskImageOptions) {
370382
this.logDriver = this.createLogDriver(props.taskImageOptions.enableLogging, props.taskImageOptions.logDriver);
371383
}

packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ export interface NetworkLoadBalancedServiceBaseProps {
6767
* The desired number of instantiations of the task definition to keep running on the service.
6868
* The minimum value is 1
6969
*
70-
* @default 1
70+
* @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is 1;
71+
* if true, the default is 1 for all new services and uses the existing services desired count
72+
* when updating an existing service.
7173
*/
7274
readonly desiredCount?: number;
7375

@@ -263,9 +265,17 @@ export interface NetworkLoadBalancedTaskImageOptions {
263265
export abstract class NetworkLoadBalancedServiceBase extends CoreConstruct {
264266
/**
265267
* The desired number of instantiations of the task definition to keep running on the service.
268+
* @deprecated - Use `internalDesiredCount` instead.
266269
*/
267270
public readonly desiredCount: number;
268271

272+
/**
273+
* The desired number of instantiations of the task definition to keep running on the service.
274+
* The default is 1 for all new services and uses the existing services desired count
275+
* when updating an existing service, if one is not provided.
276+
*/
277+
public readonly internalDesiredCount?: number;
278+
269279
/**
270280
* The Network Load Balancer for the service.
271281
*/
@@ -306,7 +316,9 @@ export abstract class NetworkLoadBalancedServiceBase extends CoreConstruct {
306316
if (props.desiredCount !== undefined && props.desiredCount < 1) {
307317
throw new Error('You must specify a desiredCount greater than 0');
308318
}
319+
309320
this.desiredCount = props.desiredCount || 1;
321+
this.internalDesiredCount = props.desiredCount;
310322

311323
const internetFacing = props.publicLoadBalancer ?? true;
312324

packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ export interface NetworkMultipleTargetGroupsServiceBaseProps {
4545
* The desired number of instantiations of the task definition to keep running on the service.
4646
* The minimum value is 1
4747
*
48-
* @default 1
48+
* @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is 1;
49+
* if true, the default is 1 for all new services and uses the existing services desired count
50+
* when updating an existing service.
4951
*/
5052
readonly desiredCount?: number;
5153

@@ -264,9 +266,17 @@ export interface NetworkTargetProps {
264266
export abstract class NetworkMultipleTargetGroupsServiceBase extends CoreConstruct {
265267
/**
266268
* The desired number of instantiations of the task definition to keep running on the service.
269+
* @deprecated - Use `internalDesiredCount` instead.
267270
*/
268271
public readonly desiredCount: number;
269272

273+
/**
274+
* The desired number of instantiations of the task definition to keep running on the service.
275+
* The default is 1 for all new services and uses the existing services desired count
276+
* when updating an existing service, if one is not provided.
277+
*/
278+
public readonly internalDesiredCount?: number;
279+
270280
/**
271281
* The Network Load Balancer for the service.
272282
*/
@@ -297,7 +307,10 @@ export abstract class NetworkMultipleTargetGroupsServiceBase extends CoreConstru
297307
this.validateInput(props);
298308

299309
this.cluster = props.cluster || this.getDefaultCluster(this, props.vpc);
310+
300311
this.desiredCount = props.desiredCount || 1;
312+
this.internalDesiredCount = props.desiredCount;
313+
301314
if (props.taskImageOptions) {
302315
this.logDriver = this.createLogDriver(props.taskImageOptions.enableLogging, props.taskImageOptions.logDriver);
303316
}

packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { IVpc } from '@aws-cdk/aws-ec2';
33
import { AwsLogDriver, BaseService, Cluster, ContainerImage, DeploymentController, ICluster, LogDriver, PropagatedTagSource, Secret } from '@aws-cdk/aws-ecs';
44
import { IQueue, Queue } from '@aws-cdk/aws-sqs';
55
import { CfnOutput, Duration, Stack } from '@aws-cdk/core';
6+
import * as cxapi from '@aws-cdk/cx-api';
67
import { Construct } from 'constructs';
78

89
// v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch.
@@ -53,7 +54,10 @@ export interface QueueProcessingServiceBaseProps {
5354
/**
5455
* The desired number of instantiations of the task definition to keep running on the service.
5556
*
56-
* @default 1
57+
* @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is 1;
58+
* if true, the minScalingCapacity is 1 for all new services and uses the existing services desired count
59+
* when updating an existing service.
60+
* @deprecated - Use `minScalingCapacity` or a literal object instead.
5761
*/
5862
readonly desiredTaskCount?: number;
5963

@@ -109,10 +113,17 @@ export interface QueueProcessingServiceBaseProps {
109113
/**
110114
* Maximum capacity to scale to.
111115
*
112-
* @default (desiredTaskCount * 2)
116+
* @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is (desiredTaskCount * 2); if true, the default is 2.
113117
*/
114118
readonly maxScalingCapacity?: number
115119

120+
/**
121+
* Minimum capacity to scale to.
122+
*
123+
* @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is the desiredTaskCount; if true, the default is 1.
124+
*/
125+
readonly minScalingCapacity?: number
126+
116127
/**
117128
* The intervals for scaling based on the SQS queue's ApproximateNumberOfMessagesVisible metric.
118129
*
@@ -214,6 +225,7 @@ export abstract class QueueProcessingServiceBase extends CoreConstruct {
214225

215226
/**
216227
* The minimum number of tasks to run.
228+
* @deprecated - Use `minCapacity` instead.
217229
*/
218230
public readonly desiredCount: number;
219231

@@ -222,6 +234,11 @@ export abstract class QueueProcessingServiceBase extends CoreConstruct {
222234
*/
223235
public readonly maxCapacity: number;
224236

237+
/**
238+
* The minimum number of instances for autoscaling to scale down to.
239+
*/
240+
public readonly minCapacity: number;
241+
225242
/**
226243
* The scaling interval for autoscaling based off an SQS Queue size.
227244
*/
@@ -272,9 +289,21 @@ export abstract class QueueProcessingServiceBase extends CoreConstruct {
272289
this.environment = { ...(props.environment || {}), QUEUE_NAME: this.sqsQueue.queueName };
273290
this.secrets = props.secrets;
274291

275-
// Determine the desired task count (minimum) and maximum scaling capacity
276292
this.desiredCount = props.desiredTaskCount ?? 1;
277-
this.maxCapacity = props.maxScalingCapacity || (2 * this.desiredCount);
293+
294+
// Determine the desired task count (minimum) and maximum scaling capacity
295+
if (!this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT)) {
296+
this.minCapacity = props.minScalingCapacity || this.desiredCount;
297+
this.maxCapacity = props.maxScalingCapacity || (2 * this.desiredCount);
298+
} else {
299+
if (props.desiredTaskCount != null) {
300+
this.minCapacity = props.minScalingCapacity || this.desiredCount;
301+
this.maxCapacity = props.maxScalingCapacity || (2 * this.desiredCount);
302+
} else {
303+
this.minCapacity = props.minScalingCapacity || 1;
304+
this.maxCapacity = props.maxScalingCapacity || 2;
305+
}
306+
}
278307

279308
if (!this.desiredCount && !this.maxCapacity) {
280309
throw new Error('maxScalingCapacity must be set and greater than 0 if desiredCount is 0');
@@ -290,7 +319,7 @@ export abstract class QueueProcessingServiceBase extends CoreConstruct {
290319
* @param service the ECS/Fargate service for which to apply the autoscaling rules to
291320
*/
292321
protected configureAutoscalingForService(service: BaseService) {
293-
const scalingTarget = service.autoScaleTaskCount({ maxCapacity: this.maxCapacity, minCapacity: this.desiredCount });
322+
const scalingTarget = service.autoScaleTaskCount({ maxCapacity: this.maxCapacity, minCapacity: this.minCapacity });
294323
scalingTarget.scaleOnCpuUtilization('CpuScaling', {
295324
targetUtilizationPercent: 50,
296325
});

packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Ec2Service, Ec2TaskDefinition } from '@aws-cdk/aws-ecs';
2+
import * as cxapi from '@aws-cdk/cx-api';
23
import { Construct } from 'constructs';
34
import { ApplicationLoadBalancedServiceBase, ApplicationLoadBalancedServiceBaseProps } from '../base/application-load-balanced-service-base';
45

@@ -117,9 +118,11 @@ export class ApplicationLoadBalancedEc2Service extends ApplicationLoadBalancedSe
117118
throw new Error('You must specify one of: taskDefinition or image');
118119
}
119120

121+
const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount;
122+
120123
this.service = new Ec2Service(this, 'Service', {
121124
cluster: this.cluster,
122-
desiredCount: this.desiredCount,
125+
desiredCount: desiredCount,
123126
taskDefinition: this.taskDefinition,
124127
assignPublicIp: false,
125128
serviceName: props.serviceName,

packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Ec2Service, Ec2TaskDefinition } from '@aws-cdk/aws-ecs';
22
import { ApplicationTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2';
3+
import * as cxapi from '@aws-cdk/cx-api';
34
import { Construct } from 'constructs';
45
import {
56
ApplicationMultipleTargetGroupsServiceBase,
@@ -136,9 +137,11 @@ export class ApplicationMultipleTargetGroupsEc2Service extends ApplicationMultip
136137
}
137138

138139
private createEc2Service(props: ApplicationMultipleTargetGroupsEc2ServiceProps): Ec2Service {
140+
const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount;
141+
139142
return new Ec2Service(this, 'Service', {
140143
cluster: this.cluster,
141-
desiredCount: this.desiredCount,
144+
desiredCount: desiredCount,
142145
taskDefinition: this.taskDefinition,
143146
assignPublicIp: false,
144147
serviceName: props.serviceName,

packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Ec2Service, Ec2TaskDefinition } from '@aws-cdk/aws-ecs';
2+
import * as cxapi from '@aws-cdk/cx-api';
23
import { Construct } from 'constructs';
34
import { NetworkLoadBalancedServiceBase, NetworkLoadBalancedServiceBaseProps } from '../base/network-load-balanced-service-base';
45

@@ -115,9 +116,11 @@ export class NetworkLoadBalancedEc2Service extends NetworkLoadBalancedServiceBas
115116
throw new Error('You must specify one of: taskDefinition or image');
116117
}
117118

119+
const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount;
120+
118121
this.service = new Ec2Service(this, 'Service', {
119122
cluster: this.cluster,
120-
desiredCount: this.desiredCount,
123+
desiredCount: desiredCount,
121124
taskDefinition: this.taskDefinition,
122125
assignPublicIp: false,
123126
serviceName: props.serviceName,

packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Ec2Service, Ec2TaskDefinition } from '@aws-cdk/aws-ecs';
22
import { NetworkTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2';
3+
import * as cxapi from '@aws-cdk/cx-api';
34
import { Construct } from 'constructs';
45
import {
56
NetworkMultipleTargetGroupsServiceBase,
@@ -136,9 +137,11 @@ export class NetworkMultipleTargetGroupsEc2Service extends NetworkMultipleTarget
136137
}
137138

138139
private createEc2Service(props: NetworkMultipleTargetGroupsEc2ServiceProps): Ec2Service {
140+
const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount;
141+
139142
return new Ec2Service(this, 'Service', {
140143
cluster: this.cluster,
141-
desiredCount: this.desiredCount,
144+
desiredCount: desiredCount,
142145
taskDefinition: this.taskDefinition,
143146
assignPublicIp: false,
144147
serviceName: props.serviceName,

0 commit comments

Comments
 (0)