diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 7edb8e0584370..9e8170f86d826 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -60,6 +60,7 @@ one to run tasks on AWS Fargate. - Use the `Ec2TaskDefinition` and `Ec2Service` constructs to run tasks on Amazon EC2 instances running in your account. - Use the `FargateTaskDefinition` and `FargateService` constructs to run tasks on instances that are managed for you by AWS. +- Use the `ExternalTaskDefinition` and `ExternalService` constructs to run AWS ECS Anywhere tasks on self-managed infrastructure. Here are the main differences: @@ -73,10 +74,12 @@ Here are the main differences: Application/Network Load Balancers. Only the AWS log driver is supported. Many host features are not supported such as adding kernel capabilities and mounting host devices/volumes inside the container. +- **AWS ECSAnywhere**: tasks are run and managed by AWS ECS Anywhere on infrastructure owned by the customer. Only Bridge networking mode is supported. Does not support autoscaling, load balancing, cloudmap or attachment of volumes. -For more information on Amazon EC2 vs AWS Fargate and networking see the AWS Documentation: -[AWS Fargate](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html) and -[Task Networking](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-networking.html). +For more information on Amazon EC2 vs AWS Fargate, networking and ECS Anywhere see the AWS Documentation: +[AWS Fargate](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html), +[Task Networking](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-networking.html), +[ECS Anywhere](https://aws.amazon.com/ecs/anywhere/) ## Clusters @@ -211,8 +214,8 @@ some supporting containers which are used to support the main container, doings things like upload logs or metrics to monitoring services. To run a task or service with Amazon EC2 launch type, use the `Ec2TaskDefinition`. For AWS Fargate tasks/services, use the -`FargateTaskDefinition`. These classes provide a simplified API that only contain -properties relevant for that specific launch type. +`FargateTaskDefinition`. For AWS ECS Anywhere use the `ExternalTaskDefinition`. These classes +provide simplified APIs that only contain properties relevant for each specific launch type. For a `FargateTaskDefinition`, specify the task size (`memoryLimitMiB` and `cpu`): @@ -248,6 +251,19 @@ const container = ec2TaskDefinition.addContainer("WebContainer", { }); ``` +For an `ExternalTaskDefinition`: + +```ts +const externalTaskDefinition = new ecs.ExternalTaskDefinition(this, 'TaskDef'); + +const container = externalTaskDefinition.addContainer("WebContainer", { + // Use an image from DockerHub + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + memoryLimitMiB: 1024 + // ... other options here ... +}); +``` + You can specify container properties when you add them to the task definition, or with various methods, e.g.: To add a port mapping when adding a container to the task definition, specify the `portMappings` option: @@ -283,6 +299,8 @@ const volume = { const container = fargateTaskDefinition.addVolume("mydatavolume"); ``` +> Note: ECS Anywhere doesn't support volume attachments in the task definition. + To use a TaskDefinition that can be used with either Amazon EC2 or AWS Fargate launch types, use the `TaskDefinition` construct. @@ -360,6 +378,18 @@ const service = new ecs.FargateService(this, 'Service', { }); ``` +ECS Anywhere service definition looks like: + +```ts +const taskDefinition; + +const service = new ecs.ExternalService(this, 'Service', { + cluster, + taskDefinition, + desiredCount: 5 +}); +``` + `Services` by default will create a security group if not provided. If you'd like to specify which security groups to use you can override the `securityGroups` property. @@ -378,6 +408,8 @@ const service = new ecs.FargateService(stack, 'Service', { }); ``` +> Note: ECS Anywhere doesn't support deployment circuit breakers and rollback. + ### Include an application/network load balancer `Services` are load balancing targets and can be added to a target group, which will be attached to an application/network load balancers: @@ -402,6 +434,8 @@ const targetGroup2 = listener.addTargets('ECS2', { }); ``` +> Note: ECS Anywhere doesn't support application/network load balancers. + Note that in the example above, the default `service` only allows you to register the first essential container or the first mapped port on the container as a target and add it to a new target group. To have more control over which container and port to register as targets, you can use `service.loadBalancerTarget()` to return a load balancing target for a specific container and port. Alternatively, you can also create all load balancer targets to be registered in this service, add them to target groups, and attach target groups to listeners accordingly. diff --git a/packages/@aws-cdk/aws-ecs/lib/external/external-service.ts b/packages/@aws-cdk/aws-ecs/lib/external/external-service.ts new file mode 100644 index 0000000000000..741ba0a7b2b29 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/external/external-service.ts @@ -0,0 +1,190 @@ +import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import * as cloudmap from '@aws-cdk/aws-servicediscovery'; +import { Resource, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { AssociateCloudMapServiceOptions, BaseService, BaseServiceOptions, CloudMapOptions, DeploymentControllerType, EcsTarget, IBaseService, IEcsLoadBalancerTarget, IService, LaunchType, PropagatedTagSource } from '../base/base-service'; +import { fromServiceAtrributes } from '../base/from-service-attributes'; +import { ScalableTaskCount } from '../base/scalable-task-count'; +import { Compatibility, LoadBalancerTargetOptions, TaskDefinition } from '../base/task-definition'; +import { ICluster } from '../cluster'; +/** + * The properties for defining a service using the External launch type. + */ +export interface ExternalServiceProps extends BaseServiceOptions { + /** + * The task definition to use for tasks in the service. + * + * [disable-awslint:ref-via-interface] + */ + readonly taskDefinition: TaskDefinition; + + /** + * The security groups to associate with the service. If you do not specify a security group, the default security group for the VPC is used. + * + * + * @default - A new security group is created. + */ + readonly securityGroups?: ec2.ISecurityGroup[]; +} + +/** + * The interface for a service using the External launch type on an ECS cluster. + */ +export interface IExternalService extends IService { + +} + +/** + * The properties to import from the service using the External launch type. + */ +export interface ExternalServiceAttributes { + /** + * The cluster that hosts the service. + */ + readonly cluster: ICluster; + + /** + * The service ARN. + * + * @default - either this, or {@link serviceName}, is required + */ + readonly serviceArn?: string; + + /** + * The name of the service. + * + * @default - either this, or {@link serviceArn}, is required + */ + readonly serviceName?: string; +} + +/** + * This creates a service using the External launch type on an ECS cluster. + * + * @resource AWS::ECS::Service + */ +export class ExternalService extends BaseService implements IExternalService { + + /** + * Imports from the specified service ARN. + */ + public static fromExternalServiceArn(scope: Construct, id: string, externalServiceArn: string): IExternalService { + class Import extends Resource implements IExternalService { + public readonly serviceArn = externalServiceArn; + public readonly serviceName = Stack.of(scope).parseArn(externalServiceArn).resourceName as string; + } + return new Import(scope, id); + } + + /** + * Imports from the specified service attrributes. + */ + public static fromExternalServiceAttributes(scope: Construct, id: string, attrs: ExternalServiceAttributes): IBaseService { + return fromServiceAtrributes(scope, id, attrs); + } + + /** + * Constructs a new instance of the ExternalService class. + */ + constructor(scope: Construct, id: string, props: ExternalServiceProps) { + if (props.minHealthyPercent !== undefined && props.maxHealthyPercent !== undefined && props.minHealthyPercent >= props.maxHealthyPercent) { + throw new Error('Minimum healthy percent must be less than maximum healthy percent.'); + } + + if (props.taskDefinition.compatibility !== Compatibility.EXTERNAL) { + throw new Error('Supplied TaskDefinition is not configured for compatibility with ECS Anywhere cluster'); + } + + if (props.cluster.defaultCloudMapNamespace !== undefined) { + throw new Error (`Cloud map integration is not supported for External service ${props.cluster.defaultCloudMapNamespace}`); + } + + if (props.cloudMapOptions !== undefined) { + throw new Error ('Cloud map options are not supported for External service'); + } + + if (props.enableExecuteCommand !== undefined) { + throw new Error ('Enable Execute Command options are not supported for External service'); + } + + if (props.capacityProviderStrategies !== undefined) { + throw new Error ('Capacity Providers are not supported for External service'); + } + + const propagateTagsFromSource = props.propagateTags ?? PropagatedTagSource.NONE; + + super(scope, id, { + ...props, + desiredCount: props.desiredCount, + maxHealthyPercent: props.maxHealthyPercent === undefined ? 100 : props.maxHealthyPercent, + minHealthyPercent: props.minHealthyPercent === undefined ? 0 : props.minHealthyPercent, + launchType: LaunchType.EXTERNAL, + propagateTags: propagateTagsFromSource, + enableECSManagedTags: props.enableECSManagedTags, + }, + { + cluster: props.cluster.clusterName, + taskDefinition: props.deploymentController?.type === DeploymentControllerType.EXTERNAL ? undefined : props.taskDefinition.taskDefinitionArn, + }, props.taskDefinition); + + this.node.addValidation({ + validate: () => !this.taskDefinition.defaultContainer ? ['A TaskDefinition must have at least one essential container'] : [], + }); + + this.node.addValidation({ + validate: () => this.networkConfiguration !== undefined ? ['Network configurations not supported for an external service'] : [], + }); + } + + /** + * Overriden method to throw error as `attachToApplicationTargetGroup` is not supported for external service + */ + public attachToApplicationTargetGroup(_targetGroup: elbv2.IApplicationTargetGroup): elbv2.LoadBalancerTargetProps { + throw new Error ('Application load balancer cannot be attached to an external service'); + } + + /** + * Overriden method to throw error as `loadBalancerTarget` is not supported for external service + */ + public loadBalancerTarget(_options: LoadBalancerTargetOptions): IEcsLoadBalancerTarget { + throw new Error ('External service cannot be attached as load balancer targets'); + } + + /** + * Overriden method to throw error as `registerLoadBalancerTargets` is not supported for external service + */ + public registerLoadBalancerTargets(..._targets: EcsTarget[]) { + throw new Error ('External service cannot be registered as load balancer targets'); + } + + /** + * Overriden method to throw error as `configureAwsVpcNetworkingWithSecurityGroups` is not supported for external service + */ + // eslint-disable-next-line max-len, no-unused-vars + protected configureAwsVpcNetworkingWithSecurityGroups(_vpc: ec2.IVpc, _assignPublicIp?: boolean, _vpcSubnets?: ec2.SubnetSelection, _securityGroups?: ec2.ISecurityGroup[]) { + throw new Error ('Only Bridge network mode is supported for external service'); + } + + /** + * Overriden method to throw error as `autoScaleTaskCount` is not supported for external service + */ + public autoScaleTaskCount(_props: appscaling.EnableScalingProps): ScalableTaskCount { + throw new Error ('Autoscaling not supported for external service'); + } + + /** + * Overriden method to throw error as `enableCloudMap` is not supported for external service + */ + public enableCloudMap(_options: CloudMapOptions): cloudmap.Service { + throw new Error ('Cloud map integration not supported for an external service'); + } + + /** + * Overriden method to throw error as `associateCloudMapService` is not supported for external service + */ + public associateCloudMapService(_options: AssociateCloudMapServiceOptions): void { + throw new Error ('Cloud map service association is not supported for an external service'); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/external/external-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/external/external-task-definition.ts new file mode 100644 index 0000000000000..de9fa8b87e9dc --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/external/external-task-definition.ts @@ -0,0 +1,91 @@ +import { Construct } from 'constructs'; +import { ImportedTaskDefinition } from '../../lib/base/_imported-task-definition'; +import { + CommonTaskDefinitionAttributes, + CommonTaskDefinitionProps, + Compatibility, + InferenceAccelerator, + ITaskDefinition, + NetworkMode, + TaskDefinition, + Volume, +} from '../base/task-definition'; + +/** + * The properties for a task definition run on an External cluster. + */ +export interface ExternalTaskDefinitionProps extends CommonTaskDefinitionProps { + +} + +/** + * The interface of a task definition run on an External cluster. + */ +export interface IExternalTaskDefinition extends ITaskDefinition { + +} + +/** + * Attributes used to import an existing External task definition + */ +export interface ExternalTaskDefinitionAttributes extends CommonTaskDefinitionAttributes { + +} + +/** + * The details of a task definition run on an External cluster. + * + * @resource AWS::ECS::TaskDefinition + */ +export class ExternalTaskDefinition extends TaskDefinition implements IExternalTaskDefinition { + + /** + * Imports a task definition from the specified task definition ARN. + */ + public static fromEc2TaskDefinitionArn(scope: Construct, id: string, externalTaskDefinitionArn: string): IExternalTaskDefinition { + return new ImportedTaskDefinition(scope, id, { + taskDefinitionArn: externalTaskDefinitionArn, + }); + } + + /** + * Imports an existing External task definition from its attributes + */ + public static fromExternalTaskDefinitionAttributes( + scope: Construct, + id: string, + attrs: ExternalTaskDefinitionAttributes, + ): IExternalTaskDefinition { + return new ImportedTaskDefinition(scope, id, { + taskDefinitionArn: attrs.taskDefinitionArn, + compatibility: Compatibility.EXTERNAL, + networkMode: NetworkMode.BRIDGE, + taskRole: attrs.taskRole, + }); + } + + /** + * Constructs a new instance of the ExternalTaskDefinition class. + */ + constructor(scope: Construct, id: string, props: ExternalTaskDefinitionProps = {}) { + super(scope, id, { + ...props, + compatibility: Compatibility.EXTERNAL, + networkMode: NetworkMode.BRIDGE, + }); + } + + /** + * Overridden method to throw error, as volumes are not supported for external task definitions + */ + public addVolume(_volume: Volume) { + throw new Error('External task definitions doesnt support volumes'); + } + + /** + * Overriden method to throw error as interface accelerators are not supported for external tasks + */ + public addInferenceAccelerator(_inferenceAccelerator: InferenceAccelerator) { + throw new Error('Cannot use inference accelerators on tasks that run on External service'); + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 7b16d7be07827..0c1cee2a56ff9 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -15,6 +15,9 @@ export * from './ec2/ec2-task-definition'; export * from './fargate/fargate-service'; export * from './fargate/fargate-task-definition'; +export * from './external/external-service'; +export * from './external/external-task-definition'; + export * from './linux-parameters'; export * from './images/asset-image'; diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index dad900b0c039b..bf15c1f8e8101 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -151,6 +151,7 @@ "props-physical-name:@aws-cdk/aws-ecs.TaskDefinitionProps", "props-physical-name:@aws-cdk/aws-ecs.Ec2TaskDefinitionProps", "props-physical-name:@aws-cdk/aws-ecs.FargateTaskDefinitionProps", + "props-physical-name:@aws-cdk/aws-ecs.ExternalTaskDefinitionProps", "docs-public-apis:@aws-cdk/aws-ecs.GelfCompressionType.GZIP", "docs-public-apis:@aws-cdk/aws-ecs.WindowsOptimizedVersion.SERVER_2016", "docs-public-apis:@aws-cdk/aws-ecs.WindowsOptimizedVersion.SERVER_2019", diff --git a/packages/@aws-cdk/aws-ecs/test/external/external-service.test.ts b/packages/@aws-cdk/aws-ecs/test/external/external-service.test.ts new file mode 100644 index 0000000000000..d01eaa14f11b9 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/external/external-service.test.ts @@ -0,0 +1,528 @@ +import '@aws-cdk/assert-internal/jest'; +import * as autoscaling from '@aws-cdk/aws-autoscaling'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import * as cloudmap from '@aws-cdk/aws-servicediscovery'; +import * as cdk from '@aws-cdk/core'; +import { nodeunitShim, Test } from 'nodeunit-shim'; +import * as ecs from '../../lib'; +import { LaunchType } from '../../lib/base/base-service'; + +nodeunitShim({ + 'When creating an External Service': { + 'with only required properties set, it correctly sets default properties'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::Service', { + TaskDefinition: { + Ref: 'ExternalTaskDef6CCBDB87', + }, + Cluster: { + Ref: 'EcsCluster97242B84', + }, + DeploymentConfiguration: { + MaximumPercent: 100, + MinimumHealthyPercent: 0, + }, + EnableECSManagedTags: false, + LaunchType: LaunchType.EXTERNAL, + }); + + test.notEqual(service.node.defaultChild, undefined); + + test.done(); + }, + }, + + 'with all properties set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + // WHEN + new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + desiredCount: 2, + healthCheckGracePeriod: cdk.Duration.seconds(60), + maxHealthyPercent: 150, + minHealthyPercent: 55, + securityGroups: [new ec2.SecurityGroup(stack, 'SecurityGroup1', { + allowAllOutbound: true, + description: 'Example', + securityGroupName: 'Bob', + vpc, + })], + serviceName: 'bonjour', + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::Service', { + TaskDefinition: { + Ref: 'ExternalTaskDef6CCBDB87', + }, + Cluster: { + Ref: 'EcsCluster97242B84', + }, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 55, + }, + DesiredCount: 2, + LaunchType: LaunchType.EXTERNAL, + ServiceName: 'bonjour', + }); + + test.done(); + }, + + 'with cloudmap set on cluster, throw error'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DNS_PRIVATE, + }); + + // THEN + expect(() => new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + desiredCount: 2, + healthCheckGracePeriod: cdk.Duration.seconds(60), + maxHealthyPercent: 150, + minHealthyPercent: 55, + securityGroups: [new ec2.SecurityGroup(stack, 'SecurityGroup1', { + allowAllOutbound: true, + description: 'Example', + securityGroupName: 'Bob', + vpc, + })], + serviceName: 'bonjour', + })).toThrow('Cloud map integration is not supported for External service' ); + + test.done(); + }, + + 'with multiple security groups, it correctly updates the cfn template'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + const securityGroup1 = new ec2.SecurityGroup(stack, 'SecurityGroup1', { + allowAllOutbound: true, + description: 'Example', + securityGroupName: 'Bingo', + vpc, + }); + const securityGroup2 = new ec2.SecurityGroup(stack, 'SecurityGroup2', { + allowAllOutbound: false, + description: 'Example', + securityGroupName: 'Rolly', + vpc, + }); + + // WHEN + new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + desiredCount: 2, + securityGroups: [securityGroup1, securityGroup2], + serviceName: 'bonjour', + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::Service', { + TaskDefinition: { + Ref: 'ExternalTaskDef6CCBDB87', + }, + Cluster: { + Ref: 'EcsCluster97242B84', + }, + DesiredCount: 2, + LaunchType: LaunchType.EXTERNAL, + ServiceName: 'bonjour', + }); + + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Example', + GroupName: 'Bingo', + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + }); + + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Example', + GroupName: 'Rolly', + SecurityGroupEgress: [ + { + CidrIp: '255.255.255.255/32', + Description: 'Disallow all traffic', + FromPort: 252, + IpProtocol: 'icmp', + ToPort: 86, + }, + ], + }); + + test.done(); + }, + + 'throws when task definition is not External compatible'(test: Test) { + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.TaskDefinition(stack, 'FargateTaskDef', { + compatibility: ecs.Compatibility.FARGATE, + cpu: '256', + memoryMiB: '512', + }); + taskDefinition.addContainer('BaseContainer', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryReservationMiB: 10, + }); + + expect(() => new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + })).toThrow('Supplied TaskDefinition is not configured for compatibility with ECS Anywhere cluster'); + + test.done(); + }, + + 'errors if minimum not less than maximum'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + taskDefinition.addContainer('BaseContainer', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryReservationMiB: 10, + }); + + // THEN + expect(() => new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + minHealthyPercent: 100, + maxHealthyPercent: 100, + })).toThrow('Minimum healthy percent must be less than maximum healthy percent.'); + + test.done(); + }, + + 'error if cloudmap options provided with external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + // THEN + expect(() => new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + cloudMapOptions: { + name: 'myApp', + }, + })).toThrow('Cloud map options are not supported for External service'); + + // THEN + test.done(); + }, + + 'error if enableExecuteCommand options provided with external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + // THEN + expect(() => new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + enableExecuteCommand: true, + })).toThrow('Enable Execute Command options are not supported for External service'); + + // THEN + test.done(); + }, + + 'error if capacityProviderStrategies options provided with external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + // WHEN + const autoScalingGroup = new autoscaling.AutoScalingGroup(stack, 'asg', { + vpc, + instanceType: new ec2.InstanceType('bogus'), + machineImage: ecs.EcsOptimizedImage.amazonLinux2(), + }); + + const capacityProvider = new ecs.AsgCapacityProvider(stack, 'provider', { + autoScalingGroup, + enableManagedTerminationProtection: false, + }); + + // THEN + expect(() => new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + capacityProviderStrategies: [{ + capacityProvider: capacityProvider.capacityProviderName, + }], + })).toThrow('Capacity Providers are not supported for External service'); + + // THEN + test.done(); + }, + + 'error when performing attachToApplicationTargetGroup to an external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + const lb = new elbv2.ApplicationLoadBalancer(stack, 'lb', { vpc }); + const listener = lb.addListener('listener', { port: 80 }); + const targetGroup = listener.addTargets('target', { + port: 80, + }); + + // THEN + expect(() => service.attachToApplicationTargetGroup(targetGroup)).toThrow('Application load balancer cannot be attached to an external service'); + + // THEN + test.done(); + }, + + 'error when performing loadBalancerTarget to an external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + // THEN + expect(() => service.loadBalancerTarget({ + containerName: 'MainContainer', + })).toThrow('External service cannot be attached as load balancer targets'); + + // THEN + test.done(); + }, + + 'error when performing registerLoadBalancerTargets to an external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const lb = new elbv2.ApplicationLoadBalancer(stack, 'lb', { vpc }); + const listener = lb.addListener('listener', { port: 80 }); + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + // THEN + expect(() => service.registerLoadBalancerTargets( + { + containerName: 'MainContainer', + containerPort: 8000, + listener: ecs.ListenerConfig.applicationListener(listener), + newTargetGroupId: 'target1', + }, + )).toThrow('External service cannot be registered as load balancer targets'); + + // THEN + test.done(); + }, + + 'error when performing autoScaleTaskCount to an external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + // THEN + expect(() => service.autoScaleTaskCount({ + maxCapacity: 2, + minCapacity: 1, + })).toThrow('Autoscaling not supported for external service'); + + // THEN + test.done(); + }, + + 'error when performing enableCloudMap to an external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + // THEN + expect(() => service.enableCloudMap({})).toThrow('Cloud map integration not supported for an external service'); + + // THEN + test.done(); + }, + + 'error when performing associateCloudMapService to an external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + const cloudMapNamespace = new cloudmap.PrivateDnsNamespace(stack, 'TestCloudMapNamespace', { + name: 'scorekeep.com', + vpc, + }); + + const cloudMapService = new cloudmap.Service(stack, 'Service', { + name: 'service-name', + namespace: cloudMapNamespace, + dnsRecordType: cloudmap.DnsRecordType.SRV, + }); + + // THEN + expect(() => service.associateCloudMapService({ + service: cloudMapService, + container: container, + containerPort: 8000, + })).toThrow('Cloud map service association is not supported for an external service'); + + // THEN + test.done(); + }, +}); diff --git a/packages/@aws-cdk/aws-ecs/test/external/external-task-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/external/external-task-definition.test.ts new file mode 100644 index 0000000000000..c5fd8f942f1f0 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/external/external-task-definition.test.ts @@ -0,0 +1,638 @@ +import '@aws-cdk/assert-internal/jest'; +import * as path from 'path'; +import { Protocol } from '@aws-cdk/aws-ec2'; +import { Repository } from '@aws-cdk/aws-ecr'; +import * as iam from '@aws-cdk/aws-iam'; +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import * as ssm from '@aws-cdk/aws-ssm'; +import * as cdk from '@aws-cdk/core'; +import { nodeunitShim, Test } from 'nodeunit-shim'; +import * as ecs from '../../lib'; + +nodeunitShim({ + 'When creating an External TaskDefinition': { + 'with only required properties set, it correctly sets default properties'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Family: 'ExternalTaskDef', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: ['EXTERNAL'], + }); + + test.done(); + }, + + 'with all properties set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef', { + executionRole: new iam.Role(stack, 'ExecutionRole', { + path: '/', + assumedBy: new iam.CompositePrincipal( + new iam.ServicePrincipal('ecs.amazonaws.com'), + new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + ), + }), + family: 'ecs-tasks', + taskRole: new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }), + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + ExecutionRoleArn: { + 'Fn::GetAtt': [ + 'ExecutionRole605A040B', + 'Arn', + ], + }, + Family: 'ecs-tasks', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: [ + 'EXTERNAL', + ], + TaskRoleArn: { + 'Fn::GetAtt': [ + 'TaskRole30FC0FBB', + 'Arn', + ], + }, + }); + + test.done(); + }, + + 'correctly sets containers'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, // add validation? + }); + + container.addPortMappings({ + containerPort: 3000, + }); + + container.addUlimits({ + hardLimit: 128, + name: ecs.UlimitName.RSS, + softLimit: 128, + }); + + container.addToExecutionPolicy(new iam.PolicyStatement({ + resources: ['*'], + actions: ['ecs:*'], + })); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Family: 'ExternalTaskDef', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: ['EXTERNAL'], + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: 'amazon/amazon-ecs-sample', + Name: 'web', + PortMappings: [{ + ContainerPort: 3000, + HostPort: 0, + Protocol: Protocol.TCP, + }], + Ulimits: [ + { + HardLimit: 128, + Name: 'rss', + SoftLimit: 128, + }, + ], + }], + }); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: 'ecs:*', + Effect: 'Allow', + Resource: '*', + }, + ], + }, + }); + + test.done(); + }, + + 'all container definition options defined'(test: Test) { + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + const secret = new secretsmanager.Secret(stack, 'Secret'); + const parameter = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'Parameter', { + parameterName: '/name', + version: 1, + }); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 2048, + cpu: 256, + disableNetworking: true, + command: ['CMD env'], + dnsSearchDomains: ['0.0.0.0'], + dnsServers: ['1.1.1.1'], + dockerLabels: { LABEL: 'label' }, + dockerSecurityOptions: ['ECS_SELINUX_CAPABLE=true'], + entryPoint: ['/app/node_modules/.bin/cdk'], + environment: { TEST_ENVIRONMENT_VARIABLE: 'test environment variable value' }, + environmentFiles: [ecs.EnvironmentFile.fromAsset(path.join(__dirname, '../demo-envfiles/test-envfile.env'))], + essential: true, + extraHosts: { EXTRAHOST: 'extra host' }, + healthCheck: { + command: ['curl localhost:8000'], + interval: cdk.Duration.seconds(20), + retries: 5, + startPeriod: cdk.Duration.seconds(10), + }, + hostname: 'webHost', + linuxParameters: new ecs.LinuxParameters(stack, 'LinuxParameters', { + initProcessEnabled: true, + sharedMemorySize: 1024, + }), + logging: new ecs.AwsLogDriver({ streamPrefix: 'prefix' }), + memoryReservationMiB: 1024, + secrets: { + SECRET: ecs.Secret.fromSecretsManager(secret), + PARAMETER: ecs.Secret.fromSsmParameter(parameter), + }, + user: 'amazon', + workingDirectory: 'app/', + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Family: 'ExternalTaskDef', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: ['EXTERNAL'], + ContainerDefinitions: [ + { + Command: [ + 'CMD env', + ], + Cpu: 256, + DisableNetworking: true, + DnsSearchDomains: [ + '0.0.0.0', + ], + DnsServers: [ + '1.1.1.1', + ], + DockerLabels: { + LABEL: 'label', + }, + DockerSecurityOptions: [ + 'ECS_SELINUX_CAPABLE=true', + ], + EntryPoint: [ + '/app/node_modules/.bin/cdk', + ], + Environment: [ + { + Name: 'TEST_ENVIRONMENT_VARIABLE', + Value: 'test environment variable value', + }, + ], + EnvironmentFiles: [{ + Type: 's3', + Value: { + 'Fn::Join': [ + '', + [ + 'arn:aws:s3:::', + { + Ref: 'AssetParameters872561bf078edd1685d50c9ff821cdd60d2b2ddfb0013c4087e79bf2bb50724dS3Bucket7B2069B7', + }, + '/', + { + 'Fn::Select': [ + 0, + { + 'Fn::Split': [ + '||', + { + Ref: 'AssetParameters872561bf078edd1685d50c9ff821cdd60d2b2ddfb0013c4087e79bf2bb50724dS3VersionKey40E12C15', + }, + ], + }, + ], + }, + { + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '||', + { + Ref: 'AssetParameters872561bf078edd1685d50c9ff821cdd60d2b2ddfb0013c4087e79bf2bb50724dS3VersionKey40E12C15', + }, + ], + }, + ], + }, + ], + ], + }, + }], + Essential: true, + ExtraHosts: [ + { + Hostname: 'EXTRAHOST', + IpAddress: 'extra host', + }, + ], + HealthCheck: { + Command: [ + 'CMD-SHELL', + 'curl localhost:8000', + ], + Interval: 20, + Retries: 5, + StartPeriod: 10, + Timeout: 5, + }, + Hostname: 'webHost', + Image: 'amazon/amazon-ecs-sample', + LinuxParameters: { + Capabilities: {}, + InitProcessEnabled: true, + SharedMemorySize: 1024, + }, + LogConfiguration: { + LogDriver: 'awslogs', + Options: { + 'awslogs-group': { + Ref: 'ExternalTaskDefwebLogGroup827719D6', + }, + 'awslogs-stream-prefix': 'prefix', + 'awslogs-region': { + Ref: 'AWS::Region', + }, + }, + }, + Memory: 2048, + MemoryReservation: 1024, + Name: 'web', + Secrets: [ + { + Name: 'SECRET', + ValueFrom: { + Ref: 'SecretA720EF05', + }, + }, + { + Name: 'PARAMETER', + ValueFrom: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ssm:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':parameter/name', + ], + ], + }, + }, + ], + User: 'amazon', + WorkingDirectory: 'app/', + }, + ], + }); + + test.done(); + }, + + 'correctly sets containers from ECR repository using all props'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, 'myECRImage', { + lifecycleRegistryId: '123456789101', + lifecycleRules: [{ + rulePriority: 10, + tagPrefixList: ['abc'], + maxImageCount: 1, + }], + removalPolicy: cdk.RemovalPolicy.DESTROY, + repositoryName: 'project-a/amazon-ecs-sample', + })), + memoryLimitMiB: 512, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECR::Repository', { + LifecyclePolicy: { + // eslint-disable-next-line max-len + LifecyclePolicyText: '{"rules":[{"rulePriority":10,"selection":{"tagStatus":"tagged","tagPrefixList":["abc"],"countType":"imageCountMoreThan","countNumber":1},"action":{"type":"expire"}}]}', + RegistryId: '123456789101', + }, + RepositoryName: 'project-a/amazon-ecs-sample', + }); + + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Family: 'ExternalTaskDef', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: ['EXTERNAL'], + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.dkr.ecr.', + { + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/', + { + Ref: 'myECRImage7DEAE474', + }, + ':latest', + ], + ], + }, + Name: 'web', + }], + }); + + test.done(); + }, + }, + + 'correctly sets containers from ECR repository using an image tag'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, 'myECRImage'), 'myTag'), + memoryLimitMiB: 512, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Family: 'ExternalTaskDef', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: ['EXTERNAL'], + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.dkr.ecr.', + { + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/', + { + Ref: 'myECRImage7DEAE474', + }, + ':myTag', + ], + ], + }, + Name: 'web', + }], + }); + + test.done(); + }, + + 'correctly sets containers from ECR repository using an image digest'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, 'myECRImage'), 'sha256:94afd1f2e64d908bc90dbca0035a5b567EXAMPLE'), + memoryLimitMiB: 512, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Family: 'ExternalTaskDef', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: ['EXTERNAL'], + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.dkr.ecr.', + { + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/', + { + Ref: 'myECRImage7DEAE474', + }, + '@sha256:94afd1f2e64d908bc90dbca0035a5b567EXAMPLE', + ], + ], + }, + Name: 'web', + }], + }); + + test.done(); + }, + + 'correctly sets containers from ECR repository using default props'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + // WHEN + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, 'myECRImage')), + memoryLimitMiB: 512, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECR::Repository', {}); + + test.done(); + }, + + 'warns when setting containers from ECR repository using fromRegistry method'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + // WHEN + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('ACCOUNT.dkr.ecr.REGION.amazonaws.com/REPOSITORY'), + memoryLimitMiB: 512, + }); + + // THEN + expect(container.node.metadata[0].data).toEqual("Proper policies need to be attached before pulling from ECR repository, or use 'fromEcrRepository'."); + + test.done(); + }, + + 'correctly sets volumes from'(test: Test) { + const stack = new cdk.Stack(); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef', {}); + + // THEN + expect(() => taskDefinition.addVolume({ + host: { + sourcePath: '/tmp/cache', + }, + name: 'scratch', + })).toThrow('External task definitions doesnt support volumes' ); + + test.done(); + }, + + 'error when interferenceAccelerators set'(test: Test) { + const stack = new cdk.Stack(); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef', {}); + + // THEN + expect(() => taskDefinition.addInferenceAccelerator({ + deviceName: 'device1', + deviceType: 'eia2.medium', + })).toThrow('Cannot use inference accelerators on tasks that run on External service'); + + test.done(); + }, +}); \ No newline at end of file