Skip to content

Commit

Permalink
feat: add a new construct library for ECS (#1058)
Browse files Browse the repository at this point in the history
Add a new construct library to start services on ECS, both on EC2 and
Fargate-based clusters.

Containers can be started from images that are publicly available on
DockerHub, images in ECR repositories, and images built directly from
sources that are stored in source code next to the CDK app.

ECS services can be used as load balancer targets, and there are
higher-level constructs available to make it easy to start a service
behind a load balancer.

BREAKING CHANGE: the ec2.Connections object has been changed to be able
to manage multiple security groups. The relevant property has been
changed from `securityGroup` to `securityGroups` (an array of security
group objects).
  • Loading branch information
SoManyHs authored and rix0rrr committed Nov 6, 2018
1 parent 0e3b217 commit ae03ddb
Show file tree
Hide file tree
Showing 86 changed files with 9,392 additions and 246 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ pack
coverage
.nyc_output
.LAST_BUILD
*.swp
3 changes: 2 additions & 1 deletion examples/cdk-examples-typescript/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.LAST_BUILD
*.snk
hello-cdk-ecs/cdk.json
*.snk
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"app": "fargate-service.yml"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# applet is loaded from the local ./test-applet.js file
applets:
LoadBalancedFargateService:
type: @aws-cdk/aws-ecs:LoadBalancedFargateServiceApplet
properties:
image: 'amazon/amazon-ecs-sample'
cpu: "2048"
memoryMiB: "1024"
37 changes: 37 additions & 0 deletions examples/cdk-examples-typescript/hello-cdk-ecs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import ec2 = require('@aws-cdk/aws-ec2');
import { InstanceType } from '@aws-cdk/aws-ec2';
import ecs = require('@aws-cdk/aws-ecs');
import cdk = require('@aws-cdk/cdk');

class BonjourECS extends cdk.Stack {
constructor(parent: cdk.App, name: string, props?: cdk.StackProps) {
super(parent, name, props);

// For better iteration speed, it might make sense to put this VPC into
// a separate stack and import it here. We then have two stacks to
// deploy, but VPC creation is slow so we'll only have to do that once
// and can iterate quickly on consuming stacks. Not doing that for now.
const vpc = new ec2.VpcNetwork(this, 'MyVpc', { maxAZs: 2 });
const cluster = new ecs.Cluster(this, 'Ec2Cluster', { vpc });
cluster.addDefaultAutoScalingGroupCapacity({
instanceType: new InstanceType("t2.xlarge"),
instanceCount: 3,
});

// Instantiate ECS Service with just cluster and image
const ecsService = new ecs.LoadBalancedEc2Service(this, "Ec2Service", {
cluster,
memoryLimitMiB: 512,
image: ecs.DockerHub.image("amazon/amazon-ecs-sample"),
});

// Output the DNS where you can access your service
new cdk.Output(this, 'LoadBalancerDNS', { value: ecsService.loadBalancer.dnsName });
}
}

const app = new cdk.App();

new BonjourECS(app, 'Bonjour');

app.run();
29 changes: 29 additions & 0 deletions examples/cdk-examples-typescript/hello-cdk-fargate/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import ec2 = require('@aws-cdk/aws-ec2');
import ecs = require('@aws-cdk/aws-ecs');
import cdk = require('@aws-cdk/cdk');

class BonjourFargate extends cdk.Stack {
constructor(parent: cdk.App, name: string, props?: cdk.StackProps) {
super(parent, name, props);

// Create VPC and Fargate Cluster
// NOTE: Limit AZs to avoid reaching resource quotas
const vpc = new ec2.VpcNetwork(this, 'MyVpc', { maxAZs: 2 });
const cluster = new ecs.Cluster(this, 'Cluster', { vpc });

// Instantiate Fargate Service with just cluster and image
const fargateService = new ecs.LoadBalancedFargateService(this, "FargateService", {
cluster,
image: ecs.DockerHub.image("amazon/amazon-ecs-sample"),
});

// Output the DNS where you can access your service
new cdk.Output(this, 'LoadBalancerDNS', { value: fargateService.loadBalancer.dnsName });
}
}

const app = new cdk.App();

new BonjourFargate(app, 'Bonjour');

app.run();
2 changes: 2 additions & 0 deletions examples/cdk-examples-typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
"@aws-cdk/aws-cognito": "^0.14.1",
"@aws-cdk/aws-dynamodb": "^0.14.1",
"@aws-cdk/aws-ec2": "^0.14.1",
"@aws-cdk/aws-ecs": "^0.14.1",
"@aws-cdk/aws-elasticloadbalancing": "^0.14.1",
"@aws-cdk/aws-elasticloadbalancingv2": "^0.14.1",
"@aws-cdk/aws-iam": "^0.14.1",
"@aws-cdk/aws-lambda": "^0.14.1",
"@aws-cdk/aws-neptune": "^0.14.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/assets/lib/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export class Asset extends cdk.Construct {
// for tooling to be able to package and upload a directory to the
// s3 bucket and plug in the bucket name and key in the correct
// parameters.
const asset: cxapi.AssetMetadataEntry = {
const asset: cxapi.FileAssetMetadataEntry = {
path: this.assetPath,
id: this.uniqueId,
packaging: props.packaging,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,21 @@ export abstract class BaseScalableAttribute extends cdk.Construct {
/**
* Scale out or in based on time
*/
protected scaleOnSchedule(id: string, props: ScalingSchedule) {
protected doScaleOnSchedule(id: string, props: ScalingSchedule) {
this.target.scaleOnSchedule(id, props);
}

/**
* Scale out or in based on a metric value
*/
protected scaleOnMetric(id: string, props: BasicStepScalingPolicyProps) {
protected doScaleOnMetric(id: string, props: BasicStepScalingPolicyProps) {
this.target.scaleOnMetric(id, props);
}

/**
* Scale out or in in order to keep a metric around a target value
*/
protected scaleToTrackMetric(id: string, props: BasicTargetTrackingScalingPolicyProps) {
protected doScaleToTrackMetric(id: string, props: BasicTargetTrackingScalingPolicyProps) {
this.target.scaleToTrackMetric(id, props);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export class AutoScalingGroup extends cdk.Construct implements cdk.ITaggable, el
vpc: props.vpc,
allowAllOutbound: props.allowAllOutbound !== false
});
this.connections = new ec2.Connections({ securityGroup: this.securityGroup });
this.connections = new ec2.Connections({ securityGroups: [this.securityGroup] });
this.securityGroups.push(this.securityGroup);
this.tags = new TagManager(this, {initialTags: props.tags});
this.tags.setTag(NAME_TAG, this.path, { overwrite: false });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class ScalableTableAttribute extends appscaling.BaseScalableAttribute {
* Scale out or in based on time
*/
public scaleOnSchedule(id: string, action: appscaling.ScalingSchedule) {
super.scaleOnSchedule(id, action);
super.doScaleOnSchedule(id, action);
}

/**
Expand All @@ -24,7 +24,7 @@ export class ScalableTableAttribute extends appscaling.BaseScalableAttribute {
? appscaling.PredefinedMetric.DynamoDBWriteCapacityUtilization
: appscaling.PredefinedMetric.DynamoDBReadCapacityUtilization;

super.scaleToTrackMetric('Tracking', {
super.doScaleToTrackMetric('Tracking', {
policyName: props.policyName,
disableScaleIn: props.disableScaleIn,
scaleInCooldownSec: props.scaleInCooldownSec,
Expand Down
125 changes: 95 additions & 30 deletions packages/@aws-cdk/aws-ec2/lib/connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ export interface ConnectionsProps {
securityGroupRule?: ISecurityGroupRule;

/**
* What securityGroup this object is managing connections for
* What securityGroup(s) this object is managing connections for
*
* @default No security
* @default No security groups
*/
securityGroup?: SecurityGroupRef;
securityGroups?: SecurityGroupRef[];

/**
* Default port range for initiating connections to and from this object
Expand All @@ -59,68 +59,102 @@ export interface ConnectionsProps {
* establishing connectivity between security groups, it will automatically
* add rules in both security groups
*
* This object can manage one or more security groups.
*/
export class Connections {
export class Connections implements IConnectable {
public readonly connections: Connections;

/**
* The default port configured for this connection peer, if available
*/
public readonly defaultPortRange?: IPortRange;

/**
* Underlying securityGroup for this Connections object, if present
*
* May be empty if this Connections object is not managing a SecurityGroup,
* but simply representing a Connectable peer.
*/
public readonly securityGroup?: SecurityGroupRef;
private readonly _securityGroups = new ReactiveList<SecurityGroupRef>();

/**
* The rule that defines how to represent this peer in a security group
*/
public readonly securityGroupRule: ISecurityGroupRule;
private readonly _securityGroupRules = new ReactiveList<ISecurityGroupRule>();

/**
* The default port configured for this connection peer, if available
*/
public readonly defaultPortRange?: IPortRange;
private skip: boolean = false;

constructor(props: ConnectionsProps) {
if (!props.securityGroupRule && !props.securityGroup) {
throw new Error('Connections: require one of securityGroupRule or securityGroup');
constructor(props: ConnectionsProps = {}) {
this.connections = this;
this._securityGroups.push(...(props.securityGroups || []));

this._securityGroupRules.push(...this._securityGroups.asArray());
if (props.securityGroupRule) {
this._securityGroupRules.push(props.securityGroupRule);
}

this.securityGroupRule = props.securityGroupRule || props.securityGroup!;
this.securityGroup = props.securityGroup;
this.defaultPortRange = props.defaultPortRange;
}

public get securityGroups(): SecurityGroupRef[] {
return this._securityGroups.asArray();
}

/**
* Add a security group to the list of security groups managed by this object
*/
public addSecurityGroup(...securityGroups: SecurityGroupRef[]) {
for (const securityGroup of securityGroups) {
this._securityGroups.push(securityGroup);
this._securityGroupRules.push(securityGroup);
}
}

/**
* Allow connections to the peer on the given port
*/
public allowTo(other: IConnectable, portRange: IPortRange, description?: string) {
if (this.securityGroup) {
this.securityGroup.addEgressRule(other.connections.securityGroupRule, portRange, description);
}
if (other.connections.securityGroup) {
other.connections.securityGroup.addIngressRule(this.securityGroupRule, portRange, description);
if (this.skip) { return; }

}
this._securityGroups.forEachAndForever(securityGroup => {
other.connections._securityGroupRules.forEachAndForever(rule => {
securityGroup.addEgressRule(rule, portRange, description);
});
});

this.skip = true;
other.connections.allowFrom(this, portRange, description);
this.skip = false;
}

/**
* Allow connections from the peer on the given port
*/
public allowFrom(other: IConnectable, portRange: IPortRange, description?: string) {
if (this.securityGroup) {
this.securityGroup.addIngressRule(other.connections.securityGroupRule, portRange, description);
}
if (other.connections.securityGroup) {
other.connections.securityGroup.addEgressRule(this.securityGroupRule, portRange, description);
}
if (this.skip) { return; }

this._securityGroups.forEachAndForever(securityGroup => {
other.connections._securityGroupRules.forEachAndForever(rule => {
securityGroup.addIngressRule(rule, portRange, description);
});
});

this.skip = true;
other.connections.allowTo(this, portRange, description);
this.skip = false;
}

/**
* Allow hosts inside the security group to connect to each other on the given port
*/
public allowInternally(portRange: IPortRange, description?: string) {
if (this.securityGroup) {
this.securityGroup.addIngressRule(this.securityGroupRule, portRange, description);
}
this._securityGroups.forEachAndForever(securityGroup => {
this._securityGroupRules.forEachAndForever(rule => {
securityGroup.addIngressRule(rule, portRange, description);
// FIXME: this seems required but we didn't use to have it. Research.
// securityGroup.addEgressRule(rule, portRange, description);
});
});
}

/**
Expand Down Expand Up @@ -192,3 +226,34 @@ export class Connections {
this.allowTo(other, this.defaultPortRange, description);
}
}

type Action<T> = (x: T) => void;

class ReactiveList<T> {
private readonly elements = new Array<T>();
private readonly listeners = new Array<Action<T>>();

public push(...xs: T[]) {
this.elements.push(...xs);
for (const listener of this.listeners) {
for (const x of xs) {
listener(x);
}
}
}

public forEachAndForever(listener: Action<T>) {
for (const element of this.elements) {
listener(element);
}
this.listeners.push(listener);
}

public asArray(): T[] {
return this.elements.slice();
}

public get length(): number {
return this.elements.length;
}
}
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-ec2/lib/security-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export abstract class SecurityGroupRef extends Construct implements ISecurityGro

public abstract readonly securityGroupId: string;
public readonly canInlineRule = false;
public readonly connections = new Connections({ securityGroup: this });
public readonly connections = new Connections({ securityGroups: [this] });

/**
* FIXME: Where to place this??
Expand Down
Loading

0 comments on commit ae03ddb

Please sign in to comment.