Skip to content

Commit

Permalink
fix(aws-ecs-patterns): update ecs-patterns to be consistent across co…
Browse files Browse the repository at this point in the history
…nstructs (#3404)

Update ecs-patterns to be consistent across constructs
  • Loading branch information
piradeepk authored and Elad Ben-Israel committed Jul 29, 2019
1 parent cf2e3f6 commit f7fbbe0
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 171 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { ICertificate } from '@aws-cdk/aws-certificatemanager';
import ec2 = require('@aws-cdk/aws-ec2');
import ecs = require('@aws-cdk/aws-ecs');
import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2');
import { IVpc } from '@aws-cdk/aws-ec2';
import { AwsLogDriver, BaseService, Cluster, ContainerImage, ICluster, LogDriver, Secret } from '@aws-cdk/aws-ecs';
import { ApplicationListener, ApplicationLoadBalancer, ApplicationTargetGroup, BaseLoadBalancer, NetworkListener,
NetworkLoadBalancer, NetworkTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2';
import { IRole } from '@aws-cdk/aws-iam';
import { AddressRecordTarget, ARecord, IHostedZone } from '@aws-cdk/aws-route53';
import route53targets = require('@aws-cdk/aws-route53-targets');
import { LoadBalancerTarget } from '@aws-cdk/aws-route53-targets';
import cdk = require('@aws-cdk/core');

export enum LoadBalancerType {
Expand All @@ -12,7 +14,7 @@ export enum LoadBalancerType {
}

/**
* Base properties for load-balanced Fargate and ECS services
* The properties for the base LoadBalancedEc2Service or LoadBalancedFargateService service.
*/
export interface LoadBalancedServiceBaseProps {
/**
Expand All @@ -21,20 +23,20 @@ export interface LoadBalancedServiceBaseProps {
*
* @default - create a new cluster; if you do not specify a cluster nor a vpc, a new VPC will be created for you as well
*/
readonly cluster?: ecs.ICluster;
readonly cluster?: ICluster;

/**
* VPC that the cluster instances or tasks are running in
* You can only specify either vpc or cluster. Alternatively, you can leave both blank
*
* @default - use vpc of cluster or create a new one
*/
readonly vpc?: ec2.IVpc;
readonly vpc?: IVpc;

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

/**
* The container port of the application load balancer attached to your Fargate service. Corresponds to container port mapping.
Expand Down Expand Up @@ -84,7 +86,7 @@ export interface LoadBalancedServiceBaseProps {
*
* @default - No secret environment variables.
*/
readonly secrets?: { [key: string]: ecs.Secret };
readonly secrets?: { [key: string]: Secret };

/**
* Whether to create an AWS log driver
Expand Down Expand Up @@ -113,24 +115,59 @@ export interface LoadBalancedServiceBaseProps {
* @default - No Route53 hosted domain zone.
*/
readonly domainZone?: IHostedZone;

/**
* Override for the Fargate Task Definition execution role
*
* @default - No value
*/
readonly executionRole?: IRole;

/**
* Override for the Fargate Task Definition task role
*
* @default - No value
*/
readonly taskRole?: IRole;

/**
* Override value for the container name
*
* @default - No value
*/
readonly containerName?: string;

/**
* Override value for the service name
*
* @default CloudFormation-generated name
*/
readonly serviceName?: string;
}

/**
* Base class for load-balanced Fargate and ECS services
* The base class for LoadBalancedEc2Service and LoadBalancedFargateService services.
*/
export abstract class LoadBalancedServiceBase extends cdk.Construct {
public readonly assignPublicIp: boolean;

public readonly desiredCount: number;

public readonly loadBalancerType: LoadBalancerType;

public readonly loadBalancer: elbv2.BaseLoadBalancer;
public readonly loadBalancer: BaseLoadBalancer;

public readonly listener: elbv2.ApplicationListener | elbv2.NetworkListener;
public readonly listener: ApplicationListener | NetworkListener;

public readonly targetGroup: elbv2.ApplicationTargetGroup | elbv2.NetworkTargetGroup;
public readonly targetGroup: ApplicationTargetGroup | NetworkTargetGroup;

public readonly cluster: ecs.ICluster;
public readonly cluster: ICluster;

public readonly logDriver?: ecs.LogDriver;
public readonly logDriver?: LogDriver;

/**
* Constructs a new instance of the LoadBalancedServiceBase class.
*/
constructor(scope: cdk.Construct, id: string, props: LoadBalancedServiceBaseProps) {
super(scope, id);

Expand All @@ -143,6 +180,9 @@ export abstract class LoadBalancedServiceBase extends cdk.Construct {
const enableLogging = props.enableLogging !== undefined ? props.enableLogging : true;
this.logDriver = enableLogging ? this.createAWSLogDriver(this.node.id) : undefined;

this.assignPublicIp = props.publicTasks !== undefined ? props.publicTasks : false;
this.desiredCount = props.desiredCount || 1;

// Load balancer
this.loadBalancerType = props.loadBalancerType !== undefined ? props.loadBalancerType : LoadBalancerType.APPLICATION;

Expand All @@ -158,9 +198,9 @@ export abstract class LoadBalancedServiceBase extends cdk.Construct {
};

if (this.loadBalancerType === LoadBalancerType.APPLICATION) {
this.loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'LB', lbProps);
this.loadBalancer = new ApplicationLoadBalancer(this, 'LB', lbProps);
} else {
this.loadBalancer = new elbv2.NetworkLoadBalancer(this, 'LB', lbProps);
this.loadBalancer = new NetworkLoadBalancer(this, 'LB', lbProps);
}

const targetProps = {
Expand All @@ -173,7 +213,7 @@ export abstract class LoadBalancedServiceBase extends cdk.Construct {
}

if (this.loadBalancerType === LoadBalancerType.APPLICATION) {
this.listener = (this.loadBalancer as elbv2.ApplicationLoadBalancer).addListener('PublicListener', {
this.listener = (this.loadBalancer as ApplicationLoadBalancer).addListener('PublicListener', {
port: hasCertificate ? 443 : 80,
open: true
});
Expand All @@ -183,7 +223,7 @@ export abstract class LoadBalancedServiceBase extends cdk.Construct {
this.listener.addCertificateArns('Arns', [props.certificate.certificateArn]);
}
} else {
this.listener = (this.loadBalancer as elbv2.NetworkLoadBalancer).addListener('PublicListener', { port: 80 });
this.listener = (this.loadBalancer as NetworkLoadBalancer).addListener('PublicListener', { port: 80 });
this.targetGroup = this.listener.addTargets('ECS', targetProps);
}

Expand All @@ -195,29 +235,29 @@ export abstract class LoadBalancedServiceBase extends cdk.Construct {
new ARecord(this, "DNS", {
zone: props.domainZone,
recordName: props.domainName,
target: AddressRecordTarget.fromAlias(new route53targets.LoadBalancerTarget(this.loadBalancer)),
target: AddressRecordTarget.fromAlias(new LoadBalancerTarget(this.loadBalancer)),
});
}

new cdk.CfnOutput(this, 'LoadBalancerDNS', { value: this.loadBalancer.loadBalancerDnsName });
}

protected getDefaultCluster(scope: cdk.Construct, vpc?: ec2.IVpc): ecs.Cluster {
protected getDefaultCluster(scope: cdk.Construct, vpc?: IVpc): Cluster {
// magic string to avoid collision with user-defined constructs
const DEFAULT_CLUSTER_ID = `EcsDefaultClusterMnL3mNNYN${vpc ? vpc.node.id : ''}`;
const stack = cdk.Stack.of(scope);
return stack.node.tryFindChild(DEFAULT_CLUSTER_ID) as ecs.Cluster || new ecs.Cluster(stack, DEFAULT_CLUSTER_ID, { vpc });
return stack.node.tryFindChild(DEFAULT_CLUSTER_ID) as Cluster || new Cluster(stack, DEFAULT_CLUSTER_ID, { vpc });
}

protected addServiceAsTarget(service: ecs.BaseService) {
protected addServiceAsTarget(service: BaseService) {
if (this.loadBalancerType === LoadBalancerType.APPLICATION) {
(this.targetGroup as elbv2.ApplicationTargetGroup).addTarget(service);
(this.targetGroup as ApplicationTargetGroup).addTarget(service);
} else {
(this.targetGroup as elbv2.NetworkTargetGroup).addTarget(service);
(this.targetGroup as NetworkTargetGroup).addTarget(service);
}
}

private createAWSLogDriver(prefix: string): ecs.AwsLogDriver {
return new ecs.AwsLogDriver({ streamPrefix: prefix });
private createAWSLogDriver(prefix: string): AwsLogDriver {
return new AwsLogDriver({ streamPrefix: prefix });
}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
import autoscaling = require('@aws-cdk/aws-applicationautoscaling');
import ecs = require('@aws-cdk/aws-ecs');
import sqs = require('@aws-cdk/aws-sqs');
import cdk = require('@aws-cdk/core');
import { ScalingInterval } from '@aws-cdk/aws-applicationautoscaling';
import { AwsLogDriver, BaseService, ContainerImage, ICluster, LogDriver, Secret } from '@aws-cdk/aws-ecs';
import { IQueue, Queue } from '@aws-cdk/aws-sqs';
import { CfnOutput, Construct } from '@aws-cdk/core';

/**
* Properties to define a queue processing service
* The properties for the base QueueProcessingEc2Service or QueueProcessingFargateService service.
*/
export interface QueueProcessingServiceBaseProps {
/**
* Cluster where service will be deployed
* The name of the cluster that hosts the service.
*/
readonly cluster: ecs.ICluster;
readonly cluster: ICluster;

/**
* The image to start.
* The image used to start a container.
*
* This string is passed directly to the Docker daemon.
* Images in the Docker Hub registry are available by default.
* Other repositories are specified with either repository-url/image:tag or repository-url/image@digest.
*/
readonly image: ecs.ContainerImage;
readonly image: ContainerImage;

/**
* The CMD value to pass to the container. A string with commands delimited by commas.
* The command that is passed to the container.
*
* If you provide a shell command as a single string, you have to quote command-line arguments.
*
* @default none
* @default - CMD value built into container image.
*/
readonly command?: string[];

/**
* Number of desired copies of running tasks
* The desired number of instantiations of the task definition to keep running on the service.
*
* @default 1
*/
Expand Down Expand Up @@ -53,7 +59,7 @@ export interface QueueProcessingServiceBaseProps {
*
* @default - No secret environment variables.
*/
readonly secrets?: { [key: string]: ecs.Secret };
readonly secrets?: { [key: string]: Secret };

/**
* A queue for which to process items from.
Expand All @@ -63,7 +69,7 @@ export interface QueueProcessingServiceBaseProps {
*
* @default 'SQSQueue with CloudFormation-generated name'
*/
readonly queue?: sqs.IQueue;
readonly queue?: IQueue;

/**
* Maximum capacity to scale to.
Expand All @@ -80,17 +86,17 @@ export interface QueueProcessingServiceBaseProps {
*
* @default [{ upper: 0, change: -1 },{ lower: 100, change: +1 },{ lower: 500, change: +5 }]
*/
readonly scalingSteps?: autoscaling.ScalingInterval[];
readonly scalingSteps?: ScalingInterval[];
}

/**
* Base class for a Fargate and ECS queue processing service
* The base class for QueueProcessingEc2Service and QueueProcessingFargateService services.
*/
export abstract class QueueProcessingServiceBase extends cdk.Construct {
export abstract class QueueProcessingServiceBase extends Construct {
/**
* The SQS queue that the service will process from
*/
public readonly sqsQueue: sqs.IQueue;
public readonly sqsQueue: IQueue;

// Properties that have defaults defined. The Queue Processing Service will handle assigning undefined properties with default
// values so that derived classes do not need to maintain the same logic.
Expand All @@ -103,7 +109,7 @@ export abstract class QueueProcessingServiceBase extends cdk.Construct {
/**
* Secret environment variables
*/
public readonly secrets?: { [key: string]: ecs.Secret };
public readonly secrets?: { [key: string]: Secret };

/**
* The minimum number of tasks to run
Expand All @@ -118,18 +124,20 @@ export abstract class QueueProcessingServiceBase extends cdk.Construct {
/**
* The scaling interval for autoscaling based off an SQS Queue size
*/
public readonly scalingSteps: autoscaling.ScalingInterval[];

public readonly scalingSteps: ScalingInterval[];
/**
* The AwsLogDriver to use for logging if logging is enabled.
*/
public readonly logDriver?: ecs.LogDriver;
public readonly logDriver?: LogDriver;

constructor(scope: cdk.Construct, id: string, props: QueueProcessingServiceBaseProps) {
/**
* Constructs a new instance of the QueueProcessingServiceBase class.
*/
constructor(scope: Construct, id: string, props: QueueProcessingServiceBaseProps) {
super(scope, id);

// Create the SQS queue if one is not provided
this.sqsQueue = props.queue !== undefined ? props.queue : new sqs.Queue(this, 'EcsProcessingQueue', {});
this.sqsQueue = props.queue !== undefined ? props.queue : new Queue(this, 'EcsProcessingQueue', {});

// Setup autoscaling scaling intervals
const defaultScalingSteps = [{ upper: 0, change: -1 }, { lower: 100, change: +1 }, { lower: 500, change: +5 }];
Expand All @@ -147,16 +155,16 @@ export abstract class QueueProcessingServiceBase extends cdk.Construct {
this.desiredCount = props.desiredTaskCount || 1;
this.maxCapacity = props.maxScalingCapacity || (2 * this.desiredCount);

new cdk.CfnOutput(this, 'SQSQueue', { value: this.sqsQueue.queueName });
new cdk.CfnOutput(this, 'SQSQueueArn', { value: this.sqsQueue.queueArn });
new CfnOutput(this, 'SQSQueue', { value: this.sqsQueue.queueName });
new CfnOutput(this, 'SQSQueueArn', { value: this.sqsQueue.queueArn });
}

/**
* Configure autoscaling based off of CPU utilization as well as the number of messages visible in the SQS queue
*
* @param service the ECS/Fargate service for which to apply the autoscaling rules to
*/
protected configureAutoscalingForService(service: ecs.BaseService) {
protected configureAutoscalingForService(service: BaseService) {
const scalingTarget = service.autoScaleTaskCount({ maxCapacity: this.maxCapacity, minCapacity: this.desiredCount });
scalingTarget.scaleOnCpuUtilization('CpuScaling', {
targetUtilizationPercent: 50,
Expand All @@ -172,7 +180,7 @@ export abstract class QueueProcessingServiceBase extends cdk.Construct {
*
* @param prefix the Cloudwatch logging prefix
*/
private createAWSLogDriver(prefix: string): ecs.AwsLogDriver {
return new ecs.AwsLogDriver({ streamPrefix: prefix });
private createAWSLogDriver(prefix: string): AwsLogDriver {
return new AwsLogDriver({ streamPrefix: prefix });
}
}
Loading

0 comments on commit f7fbbe0

Please sign in to comment.