Skip to content

Commit

Permalink
feat(aws-ecs): add ECS/Fargate QueueWorkerService constructs (#2568)
Browse files Browse the repository at this point in the history
Define a higher-level construct to model a worker queue.
  • Loading branch information
piradeepk authored and rix0rrr committed May 23, 2019
1 parent d7b0324 commit 7dd0e1a
Show file tree
Hide file tree
Showing 8 changed files with 811 additions and 0 deletions.
232 changes: 232 additions & 0 deletions design/aws-ecs-autoscaling-queue-worker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
# AWS ECS - L3 Construct for Autoscaling ECS/Fargate Service that Processes Items in a SQS Queue

To address issue [#2396](https://github.com/awslabs/aws-cdk/issues/2396), the AWS ECS CDK construct library should provide a way for customers to create a queue worker service (an AWS ECS/Fargate service that processes items from an sqs queue). This would mean adding new ECS CDK constructs `Ec2QueueWorkerService` and `FargateQueryWorkerService`, that would take in the necessary properties required to create a task definition, an SQS queue as well as an ECS/Fargate service and enable autoscaling for the service based on cpu usage and the SQS queue's approximateNumberOfMessagesVisible metric.

## General approach

The new `ecs.QueueWorkerServiceBase`, `ecs.Ec2QueueWorkerService` and `ecs.FargateQueueWorkerService` classes will create L3 constructs for:

* Ec2QueueWorkerService
* FargateQueueWorkerService

A `QueueWorkerService` will create a task definition with the specified container (on both EC2 and Fargate). An AWS SQS `Queue` will be created and autoscaling of the ECS Service will be dependent on both CPU as well as the SQS queue's `ApproximateNumberOfMessagesVisible` metric.

The `QueueWorkerService` constructs (for EC2 and Fargate) will use the following existing constructs:

* Ec2TaskDefinition/FargateTaskDefinition - To create a Task Definition for the container to start
* SQSQueue - The queue that the worker is processing from
* Ec2Service/FargateService - The Service running the container

## Code changes

Given the above, we should make the following changes to support queue workers on ECS (for both EC2 and Fargate):
1. Create `QueueWorkerServiceBaseProps` interface and `QueueWorkerServiceBase` construct
2. Create `Ec2QueueWorkerServiceProps` interface and `Ec2QueueWorkerService` construct
3. Create `FargateQueueWorkerServiceProps` interface and `FargateQueueWorkerService` construct

### Part 1: Create `QueueWorkerServiceBaseProps` interface and `QueueWorkerServiceBase` construct

The `QueueWorkerServiceBaseProps` interface will contain common properties used to construct both the Ec2QueueWorkerService and the FargateQueueWorkerService:

```ts
/**
* Properties to define a Query Worker service
*/
export interface QueueWorkerServiceBaseProps {
/**
* Cluster where service will be deployed
*/
readonly cluster: ICluster;

/**
* The image to start.
*/
readonly image: ContainerImage;

/**
* The CMD value to pass to the container as a string array.
*
* @default none
*/
readonly command?: string[];

/**
* Number of desired copies of running tasks
*
* @default 1
*/
readonly desiredTaskCount?: number;

/**
* Flag to indicate whether to enable logging
*
* @default true
*/
readonly enableLogging?: boolean;

/**
* The environment variables to pass to the container.
*
* @default 'QUEUE_NAME: queue.queueName'
*/
readonly environment?: { [key: string]: string };

/**
* A queue for which to process items from.
*
* If specified and this is a FIFO queue, the queue name must end in the string '.fifo'.
* @see https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_CreateQueue.html
*
* @default 'SQSQueue with CloudFormation-generated name'
*/
readonly queue?: IQueue;

/**
* Maximum capacity to scale to.
*
* @default (desiredTaskCount * 2)
*/
readonly maxScalingCapacity?: number

/**
* The intervals for scaling based on the SQS queue's ApproximateNumberOfMessagesVisible metric.
*
* Maps a range of metric values to a particular scaling behavior.
* https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-scaling-simple-step.html
*
* @default [{ upper: 0, change: -1 },{ lower: 100, change: +1 },{ lower: 500, change: +5 }]
*/
readonly scalingSteps: autoScaling.ScalingInterval[];
}
```

### Part 2: Create `Ec2QueueWorkerServiceProps` interface and `Ec2QueueWorkerService` construct

The `Ec2QueueWorkerServiceProps` interface will contain properties to construct the Ec2TaskDefinition, SQSQueue and Ec2Service:

```ts
/**
* Properties to define an ECS service
*/
export interface Ec2QueueWorkerServiceProps {
/**
* The minimum number of CPU units to reserve for the container.
*
* @default none
*/
readonly cpu?: number;

/**
* The hard limit (in MiB) of memory to present to the container.
*
* If your container attempts to exceed the allocated memory, the container
* is terminated.
*
* At least one of memoryLimitMiB and memoryReservationMiB is required for non-Fargate services.
*/
readonly memoryLimitMiB?: number;

/**
* The soft limit (in MiB) of memory to reserve for the container.
*
* When system memory is under contention, Docker attempts to keep the
* container memory within the limit. If the container requires more memory,
* it can consume up to the value specified by the Memory property or all of
* the available memory on the container instance—whichever comes first.
*
* At least one of memoryLimitMiB and memoryReservationMiB is required for non-Fargate services.
*/
readonly memoryReservationMiB?: number;
}
```

An example use case:
```ts
// Create the vpc and cluster used by the Queue Worker task
const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 1 });
const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc });
cluster.addCapacity('DefaultAutoScalingGroup', {
instanceType: new ec2.InstanceType('t2.micro')
});
const queue = new sqs.Queue(stack, 'WorkerQueue', {
QueueName: 'EcsWorkerQueue'
});

// Create the Queue Worker task
new Ec2QueueWorkerService(stack, 'EcsQueueWorkerService', {
cluster,
image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'),
desiredTaskCount: 2,
maxScalingCapacity: 5,
memoryReservationMiB: 512,
cpu: 256,
queue
});
```

### Part 3: Create `FargateQueueWorkerServiceProps` interface and `FargateQueueWorkerService` construct

The `FargateQueueWorkerServiceProps` interface will contain properties to construct the FargateTaskDefinition, SQSQueue and FargateService:

```ts
/**
* Properties to define an Fargate service
*/
export interface FargateQueueWorkerServiceProps {
/**
* The number of cpu units used by the task.
* Valid values, which determines your range of valid values for the memory parameter:
* 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB
* 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB
* 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB
* 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments
* 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments
*
* This default is set in the underlying FargateTaskDefinition construct.
*
* @default 256
*/
readonly cpu?: string;

/**
* The amount (in MiB) of memory used by the task.
*
* This field is required and you must use one of the following values, which determines your range of valid values
* for the cpu parameter:
*
* 0.5GB, 1GB, 2GB - Available cpu values: 256 (.25 vCPU)
*
* 1GB, 2GB, 3GB, 4GB - Available cpu values: 512 (.5 vCPU)
*
* 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB - Available cpu values: 1024 (1 vCPU)
*
* Between 4GB and 16GB in 1GB increments - Available cpu values: 2048 (2 vCPU)
*
* Between 8GB and 30GB in 1GB increments - Available cpu values: 4096 (4 vCPU)
*
* This default is set in the underlying FargateTaskDefinition construct.
*
* @default 512
*/
readonly memoryMiB?: string;
}
```

An example use case:
```ts
// Create the vpc and cluster used by the Queue Worker task
const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 });
const cluster = new ecs.Cluster(stack, 'FargateCluster', { vpc });
const queue = new sqs.Queue(stack, 'WorkerQueue', {
QueueName: 'FargateWorkerQueue'
});

// Create the Queue Worker task
new FargateQueueWorkerService(stack, 'FargateQueueWorkerService', {
cluster,
image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'),
desiredTaskCount: 2,
maxScalingCapacity: 5,
queue
});
```
68 changes: 68 additions & 0 deletions packages/@aws-cdk/aws-ecs/lib/ecs-queue-worker-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import cdk = require('@aws-cdk/cdk');
import { Ec2Service } from './ec2/ec2-service';
import { Ec2TaskDefinition } from './ec2/ec2-task-definition';
import { QueueWorkerServiceBase, QueueWorkerServiceBaseProps } from './queue-worker-service-base';

/**
* Properties to define an Ec2 query worker service
*/
export interface Ec2QueueWorkerServiceProps extends QueueWorkerServiceBaseProps {
/**
* The minimum number of CPU units to reserve for the container.
*
* @default none
*/
readonly cpu?: number;

/**
* The hard limit (in MiB) of memory to present to the container.
*
* If your container attempts to exceed the allocated memory, the container
* is terminated.
*
* At least one of memoryLimitMiB and memoryReservationMiB is required for non-Fargate services.
*/
readonly memoryLimitMiB?: number;

/**
* The soft limit (in MiB) of memory to reserve for the container.
*
* When system memory is under contention, Docker attempts to keep the
* container memory within the limit. If the container requires more memory,
* it can consume up to the value specified by the Memory property or all of
* the available memory on the container instance—whichever comes first.
*
* At least one of memoryLimitMiB and memoryReservationMiB is required for non-Fargate services.
*/
readonly memoryReservationMiB?: number;
}

/**
* Class to create an Ec2 query worker service
*/
export class Ec2QueueWorkerService extends QueueWorkerServiceBase {
constructor(scope: cdk.Construct, id: string, props: Ec2QueueWorkerServiceProps) {
super(scope, id, props);

// Create a Task Definition for the container to start
const taskDefinition = new Ec2TaskDefinition(this, 'QueueWorkerTaskDef');
taskDefinition.addContainer('QueueWorkerContainer', {
image: props.image,
memoryLimitMiB: props.memoryLimitMiB,
memoryReservationMiB: props.memoryReservationMiB,
cpu: props.cpu,
command: props.command,
environment: this.environment,
logging: this.logDriver
});

// Create an ECS service with the previously defined Task Definition and configure
// autoscaling based on cpu utilization and number of messages visible in the SQS queue.
const ecsService = new Ec2Service(this, 'QueueWorkerService', {
cluster: props.cluster,
desiredCount: this.desiredCount,
taskDefinition
});
this.configureAutoscalingForService(ecsService);
}
}
76 changes: 76 additions & 0 deletions packages/@aws-cdk/aws-ecs/lib/fargate-queue-worker-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import cdk = require('@aws-cdk/cdk');
import { FargateService } from './fargate/fargate-service';
import { FargateTaskDefinition } from './fargate/fargate-task-definition';
import { QueueWorkerServiceBase, QueueWorkerServiceBaseProps } from './queue-worker-service-base';

/**
* Properties to define a Fargate queue worker service
*/
export interface FargateQueueWorkerServiceProps extends QueueWorkerServiceBaseProps {
/**
* The number of cpu units used by the task.
* Valid values, which determines your range of valid values for the memory parameter:
* 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB
* 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB
* 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB
* 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments
* 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments
*
* This default is set in the underlying FargateTaskDefinition construct.
*
* @default 256
*/
readonly cpu?: string;

/**
* The amount (in MiB) of memory used by the task.
*
* This field is required and you must use one of the following values, which determines your range of valid values
* for the cpu parameter:
*
* 0.5GB, 1GB, 2GB - Available cpu values: 256 (.25 vCPU)
*
* 1GB, 2GB, 3GB, 4GB - Available cpu values: 512 (.5 vCPU)
*
* 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB - Available cpu values: 1024 (1 vCPU)
*
* Between 4GB and 16GB in 1GB increments - Available cpu values: 2048 (2 vCPU)
*
* Between 8GB and 30GB in 1GB increments - Available cpu values: 4096 (4 vCPU)
*
* This default is set in the underlying FargateTaskDefinition construct.
*
* @default 512
*/
readonly memoryMiB?: string;
}

/**
* Class to create a Fargate query worker service
*/
export class FargateQueueWorkerService extends QueueWorkerServiceBase {
constructor(scope: cdk.Construct, id: string, props: FargateQueueWorkerServiceProps) {
super(scope, id, props);

// Create a Task Definition for the container to start
const taskDefinition = new FargateTaskDefinition(this, 'QueueWorkerTaskDef', {
memoryMiB: props.memoryMiB !== undefined ? props.memoryMiB : '512',
cpu: props.cpu !== undefined ? props.cpu : '256',
});
taskDefinition.addContainer('QueueWorkerContainer', {
image: props.image,
command: props.command,
environment: this.environment,
logging: this.logDriver
});

// Create a Fargate service with the previously defined Task Definition and configure
// autoscaling based on cpu utilization and number of messages visible in the SQS queue.
const fargateService = new FargateService(this, 'FargateQueueWorkerService', {
cluster: props.cluster,
desiredCount: this.desiredCount,
taskDefinition
});
this.configureAutoscalingForService(fargateService);
}
}
4 changes: 4 additions & 0 deletions packages/@aws-cdk/aws-ecs/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export * from './load-balanced-fargate-service';
export * from './load-balanced-ecs-service';
export * from './load-balanced-fargate-service-applet';

export * from './queue-worker-service-base';
export * from './ecs-queue-worker-service';
export * from './fargate-queue-worker-service';

export * from './images/asset-image';
export * from './images/repository';
export * from './images/ecr';
Expand Down
Loading

0 comments on commit 7dd0e1a

Please sign in to comment.