Skip to content

Commit

Permalink
feat(ecs): enable default capacity provider strategy (#23955)
Browse files Browse the repository at this point in the history
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*
  • Loading branch information
homakk authored Feb 22, 2023
1 parent 09c2c19 commit 5a30ea6
Show file tree
Hide file tree
Showing 12 changed files with 3,471 additions and 14 deletions.
37 changes: 37 additions & 0 deletions packages/@aws-cdk/aws-ecs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,43 @@ new ecs.Ec2Service(this, 'EC2Service', {
});
```

### Cluster Default Provider Strategy

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

## Elastic Inference Accelerators

Currently, this feature is only supported for services with EC2 launch types.
Expand Down
78 changes: 64 additions & 14 deletions packages/@aws-cdk/aws-ecs/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as kms from '@aws-cdk/aws-kms';
import * as logs from '@aws-cdk/aws-logs';
import * as s3 from '@aws-cdk/aws-s3';
import * as cloudmap from '@aws-cdk/aws-servicediscovery';
import { Duration, Lazy, IResource, Resource, Stack, Aspects, IAspect, ArnFormat } from '@aws-cdk/core';
import { Duration, IResource, Resource, Stack, Aspects, ArnFormat, IAspect } from '@aws-cdk/core';
import { Construct, IConstruct } from 'constructs';
import { BottleRocketImage, EcsOptimizedAmi } from './amis';
import { InstanceDrainHook } from './drain-hook/instance-drain-hook';
Expand Down Expand Up @@ -161,6 +161,11 @@ export class Cluster extends Resource implements ICluster {
*/
private _capacityProviderNames: string[] = [];

/**
* The cluster default capacity provider strategy. This takes the form of a list of CapacityProviderStrategy objects.
*/
private _defaultCapacityProviderStrategy: CapacityProviderStrategy[] = [];

/**
* The AWS Cloud Map namespace to associate with the cluster.
*/
Expand Down Expand Up @@ -245,7 +250,7 @@ export class Cluster extends Resource implements ICluster {
// since it's harmless, but we'd prefer not to add unexpected new
// resources to the stack which could surprise users working with
// brown-field CDK apps and stacks.
Aspects.of(this).add(new MaybeCreateCapacityProviderAssociations(this, id, this._capacityProviderNames));
Aspects.of(this).add(new MaybeCreateCapacityProviderAssociations(this, id));
}

/**
Expand All @@ -259,6 +264,42 @@ export class Cluster extends Resource implements ICluster {
}
}

/**
* Add default capacity provider strategy for this cluster.
*
* @param defaultCapacityProviderStrategy cluster default capacity provider strategy. This takes the form of a list of CapacityProviderStrategy objects.
*
* For example
* [
* {
* capacityProvider: 'FARGATE',
* base: 10,
* weight: 50
* }
* ]
*/
public addDefaultCapacityProviderStrategy(defaultCapacityProviderStrategy: CapacityProviderStrategy[]) {
if (this._defaultCapacityProviderStrategy.length > 0) {
throw new Error('Cluster default capacity provider strategy is already set.');
}

if (defaultCapacityProviderStrategy.some(dcp => dcp.capacityProvider.includes('FARGATE')) && defaultCapacityProviderStrategy.some(dcp => !dcp.capacityProvider.includes('FARGATE'))) {
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.');
}

defaultCapacityProviderStrategy.forEach(dcp => {
if (!this._capacityProviderNames.includes(dcp.capacityProvider)) {
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.`);
}
});

const defaultCapacityProvidersWithBase = defaultCapacityProviderStrategy.filter(dcp => !!dcp.base);
if (defaultCapacityProvidersWithBase.length > 1) {
throw new Error('Only 1 capacity provider in a capacity provider strategy can have a nonzero base.');
}
this._defaultCapacityProviderStrategy = defaultCapacityProviderStrategy;
}

