Skip to content

Commit 5a30ea6

Browse files
authored
feat(ecs): enable default capacity provider strategy (#23955)
feat(aws-cdk):Adding Support for DefaultCapacityProviderStrategy for L2_cluster A capacity provider strategy determines whether ECS tasks are launched on EC2 instances or Fargate/Fargate Spot. It can be specified at the cluster, service, or task level, and consists of one or more capacity providers. You can specify an optional base and weight value for finer control of how tasks are launched. The `base` specifies a minimum number of tasks on one capacity provider, and the `weight`s of each capacity provider determine how tasks are distributed after `base` is satisfied. You can associate a default capacity provider strategy with an Amazon ECS cluster. After you do this, a default capacity provider strategy is used when creating a service or running a standalone task in the cluster and whenever a custom capacity provider strategy or a launch type isn't specified. We recommend that you define a default capacity provider strategy for each cluster. For more information visit https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cluster-capacity-providers.html When the service does not have a capacity provider strategy, the cluster's default capacity provider strategy will be used. Default Capacity Provider Strategy can be added by using the method `addDefaultCapacityProviderStrategy`. A capacity provider strategy cannot contain a mix of EC2 Autoscaling Group capacity providers and Fargate providers. ```ts declare const capacityProvider: ecs.CapacityProvider; const cluster = new ecs.Cluster(stack, 'EcsCluster', { enableFargateCapacityProviders: true, }); cluster.addAsgCapacityProvider(capacityProvider); cluster.addDefaultCapacityProviderStrategy([ { capacityProvider: 'FARGATE', base: 10, weight: 50 }, { capacityProvider: 'FARGATE_SPOT', weight: 50 }, ]); ``` ```ts declare const capacityProvider: ecs.CapacityProvider; const cluster = new ecs.Cluster(stack, 'EcsCluster', { enableFargateCapacityProviders: true, }); cluster.addAsgCapacityProvider(capacityProvider); cluster.addDefaultCapacityProviderStrategy([ { capacityProvider: capacityProvider.capacityProviderName }, ]); ``` Related #15230 yarn build && yarn test results <img width="680" alt="image" src="https://user-images.githubusercontent.com/115483524/216092468-82864aea-b809-48de-9cec-f4b54ec1e541.png"> > Describe the reason for this change, what the solution is, and any > important design decisions you made. > > Remember to follow the [CONTRIBUTING GUIDE] and [DESIGN GUIDELINES] for any > code you submit. > > [CONTRIBUTING GUIDE]: https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md > [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*
1 parent 09c2c19 commit 5a30ea6

12 files changed

+3471
-14
lines changed

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,6 +1153,43 @@ new ecs.Ec2Service(this, 'EC2Service', {
11531153
});
11541154
```
11551155

1156+
### Cluster Default Provider Strategy
1157+
1158+
A capacity provider strategy determines whether ECS tasks are launched on EC2 instances or Fargate/Fargate Spot. It can be specified at the cluster, service, or task level, and consists of one or more capacity providers. You can specify an optional base and weight value for finer control of how tasks are launched. The `base` specifies a minimum number of tasks on one capacity provider, and the `weight`s of each capacity provider determine how tasks are distributed after `base` is satisfied.
1159+
1160+
You can associate a default capacity provider strategy with an Amazon ECS cluster. After you do this, a default capacity provider strategy is used when creating a service or running a standalone task in the cluster and whenever a custom capacity provider strategy or a launch type isn't specified. We recommend that you define a default capacity provider strategy for each cluster.
1161+
1162+
For more information visit https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cluster-capacity-providers.html
1163+
1164+
When the service does not have a capacity provider strategy, the cluster's default capacity provider strategy will be used. Default Capacity Provider Strategy can be added by using the method `addDefaultCapacityProviderStrategy`. A capacity provider strategy cannot contain a mix of EC2 Autoscaling Group capacity providers and Fargate providers.
1165+
1166+
```ts
1167+
declare const capacityProvider: ecs.CapacityProvider;
1168+
1169+
const cluster = new ecs.Cluster(stack, 'EcsCluster', {
1170+
enableFargateCapacityProviders: true,
1171+
});
1172+
cluster.addAsgCapacityProvider(capacityProvider);
1173+
1174+
cluster.addDefaultCapacityProviderStrategy([
1175+
{ capacityProvider: 'FARGATE', base: 10, weight: 50 },
1176+
{ capacityProvider: 'FARGATE_SPOT', weight: 50 },
1177+
]);
1178+
```
1179+
1180+
```ts
1181+
declare const capacityProvider: ecs.CapacityProvider;
1182+
1183+
const cluster = new ecs.Cluster(stack, 'EcsCluster', {
1184+
enableFargateCapacityProviders: true,
1185+
});
1186+
cluster.addAsgCapacityProvider(capacityProvider);
1187+
1188+
cluster.addDefaultCapacityProviderStrategy([
1189+
{ capacityProvider: capacityProvider.capacityProviderName },
1190+
]);
1191+
```
1192+
11561193
## Elastic Inference Accelerators
11571194

11581195
Currently, this feature is only supported for services with EC2 launch types.

packages/@aws-cdk/aws-ecs/lib/cluster.ts

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as kms from '@aws-cdk/aws-kms';
66
import * as logs from '@aws-cdk/aws-logs';
77
import * as s3 from '@aws-cdk/aws-s3';
88
import * as cloudmap from '@aws-cdk/aws-servicediscovery';
9-
import { Duration, Lazy, IResource, Resource, Stack, Aspects, IAspect, ArnFormat } from '@aws-cdk/core';
9+
import { Duration, IResource, Resource, Stack, Aspects, ArnFormat, IAspect } from '@aws-cdk/core';
1010
import { Construct, IConstruct } from 'constructs';
1111
import { BottleRocketImage, EcsOptimizedAmi } from './amis';
1212
import { InstanceDrainHook } from './drain-hook/instance-drain-hook';
@@ -161,6 +161,11 @@ export class Cluster extends Resource implements ICluster {
161161
*/
162162
private _capacityProviderNames: string[] = [];
163163

164+
/**
165+
* The cluster default capacity provider strategy. This takes the form of a list of CapacityProviderStrategy objects.
166+
*/
167+
private _defaultCapacityProviderStrategy: CapacityProviderStrategy[] = [];
168+
164169
/**
165170
* The AWS Cloud Map namespace to associate with the cluster.
166171
*/
@@ -245,7 +250,7 @@ export class Cluster extends Resource implements ICluster {
245250
// since it's harmless, but we'd prefer not to add unexpected new
246251
// resources to the stack which could surprise users working with
247252
// brown-field CDK apps and stacks.
248-
Aspects.of(this).add(new MaybeCreateCapacityProviderAssociations(this, id, this._capacityProviderNames));
253+
Aspects.of(this).add(new MaybeCreateCapacityProviderAssociations(this, id));
249254
}
250255

251256
/**
@@ -259,6 +264,42 @@ export class Cluster extends Resource implements ICluster {
259264
}
260265
}
261266

267+
/**
268+
* Add default capacity provider strategy for this cluster.
269+
*
270+
* @param defaultCapacityProviderStrategy cluster default capacity provider strategy. This takes the form of a list of CapacityProviderStrategy objects.
271+
*
272+
* For example
273+
* [
274+
* {
275+
* capacityProvider: 'FARGATE',
276+
* base: 10,
277+
* weight: 50
278+
* }
279+
* ]
280+
*/
281+
public addDefaultCapacityProviderStrategy(defaultCapacityProviderStrategy: CapacityProviderStrategy[]) {
282+
if (this._defaultCapacityProviderStrategy.length > 0) {
283+
throw new Error('Cluster default capacity provider strategy is already set.');
284+
}
285+
286+
if (defaultCapacityProviderStrategy.some(dcp => dcp.capacityProvider.includes('FARGATE')) && defaultCapacityProviderStrategy.some(dcp => !dcp.capacityProvider.includes('FARGATE'))) {
287+
throw new Error('A capacity provider strategy cannot contain a mix of capacity providers using Auto Scaling groups and Fargate providers. Specify one or the other and try again.');
288+
}
289+
290+
defaultCapacityProviderStrategy.forEach(dcp => {
291+
if (!this._capacityProviderNames.includes(dcp.capacityProvider)) {
292+
throw new Error(`Capacity provider ${dcp.capacityProvider} must be added to the cluster with addAsgCapacityProvider() before it can be used in a default capacity provider strategy.`);
293+
}
294+
});
295+
296+
const defaultCapacityProvidersWithBase = defaultCapacityProviderStrategy.filter(dcp => !!dcp.base);
297+
if (defaultCapacityProvidersWithBase.length > 1) {
298+
throw new Error('Only 1 capacity provider in a capacity provider strategy can have a nonzero base.');
299+
}
300+
this._defaultCapacityProviderStrategy = defaultCapacityProviderStrategy;
301+
}
302+
262303
private renderExecuteCommandConfiguration(): CfnCluster.ClusterConfigurationProperty {
263304
return {
264305
executeCommandConfiguration: {
@@ -332,6 +373,20 @@ export class Cluster extends Resource implements ICluster {
332373
return sdNamespace;
333374
}
334375

376+
/**
377+
* Getter for _defaultCapacityProviderStrategy. This is necessary to correctly create Capacity Provider Associations.
378+
*/
379+
public get defaultCapacityProviderStrategy() {
380+
return this._defaultCapacityProviderStrategy;
381+
}
382+
383+
/**
384+
* Getter for _capacityProviderNames added to cluster
385+
*/
386+
public get capacityProviderNames() {
387+
return this._capacityProviderNames;
388+
}
389+
335390
/**
336391
* Getter for namespace added to cluster
337392
*/
@@ -937,8 +992,6 @@ enum ContainerInsights {
937992

938993
/**
939994
* A Capacity Provider strategy to use for the service.
940-
*
941-
* NOTE: defaultCapacityProviderStrategy on cluster not currently supported.
942995
*/
943996
export interface CapacityProviderStrategy {
944997
/**
@@ -1196,26 +1249,23 @@ export class AsgCapacityProvider extends Construct {
11961249
* the caller created any EC2 Capacity Providers.
11971250
*/
11981251
class MaybeCreateCapacityProviderAssociations implements IAspect {
1199-
private scope: Construct;
1252+
private scope: Cluster;
12001253
private id: string;
1201-
private capacityProviders: string[]
1202-
private resource?: CfnClusterCapacityProviderAssociations
1254+
private resource?: CfnClusterCapacityProviderAssociations;
12031255

1204-
constructor(scope: Construct, id: string, capacityProviders: string[]) {
1256+
constructor(scope: Cluster, id: string) {
12051257
this.scope = scope;
12061258
this.id = id;
1207-
this.capacityProviders = capacityProviders;
12081259
}
12091260

12101261
public visit(node: IConstruct): void {
12111262
if (node instanceof Cluster) {
1212-
if (this.capacityProviders.length > 0 && !this.resource) {
1213-
const resource = new CfnClusterCapacityProviderAssociations(this.scope, this.id, {
1263+
if ((this.scope.defaultCapacityProviderStrategy.length > 0 || this.scope.capacityProviderNames.length > 0 && !this.resource)) {
1264+
this.resource = new CfnClusterCapacityProviderAssociations(this.scope, this.id, {
12141265
cluster: node.clusterName,
1215-
defaultCapacityProviderStrategy: [],
1216-
capacityProviders: Lazy.list({ produce: () => this.capacityProviders }),
1266+
defaultCapacityProviderStrategy: this.scope.defaultCapacityProviderStrategy,
1267+
capacityProviders: this.scope.capacityProviderNames,
12171268
});
1218-
this.resource = resource;
12191269
}
12201270
}
12211271
}

packages/@aws-cdk/aws-ecs/test/cluster.test.ts

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2228,6 +2228,191 @@ describe('cluster', () => {
22282228

22292229
});
22302230

2231+
test('should throw an error if capacity provider with default strategy is not present in capacity providers', () => {
2232+
// GIVEN
2233+
const app = new cdk.App();
2234+
const stack = new cdk.Stack(app, 'test');
2235+
2236+
// THEN
2237+
expect(() => {
2238+
new ecs.Cluster(stack, 'EcsCluster', {
2239+
enableFargateCapacityProviders: true,
2240+
}).addDefaultCapacityProviderStrategy([
2241+
{ capacityProvider: 'test capacityProvider', base: 10, weight: 50 },
2242+
]);
2243+
}).toThrow('Capacity provider test capacityProvider must be added to the cluster with addAsgCapacityProvider() before it can be used in a default capacity provider strategy.');
2244+
});
2245+
2246+
test('should throw an error when capacity providers is length 0 and default capacity provider startegy specified', () => {
2247+
const app = new cdk.App();
2248+
const stack = new cdk.Stack(app, 'test');
2249+
2250+
// THEN
2251+
expect(() => {
2252+
new ecs.Cluster(stack, 'EcsCluster', {
2253+
enableFargateCapacityProviders: false,
2254+
}).addDefaultCapacityProviderStrategy([
2255+
{ capacityProvider: 'test capacityProvider', base: 10, weight: 50 },
2256+
]);
2257+
}).toThrow('Capacity provider test capacityProvider must be added to the cluster with addAsgCapacityProvider() before it can be used in a default capacity provider strategy.');
2258+
});
2259+
2260+
test('should throw an error when more than 1 default capacity provider have base specified', () => {
2261+
const app = new cdk.App();
2262+
const stack = new cdk.Stack(app, 'test');
2263+
2264+
// THEN
2265+
expect(() => {
2266+
new ecs.Cluster(stack, 'EcsCluster', {
2267+
enableFargateCapacityProviders: true,
2268+
}).addDefaultCapacityProviderStrategy([
2269+
{ capacityProvider: 'FARGATE', base: 10, weight: 50 },
2270+
{ capacityProvider: 'FARGATE_SPOT', base: 10, weight: 50 },
2271+
]);
2272+
}).toThrow(/Only 1 capacity provider in a capacity provider strategy can have a nonzero base./);
2273+
});
2274+
2275+
test('should throw an error when a capacity provider strategy contains a mix of Auto Scaling groups and Fargate providers', () => {
2276+
const app = new cdk.App();
2277+
const stack = new cdk.Stack(app, 'test');
2278+
const vpc = new ec2.Vpc(stack, 'Vpc');
2279+
const autoScalingGroup = new autoscaling.AutoScalingGroup(stack, 'asg', {
2280+
vpc,
2281+
instanceType: new ec2.InstanceType('bogus'),
2282+
machineImage: ecs.EcsOptimizedImage.amazonLinux2(),
2283+
});
2284+
const cluster = new ecs.Cluster(stack, 'EcsCluster', {
2285+
enableFargateCapacityProviders: true,
2286+
});
2287+
const capacityProvider = new ecs.AsgCapacityProvider(stack, 'provider', {
2288+
autoScalingGroup,
2289+
enableManagedTerminationProtection: false,
2290+
});
2291+
cluster.addAsgCapacityProvider(capacityProvider);
2292+
2293+
// THEN
2294+
expect(() => {
2295+
cluster.addDefaultCapacityProviderStrategy([
2296+
{ capacityProvider: 'FARGATE', base: 10, weight: 50 },
2297+
{ capacityProvider: 'FARGATE_SPOT' },
2298+
{ capacityProvider: capacityProvider.capacityProviderName },
2299+
]);
2300+
}).toThrow(/A capacity provider strategy cannot contain a mix of capacity providers using Auto Scaling groups and Fargate providers. Specify one or the other and try again./);
2301+
});
2302+
2303+
test('should throw an error if addDefaultCapacityProviderStrategy is called more than once', () => {
2304+
// GIVEN
2305+
const app = new cdk.App();
2306+
const stack = new cdk.Stack(app, 'test');
2307+
2308+
// THEN
2309+
expect(() => {
2310+
const cluster = new ecs.Cluster(stack, 'EcsCluster', {
2311+
enableFargateCapacityProviders: true,
2312+
});
2313+
cluster.addDefaultCapacityProviderStrategy([
2314+
{ capacityProvider: 'FARGATE', base: 10, weight: 50 },
2315+
{ capacityProvider: 'FARGATE_SPOT' },
2316+
]);
2317+
cluster.addDefaultCapacityProviderStrategy([
2318+
{ capacityProvider: 'FARGATE', base: 10, weight: 50 },
2319+
{ capacityProvider: 'FARGATE_SPOT' },
2320+
]);
2321+
}).toThrow(/Cluster default capacity provider strategy is already set./);
2322+
});
2323+
2324+
test('can add ASG capacity via Capacity Provider with default capacity provider', () => {
2325+
// GIVEN
2326+
const app = new cdk.App();
2327+
const stack = new cdk.Stack(app, 'test');
2328+
const vpc = new ec2.Vpc(stack, 'Vpc');
2329+
const cluster = new ecs.Cluster(stack, 'EcsCluster', {
2330+
enableFargateCapacityProviders: true,
2331+
});
2332+
2333+
cluster.addDefaultCapacityProviderStrategy([
2334+
{ capacityProvider: 'FARGATE', base: 10, weight: 50 },
2335+
{ capacityProvider: 'FARGATE_SPOT' },
2336+
]);
2337+
2338+
const autoScalingGroup = new autoscaling.AutoScalingGroup(stack, 'asg', {
2339+
vpc,
2340+
instanceType: new ec2.InstanceType('bogus'),
2341+
machineImage: ecs.EcsOptimizedImage.amazonLinux2(),
2342+
});
2343+
2344+
// WHEN
2345+
const capacityProvider = new ecs.AsgCapacityProvider(stack, 'provider', {
2346+
autoScalingGroup,
2347+
enableManagedTerminationProtection: false,
2348+
});
2349+
2350+
cluster.addAsgCapacityProvider(capacityProvider);
2351+
2352+
// THEN
2353+
Template.fromStack(stack).hasResourceProperties('AWS::ECS::ClusterCapacityProviderAssociations', {
2354+
Cluster: {
2355+
Ref: 'EcsCluster97242B84',
2356+
},
2357+
CapacityProviders: [
2358+
'FARGATE',
2359+
'FARGATE_SPOT',
2360+
{
2361+
Ref: 'providerD3FF4D3A',
2362+
},
2363+
],
2364+
DefaultCapacityProviderStrategy: [
2365+
{ CapacityProvider: 'FARGATE', Base: 10, Weight: 50 },
2366+
{ CapacityProvider: 'FARGATE_SPOT' },
2367+
],
2368+
});
2369+
});
2370+
2371+
test('can add ASG default capacity provider', () => {
2372+
// GIVEN
2373+
const app = new cdk.App();
2374+
const stack = new cdk.Stack(app, 'test');
2375+
const vpc = new ec2.Vpc(stack, 'Vpc');
2376+
const cluster = new ecs.Cluster(stack, 'EcsCluster');
2377+
2378+
const autoScalingGroup = new autoscaling.AutoScalingGroup(stack, 'asg', {
2379+
vpc,
2380+
instanceType: new ec2.InstanceType('bogus'),
2381+
machineImage: ecs.EcsOptimizedImage.amazonLinux2(),
2382+
});
2383+
2384+
// WHEN
2385+
const capacityProvider = new ecs.AsgCapacityProvider(stack, 'provider', {
2386+
autoScalingGroup,
2387+
enableManagedTerminationProtection: false,
2388+
});
2389+
2390+
cluster.addAsgCapacityProvider(capacityProvider);
2391+
2392+
cluster.addDefaultCapacityProviderStrategy([
2393+
{ capacityProvider: capacityProvider.capacityProviderName },
2394+
]);
2395+
2396+
// THEN
2397+
Template.fromStack(stack).hasResourceProperties('AWS::ECS::ClusterCapacityProviderAssociations', {
2398+
Cluster: {
2399+
Ref: 'EcsCluster97242B84',
2400+
},
2401+
CapacityProviders: [
2402+
{
2403+
Ref: 'providerD3FF4D3A',
2404+
},
2405+
],
2406+
DefaultCapacityProviderStrategy: [
2407+
{
2408+
CapacityProvider: {
2409+
Ref: 'providerD3FF4D3A',
2410+
},
2411+
},
2412+
],
2413+
});
2414+
});
2415+
22312416
test('correctly sets log configuration for execute command', () => {
22322417
// GIVEN
22332418
const app = new cdk.App();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"version": "29.0.0",
3+
"files": {
4+
"21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": {
5+
"source": {
6+
"path": "CapacityProvidersDefaultTestDeployAssert30F9785A.template.json",
7+
"packaging": "file"
8+
},
9+
"destinations": {
10+
"current_account-current_region": {
11+
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
12+
"objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json",
13+
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
14+
}
15+
}
16+
}
17+
},
18+
"dockerImages": {}
19+
}

0 commit comments

Comments
 (0)