From e7f97fdc3f3cae38b5a70ba748badc6534f86a7d Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Tue, 23 Feb 2021 14:11:26 -0700 Subject: [PATCH 1/3] feat(ecs): add a service extension interface to ECS services --- packages/@aws-cdk/aws-ecs/README.md | 25 +++++++++++ .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 22 ++++++++++ .../aws-ecs/test/base-service.test.ts | 43 +++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 packages/@aws-cdk/aws-ecs/test/base-service.test.ts diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 438f63e14e009..48af512d2a1da 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -717,3 +717,28 @@ new ecs.FargateService(stack, 'FargateService', { app.synth(); ``` + +## Add Service Extensions + +You may create and apply packaged sets of modifications to ECS +services by implementing `IServiceExtension` in your code and +adding the extension to your ECS service. + +```ts +class MyStandardScaling implements ecs.IServiceExtension { + extend(service: ecs.BaseService): void { + service.autoScaleTaskCount({ + maxCapacity: 100, + minCapacity: 2, + }).scaleOnCpuUtilization('Target40', { + targetUtilizationPercent: 40, + }); + } +} + +// Create the service +const service = new ecs.FargateService(...); + +// Apply your neatly packaged modification to the service. +service.addExtension(new MyStandardScaling()); +``` diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 8e2483f98838f..3e7e086362f58 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -599,6 +599,16 @@ export abstract class BaseService extends Resource return cloudmapService; } + /** + * Adds the specified extention to the service. + * + * Extension can be used to apply a packaged modification to + * a service. + */ + public addExtension(extension: IServiceExtension) { + extension.extend(this); + } + /** * This method returns the specified CloudWatch metric name for this service. */ @@ -885,3 +895,15 @@ export enum PropagatedTagSource { */ NONE = 'NONE' } + +/** + * An interface for creating reusable extensions for ECS services. + */ +export interface IServiceExtension { + /** + * Apply the extension to the given BaseService + * + * @param service [disable-awslint:ref-via-interface] + */ + extend(service: BaseService): void; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/base-service.test.ts b/packages/@aws-cdk/aws-ecs/test/base-service.test.ts new file mode 100644 index 0000000000000..710df9617a2f9 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/base-service.test.ts @@ -0,0 +1,43 @@ +import * as cdk from '@aws-cdk/core'; +import * as ecs from '../lib'; +import { expect as expectCDK, haveResource } from '@aws-cdk/assert'; + +describe('service extensions', () => { + test('allows the user to add an extension', () => { + // GIVEN + const stack = new cdk.Stack(); + const cluster = new ecs.Cluster(stack, 'Cluster'); + + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDefinition'); + const mainContainer = taskDefinition.addContainer('main', { + image: ecs.ContainerImage.fromRegistry('nginx'), + }); + mainContainer.addPortMappings({ containerPort: 80 }); + + const service = new ecs.FargateService(stack, 'Service', { + cluster, + taskDefinition, + }); + + class MyStandardScaling implements ecs.IServiceExtension { + extend(s: ecs.BaseService): void { + s.autoScaleTaskCount({ + maxCapacity: 100, + minCapacity: 2, + }).scaleOnCpuUtilization('Target40', { + targetUtilizationPercent: 40, + }); + } + } + + const myStandardScalingExtension = new MyStandardScaling(); + const extendSpy = jest.spyOn(myStandardScalingExtension, 'extend'); + + // WHEN + service.addExtension(myStandardScalingExtension); + + // THEN + expect(extendSpy).toBeCalledWith(service); + expectCDK(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalableTarget')); + }); +}); \ No newline at end of file From 24ff911d51e8226f9bc132c27653570ffd145213 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Thu, 25 Feb 2021 11:45:48 -0700 Subject: [PATCH 2/3] refactor: change to fargate- and ec2-specific service extension interfaces --- packages/@aws-cdk/aws-ecs/README.md | 22 +++-- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 22 ----- .../@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts | 28 +++++++ .../aws-ecs/lib/fargate/fargate-service.ts | 28 +++++++ .../aws-ecs/test/base-service.test.ts | 43 ---------- .../aws-ecs/test/service-extensions.test.ts | 84 +++++++++++++++++++ 6 files changed, 153 insertions(+), 74 deletions(-) delete mode 100644 packages/@aws-cdk/aws-ecs/test/base-service.test.ts create mode 100644 packages/@aws-cdk/aws-ecs/test/service-extensions.test.ts diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 48af512d2a1da..60914ae46922c 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -720,13 +720,14 @@ app.synth(); ## Add Service Extensions -You may create and apply packaged sets of modifications to ECS -services by implementing `IServiceExtension` in your code and -adding the extension to your ECS service. +You may create and apply packaged sets of modifications to ECS services by +implementing one or both of `IFargateServiceExtension` or `IEc2ServiceExtension` +in your code and by adding the extension to your ECS service. ```ts -class MyStandardScaling implements ecs.IServiceExtension { - extend(service: ecs.BaseService): void { +class MyStandardScaling implements ecs.IFargateServiceExtension, ecs.IEc2ServiceExtension { + // Compatible with both FargateService and Ec2Service + extend(service: ecs.FargateService | ecs.Ec2Service): void { service.autoScaleTaskCount({ maxCapacity: 100, minCapacity: 2, @@ -736,9 +737,12 @@ class MyStandardScaling implements ecs.IServiceExtension { } } -// Create the service -const service = new ecs.FargateService(...); - +// Create a fargate service +const fargateService = new ecs.FargateService(...); // Apply your neatly packaged modification to the service. -service.addExtension(new MyStandardScaling()); +fargateService.addExtension(new MyStandardScaling()); + +// Or create an EC2 service and apply it there. +const ec2Service = new ecs.Ec2Service(...); +ec2Service.addExtension(new MyStandardScaling()); ``` diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 3e7e086362f58..8e2483f98838f 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -599,16 +599,6 @@ export abstract class BaseService extends Resource return cloudmapService; } - /** - * Adds the specified extention to the service. - * - * Extension can be used to apply a packaged modification to - * a service. - */ - public addExtension(extension: IServiceExtension) { - extension.extend(this); - } - /** * This method returns the specified CloudWatch metric name for this service. */ @@ -895,15 +885,3 @@ export enum PropagatedTagSource { */ NONE = 'NONE' } - -/** - * An interface for creating reusable extensions for ECS services. - */ -export interface IServiceExtension { - /** - * Apply the extension to the given BaseService - * - * @param service [disable-awslint:ref-via-interface] - */ - extend(service: BaseService): void; -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts index 18c6df350fb4e..1e18b598d7bd6 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -258,6 +258,16 @@ export class Ec2Service extends BaseService implements IEc2Service { } } + /** + * Adds the specified extention to the service. + * + * Extension can be used to apply a packaged modification to + * a service. + */ + public addExtension(extension: IEc2ServiceExtension) { + extension.extend(this); + } + /** * Validates this Ec2Service. */ @@ -335,3 +345,21 @@ export class BuiltInAttributes { */ public static readonly OS_TYPE = 'attribute:ecs.os-type'; } + +/** + * An extension for `FargateService` + * + * Classes that want to make changes to a FargateService (such as applying a + * standard autoscaling pattern) can implement this interface, and can then + * be "added" to a service like so: + * + * service.addExtension(new MyExtension("some_parameter")); + */ +export interface IEc2ServiceExtension { + /** + * Apply the extension to the given service + * + * @param service [disable-awslint:ref-via-interface] + */ + extend(service: Ec2Service): void; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index 1db94fc5286e0..52b6789856e3a 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -176,6 +176,16 @@ export class FargateService extends BaseService implements IFargateService { throw new Error('A TaskDefinition must have at least one essential container'); } } + + /** + * Adds the specified extention to the service. + * + * Extension can be used to apply a packaged modification to + * a service. + */ + public addExtension(extension: IFargateServiceExtension) { + extension.extend(this); + } } /** @@ -233,3 +243,21 @@ const SECRET_JSON_FIELD_UNSUPPORTED_PLATFORM_VERSIONS = [ FargatePlatformVersion.VERSION1_2, FargatePlatformVersion.VERSION1_3, ]; + +/** + * An extension for `FargateService` + * + * Classes that want to make changes to a FargateService (such as applying a + * standard autoscaling pattern) can implement this interface, and can then + * be "added" to a service like so: + * + * service.addExtension(new MyExtension("some_parameter")); + */ +export interface IFargateServiceExtension { + /** + * Apply the extension to the given service + * + * @param service [disable-awslint:ref-via-interface] + */ + extend(service: FargateService): void; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/base-service.test.ts b/packages/@aws-cdk/aws-ecs/test/base-service.test.ts deleted file mode 100644 index 710df9617a2f9..0000000000000 --- a/packages/@aws-cdk/aws-ecs/test/base-service.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as cdk from '@aws-cdk/core'; -import * as ecs from '../lib'; -import { expect as expectCDK, haveResource } from '@aws-cdk/assert'; - -describe('service extensions', () => { - test('allows the user to add an extension', () => { - // GIVEN - const stack = new cdk.Stack(); - const cluster = new ecs.Cluster(stack, 'Cluster'); - - const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDefinition'); - const mainContainer = taskDefinition.addContainer('main', { - image: ecs.ContainerImage.fromRegistry('nginx'), - }); - mainContainer.addPortMappings({ containerPort: 80 }); - - const service = new ecs.FargateService(stack, 'Service', { - cluster, - taskDefinition, - }); - - class MyStandardScaling implements ecs.IServiceExtension { - extend(s: ecs.BaseService): void { - s.autoScaleTaskCount({ - maxCapacity: 100, - minCapacity: 2, - }).scaleOnCpuUtilization('Target40', { - targetUtilizationPercent: 40, - }); - } - } - - const myStandardScalingExtension = new MyStandardScaling(); - const extendSpy = jest.spyOn(myStandardScalingExtension, 'extend'); - - // WHEN - service.addExtension(myStandardScalingExtension); - - // THEN - expect(extendSpy).toBeCalledWith(service); - expectCDK(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalableTarget')); - }); -}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/service-extensions.test.ts b/packages/@aws-cdk/aws-ecs/test/service-extensions.test.ts new file mode 100644 index 0000000000000..dc773eb995dcc --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/service-extensions.test.ts @@ -0,0 +1,84 @@ +import * as cdk from '@aws-cdk/core'; +import * as ecs from '../lib'; +import { expect as expectCDK, haveResource } from '@aws-cdk/assert'; + +describe('service extensions', () => { + test('allows the user to create an extension that works for both ec2 and fargate services', () => { + // GIVEN + const stack = new cdk.Stack(); + const cluster = new ecs.Cluster(stack, 'Cluster'); + + // A very compatible task definition + const taskDefinition = new ecs.TaskDefinition(stack, 'TaskDefinition', { + compatibility: ecs.Compatibility.EC2_AND_FARGATE, + cpu: '256', + memoryMiB: '512', + }); + const mainContainer = taskDefinition.addContainer('main', { + image: ecs.ContainerImage.fromRegistry('nginx'), + memoryLimitMiB: 512, + }); + mainContainer.addPortMappings({ containerPort: 80 }); + + // A very compatible service extension + class MyStandardScaling implements ecs.IFargateServiceExtension, ecs.IEc2ServiceExtension { + extend(s: ecs.FargateService | ecs.Ec2Service): void { + s.autoScaleTaskCount({ + maxCapacity: 100, + minCapacity: 2, + }).scaleOnCpuUtilization('Target40', { + targetUtilizationPercent: 40, + }); + } + } + + const myStandardScalingExtension = new MyStandardScaling(); + const extendSpy = jest.spyOn(myStandardScalingExtension, 'extend'); + + const fargateService = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + + const ec2Service = new ecs.FargateService(stack, 'Ec2Service', { + cluster, + taskDefinition, + }); + + // WHEN + fargateService.addExtension(myStandardScalingExtension); + ec2Service.addExtension(myStandardScalingExtension); + + // THEN + expect(extendSpy).toBeCalledWith(fargateService); + expect(extendSpy).toBeCalledWith(ec2Service); + + expectCDK(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + ResourceId: { + 'Fn::Join': [ + '', + [ + 'service/', + { Ref: 'ClusterEB0386A7' }, + '/', + { 'Fn::GetAtt': ['FargateServiceAC2B3B85', 'Name'] }, + ], + ], + }, + })); + + expectCDK(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + ResourceId: { + 'Fn::Join': [ + '', + [ + 'service/', + { Ref: 'ClusterEB0386A7' }, + '/', + { 'Fn::GetAtt': ['Ec2Service04A33183', 'Name'] }, + ], + ], + }, + })); + }); +}); \ No newline at end of file From 9aff21c024ba4c2d7e54e944d5c7d882c72ef527 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Fri, 26 Feb 2021 09:49:07 -0700 Subject: [PATCH 3/3] fix: typos in addExtension docblocks --- packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts | 2 +- packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts index f7b0f05c92800..a2e1d4aea2ec2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts @@ -467,7 +467,7 @@ export class TaskDefinition extends TaskDefinitionBase { } /** - * Adds the specified extention to the task definition. + * Adds the specified extension to the task definition. * * Extension can be used to apply a packaged modification to * a task definition. diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts index 1e18b598d7bd6..70d40241fdd8c 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -259,7 +259,7 @@ export class Ec2Service extends BaseService implements IEc2Service { } /** - * Adds the specified extention to the service. + * Adds the specified extension to the service. * * Extension can be used to apply a packaged modification to * a service.