diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 7f0338ed3ff18..5e63747de137f 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -14,16 +14,13 @@ This package contains constructs for working with **Amazon Elastic Container Service** (Amazon ECS). -Amazon ECS is a highly scalable, fast, container management service -that makes it easy to run, stop, -and manage Docker containers on a cluster of Amazon EC2 instances. +Amazon Elastic Container Service (Amazon ECS) is a fully managed container orchestration service. For further information on Amazon ECS, see the [Amazon ECS documentation](https://docs.aws.amazon.com/ecs) -The following example creates an Amazon ECS cluster, -adds capacity to it, -and instantiates the Amazon ECS Service with an automatic load balancer. +The following example creates an Amazon ECS cluster, adds capacity to it, and +runs a service on it: ```ts import * as ecs from '@aws-cdk/aws-ecs'; @@ -496,38 +493,6 @@ scaling.scaleOnRequestCount('RequestScaling', { Task auto-scaling is powered by *Application Auto-Scaling*. See that section for details. -## Instance Auto-Scaling - -If you're running on AWS Fargate, AWS manages the physical machines that your -containers are running on for you. If you're running an Amazon ECS cluster however, -your Amazon EC2 instances might fill up as your number of Tasks goes up. - -To avoid placement errors, configure auto-scaling for your -Amazon EC2 instance group so that your instance count scales with demand. To keep -your Amazon EC2 instances halfway loaded, scaling up to a maximum of 30 instances -if required: - -```ts -const autoScalingGroup = cluster.addCapacity('DefaultAutoScalingGroup', { - instanceType: new ec2.InstanceType("t2.xlarge"), - minCapacity: 3, - maxCapacity: 30, - desiredCapacity: 3, - - // Give instances 5 minutes to drain running tasks when an instance is - // terminated. This is the default, turn this off by specifying 0 or - // change the timeout up to 900 seconds. - taskDrainTime: Duration.seconds(300) -}); - -autoScalingGroup.scaleOnCpuUtilization('KeepCpuHalfwayLoaded', { - targetUtilizationPercent: 50 -}); -``` - -See the `@aws-cdk/aws-autoscaling` library for more autoscaling options -you can configure on your instances. - ## Integration with CloudWatch Events To start an Amazon ECS task on an Amazon EC2-backed Cluster, instantiate an @@ -760,25 +725,24 @@ ecsService.associateCloudMapService({ ## Capacity Providers -Currently, only `FARGATE` and `FARGATE_SPOT` capacity providers are supported. - -To enable capacity providers on your cluster, set the `capacityProviders` field -to [`FARGATE`, `FARGATE_SPOT`]. Then, specify capacity provider strategies on -the `capacityProviderStrategies` field for your Fargate Service. - -```ts -import * as cdk from '@aws-cdk/core'; -import * as ec2 from '@aws-cdk/aws-ec2'; -import * as ecs from '../../lib'; +There are two major families of Capacity Providers: [AWS +Fargate](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/fargate-capacity-providers.html) +(including Fargate Spot) and EC2 [Auto Scaling +Group](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/asg-capacity-providers.html) +Capacity Providers. Both are supported. -const app = new cdk.App(); -const stack = new cdk.Stack(app, 'aws-ecs-integ-capacity-provider'); +### Fargate Capacity Providers -const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 2 }); +To enable Fargate capacity providers, you can either set +`enableFargateCapacityProviders` to `true` when creating your cluster, or by +invoking the `enableFargateCapacityProviders()` method after creating your +cluster. This will add both `FARGATE` and `FARGATE_SPOT` as available capacity +providers on your cluster. +```ts const cluster = new ecs.Cluster(stack, 'FargateCPCluster', { vpc, - capacityProviders: ['FARGATE', 'FARGATE_SPOT'], + enableFargateCapacityProviders: true, }); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef'); @@ -801,8 +765,57 @@ new ecs.FargateService(stack, 'FargateService', { } ], }); +``` + +### Auto Scaling Group Capacity Providers + +To add an Auto Scaling Group Capacity Provider, first create an EC2 Auto Scaling +Group. Then, create an `AsgCapacityProvider` and pass the Auto Scaling Group to +it in the constructor. Then add the Capacity Provider to the cluster. Finally, +you can refer to the Provider by its name in your service's or task's Capacity +Provider strategy. + +By default, an Auto Scaling Group Capacity Provider will manage the Auto Scaling +Group's size for you. It will also enable managed termination protection, in +order to prevent EC2 Auto Scaling from terminating EC2 instances that have tasks +running on them. If you want to disable this behavior, set both +`enableManagedScaling` to and `enableManagedTerminationProtection` to `false`. -app.synth(); +```ts +const cluster = new ecs.Cluster(stack, 'Cluster', { + vpc, +}); + +const autoScalingGroup = new autoscaling.AutoScalingGroup(stack, 'ASG', { + vpc, + instanceType: new ec2.InstanceType('t2.micro'), + machineImage: ecs.EcsOptimizedImage.amazonLinux2(), + minCapacity: 0, + maxCapacity: 100, +}); + +const capacityProvider = new ecs.AsgCapacityProvider(stack, 'AsgCapacityProvider', { + autoScalingGroup, +}); +cluster.addAsgCapacityProvider(capacityProvider); + +const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + +taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample', + memoryReservationMiB: 256, +}); + +new ecs.Ec2Service(stack, 'EC2Service', { + cluster, + taskDefinition, + capacityProviderStrategies: [ + { + capacityProvider: capacityProvider.capacityProviderName, + weight: 1, + } + ], +}); ``` ## Elastic Inference Accelerators @@ -810,7 +823,7 @@ app.synth(); Currently, this feature is only supported for services with EC2 launch types. To add elastic inference accelerators to your EC2 instance, first add -`inferenceAccelerators` field to the EC2TaskDefinition and set the `deviceName` +`inferenceAccelerators` field to the Ec2TaskDefinition and set the `deviceName` and `deviceType` properties. ```ts diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index a65efaa83e17e..294a88fcb1858 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -5,11 +5,11 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as ssm from '@aws-cdk/aws-ssm'; -import { Duration, Lazy, IResource, Resource, Stack } from '@aws-cdk/core'; +import { Duration, Lazy, IResource, Resource, Stack, Aspects, IAspect, IConstruct } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { InstanceDrainHook } from './drain-hook/instance-drain-hook'; import { ECSMetrics } from './ecs-canned-metrics.generated'; -import { CfnCluster } from './ecs.generated'; +import { CfnCluster, CfnCapacityProvider, CfnClusterCapacityProviderAssociations } from './ecs.generated'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line @@ -52,9 +52,17 @@ export interface ClusterProps { * The capacity providers to add to the cluster * * @default - None. Currently only FARGATE and FARGATE_SPOT are supported. + * @deprecated Use {@link ClusterProps.enableFargateCapacityProviders} instead. */ readonly capacityProviders?: string[]; + /** + * Whether to enable Fargate Capacity Providers + * + * @default false + */ + readonly enableFargateCapacityProviders?: boolean; + /** * If true CloudWatch Container Insights will be enabled for the cluster * @@ -109,11 +117,14 @@ export class Cluster extends Resource implements ICluster { public readonly clusterName: string; /** - * The capacity providers associated with the cluster. - * - * Currently only FARGATE and FARGATE_SPOT are supported. + * The cluster-level (FARGATE, FARGATE_SPOT) capacity providers. */ - private _capacityProviders: string[] = []; + private _fargateCapacityProviders: string[] = []; + + /** + * The EC2 Auto Scaling Group capacity providers associated with the cluster. + */ + private _asgCapacityProviders: AsgCapacityProvider[] = []; /** * The AWS Cloud Map namespace to associate with the cluster. @@ -148,12 +159,15 @@ export class Cluster extends Resource implements ICluster { clusterSettings = [{ name: 'containerInsights', value: props.containerInsights ? ContainerInsights.ENABLED : ContainerInsights.DISABLED }]; } - this._capacityProviders = props.capacityProviders ?? []; + this._fargateCapacityProviders = props.capacityProviders ?? []; + if (props.enableFargateCapacityProviders) { + this.enableFargateCapacityProviders(); + } const cluster = new CfnCluster(this, 'Resource', { clusterName: this.physicalName, clusterSettings, - capacityProviders: Lazy.list({ produce: () => this._capacityProviders }, { omitEmpty: true }), + capacityProviders: Lazy.list({ produce: () => this._fargateCapacityProviders }, { omitEmpty: true }), }); this.clusterArn = this.getResourceArnAttribute(cluster.attrArn, { @@ -173,6 +187,24 @@ export class Cluster extends Resource implements ICluster { this._autoscalingGroup = props.capacity !== undefined ? this.addCapacity('DefaultAutoScalingGroup', props.capacity) : undefined; + + // Only create cluster capacity provider associations if there are any EC2 + // capacity providers. Ordinarily we'd just add the construct to the tree + // 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._asgCapacityProviders)); + } + + /** + * Enable the Fargate capacity providers for this cluster. + */ + public enableFargateCapacityProviders() { + for (const provider of ['FARGATE', 'FARGATE_SPOT']) { + if (!this._fargateCapacityProviders.includes(provider)) { + this._fargateCapacityProviders.push(provider); + } + } } /** @@ -214,6 +246,8 @@ export class Cluster extends Resource implements ICluster { * This method adds compute capacity to a cluster by creating an AutoScalingGroup with the specified options. * * Returns the AutoScalingGroup so you can add autoscaling settings to it. + * + * @deprecated Use {@link Cluster.addAsgCapacityProvider} instead. */ public addCapacity(id: string, options: AddCapacityOptions): autoscaling.AutoScalingGroup { if (options.machineImage && options.machineImageType) { @@ -238,9 +272,31 @@ export class Cluster extends Resource implements ICluster { return autoScalingGroup; } + /** + * This method adds an Auto Scaling Group Capacity Provider to a cluster. + * + * @param provider the capacity provider to add to this cluster. + */ + public addAsgCapacityProvider(provider: AsgCapacityProvider, options: AddAutoScalingGroupCapacityOptions = {}) { + // Don't add the same capacity provider more than once. + if (this._asgCapacityProviders.includes(provider)) { + return; + } + + this._hasEc2Capacity = true; + this.configureAutoScalingGroup(provider.autoScalingGroup, { + ...options, + // Don't enable the instance-draining lifecycle hook if managed termination protection is enabled + taskDrainTime: provider.enableManagedTerminationProtection ? Duration.seconds(0) : options.taskDrainTime, + }); + + this._asgCapacityProviders.push(provider); + } + /** * This method adds compute capacity to a cluster using the specified AutoScalingGroup. * + * @deprecated Use {@link Cluster.addAsgCapacityProvider} instead. * @param autoScalingGroup the ASG to add to this cluster. * [disable-awslint:ref-via-interface] is needed in order to install the ECS * agent by updating the ASGs user data. @@ -248,8 +304,11 @@ export class Cluster extends Resource implements ICluster { public addAutoScalingGroup(autoScalingGroup: autoscaling.AutoScalingGroup, options: AddAutoScalingGroupCapacityOptions = {}) { this._hasEc2Capacity = true; this.connections.connections.addSecurityGroup(...autoScalingGroup.connections.securityGroups); + this.configureAutoScalingGroup(autoScalingGroup, options); + } - if ( autoScalingGroup.osType === ec2.OperatingSystemType.WINDOWS ) { + private configureAutoScalingGroup(autoScalingGroup: autoscaling.AutoScalingGroup, options: AddAutoScalingGroupCapacityOptions = {}) { + if (autoScalingGroup.osType === ec2.OperatingSystemType.WINDOWS) { this.configureWindowsAutoScalingGroup(autoScalingGroup, options); } else { // Tie instances to cluster @@ -342,17 +401,19 @@ export class Cluster extends Resource implements ICluster { } /** - * addCapacityProvider adds the name of a capacityProvider to the list of supproted capacityProviders for a cluster. + * This method enables the Fargate or Fargate Spot capacity providers on the cluster. * * @param provider the capacity provider to add to this cluster. + * @deprecated Use {@link enableFargateCapacityProviders} instead. + * @see {@link addAsgCapacityProvider} to add an Auto Scaling Group capacity provider to the cluster. */ public addCapacityProvider(provider: string) { if (!(provider === 'FARGATE' || provider === 'FARGATE_SPOT')) { throw new Error('CapacityProvider not supported'); } - if (!this._capacityProviders.includes(provider)) { - this._capacityProviders.push(provider); + if (!this._fargateCapacityProviders.includes(provider)) { + this._fargateCapacityProviders.push(provider); } } @@ -859,6 +920,7 @@ export interface AddAutoScalingGroupCapacityOptions { * * Set to 0 to disable task draining. * + * @deprecated The lifecycle draining hook is not configured if using the EC2 Capacity Provider. Enable managed termination protection instead. * @default Duration.minutes(5) */ readonly taskDrainTime?: Duration; @@ -975,7 +1037,7 @@ enum ContainerInsights { */ export interface CapacityProviderStrategy { /** - * The name of the Capacity Provider. Currently only FARGATE and FARGATE_SPOT are supported. + * The name of the capacity provider. */ readonly capacityProvider: string; @@ -997,3 +1059,137 @@ capacity provider. The weight value is taken into consideration after the base v */ readonly weight?: number; } + +/** + * The options for creating an Auto Scaling Group Capacity Provider. + */ +export interface AsgCapacityProviderProps extends AddAutoScalingGroupCapacityOptions { + /** + * The name for the capacity provider. + * + * @default CloudFormation-generated name + */ + readonly capacityProviderName?: string; + + /** + * The autoscaling group to add as a Capacity Provider. + */ + readonly autoScalingGroup: autoscaling.IAutoScalingGroup; + + /** + * Whether to enable managed scaling + * + * @default true + */ + readonly enableManagedScaling?: boolean; + + /** + * Whether to enable managed termination protection + * + * @default true + */ + readonly enableManagedTerminationProtection?: boolean; + + /** + * Maximum scaling step size. In most cases this should be left alone. + * + * @default 1000 + */ + readonly maximumScalingStepSize?: number; + + /** + * Minimum scaling step size. In most cases this should be left alone. + * + * @default 1 + */ + readonly minimumScalingStepSize?: number; + + /** + * Target capacity percent. In most cases this should be left alone. + * + * @default 100 + */ + readonly targetCapacityPercent?: number; +} + +/** + * An Auto Scaling Group Capacity Provider. This allows an ECS cluster to target + * a specific EC2 Auto Scaling Group for the placement of tasks. Optionally (and + * recommended), ECS can manage the number of instances in the ASG to fit the + * tasks, and can ensure that instances are not prematurely terminated while + * there are still tasks running on them. + */ +export class AsgCapacityProvider extends CoreConstruct { + /** + * Capacity provider name + * @default Chosen by CloudFormation + */ + readonly capacityProviderName: string; + + /** + * Auto Scaling Group + */ + readonly autoScalingGroup: autoscaling.AutoScalingGroup; + + /** + * Whether managed termination protection is enabled + */ + readonly enableManagedTerminationProtection?: boolean; + + constructor(scope: Construct, id: string, props: AsgCapacityProviderProps) { + super(scope, id); + + this.autoScalingGroup = props.autoScalingGroup as autoscaling.AutoScalingGroup; + + this.enableManagedTerminationProtection = + props.enableManagedTerminationProtection === undefined ? true : props.enableManagedTerminationProtection; + + if (this.enableManagedTerminationProtection) { + this.autoScalingGroup.protectNewInstancesFromScaleIn(); + } + + const capacityProvider = new CfnCapacityProvider(this, id, { + name: props.capacityProviderName, + autoScalingGroupProvider: { + autoScalingGroupArn: this.autoScalingGroup.autoScalingGroupName, + managedScaling: props.enableManagedScaling === false ? undefined : { + status: 'ENABLED', + targetCapacity: props.targetCapacityPercent || 100, + maximumScalingStepSize: props.maximumScalingStepSize, + minimumScalingStepSize: props.minimumScalingStepSize, + }, + managedTerminationProtection: this.enableManagedTerminationProtection ? 'ENABLED' : 'DISABLED', + }, + }); + + this.capacityProviderName = capacityProvider.ref; + } +} + +/** + * A visitor that adds a capacity provider association to a Cluster only if + * the caller created any EC2 Capacity Providers. + */ +class MaybeCreateCapacityProviderAssociations implements IAspect { + private scope: CoreConstruct; + private id: string; + private capacityProviders: AsgCapacityProvider[] + + constructor(scope: CoreConstruct, id: string, capacityProviders: AsgCapacityProvider[]) { + this.scope = scope; + this.id = id; + this.capacityProviders = capacityProviders; + } + public visit(node: IConstruct): void { + if (node instanceof Cluster) { + const providers = this.capacityProviders.map(p => p.capacityProviderName).filter(p => p !== 'FARGATE' && p !== 'FARGATE_SPOT'); + if (providers.length > 0) { + new CfnClusterCapacityProviderAssociations(this.scope, this.id, { + cluster: node.clusterName, + defaultCapacityProviderStrategy: [], + capacityProviders: Lazy.list({ produce: () => providers }), + }); + } + } + } +} diff --git a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts index 713338284e57b..a76c98377c1db 100644 --- a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts @@ -4,7 +4,9 @@ import { haveResource, haveResourceLike, ResourcePart, + ABSENT, } from '@aws-cdk/assert-internal'; +import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as kms from '@aws-cdk/aws-kms'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; @@ -1695,7 +1697,7 @@ nodeunitShim({ test.done(); }, - 'allows specifying capacityProviders'(test: Test) { + 'allows specifying capacityProviders (deprecated)'(test: Test) { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1711,6 +1713,59 @@ nodeunitShim({ test.done(); }, + 'allows specifying Fargate capacityProviders'(test: Test) { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'test'); + + // WHEN + new ecs.Cluster(stack, 'EcsCluster', { + enableFargateCapacityProviders: true, + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::Cluster', { + CapacityProviders: ['FARGATE', 'FARGATE_SPOT'], + })); + + test.done(); + }, + + 'allows specifying capacityProviders (alternate method)'(test: Test) { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'test'); + + // WHEN + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + cluster.enableFargateCapacityProviders(); + + // THEN + expect(stack).to(haveResource('AWS::ECS::Cluster', { + CapacityProviders: ['FARGATE', 'FARGATE_SPOT'], + })); + + test.done(); + }, + + 'allows adding capacityProviders post-construction (deprecated)'(test: Test) { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'test'); + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + + // WHEN + cluster.addCapacityProvider('FARGATE'); + cluster.addCapacityProvider('FARGATE'); // does not add twice + + // THEN + expect(stack).to(haveResource('AWS::ECS::Cluster', { + CapacityProviders: ['FARGATE'], + })); + + test.done(); + }, + 'allows adding capacityProviders post-construction'(test: Test) { // GIVEN const app = new cdk.App(); @@ -1742,4 +1797,154 @@ nodeunitShim({ test.done(); }, + + 'creates ASG capacity providers with expected defaults'(test: Test) { + // GIVEN + 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(), + }); + + // WHEN + new ecs.AsgCapacityProvider(stack, 'provider', { + autoScalingGroup, + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::CapacityProvider', { + AutoScalingGroupProvider: { + AutoScalingGroupArn: { + Ref: 'asgASG4D014670', + }, + ManagedScaling: { + Status: 'ENABLED', + TargetCapacity: 100, + }, + ManagedTerminationProtection: 'ENABLED', + }, + })); + test.done(); + }, + + 'can disable managed scaling for ASG capacity provider'(test: Test) { + // GIVEN + 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(), + }); + + // WHEN + new ecs.AsgCapacityProvider(stack, 'provider', { + autoScalingGroup, + enableManagedScaling: false, + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::CapacityProvider', { + AutoScalingGroupProvider: { + AutoScalingGroupArn: { + Ref: 'asgASG4D014670', + }, + ManagedScaling: ABSENT, + ManagedTerminationProtection: 'ENABLED', + }, + })); + test.done(); + }, + + 'capacity provider enables ASG new instance scale-in protection by default'(test: Test) { + // GIVEN + 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(), + }); + + // WHEN + new ecs.AsgCapacityProvider(stack, 'provider', { + autoScalingGroup, + }); + + // THEN + expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + NewInstancesProtectedFromScaleIn: true, + })); + test.done(); + }, + + 'capacity provider disables ASG new instance scale-in protection'(test: Test) { + // GIVEN + 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(), + }); + + // WHEN + new ecs.AsgCapacityProvider(stack, 'provider', { + autoScalingGroup, + enableManagedTerminationProtection: false, + }); + + // THEN + expect(stack).notTo(haveResource('AWS::AutoScaling::AutoScalingGroup', { + NewInstancesProtectedFromScaleIn: true, + })); + test.done(); + }, + + 'can add ASG capacity via Capacity Provider'(test: Test) { + // 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, + }); + + // These should not be added at the association level + cluster.enableFargateCapacityProviders(); + + // Ensure not added twice + cluster.addAsgCapacityProvider(capacityProvider); + cluster.addAsgCapacityProvider(capacityProvider); + + // THEN + expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + Cluster: { + Ref: 'EcsCluster97242B84', + }, + CapacityProviders: [ + { + Ref: 'providerD3FF4D3A', + }, + ], + DefaultCapacityProviderStrategy: [], + })); + test.done(); + }, }); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts index f6d6a47efc6c3..c63c86cce9f65 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts @@ -1,4 +1,5 @@ import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; +import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elb from '@aws-cdk/aws-elasticloadbalancing'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; @@ -236,6 +237,58 @@ nodeunitShim({ test.done(); }, + 'with autoscaling group capacity provider'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + 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); + + const taskDefinition = new ecs.TaskDefinition(stack, 'ServerTask', { + compatibility: ecs.Compatibility.EC2, + }); + taskDefinition.addContainer('app', { + image: new ecs.RepositoryImage('bogus'), + cpu: 1024, + memoryReservationMiB: 900, + portMappings: [{ + containerPort: 80, + }], + }); + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + desiredCount: 0, + capacityProviderStrategies: [{ + capacityProvider: capacityProvider.capacityProviderName, + }], + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::Service', { + CapacityProviderStrategy: [ + { + CapacityProvider: { + Ref: 'providerD3FF4D3A', + }, + }, + ], + })); + test.done(); + }, + 'with multiple security groups, it correctly updates the cfn template'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.capacity-provider.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.capacity-provider.expected.json new file mode 100644 index 0000000000000..d4e008750fe93 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.capacity-provider.expected.json @@ -0,0 +1,655 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "integ-ec2-capacity-provider/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "integ-ec2-capacity-provider/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-ec2-capacity-provider/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "integ-ec2-capacity-provider/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-ec2-capacity-provider/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "integ-ec2-capacity-provider/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-ec2-capacity-provider/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "integ-ec2-capacity-provider/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-ec2-capacity-provider/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "integ-ec2-capacity-provider/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-ec2-capacity-provider/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "integ-ec2-capacity-provider/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-ec2-capacity-provider/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "integ-ec2-capacity-provider/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "EC2CPClusterD5F0FD32": { + "Type": "AWS::ECS::Cluster" + }, + "EC2CPCluster4CFED4DD": { + "Type": "AWS::ECS::ClusterCapacityProviderAssociations", + "Properties": { + "CapacityProviders": [ + { + "Ref": "EC2CapacityProvider5A2E35CD" + } + ], + "Cluster": { + "Ref": "EC2CPClusterD5F0FD32" + }, + "DefaultCapacityProviderStrategy": [] + } + }, + "TaskDefTaskRole1EDB4A67": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDef54694570": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "amazon/amazon-ecs-sample", + "MemoryReservation": 256, + "Name": "web" + } + ], + "Family": "integec2capacityproviderTaskDefA6140A6B", + "NetworkMode": "bridge", + "RequiresCompatibilities": [ + "EC2" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + } + } + }, + "ASGInstanceSecurityGroup0525485D": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "integ-ec2-capacity-provider/ASG/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "integ-ec2-capacity-provider/ASG" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "ASGInstanceRoleE263A41B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ec2.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-ec2-capacity-provider/ASG" + } + ] + } + }, + "ASGInstanceRoleDefaultPolicy7636D8BF": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecs:DeregisterContainerInstance", + "ecs:RegisterContainerInstance", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EC2CPClusterD5F0FD32", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EC2CPClusterD5F0FD32", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:DiscoverPollEndpoint", + "ecr:GetAuthorizationToken", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ASGInstanceRoleDefaultPolicy7636D8BF", + "Roles": [ + { + "Ref": "ASGInstanceRoleE263A41B" + } + ] + } + }, + "ASGInstanceProfile0A2834D7": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "ASGInstanceRoleE263A41B" + } + ] + } + }, + "ASGLaunchConfigC00AF12B": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": { + "Ref": "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "InstanceType": "t2.micro", + "IamInstanceProfile": { + "Ref": "ASGInstanceProfile0A2834D7" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "ASGInstanceSecurityGroup0525485D", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\necho ECS_CLUSTER=", + { + "Ref": "EC2CPClusterD5F0FD32" + }, + " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save\necho ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config" + ] + ] + } + } + }, + "DependsOn": [ + "ASGInstanceRoleDefaultPolicy7636D8BF", + "ASGInstanceRoleE263A41B" + ] + }, + "ASG46ED3070": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": "1", + "MinSize": "1", + "LaunchConfigurationName": { + "Ref": "ASGLaunchConfigC00AF12B" + }, + "NewInstancesProtectedFromScaleIn": true, + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "integ-ec2-capacity-provider/ASG" + } + ], + "VPCZoneIdentifier": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + }, + "UpdatePolicy": { + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true + } + } + }, + "EC2CapacityProvider5A2E35CD": { + "Type": "AWS::ECS::CapacityProvider", + "Properties": { + "AutoScalingGroupProvider": { + "AutoScalingGroupArn": { + "Ref": "ASG46ED3070" + }, + "ManagedScaling": { + "Status": "ENABLED", + "TargetCapacity": 100 + }, + "ManagedTerminationProtection": "ENABLED" + } + } + }, + "EC2Service5392EF94": { + "Type": "AWS::ECS::Service", + "Properties": { + "CapacityProviderStrategy": [ + { + "CapacityProvider": { + "Ref": "EC2CapacityProvider5A2E35CD" + }, + "Weight": 1 + } + ], + "Cluster": { + "Ref": "EC2CPClusterD5F0FD32" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "EnableECSManagedTags": false, + "SchedulingStrategy": "REPLICA", + "TaskDefinition": { + "Ref": "TaskDef54694570" + } + } + } + }, + "Parameters": { + "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.capacity-provider.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.capacity-provider.ts new file mode 100644 index 0000000000000..f82ce6a9f9f56 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.capacity-provider.ts @@ -0,0 +1,46 @@ +import * as autoscaling from '@aws-cdk/aws-autoscaling'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import * as ecs from '../../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'integ-ec2-capacity-provider'); + +const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 2 }); + +const cluster = new ecs.Cluster(stack, 'EC2CPCluster', { + vpc, +}); + +const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + +taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryReservationMiB: 256, +}); + +const autoScalingGroup = new autoscaling.AutoScalingGroup(stack, 'ASG', { + vpc, + instanceType: new ec2.InstanceType('t2.micro'), + machineImage: ecs.EcsOptimizedImage.amazonLinux2(), +}); + +const cp = new ecs.AsgCapacityProvider(stack, 'EC2CapacityProvider', { + autoScalingGroup, +}); + +cluster.addAsgCapacityProvider(cp); + +new ecs.Ec2Service(stack, 'EC2Service', { + cluster, + taskDefinition, + capacityProviderStrategies: [ + { + capacityProvider: cp.capacityProviderName, + weight: 1, + }, + ], +}); + +app.synth(); + diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts index 0bdbeac0c04d5..a75ff256cc457 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; +import { expect, haveResource, haveResourceLike, ABSENT } from '@aws-cdk/assert-internal'; import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; @@ -110,7 +110,7 @@ nodeunitShim({ test.done(); }, - 'does not set launchType when capacity provider strategies specified'(test: Test) { + 'does not set launchType when capacity provider strategies specified (deprecated)'(test: Test) { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -195,6 +195,93 @@ nodeunitShim({ test.done(); }, + 'does not set launchType when capacity provider strategies specified'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { + vpc, + }); + cluster.enableFargateCapacityProviders(); + + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + container.addPortMappings({ containerPort: 8000 }); + + new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + capacityProviderStrategies: [ + { + capacityProvider: 'FARGATE_SPOT', + weight: 2, + }, + { + capacityProvider: 'FARGATE', + weight: 1, + }, + ], + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::Cluster', { + CapacityProviders: ['FARGATE', 'FARGATE_SPOT'], + })); + + expect(stack).to(haveResource('AWS::ECS::Service', { + TaskDefinition: { + Ref: 'FargateTaskDefC6FB60B4', + }, + Cluster: { + Ref: 'EcsCluster97242B84', + }, + DeploymentConfiguration: { + MaximumPercent: 200, + MinimumHealthyPercent: 50, + }, + // no launch type + LaunchType: ABSENT, + CapacityProviderStrategy: [ + { + CapacityProvider: 'FARGATE_SPOT', + Weight: 2, + }, + { + CapacityProvider: 'FARGATE', + Weight: 1, + }, + ], + EnableECSManagedTags: false, + NetworkConfiguration: { + AwsvpcConfiguration: { + AssignPublicIp: 'DISABLED', + SecurityGroups: [ + { + 'Fn::GetAtt': [ + 'FargateServiceSecurityGroup0A0E79CB', + 'GroupId', + ], + }, + ], + Subnets: [ + { + Ref: 'MyVpcPrivateSubnet1Subnet5057CF7E', + }, + { + Ref: 'MyVpcPrivateSubnet2Subnet0040C983', + }, + ], + }, + }, + })); + + test.done(); + }, + 'with custom cloudmap namespace'(test: Test) { // GIVEN const stack = new cdk.Stack();