private renderExecuteCommandConfiguration(): CfnCluster.ClusterConfigurationProperty {
return {
executeCommandConfiguration: {
Expand Down Expand Up @@ -332,6 +373,20 @@ export class Cluster extends Resource implements ICluster {
return sdNamespace;
}

/**
* Getter for _defaultCapacityProviderStrategy. This is necessary to correctly create Capacity Provider Associations.
*/
public get defaultCapacityProviderStrategy() {
return this._defaultCapacityProviderStrategy;
}

/**
* Getter for _capacityProviderNames added to cluster
*/
public get capacityProviderNames() {
return this._capacityProviderNames;
}

/**
* Getter for namespace added to cluster
*/
Expand Down Expand Up @@ -937,8 +992,6 @@ enum ContainerInsights {

/**
* A Capacity Provider strategy to use for the service.
*
* NOTE: defaultCapacityProviderStrategy on cluster not currently supported.
*/
export interface CapacityProviderStrategy {
/**
Expand Down Expand Up @@ -1196,26 +1249,23 @@ export class AsgCapacityProvider extends Construct {
* the caller created any EC2 Capacity Providers.
*/
class MaybeCreateCapacityProviderAssociations implements IAspect {
private scope: Construct;
private scope: Cluster;
private id: string;
private capacityProviders: string[]
private resource?: CfnClusterCapacityProviderAssociations
private resource?: CfnClusterCapacityProviderAssociations;

constructor(scope: Construct, id: string, capacityProviders: string[]) {
constructor(scope: Cluster, id: string) {
this.scope = scope;
this.id = id;
this.capacityProviders = capacityProviders;
}

public visit(node: IConstruct): void {
if (node instanceof Cluster) {
if (this.capacityProviders.length > 0 && !this.resource) {
const resource = new CfnClusterCapacityProviderAssociations(this.scope, this.id, {
if ((this.scope.defaultCapacityProviderStrategy.length > 0 || this.scope.capacityProviderNames.length > 0 && !this.resource)) {
this.resource = new CfnClusterCapacityProviderAssociations(this.scope, this.id, {
cluster: node.clusterName,
defaultCapacityProviderStrategy: [],
capacityProviders: Lazy.list({ produce: () => this.capacityProviders }),
defaultCapacityProviderStrategy: this.scope.defaultCapacityProviderStrategy,
capacityProviders: this.scope.capacityProviderNames,
});
this.resource = resource;
}
}
}
Expand Down
185 changes: 185 additions & 0 deletions packages/@aws-cdk/aws-ecs/test/cluster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2228,6 +2228,191 @@ describe('cluster', () => {

});

test('should throw an error if capacity provider with default strategy is not present in capacity providers', () => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app, 'test');

// THEN
expect(() => {
new ecs.Cluster(stack, 'EcsCluster', {
enableFargateCapacityProviders: true,
}).addDefaultCapacityProviderStrategy([
{ capacityProvider: 'test capacityProvider', base: 10, weight: 50 },
]);
}).toThrow('Capacity provider test capacityProvider must be added to the cluster with addAsgCapacityProvider() before it can be used in a default capacity provider strategy.');
});

test('should throw an error when capacity providers is length 0 and default capacity provider startegy specified', () => {
const app = new cdk.App();
const stack = new cdk.Stack(app, 'test');

// THEN
expect(() => {
new ecs.Cluster(stack, 'EcsCluster', {
enableFargateCapacityProviders: false,
}).addDefaultCapacityProviderStrategy([
{ capacityProvider: 'test capacityProvider', base: 10, weight: 50 },
]);
}).toThrow('Capacity provider test capacityProvider must be added to the cluster with addAsgCapacityProvider() before it can be used in a default capacity provider strategy.');
});

test('should throw an error when more than 1 default capacity provider have base specified', () => {
const app = new cdk.App();
const stack = new cdk.Stack(app, 'test');

// THEN
expect(() => {
new ecs.Cluster(stack, 'EcsCluster', {
enableFargateCapacityProviders: true,
}).addDefaultCapacityProviderStrategy([
{ capacityProvider: 'FARGATE', base: 10, weight: 50 },
{ capacityProvider: 'FARGATE_SPOT', base: 10, weight: 50 },
]);
}).toThrow(/Only 1 capacity provider in a capacity provider strategy can have a nonzero base./);
});

test('should throw an error when a capacity provider strategy contains a mix of Auto Scaling groups and Fargate providers', () => {
const app = new cdk.App();
const stack = new cdk.Stack(app, 'test');
const vpc = new ec2.Vpc(stack, 'Vpc');
const autoScalingGroup = new autoscaling.AutoScalingGroup(stack, 'asg', {
vpc,
instanceType: new ec2.InstanceType('bogus'),
machineImage: ecs.EcsOptimizedImage.amazonLinux2(),
});
const cluster = new ecs.Cluster(stack, 'EcsCluster', {
enableFargateCapacityProviders: true,
});
const capacityProvider = new ecs.AsgCapacityProvider(stack, 'provider', {
autoScalingGroup,
enableManagedTerminationProtection: false,
});
cluster.addAsgCapacityProvider(capacityProvider);

// THEN
expect(() => {
cluster.addDefaultCapacityProviderStrategy([
{ capacityProvider: 'FARGATE', base: 10, weight: 50 },
{ capacityProvider: 'FARGATE_SPOT' },
{ capacityProvider: capacityProvider.capacityProviderName },
]);
}).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./);
});

test('should throw an error if addDefaultCapacityProviderStrategy is called more than once', () => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app, 'test');

// THEN
expect(() => {
const cluster = new ecs.Cluster(stack, 'EcsCluster', {
enableFargateCapacityProviders: true,
});
cluster.addDefaultCapacityProviderStrategy([
{ capacityProvider: 'FARGATE', base: 10, weight: 50 },
{ capacityProvider: 'FARGATE_SPOT' },
]);
cluster.addDefaultCapacityProviderStrategy([
{ capacityProvider: 'FARGATE', base: 10, weight: 50 },
{ capacityProvider: 'FARGATE_SPOT' },
]);
}).toThrow(/Cluster default capacity provider strategy is already set./);
});

test('can add ASG capacity via Capacity Provider with default capacity provider', () => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app, 'test');
const vpc = new ec2.Vpc(stack, 'Vpc');
const cluster = new ecs.Cluster(stack, 'EcsCluster', {
enableFargateCapacityProviders: true,
});

cluster.addDefaultCapacityProviderStrategy([
{ capacityProvider: 'FARGATE', base: 10, weight: 50 },
{ capacityProvider: 'FARGATE_SPOT' },
]);

const autoScalingGroup = new autoscaling.AutoScalingGroup(stack, 'asg', {
vpc,
instanceType: new ec2.InstanceType('bogus'),
machineImage: ecs.EcsOptimizedImage.amazonLinux2(),
});

// WHEN
const capacityProvider = new ecs.AsgCapacityProvider(stack, 'provider', {
autoScalingGroup,
enableManagedTerminationProtection: false,
});

cluster.addAsgCapacityProvider(capacityProvider);

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::ECS::ClusterCapacityProviderAssociations', {
Cluster: {
Ref: 'EcsCluster97242B84',
},
CapacityProviders: [
'FARGATE',
'FARGATE_SPOT',
{
Ref: 'providerD3FF4D3A',
},
],
DefaultCapacityProviderStrategy: [
{ CapacityProvider: 'FARGATE', Base: 10, Weight: 50 },
{ CapacityProvider: 'FARGATE_SPOT' },
],
});
});

test('can add ASG default capacity provider', () => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app, 'test');
const vpc = new ec2.Vpc(stack, 'Vpc');
const cluster = new ecs.Cluster(stack, 'EcsCluster');

const autoScalingGroup = new autoscaling.AutoScalingGroup(stack, 'asg', {
vpc,
instanceType: new ec2.InstanceType('bogus'),
machineImage: ecs.EcsOptimizedImage.amazonLinux2(),
});

// WHEN
const capacityProvider = new ecs.AsgCapacityProvider(stack, 'provider', {
autoScalingGroup,
enableManagedTerminationProtection: false,
});

cluster.addAsgCapacityProvider(capacityProvider);

cluster.addDefaultCapacityProviderStrategy([
{ capacityProvider: capacityProvider.capacityProviderName },
]);

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::ECS::ClusterCapacityProviderAssociations', {
Cluster: {
Ref: 'EcsCluster97242B84',
},
CapacityProviders: [
{
Ref: 'providerD3FF4D3A',
},
],
DefaultCapacityProviderStrategy: [
{
CapacityProvider: {
Ref: 'providerD3FF4D3A',
},
},
],
});
});

test('correctly sets log configuration for execute command', () => {
// GIVEN
const app = new cdk.App();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"version": "29.0.0",
"files": {
"21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": {
"source": {
"path": "CapacityProvidersDefaultTestDeployAssert30F9785A.template.json",
"packaging": "file"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
}
},
"dockerImages": {}
}
Loading

0 comments on commit 5a30ea6

Please sign in to comment.