Skip to content

Commit

Permalink
feat(aws-ecs): New CDK constructs for ECS Anywhere task and service d…
Browse files Browse the repository at this point in the history
…efinitions (#14931)

Here is how the user experience will looks like, when using the new constructs for provisioning the `ECS Anywhere` resources:
```typescript
// Stack definition
const stack = new cdk.Stack();

// ECS Task definition
const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef');

// Main container
const container = taskDefinition.addContainer('web', {
  image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'),
  memoryLimitMiB: 512,
});

// Port mapping
container.addPortMappings({
  containerPort: 3000,
});

// ECS Service definition
new ecs.ExternalService(stack, 'ExternalService', {
  cluster,
  taskDefinition,
});
```

> Note: Currently ECS anywhere doesn't support autoscaling, load balancing, `AWS Cloudmap` discovery and attachment of volumes. So validation rules are created part of this pull request.

**This is a follow up for the below PR:**
[#14811

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
hariohmprasath committed Jul 13, 2021
1 parent eeeec5d commit 3592b26
Show file tree
Hide file tree
Showing 7 changed files with 1,490 additions and 5 deletions.
44 changes: 39 additions & 5 deletions packages/@aws-cdk/aws-ecs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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

Expand Down Expand Up @@ -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`):

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand All @@ -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:
Expand All @@ -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.
Expand Down
190 changes: 190 additions & 0 deletions packages/@aws-cdk/aws-ecs/lib/external/external-service.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
91 changes: 91 additions & 0 deletions packages/@aws-cdk/aws-ecs/lib/external/external-task-definition.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
3 changes: 3 additions & 0 deletions packages/@aws-cdk/aws-ecs/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading

0 comments on commit 3592b26

Please sign in to comment.