From 939452c6d6baf13dadef86e1f7194aa320392096 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 23 Aug 2018 14:31:20 -0700 Subject: [PATCH 01/97] Ignore swp files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7b89f50668180..5a03c2b774932 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ pack coverage .nyc_output .LAST_BUILD +*.swp From 7302a2f4250659f5e12c6ea6c9a2264e6f92b885 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 23 Aug 2018 14:30:54 -0700 Subject: [PATCH 02/97] Add ECS lib and example --- .../hello-cdk-ecs/cdk.json | 26 ++++++++ .../hello-cdk-ecs/index.ts | 15 +++++ examples/cdk-examples-typescript/package.json | 1 + packages/@aws-cdk/aws-ecs/lib/cluster.ts | 59 +++++++++++++++++++ packages/@aws-cdk/aws-ecs/lib/index.ts | 1 + packages/@aws-cdk/aws-ecs/package.json | 4 +- 6 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json create mode 100644 examples/cdk-examples-typescript/hello-cdk-ecs/index.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/cluster.ts diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json b/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json new file mode 100644 index 0000000000000..3d3c2f9878030 --- /dev/null +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json @@ -0,0 +1,26 @@ +{ + "app": "node index", + "context": { + "availability-zones:585695036304:us-east-1": [ + "us-east-1a", + "us-east-1b", + "us-east-1c", + "us-east-1d", + "us-east-1e", + "us-east-1f" + ], + "ssm:585695036304:us-east-1:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-14c5486b", + "availability-zones:585695036304:eu-west-2": [ + "eu-west-2a", + "eu-west-2b", + "eu-west-2c" + ], + "ssm:585695036304:eu-west-2:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-a36f8dc4", + "availability-zones:585695036304:eu-west-1": [ + "eu-west-1a", + "eu-west-1b", + "eu-west-1c" + ], + "ssm:585695036304:eu-west-1:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-ca0135b3" + } +} \ No newline at end of file diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts new file mode 100644 index 0000000000000..948b831ecf3b5 --- /dev/null +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -0,0 +1,15 @@ +import ecs = require('@aws-cdk/aws-ecs'); +import cdk = require('@aws-cdk/cdk'); + +class HelloECS extends cdk.Stack { + constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { + super(parent, name, props); + new ecs.Cluster(this, 'MyCluster'); + } +} + +const app = new cdk.App(process.argv); + +new HelloECS(app, 'Hello'); + +process.stdout.write(app.run()); diff --git a/examples/cdk-examples-typescript/package.json b/examples/cdk-examples-typescript/package.json index 3a2ef91dcbf36..fff2dee2aed30 100644 --- a/examples/cdk-examples-typescript/package.json +++ b/examples/cdk-examples-typescript/package.json @@ -28,6 +28,7 @@ "@aws-cdk/aws-cognito": "^0.8.2", "@aws-cdk/aws-dynamodb": "^0.8.2", "@aws-cdk/aws-ec2": "^0.8.2", + "@aws-cdk/aws-ecs": "^0.8.2", "@aws-cdk/aws-iam": "^0.8.2", "@aws-cdk/aws-lambda": "^0.8.2", "@aws-cdk/aws-neptune": "^0.8.2", diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts new file mode 100644 index 0000000000000..d70b930073fa1 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -0,0 +1,59 @@ +import autoscaling = require('@aws-cdk/aws-autoscaling'); +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { cloudformation, ClusterArn} from './ecs.generated'; + +export interface ClusterProps { + /** + * A name for the cluster. + * @default CloudFormation-generated name + */ + clusterName?: string; + + vpc: ec2.VpcNetworkRef; + + asg: autoscaling.AutoScalingGroup; +} + +export class ClusterName extends cdk.Token { +} + +export class Cluster extends cdk.Construct { + + public readonly clusterArn: ClusterArn; + + public readonly clusterName: ClusterName; + + constructor(parent: cdk.Construct, name: string, props: ClusterProps = {}) { + super(parent, name); + + const cluster = new cloudformation.ClusterResource(this, "Resource", {clusterName: props.clusterName}); + + this.clusterArn = cluster.clusterArn; + + this.clusterName = new ClusterName(cluster.ref); + + const taskDef = new cloudformation.TaskDefinitionResource(this, "TaskDef", { + family: "Service", + memory: "512", + cpu: "256", + containerDefinitions: [{ + name: "web", + image: "amazon/amazon-ecs-sample", + cpu: 10, + memory: 1024, + essential: true + }] + }); + + new cloudformation.ServiceResource(this, "Service", { + cluster: this.clusterName, + taskDefinition: taskDef.ref, + desiredCount: 1, + deploymentConfiguration: { + maximumPercent: 200, + minimumHealthyPercent: 75 + } + }); + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 8cccdf3fb43d7..fe07251d7062c 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -1,2 +1,3 @@ // AWS::ECS CloudFormation Resources: export * from './ecs.generated'; +export * from './cluster'; diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 861cf4d400b82..28916a84773f3 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -52,7 +52,9 @@ "pkglint": "^0.8.2" }, "dependencies": { - "@aws-cdk/cdk": "^0.8.2" + "@aws-cdk/cdk": "^0.8.2", + "@aws-cdk/aws-autoscaling": "^0.8.2", + "@aws-cdk/aws-ec2": "^0.8.2" }, "homepage": "https://github.com/awslabs/aws-cdk" } From 4c7127a3affcf14912dbac064cf837164ff021ae Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 24 Aug 2018 01:30:34 -0700 Subject: [PATCH 03/97] Add service and task def constructs --- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 27 ++++----------- packages/@aws-cdk/aws-ecs/lib/index.ts | 5 ++- packages/@aws-cdk/aws-ecs/lib/service.ts | 26 ++++++++++++++ .../@aws-cdk/aws-ecs/lib/task-definition.ts | 34 +++++++++++++++++++ 4 files changed, 71 insertions(+), 21 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/service.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/task-definition.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index d70b930073fa1..a716a9385fdbf 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -2,6 +2,8 @@ import autoscaling = require('@aws-cdk/aws-autoscaling'); import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { cloudformation, ClusterArn} from './ecs.generated'; +import { Service } from './service'; +import { TaskDefinition } from './task-definition'; export interface ClusterProps { /** @@ -10,9 +12,9 @@ export interface ClusterProps { */ clusterName?: string; - vpc: ec2.VpcNetworkRef; + vpc?: ec2.VpcNetworkRef; - asg: autoscaling.AutoScalingGroup; + asg?: autoscaling.AutoScalingGroup; } export class ClusterName extends cdk.Token { @@ -33,27 +35,12 @@ export class Cluster extends cdk.Construct { this.clusterName = new ClusterName(cluster.ref); - const taskDef = new cloudformation.TaskDefinitionResource(this, "TaskDef", { - family: "Service", - memory: "512", - cpu: "256", - containerDefinitions: [{ - name: "web", - image: "amazon/amazon-ecs-sample", - cpu: 10, - memory: 1024, - essential: true - }] - }); + const taskDef = new TaskDefinition(this, "MyTD"); - new cloudformation.ServiceResource(this, "Service", { + new Service(this, "Service", { cluster: this.clusterName, - taskDefinition: taskDef.ref, + taskDefinition: taskDef.taskDefinitionArn, desiredCount: 1, - deploymentConfiguration: { - maximumPercent: 200, - minimumHealthyPercent: 75 - } }); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index fe07251d7062c..53babeb50b7a4 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -1,3 +1,6 @@ +export * from './cluster'; +export * from './service'; +export * from './task-definition'; + // AWS::ECS CloudFormation Resources: export * from './ecs.generated'; -export * from './cluster'; diff --git a/packages/@aws-cdk/aws-ecs/lib/service.ts b/packages/@aws-cdk/aws-ecs/lib/service.ts new file mode 100644 index 0000000000000..5ae7537885b35 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/service.ts @@ -0,0 +1,26 @@ +import cdk = require('@aws-cdk/cdk'); +import { ClusterName } from './cluster'; +import { TaskDefinitionArn } from './task-definition'; +import { cloudformation } from './ecs.generated'; + +export interface ServiceProps { + cluster: ClusterName; + taskDefinition: TaskDefinitionArn; + desiredCount: number; +} + +export class Service extends cdk.Construct { + constructor(parent: cdk.Construct, name: string, props: ServiceProps) { + super(parent, name); + + new cloudformation.ServiceResource(this, "Service", { + cluster: props.cluster, + taskDefinition: props.taskDefinition, + desiredCount: props.desiredCount, + deploymentConfiguration: { + maximumPercent: 200, + minimumHealthyPercent: 75 + } + }); + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts new file mode 100644 index 0000000000000..328c33fe0690e --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -0,0 +1,34 @@ +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from './ecs.generated'; + +export interface TaskDefinitionProps { + cpu?: string; + memory?: string; + // specify cpu, memory, image url for containers +} + +export class TaskDefinitionArn extends cdk.Token { +} + +export class TaskDefinition extends cdk.Construct { + public readonly taskDefinitionArn: TaskDefinitionArn; + + constructor(parent: cdk.Construct, name: string, _props: TaskDefinitionProps = {}) { + super(parent, name); + + const taskDef = new cloudformation.TaskDefinitionResource(this, "TaskDef", { + family: "ecs-demo", + memory: "512", + cpu: "256", + containerDefinitions: [{ + name: "web", + image: "amazon/amazon-ecs-sample", + cpu: 10, + memory: 128, + essential: true + }] + }); + + this.taskDefinitionArn = taskDef.ref; + } +} From 98749ec3f0694f294a32beae615128a960419d9f Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 24 Aug 2018 02:15:57 -0700 Subject: [PATCH 04/97] WIP adding VPC/ASG --- .../hello-cdk-ecs/cdk.json | 9 +++++++-- .../hello-cdk-ecs/index.ts | 6 +++--- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json b/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json index 3d3c2f9878030..a49176a091559 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json @@ -21,6 +21,11 @@ "eu-west-1b", "eu-west-1c" ], - "ssm:585695036304:eu-west-1:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-ca0135b3" + "ssm:585695036304:eu-west-1:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-ca0135b3", + "availability-zones:794715269151:us-west-2": [ + "us-west-2a", + "us-west-2b", + "us-west-2c" + ] } -} \ No newline at end of file +} diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 948b831ecf3b5..9fce96e972253 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -1,15 +1,15 @@ import ecs = require('@aws-cdk/aws-ecs'); import cdk = require('@aws-cdk/cdk'); -class HelloECS extends cdk.Stack { +class BonjourECS extends cdk.Stack { constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { super(parent, name, props); - new ecs.Cluster(this, 'MyCluster'); + new ecs.Cluster(this, 'DemoCluster'); } } const app = new cdk.App(process.argv); -new HelloECS(app, 'Hello'); +new BonjourECS(app, 'Goede Morgen'); process.stdout.write(app.run()); diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index a716a9385fdbf..b3ecad800d452 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -26,6 +26,8 @@ export class Cluster extends cdk.Construct { public readonly clusterName: ClusterName; + public readonly fleet: autoscaling.AutoScalingGroup; + constructor(parent: cdk.Construct, name: string, props: ClusterProps = {}) { super(parent, name); @@ -42,5 +44,18 @@ export class Cluster extends cdk.Construct { taskDefinition: taskDef.taskDefinitionArn, desiredCount: 1, }); + + const vpc = new ec2.VpcNetwork(this, 'MyVpc', { + maxAZs: 2 + }); + + const fleet = new autoscaling.AutoScalingGroup(this, 'MyASG', { + vpc, + instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), + machineImage: new ec2.GenericLinuxImage({'us-west-2':'ami-00d4f478'}) + }); + + this.fleet = fleet; + } } From 6d0942fcd1916867732300a609589301b318c9e7 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 24 Aug 2018 14:36:51 +0200 Subject: [PATCH 05/97] Refactor ECS demo - Fix linting errors - VPC is now an argument and not created inside the cluster - Link the AutoScalingGroup up to the cluster via userData - Give the ASG instance role the required IAM permissions - Extend ECS-optimized AMI table with all regions - Add an example configuration option to deny containers access to EC2 instance metadata service. --- .../hello-cdk-ecs/cdk.json | 10 ++ .../hello-cdk-ecs/index.ts | 16 +++- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 95 ++++++++++++++++--- packages/@aws-cdk/aws-ecs/lib/service.ts | 2 +- 4 files changed, 109 insertions(+), 14 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json b/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json index a49176a091559..841c948f0b918 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json @@ -26,6 +26,16 @@ "us-west-2a", "us-west-2b", "us-west-2c" + ], + "availability-zones:993655754359:us-west-2": [ + "us-west-2a", + "us-west-2b", + "us-west-2c" + ], + "availability-zones:993655754359:eu-west-1": [ + "eu-west-1a", + "eu-west-1b", + "eu-west-1c" ] } } diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 9fce96e972253..186fdcb54edb9 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -1,15 +1,27 @@ +import ec2 = require('@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); - new ecs.Cluster(this, 'DemoCluster'); + + // 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 + }); + + new ecs.Cluster(this, 'DemoCluster', { + vpc + }); } } const app = new cdk.App(process.argv); -new BonjourECS(app, 'Goede Morgen'); +new BonjourECS(app, 'GoedeMorgen'); process.stdout.write(app.run()); diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index b3ecad800d452..2bf7228a6e07b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -8,16 +8,47 @@ import { TaskDefinition } from './task-definition'; export interface ClusterProps { /** * A name for the cluster. + * * @default CloudFormation-generated name */ clusterName?: string; - vpc?: ec2.VpcNetworkRef; + /** + * The VPC where your ECS instances will be running + */ + vpc: ec2.VpcNetworkRef; - asg?: autoscaling.AutoScalingGroup; + /** + * Whether or not the containers can access the instance role + * + * @default false + */ + containersAccessInstanceRole?: boolean; } -export class ClusterName extends cdk.Token { +// This works for now but how will we keep this list up to date? +export const ECS_OPTIMIZED_AMI = new ec2.GenericLinuxImage({ + 'us-east-2': 'ami-028a9de0a7e353ed9', + 'us-east-1': 'ami-00129b193dc81bc31', + 'us-west-2': 'ami-00d4f478', + 'us-west-1': 'ami-0d438d09af26c9583', + 'eu-west-2': 'ami-a44db8c3', + 'eu-west-3': 'ami-07da674f0655ef4e1', + 'eu-west-1': 'ami-0af844a965e5738db', + 'eu-central-1': 'ami-0291ba887ba0d515f', + 'ap-northeast-2': 'ami-047d2a61f94f862dc', + 'ap-northeast-1': 'ami-0041c416aa23033a2', + 'ap-southeast-2': 'ami-0092e55c70015d8c3', + 'ap-southeast-1': 'ami-091bf462afdb02c60', + 'ca-central-1': 'ami-192fa27d', + 'ap-south-1': 'ami-0c179ca015d301829', + 'sa-east-1': 'ami-0018ff8ee48970ac3', + 'us-gov-west-1': 'ami-c6079ba7', +}); + +// Needs to inherit from CloudFormationToken to make the string substitution +// downstairs work. This is temporary, will go away in the near future. +export class ClusterName extends cdk.CloudFormationToken { } export class Cluster extends cdk.Construct { @@ -26,9 +57,9 @@ export class Cluster extends cdk.Construct { public readonly clusterName: ClusterName; - public readonly fleet: autoscaling.AutoScalingGroup; + public readonly fleet: autoscaling.AutoScalingGroup; - constructor(parent: cdk.Construct, name: string, props: ClusterProps = {}) { + constructor(parent: cdk.Construct, name: string, props: ClusterProps) { super(parent, name); const cluster = new cloudformation.ClusterResource(this, "Resource", {clusterName: props.clusterName}); @@ -45,16 +76,58 @@ export class Cluster extends cdk.Construct { desiredCount: 1, }); - const vpc = new ec2.VpcNetwork(this, 'MyVpc', { - maxAZs: 2 - }); - const fleet = new autoscaling.AutoScalingGroup(this, 'MyASG', { - vpc, + vpc: props.vpc, instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), - machineImage: new ec2.GenericLinuxImage({'us-west-2':'ami-00d4f478'}) + machineImage: ECS_OPTIMIZED_AMI, + updateType: autoscaling.UpdateType.ReplacingUpdate }); + // Tie instances to cluster + fleet.addUserData(`echo ECS_CLUSTER=${this.clusterName} >> /etc/ecs/ecs.config`); + + if (!props.containersAccessInstanceRole) { + // Deny containers access to instance metadata service + // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html + fleet.addUserData('sudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP'); + fleet.addUserData('sudo service iptables save'); + } + + // Note: if the fleet doesn't launch or doesn't register itself with + // ECS, *Cluster* stabilization will fail after timing our for an hour + // or so, because the *Service* doesn't have any running instances. + // During this time, you CANNOT DO ANYTHING ELSE WITH YOUR STACK. + // + // Apart from the weird relationship here between Cluster and Service + // (why is Cluster failing and not Service?), the experience is... + // + // NOT GREAT. + // + // Also, there's sort of a bidirectional dependency between Cluster and ASG: + // + // - ASG depends on Cluster to get the ClusterName (which needs to go into + // UserData). + // - Cluster depends on ASG to boot up, so the service is launched, so the + // Cluster can stabilize. + + // ECS instances must be able to do these things + // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html + fleet.addToRolePolicy(new cdk.PolicyStatement().addActions( + "ecs:CreateCluster", + "ecs:DeregisterContainerInstance", + "ecs:DiscoverPollEndpoint", + "ecs:Poll", + "ecs:RegisterContainerInstance", + "ecs:StartTelemetrySession", + "ecs:Submit*", + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "logs:CreateLogStream", + "logs:PutLogEvents" + ).addAllResources()); // Conceivably we might do better than all resources and add targeted ARNs + this.fleet = fleet; } diff --git a/packages/@aws-cdk/aws-ecs/lib/service.ts b/packages/@aws-cdk/aws-ecs/lib/service.ts index 5ae7537885b35..3e03940620b12 100644 --- a/packages/@aws-cdk/aws-ecs/lib/service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/service.ts @@ -1,7 +1,7 @@ import cdk = require('@aws-cdk/cdk'); import { ClusterName } from './cluster'; -import { TaskDefinitionArn } from './task-definition'; import { cloudformation } from './ecs.generated'; +import { TaskDefinitionArn } from './task-definition'; export interface ServiceProps { cluster: ClusterName; From bddfe561b526a2b0c8e427630e23fbfabda50ebb Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 24 Aug 2018 08:17:03 -0700 Subject: [PATCH 06/97] Add more props on Service --- packages/@aws-cdk/aws-ecs/lib/service.ts | 72 ++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/service.ts b/packages/@aws-cdk/aws-ecs/lib/service.ts index 3e03940620b12..a6672544b89c5 100644 --- a/packages/@aws-cdk/aws-ecs/lib/service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/service.ts @@ -4,9 +4,68 @@ import { cloudformation } from './ecs.generated'; import { TaskDefinitionArn } from './task-definition'; export interface ServiceProps { - cluster: ClusterName; + /** + * Cluster where service will be deployed + */ + cluster: ClusterName; // should be required? do we assume 'default' exists? + + /** + * Task Definition used for running tasks in the service + */ taskDefinition: TaskDefinitionArn; - desiredCount: number; + + /** + * Number of desired copies of running tasks + * + * @default 1 + */ + desiredCount?: number; + + /** + * A name for the service. + * + * @default CloudFormation-generated name + */ + serviceName?: string; + + /** + * Whether the service is hosted in EC2 or Fargate + * + * @default EC2 + */ + launchType?: string; // maybe unnecessary if we have different ECS vs FG service + + /** + * The maximum number of tasks, specified as a percentage of the Amazon ECS service's DesiredCount value, that can run in a service during a deployment. + * + * @default 200 + */ + maximumPercent?: number; + + /** + * The minimum number of tasks, specified as a percentage of + * the Amazon ECS service's DesiredCount value, that must + * continue to run and remain healthy during a deployment. + * + * @default 50 + */ + minimumHealthyPercent?: number; + + /** + * The name or ARN of an AWS Identity and Access Management (IAM) role that allows your Amazon ECS container agent to make calls to your load balancer. + */ + role?: string; + + ///////// TBD /////////////////////////////// + // healthCheckGracePeriodSeconds?: number; // only needed with load balancers + // loadBalancers?: LoadBalancer[]; + // placementConstraints?: PlacementConstraint[]; + // placementStrategies?: PlacementStrategy[]; + // networkConfiguration?: NetworkConfiguration; + // serviceRegistries?: ServiceRegistry[]; + // + // platformVersion?: string; // FARGATE ONLY. default is LATEST. Other options: 1.2.0, 1.1.0, 1.0.0 + //////////////////////////////////////////// } export class Service extends cdk.Construct { @@ -17,10 +76,13 @@ export class Service extends cdk.Construct { cluster: props.cluster, taskDefinition: props.taskDefinition, desiredCount: props.desiredCount, + serviceName: props.serviceName, + launchType: props.launchType, deploymentConfiguration: { - maximumPercent: 200, - minimumHealthyPercent: 75 - } + maximumPercent: props.maximumPercent, + minimumHealthyPercent: props.minimumHealthyPercent + }, + role: props.role, }); } } From 3574611ec4a578ca9ec75dd7efa5d6eca1500a5c Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 6 Sep 2018 14:48:41 -0700 Subject: [PATCH 07/97] Add TODO for AMI IDs --- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 2bf7228a6e07b..185a02d7e41e7 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -26,7 +26,7 @@ export interface ClusterProps { containersAccessInstanceRole?: boolean; } -// This works for now but how will we keep this list up to date? +// TODO: replace this with call to SSM, stored as "/aws/service/ecs/optimized-ami/amazon-linux/recommended" export const ECS_OPTIMIZED_AMI = new ec2.GenericLinuxImage({ 'us-east-2': 'ami-028a9de0a7e353ed9', 'us-east-1': 'ami-00129b193dc81bc31', From a63c377e4fb9c3c90d61467d239732a0b4288fa9 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 6 Sep 2018 17:43:10 -0700 Subject: [PATCH 08/97] Weird hack? Was getting error: lib/index.ts(7,1): error TS2308: Module './cluster' has already exported a member named 'ClusterName'. Consider explicitly re-exporting to resolve the ambiguity. lib/index.ts(7,1): error TS2308: Module './task-definition' has already exported a member named 'TaskDefinitionArn'. Consider explicitly re-exporting to resolve the ambiguity. This seemed to make npm run build happy --- packages/@aws-cdk/aws-ecs/lib/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 53babeb50b7a4..4a2b652775532 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -3,4 +3,4 @@ export * from './service'; export * from './task-definition'; // AWS::ECS CloudFormation Resources: -export * from './ecs.generated'; +export {ClusterName, TaskDefinitionArn} from './ecs.generated'; From b00e0dd52ad5682fb90d180fe18fc554daa88833 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 6 Sep 2018 18:59:36 -0700 Subject: [PATCH 09/97] Update example to instantiate service separately --- .../cdk-examples-typescript/hello-cdk-ecs/index.ts | 10 +++++++++- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 10 ---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 186fdcb54edb9..b31a9a8ddf16a 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -14,9 +14,17 @@ class BonjourECS extends cdk.Stack { maxAZs: 2 }); - new ecs.Cluster(this, 'DemoCluster', { + const cluster = new ecs.Cluster(this, 'DemoCluster', { vpc }); + + const taskDef = new ecs.TaskDefinition(this, "MyTD"); + + new ecs.Service(this, "Service", { + cluster: cluster.clusterName, + taskDefinition: taskDef.taskDefinitionArn, + desiredCount: 1, + }); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 185a02d7e41e7..9b5cd69431375 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -2,8 +2,6 @@ import autoscaling = require('@aws-cdk/aws-autoscaling'); import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { cloudformation, ClusterArn} from './ecs.generated'; -import { Service } from './service'; -import { TaskDefinition } from './task-definition'; export interface ClusterProps { /** @@ -68,14 +66,6 @@ export class Cluster extends cdk.Construct { this.clusterName = new ClusterName(cluster.ref); - const taskDef = new TaskDefinition(this, "MyTD"); - - new Service(this, "Service", { - cluster: this.clusterName, - taskDefinition: taskDef.taskDefinitionArn, - desiredCount: 1, - }); - const fleet = new autoscaling.AutoScalingGroup(this, 'MyASG', { vpc: props.vpc, instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), From 80ba2e467a490a29efe800a1ea85b8582b0c8c65 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 11 Sep 2018 23:04:05 -0700 Subject: [PATCH 10/97] WIP Add Container Definitions --- .../hello-cdk-ecs/index.ts | 26 +++- .../@aws-cdk/aws-ecs/lib/task-definition.ts | 121 ++++++++++++++++-- 2 files changed, 135 insertions(+), 12 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index b31a9a8ddf16a..72651448ed449 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -18,7 +18,31 @@ class BonjourECS extends cdk.Stack { vpc }); - const taskDef = new ecs.TaskDefinition(this, "MyTD"); + // name, image, cpu, memory, port (with default) + // + // Include in constructs: + // - networking - include SD, ALB + // - logging - cloudwatch logs integration? talk to nathan about 3rd + // party integrations - aggregated logging across the service + // (instead of per task). Probably prometheus or elk? + // - tracing aws-xray-fargate - CNCF opentracing standard - jaeger, + // zipkin. + // - so x-ray is a container that is hooked up to sidecars that come + // with the application container itself + // - autoscaling - application autoscaling (Fargate focused?) + + const taskDef = new ecs.TaskDefinition(this, "MyTD", { + family: "ecs-task-definition", + containerDefinitions: [ + { + name: "web", + image: "amazon/amazon-ecs-sample", + cpu: 1024, + memory: 512, + essential: true + } + ] + }); new ecs.Service(this, "Service", { cluster: cluster.clusterName, diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index 328c33fe0690e..1492e7629996f 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -2,9 +2,61 @@ import cdk = require('@aws-cdk/cdk'); import { cloudformation } from './ecs.generated'; export interface TaskDefinitionProps { + /** + * The number of cpu units used by the task. If using the EC2 launch type, + * this field is optional. Supported values are between 128 CPU units + * (0.125 vCPUs) and 10240 CPU units (10 vCPUs). If you are using the + * Fargate launch type, this field is required and you must use one of the + * following 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 + * + * @default 256 + */ cpu?: string; + + /** + * The amount (in MiB) of memory used by the task. If using the EC2 launch + * type, this field is optional and any value can be used. If you are using + * the Fargate launch type, 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) + * + * @default 512 + */ memory?: string; - // specify cpu, memory, image url for containers + + /** + * Namespace for task definition versions + * + * @default CloudFormation-generated name + */ + family?: string; + + // taskRoleArn?: string; // goes with container defs? + + networkMode?: string; + + containerDefinitions: ContainerDefinition[]; + // + // executionRoleArn:? string; + // volumes?: VolumeDefinition[]; + // placementConstraints?: PlacementConstraint[]; + // requiresCompatibilities?: string[]; // FARGATE or EC2 -- set on ECS TD vs FG TD } export class TaskDefinitionArn extends cdk.Token { @@ -12,23 +64,70 @@ export class TaskDefinitionArn extends cdk.Token { export class TaskDefinition extends cdk.Construct { public readonly taskDefinitionArn: TaskDefinitionArn; + // public readonly containerDefinitions: ContainerDefinition[]; + private readonly containerDefinitions: cloudformation.TaskDefinitionResource.ContainerDefinitionProperty[] = []; - constructor(parent: cdk.Construct, name: string, _props: TaskDefinitionProps = {}) { + constructor(parent: cdk.Construct, name: string, props: TaskDefinitionProps) { super(parent, name); const taskDef = new cloudformation.TaskDefinitionResource(this, "TaskDef", { family: "ecs-demo", - memory: "512", - cpu: "256", - containerDefinitions: [{ - name: "web", - image: "amazon/amazon-ecs-sample", - cpu: 10, - memory: 128, - essential: true - }] + memory: props.memory, + cpu: props.cpu, + containerDefinitions: new cdk.Token(() => this.containerDefinitions), // ???? }); + props.containerDefinitions.forEach(c => this.addContainer(c)); + this.taskDefinitionArn = taskDef.ref; } + + private addContainer(definition:ContainerDefinition) { + const cd = this.renderContainerDefininition(definition); + this.containerDefinitions.push(cd); + } + + // Populates task definition with container definition + private renderContainerDefininition(definition: ContainerDefinition): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { + return { + name: definition.name, + image: definition.image, + cpu: definition.cpu, + memory: definition.memory, + essential: definition.essential, + command: definition.command + }; + } +} + +export interface ContainerDefinition{ + name: string; + image: string; + + command?: string[]; + cpu?: number; + disableNetworking?: boolean; + dnsSearchDomains?: string[]; + dnsServers?: string[]; + dockerLabels?: string[]; + dockerSecurityOptions?: string[]; + entryPoint?: string[]; + essential?: boolean; + hostname?: string; + links?: string[]; + memory?: number; + memoryReservation?: number; + privileged?: boolean; + readonlyRootFilesystem?: boolean; + user?: string; + workingDirectory?: string } + // environment?: list of key-value; + // extraHosts?: hostEntry[]; + // healthCheck?: healthCheck; + // linuxParameters: linuxParam[]; + // logConfiguration: logConfig[]; + // mountPoints?: mountPoint[]; + // portMappings?: portMapping[]; + // ulimits?: ulimit[]; + // volumesFrom?: volumeFrom[]; From 0980f520f4d30be04379dff199e463e0c8928975 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 14 Sep 2018 14:48:53 -0700 Subject: [PATCH 11/97] WIP Add other Task Def properties --- .../hello-cdk-ecs/index.ts | 3 + packages/@aws-cdk/aws-ecs/lib/cluster.ts | 1 + packages/@aws-cdk/aws-ecs/lib/service.ts | 8 +- .../@aws-cdk/aws-ecs/lib/task-definition.ts | 187 +++++++++++++++--- 4 files changed, 167 insertions(+), 32 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 72651448ed449..08f3ba4220012 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -33,6 +33,9 @@ class BonjourECS extends cdk.Stack { const taskDef = new ecs.TaskDefinition(this, "MyTD", { family: "ecs-task-definition", + placementConstraints: [{ + type: "distinctInstance" + }], containerDefinitions: [ { name: "web", diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 9b5cd69431375..bae9f5823afcd 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -25,6 +25,7 @@ export interface ClusterProps { } // TODO: replace this with call to SSM, stored as "/aws/service/ecs/optimized-ami/amazon-linux/recommended" +// https://raw.githubusercontent.com/awslabs/aws-cloudformation-templates/master/aws/services/ECS/EC2LaunchType/clusters/public-vpc.yml export const ECS_OPTIMIZED_AMI = new ec2.GenericLinuxImage({ 'us-east-2': 'ami-028a9de0a7e353ed9', 'us-east-1': 'ami-00129b193dc81bc31', diff --git a/packages/@aws-cdk/aws-ecs/lib/service.ts b/packages/@aws-cdk/aws-ecs/lib/service.ts index a6672544b89c5..9aaffd7ff3a69 100644 --- a/packages/@aws-cdk/aws-ecs/lib/service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/service.ts @@ -36,7 +36,9 @@ export interface ServiceProps { launchType?: string; // maybe unnecessary if we have different ECS vs FG service /** - * The maximum number of tasks, specified as a percentage of the Amazon ECS service's DesiredCount value, that can run in a service during a deployment. + * The maximum number of tasks, specified as a percentage of the Amazon ECS + * service's DesiredCount value, that can run in a service during a + * deployment. * * @default 200 */ @@ -52,7 +54,9 @@ export interface ServiceProps { minimumHealthyPercent?: number; /** - * The name or ARN of an AWS Identity and Access Management (IAM) role that allows your Amazon ECS container agent to make calls to your load balancer. + * The name or ARN of an AWS Identity and Access Management (IAM) role that + * allows your Amazon ECS container agent to make calls to your load + * balancer. */ role?: string; diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index 1492e7629996f..4b2f02d235ebf 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -2,6 +2,9 @@ import cdk = require('@aws-cdk/cdk'); import { cloudformation } from './ecs.generated'; export interface TaskDefinitionProps { + + containerDefinitions: ContainerDefinition[]; + /** * The number of cpu units used by the task. If using the EC2 launch type, * this field is optional. Supported values are between 128 CPU units @@ -19,6 +22,22 @@ export interface TaskDefinitionProps { */ cpu?: string; + /** + * The Amazon Resource Name (ARN) of the task execution role that + * containers in this task can assume. All containers in this task are + * granted the permissions that are specified in this role. + * + * Needed in Fargate to communicate with Cloudwatch Logs and ECR. + */ + executionRoleArn?: string; + + /** + * Namespace for task definition versions + * + * @default CloudFormation-generated name + */ + family?: string; + /** * The amount (in MiB) of memory used by the task. If using the EC2 launch * type, this field is optional and any value can be used. If you are using @@ -41,22 +60,40 @@ export interface TaskDefinitionProps { memory?: string; /** - * Namespace for task definition versions + * The Docker networking mode to use for the containers in the task, such as none, bridge, or host. + * For Fargate or to use task networking, "awsvpc" mode is required. * - * @default CloudFormation-generated name + * @default bridge */ - family?: string; - - // taskRoleArn?: string; // goes with container defs? - networkMode?: string; - containerDefinitions: ContainerDefinition[]; - // - // executionRoleArn:? string; - // volumes?: VolumeDefinition[]; - // placementConstraints?: PlacementConstraint[]; + /** + * An array of placement constraint objects to use for the task. You can + * specify a maximum of 10 constraints per task (this limit includes + * constraints in the task definition and those specified at run time). + * + * Not supported in Fargate. + */ + placementConstraints?: PlacementConstraint[]; + + /** + * Valid values include EC2 and FARGATE. + * + * @default EC2 + */ // requiresCompatibilities?: string[]; // FARGATE or EC2 -- set on ECS TD vs FG TD + + /** + * The Amazon Resource Name (ARN) of an AWS Identity and Access Management + * (IAM) role that grants containers in the task permission to call AWS + * APIs on your behalf + */ + taskRoleArn?: string; + + /** + * See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide//task_definition_parameters.html#volumes + */ + volumes?: Volume[]; } export class TaskDefinitionArn extends cdk.Token { @@ -64,43 +101,109 @@ export class TaskDefinitionArn extends cdk.Token { export class TaskDefinition extends cdk.Construct { public readonly taskDefinitionArn: TaskDefinitionArn; - // public readonly containerDefinitions: ContainerDefinition[]; private readonly containerDefinitions: cloudformation.TaskDefinitionResource.ContainerDefinitionProperty[] = []; + private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; + private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; constructor(parent: cdk.Construct, name: string, props: TaskDefinitionProps) { super(parent, name); + props.containerDefinitions.forEach(cd => this.addContainer(cd)); + + if (props.placementConstraints) { + props.placementConstraints.forEach(pc => this.addPlacementConstraint(pc)); + } + + if (props.volumes) { + props.volumes.forEach(v => this.addVolume(v)); + } + const taskDef = new cloudformation.TaskDefinitionResource(this, "TaskDef", { - family: "ecs-demo", - memory: props.memory, + containerDefinitions: new cdk.Token(() => this.containerDefinitions), cpu: props.cpu, - containerDefinitions: new cdk.Token(() => this.containerDefinitions), // ???? + executionRoleArn: props.executionRoleArn, + family: props.family, + memory: props.memory, + networkMode: props.networkMode, + placementConstraints: new cdk.Token(() => this.placementConstraints), + taskRoleArn: props.taskRoleArn }); - props.containerDefinitions.forEach(c => this.addContainer(c)); - this.taskDefinitionArn = taskDef.ref; } - private addContainer(definition:ContainerDefinition) { + private addContainer(definition: ContainerDefinition) { const cd = this.renderContainerDefininition(definition); this.containerDefinitions.push(cd); } + private addPlacementConstraint(constraint: PlacementConstraint) { + const pc = this.renderPlacementConstraint(constraint); + this.placementConstraints.push(pc); + } + + private addVolume(volume: Volume) { + // const v = this.renderVolume(volume); + this.volumes.push(volume); + } + // Populates task definition with container definition private renderContainerDefininition(definition: ContainerDefinition): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { + // const logConfigs = this.renderLogConfiguration(definition.logConfiguration); // what to do if undefined? + return { name: definition.name, image: definition.image, + command: definition.command, cpu: definition.cpu, - memory: definition.memory, + disableNetworking: definition.disableNetworking, + dnsSearchDomains: definition.dnsSearchDomains, + dnsServers: definition.dnsServers, + // dockerLabels: definition.dockerLabels, + dockerSecurityOptions: definition.dockerSecurityOptions, + entryPoint: definition.entryPoint, essential: definition.essential, - command: definition.command + hostname: definition.hostname, + links: definition.links, + // logConfiguration: logConfigs, // only set if passed in? + memory: definition.memory, + memoryReservation: definition.memoryReservation, + privileged: definition.privileged, + readonlyRootFilesystem: definition.readonlyRootFilesystem, + user: definition.user, + workingDirectory: definition.workingDirectory }; } + + private renderPlacementConstraint(pc: PlacementConstraint): cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty { + return { + type: pc.type, + expression: pc.expression + }; + } + + // private renderLogConfiguration(lc: LogConfiguration[]): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty.LogConfiguration[] { + // return { + // logDriver: lc.logDriver, + // options: lc.options + // }; + // } + + // private renderVolume(volume: Volume): cloudformation.TaskDefinitionResource.VolumeProperty { + // return { + // host: this.renderHost(volume.host), + // name: volume.name + // }; + // } + + // private renderHost(host: Host): cloudformation.TaskDefinitionResource.VolumeProperty.Host { + // return { + // sourcePath: host.sourcePath + // } + // } } -export interface ContainerDefinition{ +export interface ContainerDefinition { name: string; image: string; @@ -115,6 +218,7 @@ export interface ContainerDefinition{ essential?: boolean; hostname?: string; links?: string[]; + logConfiguration?: LogConfiguration[]; memory?: number; memoryReservation?: number; privileged?: boolean; @@ -122,12 +226,35 @@ export interface ContainerDefinition{ user?: string; workingDirectory?: string } - // environment?: list of key-value; - // extraHosts?: hostEntry[]; - // healthCheck?: healthCheck; - // linuxParameters: linuxParam[]; - // logConfiguration: logConfig[]; - // mountPoints?: mountPoint[]; - // portMappings?: portMapping[]; - // ulimits?: ulimit[]; - // volumesFrom?: volumeFrom[]; +// environment?: list of key-value; +// extraHosts?: hostEntry[]; +// healthCheck?: healthCheck; +// linuxParameters: linuxParam[]; +// mountPoints?: mountPoint[]; +// portMappings?: portMapping[]; +// ulimits?: ulimit[]; +// volumesFrom?: volumeFrom[]; + +export interface PlacementConstraint { + expression?: string; + type: string; // PlacementConstraintType; +} + +// enum PlacementConstraintType{ +// DistinctInstance = "distinctInstance", +// MemberOf = "memberOf" +// } + +export interface Volume { + host?: Host; + name?: string; +} + +export interface Host { + sourcePath?: string; +} + +export interface LogConfiguration { + logDriver: string; + // options?: +} From 2d53d89f7480bfab41bc2295f4a871a244cac88f Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 28 Sep 2018 11:46:29 +0200 Subject: [PATCH 12/97] Indentation 4 -> 2 --- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 204 ++++----- packages/@aws-cdk/aws-ecs/lib/service.ts | 146 +++--- .../@aws-cdk/aws-ecs/lib/task-definition.ts | 430 +++++++++--------- 3 files changed, 390 insertions(+), 390 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index bae9f5823afcd..47424688286cf 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -4,45 +4,45 @@ import cdk = require('@aws-cdk/cdk'); import { cloudformation, ClusterArn} from './ecs.generated'; export interface ClusterProps { - /** - * A name for the cluster. - * - * @default CloudFormation-generated name - */ - clusterName?: string; - - /** - * The VPC where your ECS instances will be running - */ - vpc: ec2.VpcNetworkRef; - - /** - * Whether or not the containers can access the instance role - * - * @default false - */ - containersAccessInstanceRole?: boolean; + /** + * A name for the cluster. + * + * @default CloudFormation-generated name + */ + clusterName?: string; + + /** + * The VPC where your ECS instances will be running + */ + vpc: ec2.VpcNetworkRef; + + /** + * Whether or not the containers can access the instance role + * + * @default false + */ + containersAccessInstanceRole?: boolean; } // TODO: replace this with call to SSM, stored as "/aws/service/ecs/optimized-ami/amazon-linux/recommended" // https://raw.githubusercontent.com/awslabs/aws-cloudformation-templates/master/aws/services/ECS/EC2LaunchType/clusters/public-vpc.yml export const ECS_OPTIMIZED_AMI = new ec2.GenericLinuxImage({ - 'us-east-2': 'ami-028a9de0a7e353ed9', - 'us-east-1': 'ami-00129b193dc81bc31', - 'us-west-2': 'ami-00d4f478', - 'us-west-1': 'ami-0d438d09af26c9583', - 'eu-west-2': 'ami-a44db8c3', - 'eu-west-3': 'ami-07da674f0655ef4e1', - 'eu-west-1': 'ami-0af844a965e5738db', - 'eu-central-1': 'ami-0291ba887ba0d515f', - 'ap-northeast-2': 'ami-047d2a61f94f862dc', - 'ap-northeast-1': 'ami-0041c416aa23033a2', - 'ap-southeast-2': 'ami-0092e55c70015d8c3', - 'ap-southeast-1': 'ami-091bf462afdb02c60', - 'ca-central-1': 'ami-192fa27d', - 'ap-south-1': 'ami-0c179ca015d301829', - 'sa-east-1': 'ami-0018ff8ee48970ac3', - 'us-gov-west-1': 'ami-c6079ba7', + 'us-east-2': 'ami-028a9de0a7e353ed9', + 'us-east-1': 'ami-00129b193dc81bc31', + 'us-west-2': 'ami-00d4f478', + 'us-west-1': 'ami-0d438d09af26c9583', + 'eu-west-2': 'ami-a44db8c3', + 'eu-west-3': 'ami-07da674f0655ef4e1', + 'eu-west-1': 'ami-0af844a965e5738db', + 'eu-central-1': 'ami-0291ba887ba0d515f', + 'ap-northeast-2': 'ami-047d2a61f94f862dc', + 'ap-northeast-1': 'ami-0041c416aa23033a2', + 'ap-southeast-2': 'ami-0092e55c70015d8c3', + 'ap-southeast-1': 'ami-091bf462afdb02c60', + 'ca-central-1': 'ami-192fa27d', + 'ap-south-1': 'ami-0c179ca015d301829', + 'sa-east-1': 'ami-0018ff8ee48970ac3', + 'us-gov-west-1': 'ami-c6079ba7', }); // Needs to inherit from CloudFormationToken to make the string substitution @@ -52,74 +52,74 @@ export class ClusterName extends cdk.CloudFormationToken { export class Cluster extends cdk.Construct { - public readonly clusterArn: ClusterArn; - - public readonly clusterName: ClusterName; - - public readonly fleet: autoscaling.AutoScalingGroup; - - constructor(parent: cdk.Construct, name: string, props: ClusterProps) { - super(parent, name); - - const cluster = new cloudformation.ClusterResource(this, "Resource", {clusterName: props.clusterName}); - - this.clusterArn = cluster.clusterArn; - - this.clusterName = new ClusterName(cluster.ref); - - const fleet = new autoscaling.AutoScalingGroup(this, 'MyASG', { - vpc: props.vpc, - instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), - machineImage: ECS_OPTIMIZED_AMI, - updateType: autoscaling.UpdateType.ReplacingUpdate - }); - - // Tie instances to cluster - fleet.addUserData(`echo ECS_CLUSTER=${this.clusterName} >> /etc/ecs/ecs.config`); - - if (!props.containersAccessInstanceRole) { - // Deny containers access to instance metadata service - // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html - fleet.addUserData('sudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP'); - fleet.addUserData('sudo service iptables save'); - } - - // Note: if the fleet doesn't launch or doesn't register itself with - // ECS, *Cluster* stabilization will fail after timing our for an hour - // or so, because the *Service* doesn't have any running instances. - // During this time, you CANNOT DO ANYTHING ELSE WITH YOUR STACK. - // - // Apart from the weird relationship here between Cluster and Service - // (why is Cluster failing and not Service?), the experience is... - // - // NOT GREAT. - // - // Also, there's sort of a bidirectional dependency between Cluster and ASG: - // - // - ASG depends on Cluster to get the ClusterName (which needs to go into - // UserData). - // - Cluster depends on ASG to boot up, so the service is launched, so the - // Cluster can stabilize. - - // ECS instances must be able to do these things - // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html - fleet.addToRolePolicy(new cdk.PolicyStatement().addActions( - "ecs:CreateCluster", - "ecs:DeregisterContainerInstance", - "ecs:DiscoverPollEndpoint", - "ecs:Poll", - "ecs:RegisterContainerInstance", - "ecs:StartTelemetrySession", - "ecs:Submit*", - "ecr:GetAuthorizationToken", - "ecr:BatchCheckLayerAvailability", - "ecr:GetDownloadUrlForLayer", - "ecr:BatchGetImage", - "logs:CreateLogStream", - "logs:PutLogEvents" - ).addAllResources()); // Conceivably we might do better than all resources and add targeted ARNs - - this.fleet = fleet; + public readonly clusterArn: ClusterArn; + public readonly clusterName: ClusterName; + + public readonly fleet: autoscaling.AutoScalingGroup; + + constructor(parent: cdk.Construct, name: string, props: ClusterProps) { + super(parent, name); + + const cluster = new cloudformation.ClusterResource(this, "Resource", {clusterName: props.clusterName}); + + this.clusterArn = cluster.clusterArn; + + this.clusterName = new ClusterName(cluster.ref); + + const fleet = new autoscaling.AutoScalingGroup(this, 'MyASG', { + vpc: props.vpc, + instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), + machineImage: ECS_OPTIMIZED_AMI, + updateType: autoscaling.UpdateType.ReplacingUpdate + }); + + // Tie instances to cluster + fleet.addUserData(`echo ECS_CLUSTER=${this.clusterName} >> /etc/ecs/ecs.config`); + + if (!props.containersAccessInstanceRole) { + // Deny containers access to instance metadata service + // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html + fleet.addUserData('sudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP'); + fleet.addUserData('sudo service iptables save'); } + + // Note: if the fleet doesn't launch or doesn't register itself with + // ECS, *Cluster* stabilization will fail after timing our for an hour + // or so, because the *Service* doesn't have any running instances. + // During this time, you CANNOT DO ANYTHING ELSE WITH YOUR STACK. + // + // Apart from the weird relationship here between Cluster and Service + // (why is Cluster failing and not Service?), the experience is... + // + // NOT GREAT. + // + // Also, there's sort of a bidirectional dependency between Cluster and ASG: + // + // - ASG depends on Cluster to get the ClusterName (which needs to go into + // UserData). + // - Cluster depends on ASG to boot up, so the service is launched, so the + // Cluster can stabilize. + + // ECS instances must be able to do these things + // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html + fleet.addToRolePolicy(new cdk.PolicyStatement().addActions( + "ecs:CreateCluster", + "ecs:DeregisterContainerInstance", + "ecs:DiscoverPollEndpoint", + "ecs:Poll", + "ecs:RegisterContainerInstance", + "ecs:StartTelemetrySession", + "ecs:Submit*", + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "logs:CreateLogStream", + "logs:PutLogEvents" + ).addAllResources()); // Conceivably we might do better than all resources and add targeted ARNs + + this.fleet = fleet; + + } } diff --git a/packages/@aws-cdk/aws-ecs/lib/service.ts b/packages/@aws-cdk/aws-ecs/lib/service.ts index 9aaffd7ff3a69..f8b038745c209 100644 --- a/packages/@aws-cdk/aws-ecs/lib/service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/service.ts @@ -4,89 +4,89 @@ import { cloudformation } from './ecs.generated'; import { TaskDefinitionArn } from './task-definition'; export interface ServiceProps { - /** - * Cluster where service will be deployed - */ - cluster: ClusterName; // should be required? do we assume 'default' exists? + /** + * Cluster where service will be deployed + */ + cluster: ClusterName; // should be required? do we assume 'default' exists? - /** - * Task Definition used for running tasks in the service - */ - taskDefinition: TaskDefinitionArn; + /** + * Task Definition used for running tasks in the service + */ + taskDefinition: TaskDefinitionArn; - /** - * Number of desired copies of running tasks - * - * @default 1 - */ - desiredCount?: number; + /** + * Number of desired copies of running tasks + * + * @default 1 + */ + desiredCount?: number; - /** - * A name for the service. - * - * @default CloudFormation-generated name - */ - serviceName?: string; + /** + * A name for the service. + * + * @default CloudFormation-generated name + */ + serviceName?: string; - /** - * Whether the service is hosted in EC2 or Fargate - * - * @default EC2 - */ - launchType?: string; // maybe unnecessary if we have different ECS vs FG service + /** + * Whether the service is hosted in EC2 or Fargate + * + * @default EC2 + */ + launchType?: string; // maybe unnecessary if we have different ECS vs FG service - /** - * The maximum number of tasks, specified as a percentage of the Amazon ECS - * service's DesiredCount value, that can run in a service during a - * deployment. - * - * @default 200 - */ - maximumPercent?: number; + /** + * The maximum number of tasks, specified as a percentage of the Amazon ECS + * service's DesiredCount value, that can run in a service during a + * deployment. + * + * @default 200 + */ + maximumPercent?: number; - /** - * The minimum number of tasks, specified as a percentage of - * the Amazon ECS service's DesiredCount value, that must - * continue to run and remain healthy during a deployment. - * - * @default 50 - */ - minimumHealthyPercent?: number; + /** + * The minimum number of tasks, specified as a percentage of + * the Amazon ECS service's DesiredCount value, that must + * continue to run and remain healthy during a deployment. + * + * @default 50 + */ + minimumHealthyPercent?: number; - /** - * The name or ARN of an AWS Identity and Access Management (IAM) role that - * allows your Amazon ECS container agent to make calls to your load - * balancer. - */ - role?: string; + /** + * The name or ARN of an AWS Identity and Access Management (IAM) role that + * allows your Amazon ECS container agent to make calls to your load + * balancer. + */ + role?: string; - ///////// TBD /////////////////////////////// - // healthCheckGracePeriodSeconds?: number; // only needed with load balancers - // loadBalancers?: LoadBalancer[]; - // placementConstraints?: PlacementConstraint[]; - // placementStrategies?: PlacementStrategy[]; - // networkConfiguration?: NetworkConfiguration; - // serviceRegistries?: ServiceRegistry[]; - // - // platformVersion?: string; // FARGATE ONLY. default is LATEST. Other options: 1.2.0, 1.1.0, 1.0.0 - //////////////////////////////////////////// + ///////// TBD /////////////////////////////// + // healthCheckGracePeriodSeconds?: number; // only needed with load balancers + // loadBalancers?: LoadBalancer[]; + // placementConstraints?: PlacementConstraint[]; + // placementStrategies?: PlacementStrategy[]; + // networkConfiguration?: NetworkConfiguration; + // serviceRegistries?: ServiceRegistry[]; + // + // platformVersion?: string; // FARGATE ONLY. default is LATEST. Other options: 1.2.0, 1.1.0, 1.0.0 + //////////////////////////////////////////// } export class Service extends cdk.Construct { - constructor(parent: cdk.Construct, name: string, props: ServiceProps) { - super(parent, name); + constructor(parent: cdk.Construct, name: string, props: ServiceProps) { + super(parent, name); - new cloudformation.ServiceResource(this, "Service", { - cluster: props.cluster, - taskDefinition: props.taskDefinition, - desiredCount: props.desiredCount, - serviceName: props.serviceName, - launchType: props.launchType, - deploymentConfiguration: { - maximumPercent: props.maximumPercent, - minimumHealthyPercent: props.minimumHealthyPercent - }, - role: props.role, - }); - } + new cloudformation.ServiceResource(this, "Service", { + cluster: props.cluster, + taskDefinition: props.taskDefinition, + desiredCount: props.desiredCount, + serviceName: props.serviceName, + launchType: props.launchType, + deploymentConfiguration: { + maximumPercent: props.maximumPercent, + minimumHealthyPercent: props.minimumHealthyPercent + }, + role: props.role, + }); + } } diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index 4b2f02d235ebf..06293831512c3 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -3,228 +3,228 @@ import { cloudformation } from './ecs.generated'; export interface TaskDefinitionProps { - containerDefinitions: ContainerDefinition[]; - - /** - * The number of cpu units used by the task. If using the EC2 launch type, - * this field is optional. Supported values are between 128 CPU units - * (0.125 vCPUs) and 10240 CPU units (10 vCPUs). If you are using the - * Fargate launch type, this field is required and you must use one of the - * following 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 - * - * @default 256 - */ - cpu?: string; - - /** - * The Amazon Resource Name (ARN) of the task execution role that - * containers in this task can assume. All containers in this task are - * granted the permissions that are specified in this role. - * - * Needed in Fargate to communicate with Cloudwatch Logs and ECR. - */ - executionRoleArn?: string; - - /** - * Namespace for task definition versions - * - * @default CloudFormation-generated name - */ - family?: string; - - /** - * The amount (in MiB) of memory used by the task. If using the EC2 launch - * type, this field is optional and any value can be used. If you are using - * the Fargate launch type, 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) - * - * @default 512 - */ - memory?: string; - - /** - * The Docker networking mode to use for the containers in the task, such as none, bridge, or host. - * For Fargate or to use task networking, "awsvpc" mode is required. - * - * @default bridge - */ - networkMode?: string; - - /** - * An array of placement constraint objects to use for the task. You can - * specify a maximum of 10 constraints per task (this limit includes - * constraints in the task definition and those specified at run time). - * - * Not supported in Fargate. - */ - placementConstraints?: PlacementConstraint[]; - - /** - * Valid values include EC2 and FARGATE. - * - * @default EC2 - */ - // requiresCompatibilities?: string[]; // FARGATE or EC2 -- set on ECS TD vs FG TD - - /** - * The Amazon Resource Name (ARN) of an AWS Identity and Access Management - * (IAM) role that grants containers in the task permission to call AWS - * APIs on your behalf - */ - taskRoleArn?: string; - - /** - * See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide//task_definition_parameters.html#volumes - */ - volumes?: Volume[]; + containerDefinitions: ContainerDefinition[]; + + /** + * The number of cpu units used by the task. If using the EC2 launch type, + * this field is optional. Supported values are between 128 CPU units + * (0.125 vCPUs) and 10240 CPU units (10 vCPUs). If you are using the + * Fargate launch type, this field is required and you must use one of the + * following 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 + * + * @default 256 + */ + cpu?: string; + + /** + * The Amazon Resource Name (ARN) of the task execution role that + * containers in this task can assume. All containers in this task are + * granted the permissions that are specified in this role. + * + * Needed in Fargate to communicate with Cloudwatch Logs and ECR. + */ + executionRoleArn?: string; + + /** + * Namespace for task definition versions + * + * @default CloudFormation-generated name + */ + family?: string; + + /** + * The amount (in MiB) of memory used by the task. If using the EC2 launch + * type, this field is optional and any value can be used. If you are using + * the Fargate launch type, 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) + * + * @default 512 + */ + memory?: string; + + /** + * The Docker networking mode to use for the containers in the task, such as none, bridge, or host. + * For Fargate or to use task networking, "awsvpc" mode is required. + * + * @default bridge + */ + networkMode?: string; + + /** + * An array of placement constraint objects to use for the task. You can + * specify a maximum of 10 constraints per task (this limit includes + * constraints in the task definition and those specified at run time). + * + * Not supported in Fargate. + */ + placementConstraints?: PlacementConstraint[]; + + /** + * Valid values include EC2 and FARGATE. + * + * @default EC2 + */ + // requiresCompatibilities?: string[]; // FARGATE or EC2 -- set on ECS TD vs FG TD + + /** + * The Amazon Resource Name (ARN) of an AWS Identity and Access Management + * (IAM) role that grants containers in the task permission to call AWS + * APIs on your behalf + */ + taskRoleArn?: string; + + /** + * See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide//task_definition_parameters.html#volumes + */ + volumes?: Volume[]; } export class TaskDefinitionArn extends cdk.Token { } export class TaskDefinition extends cdk.Construct { - public readonly taskDefinitionArn: TaskDefinitionArn; - private readonly containerDefinitions: cloudformation.TaskDefinitionResource.ContainerDefinitionProperty[] = []; - private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; - private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; - - constructor(parent: cdk.Construct, name: string, props: TaskDefinitionProps) { - super(parent, name); - - props.containerDefinitions.forEach(cd => this.addContainer(cd)); - - if (props.placementConstraints) { - props.placementConstraints.forEach(pc => this.addPlacementConstraint(pc)); - } - - if (props.volumes) { - props.volumes.forEach(v => this.addVolume(v)); - } - - const taskDef = new cloudformation.TaskDefinitionResource(this, "TaskDef", { - containerDefinitions: new cdk.Token(() => this.containerDefinitions), - cpu: props.cpu, - executionRoleArn: props.executionRoleArn, - family: props.family, - memory: props.memory, - networkMode: props.networkMode, - placementConstraints: new cdk.Token(() => this.placementConstraints), - taskRoleArn: props.taskRoleArn - }); - - this.taskDefinitionArn = taskDef.ref; - } + public readonly taskDefinitionArn: TaskDefinitionArn; + private readonly containerDefinitions: cloudformation.TaskDefinitionResource.ContainerDefinitionProperty[] = []; + private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; + private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; - private addContainer(definition: ContainerDefinition) { - const cd = this.renderContainerDefininition(definition); - this.containerDefinitions.push(cd); - } + constructor(parent: cdk.Construct, name: string, props: TaskDefinitionProps) { + super(parent, name); - private addPlacementConstraint(constraint: PlacementConstraint) { - const pc = this.renderPlacementConstraint(constraint); - this.placementConstraints.push(pc); - } - - private addVolume(volume: Volume) { - // const v = this.renderVolume(volume); - this.volumes.push(volume); - } + props.containerDefinitions.forEach(cd => this.addContainer(cd)); - // Populates task definition with container definition - private renderContainerDefininition(definition: ContainerDefinition): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { - // const logConfigs = this.renderLogConfiguration(definition.logConfiguration); // what to do if undefined? - - return { - name: definition.name, - image: definition.image, - command: definition.command, - cpu: definition.cpu, - disableNetworking: definition.disableNetworking, - dnsSearchDomains: definition.dnsSearchDomains, - dnsServers: definition.dnsServers, - // dockerLabels: definition.dockerLabels, - dockerSecurityOptions: definition.dockerSecurityOptions, - entryPoint: definition.entryPoint, - essential: definition.essential, - hostname: definition.hostname, - links: definition.links, - // logConfiguration: logConfigs, // only set if passed in? - memory: definition.memory, - memoryReservation: definition.memoryReservation, - privileged: definition.privileged, - readonlyRootFilesystem: definition.readonlyRootFilesystem, - user: definition.user, - workingDirectory: definition.workingDirectory - }; + if (props.placementConstraints) { + props.placementConstraints.forEach(pc => this.addPlacementConstraint(pc)); } - private renderPlacementConstraint(pc: PlacementConstraint): cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty { - return { - type: pc.type, - expression: pc.expression - }; + if (props.volumes) { + props.volumes.forEach(v => this.addVolume(v)); } - // private renderLogConfiguration(lc: LogConfiguration[]): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty.LogConfiguration[] { - // return { - // logDriver: lc.logDriver, - // options: lc.options - // }; - // } - - // private renderVolume(volume: Volume): cloudformation.TaskDefinitionResource.VolumeProperty { - // return { - // host: this.renderHost(volume.host), - // name: volume.name - // }; - // } - - // private renderHost(host: Host): cloudformation.TaskDefinitionResource.VolumeProperty.Host { - // return { - // sourcePath: host.sourcePath - // } - // } + const taskDef = new cloudformation.TaskDefinitionResource(this, "TaskDef", { + containerDefinitions: new cdk.Token(() => this.containerDefinitions), + cpu: props.cpu, + executionRoleArn: props.executionRoleArn, + family: props.family, + memory: props.memory, + networkMode: props.networkMode, + placementConstraints: new cdk.Token(() => this.placementConstraints), + taskRoleArn: props.taskRoleArn + }); + + this.taskDefinitionArn = taskDef.ref; + } + + private addContainer(definition: ContainerDefinition) { + const cd = this.renderContainerDefininition(definition); + this.containerDefinitions.push(cd); + } + + private addPlacementConstraint(constraint: PlacementConstraint) { + const pc = this.renderPlacementConstraint(constraint); + this.placementConstraints.push(pc); + } + + private addVolume(volume: Volume) { + // const v = this.renderVolume(volume); + this.volumes.push(volume); + } + + // Populates task definition with container definition + private renderContainerDefininition(definition: ContainerDefinition): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { + // const logConfigs = this.renderLogConfiguration(definition.logConfiguration); // what to do if undefined? + + return { + name: definition.name, + image: definition.image, + command: definition.command, + cpu: definition.cpu, + disableNetworking: definition.disableNetworking, + dnsSearchDomains: definition.dnsSearchDomains, + dnsServers: definition.dnsServers, + // dockerLabels: definition.dockerLabels, + dockerSecurityOptions: definition.dockerSecurityOptions, + entryPoint: definition.entryPoint, + essential: definition.essential, + hostname: definition.hostname, + links: definition.links, + // logConfiguration: logConfigs, // only set if passed in? + memory: definition.memory, + memoryReservation: definition.memoryReservation, + privileged: definition.privileged, + readonlyRootFilesystem: definition.readonlyRootFilesystem, + user: definition.user, + workingDirectory: definition.workingDirectory + }; + } + + private renderPlacementConstraint(pc: PlacementConstraint): cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty { + return { + type: pc.type, + expression: pc.expression + }; + } + + // private renderLogConfiguration(lc: LogConfiguration[]): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty.LogConfiguration[] { + // return { + // logDriver: lc.logDriver, + // options: lc.options + // }; + // } + + // private renderVolume(volume: Volume): cloudformation.TaskDefinitionResource.VolumeProperty { + // return { + // host: this.renderHost(volume.host), + // name: volume.name + // }; + // } + + // private renderHost(host: Host): cloudformation.TaskDefinitionResource.VolumeProperty.Host { + // return { + // sourcePath: host.sourcePath + // } + // } } export interface ContainerDefinition { - name: string; - image: string; - - command?: string[]; - cpu?: number; - disableNetworking?: boolean; - dnsSearchDomains?: string[]; - dnsServers?: string[]; - dockerLabels?: string[]; - dockerSecurityOptions?: string[]; - entryPoint?: string[]; - essential?: boolean; - hostname?: string; - links?: string[]; - logConfiguration?: LogConfiguration[]; - memory?: number; - memoryReservation?: number; - privileged?: boolean; - readonlyRootFilesystem?: boolean; - user?: string; - workingDirectory?: string + name: string; + image: string; + + command?: string[]; + cpu?: number; + disableNetworking?: boolean; + dnsSearchDomains?: string[]; + dnsServers?: string[]; + dockerLabels?: string[]; + dockerSecurityOptions?: string[]; + entryPoint?: string[]; + essential?: boolean; + hostname?: string; + links?: string[]; + logConfiguration?: LogConfiguration[]; + memory?: number; + memoryReservation?: number; + privileged?: boolean; + readonlyRootFilesystem?: boolean; + user?: string; + workingDirectory?: string } // environment?: list of key-value; // extraHosts?: hostEntry[]; @@ -236,25 +236,25 @@ export interface ContainerDefinition { // volumesFrom?: volumeFrom[]; export interface PlacementConstraint { - expression?: string; - type: string; // PlacementConstraintType; + expression?: string; + type: string; // PlacementConstraintType; } // enum PlacementConstraintType{ -// DistinctInstance = "distinctInstance", -// MemberOf = "memberOf" +// DistinctInstance = "distinctInstance", +// MemberOf = "memberOf" // } export interface Volume { - host?: Host; - name?: string; + host?: Host; + name?: string; } export interface Host { - sourcePath?: string; + sourcePath?: string; } export interface LogConfiguration { - logDriver: string; - // options?: + logDriver: string; + // options?: } From dbdab8606b758a7fb3d668dfb22b53b307eeebd8 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 28 Sep 2018 12:13:37 +0200 Subject: [PATCH 13/97] Fix type errors --- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 9 ++++----- packages/@aws-cdk/aws-ecs/lib/index.ts | 2 +- packages/@aws-cdk/aws-ecs/lib/service.ts | 12 ++++++------ packages/@aws-cdk/aws-ecs/lib/task-definition.ts | 5 +---- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 47424688286cf..8e7e50f3681b8 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -1,7 +1,7 @@ import autoscaling = require('@aws-cdk/aws-autoscaling'); import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); -import { cloudformation, ClusterArn} from './ecs.generated'; +import { cloudformation } from './ecs.generated'; export interface ClusterProps { /** @@ -52,9 +52,9 @@ export class ClusterName extends cdk.CloudFormationToken { export class Cluster extends cdk.Construct { - public readonly clusterArn: ClusterArn; + public readonly clusterArn: string; - public readonly clusterName: ClusterName; + public readonly clusterName: string; public readonly fleet: autoscaling.AutoScalingGroup; @@ -64,8 +64,7 @@ export class Cluster extends cdk.Construct { const cluster = new cloudformation.ClusterResource(this, "Resource", {clusterName: props.clusterName}); this.clusterArn = cluster.clusterArn; - - this.clusterName = new ClusterName(cluster.ref); + this.clusterName = cluster.ref; const fleet = new autoscaling.AutoScalingGroup(this, 'MyASG', { vpc: props.vpc, diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 4a2b652775532..53babeb50b7a4 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -3,4 +3,4 @@ export * from './service'; export * from './task-definition'; // AWS::ECS CloudFormation Resources: -export {ClusterName, TaskDefinitionArn} from './ecs.generated'; +export * from './ecs.generated'; diff --git a/packages/@aws-cdk/aws-ecs/lib/service.ts b/packages/@aws-cdk/aws-ecs/lib/service.ts index f8b038745c209..843532dc07c8d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/service.ts @@ -1,18 +1,18 @@ import cdk = require('@aws-cdk/cdk'); -import { ClusterName } from './cluster'; +import { Cluster } from './cluster'; import { cloudformation } from './ecs.generated'; -import { TaskDefinitionArn } from './task-definition'; +import { TaskDefinition } from './task-definition'; export interface ServiceProps { /** * Cluster where service will be deployed */ - cluster: ClusterName; // should be required? do we assume 'default' exists? + cluster: Cluster; // should be required? do we assume 'default' exists? /** * Task Definition used for running tasks in the service */ - taskDefinition: TaskDefinitionArn; + taskDefinition: TaskDefinition; /** * Number of desired copies of running tasks @@ -77,8 +77,8 @@ export class Service extends cdk.Construct { super(parent, name); new cloudformation.ServiceResource(this, "Service", { - cluster: props.cluster, - taskDefinition: props.taskDefinition, + cluster: props.cluster.clusterName, + taskDefinition: props.taskDefinition.taskDefinitionArn, desiredCount: props.desiredCount, serviceName: props.serviceName, launchType: props.launchType, diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index 06293831512c3..55856da1372cf 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -96,11 +96,8 @@ export interface TaskDefinitionProps { volumes?: Volume[]; } -export class TaskDefinitionArn extends cdk.Token { -} - export class TaskDefinition extends cdk.Construct { - public readonly taskDefinitionArn: TaskDefinitionArn; + public readonly taskDefinitionArn: string; private readonly containerDefinitions: cloudformation.TaskDefinitionResource.ContainerDefinitionProperty[] = []; private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; From ee6b427eb82a16c2a3d14ee1af9fe5444e64b488 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 28 Sep 2018 13:32:28 +0200 Subject: [PATCH 14/97] Use SSM parameter for ECS Optimized AMI --- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 38 ++++++++---------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 8e7e50f3681b8..efea48df8b428 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -24,30 +24,18 @@ export interface ClusterProps { containersAccessInstanceRole?: boolean; } -// TODO: replace this with call to SSM, stored as "/aws/service/ecs/optimized-ami/amazon-linux/recommended" -// https://raw.githubusercontent.com/awslabs/aws-cloudformation-templates/master/aws/services/ECS/EC2LaunchType/clusters/public-vpc.yml -export const ECS_OPTIMIZED_AMI = new ec2.GenericLinuxImage({ - 'us-east-2': 'ami-028a9de0a7e353ed9', - 'us-east-1': 'ami-00129b193dc81bc31', - 'us-west-2': 'ami-00d4f478', - 'us-west-1': 'ami-0d438d09af26c9583', - 'eu-west-2': 'ami-a44db8c3', - 'eu-west-3': 'ami-07da674f0655ef4e1', - 'eu-west-1': 'ami-0af844a965e5738db', - 'eu-central-1': 'ami-0291ba887ba0d515f', - 'ap-northeast-2': 'ami-047d2a61f94f862dc', - 'ap-northeast-1': 'ami-0041c416aa23033a2', - 'ap-southeast-2': 'ami-0092e55c70015d8c3', - 'ap-southeast-1': 'ami-091bf462afdb02c60', - 'ca-central-1': 'ami-192fa27d', - 'ap-south-1': 'ami-0c179ca015d301829', - 'sa-east-1': 'ami-0018ff8ee48970ac3', - 'us-gov-west-1': 'ami-c6079ba7', -}); - -// Needs to inherit from CloudFormationToken to make the string substitution -// downstairs work. This is temporary, will go away in the near future. -export class ClusterName extends cdk.CloudFormationToken { +/** + * Construct a Linux machine image from the latest ECS Optimized AMI published in SSM + */ +export class EcsOptimizedAmi implements ec2.IMachineImageSource { + private static AmiParamterName = "/aws/service/ecs/optimized-ami/amazon-linux/recommended"; + + public getImage(parent: cdk.Construct): ec2.MachineImage { + const ssmProvider = new cdk.SSMParameterProvider(parent); + + const ami = ssmProvider.getString(EcsOptimizedAmi.AmiParamterName); + return new ec2.MachineImage(ami, new ec2.LinuxOS()); + } } export class Cluster extends cdk.Construct { @@ -69,7 +57,7 @@ export class Cluster extends cdk.Construct { const fleet = new autoscaling.AutoScalingGroup(this, 'MyASG', { vpc: props.vpc, instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), - machineImage: ECS_OPTIMIZED_AMI, + machineImage: new EcsOptimizedAmi(), updateType: autoscaling.UpdateType.ReplacingUpdate }); From 2b16dad7634988e770631cf4bc09588efa5da03b Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 28 Sep 2018 13:40:07 +0200 Subject: [PATCH 15/97] Move containerdefinition to its own file --- .../aws-ecs/lib/container-definition.ts | 37 ++++++++++++++++++ packages/@aws-cdk/aws-ecs/lib/index.ts | 2 + .../aws-ecs/lib/log-drivers/aws-log-driver.ts | 4 ++ .../@aws-cdk/aws-ecs/lib/task-definition.ts | 38 +------------------ 4 files changed, 44 insertions(+), 37 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/container-definition.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts new file mode 100644 index 0000000000000..6e571fe6a72c0 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -0,0 +1,37 @@ +export interface ContainerDefinition { + name: string; + image: string; + + command?: string[]; + cpu?: number; + disableNetworking?: boolean; + dnsSearchDomains?: string[]; + dnsServers?: string[]; + dockerLabels?: string[]; + dockerSecurityOptions?: string[]; + entryPoint?: string[]; + essential?: boolean; + hostname?: string; + links?: string[]; + logConfiguration?: LogConfiguration[]; + memory?: number; + memoryReservation?: number; + privileged?: boolean; + readonlyRootFilesystem?: boolean; + user?: string; + workingDirectory?: string +} + +export interface LogConfiguration { + logDriver: string; + // options?: +} + +// environment?: list of key-value; +// extraHosts?: hostEntry[]; +// healthCheck?: healthCheck; +// linuxParameters: linuxParam[]; +// mountPoints?: mountPoint[]; +// portMappings?: portMapping[]; +// ulimits?: ulimit[]; +// volumesFrom?: volumeFrom[]; diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 53babeb50b7a4..9658da2d11cde 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -1,6 +1,8 @@ export * from './cluster'; export * from './service'; export * from './task-definition'; +export * from './container-definition'; +export * from './log-drivers/aws-log-driver'; // AWS::ECS CloudFormation Resources: export * from './ecs.generated'; diff --git a/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts b/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts new file mode 100644 index 0000000000000..ed581cac5580a --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts @@ -0,0 +1,4 @@ + +export class AwsLogDriver { + +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index 55856da1372cf..97113604a63cd 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -1,4 +1,5 @@ import cdk = require('@aws-cdk/cdk'); +import { ContainerDefinition } from './container-definition'; import { cloudformation } from './ecs.generated'; export interface TaskDefinitionProps { @@ -200,38 +201,6 @@ export class TaskDefinition extends cdk.Construct { // } } -export interface ContainerDefinition { - name: string; - image: string; - - command?: string[]; - cpu?: number; - disableNetworking?: boolean; - dnsSearchDomains?: string[]; - dnsServers?: string[]; - dockerLabels?: string[]; - dockerSecurityOptions?: string[]; - entryPoint?: string[]; - essential?: boolean; - hostname?: string; - links?: string[]; - logConfiguration?: LogConfiguration[]; - memory?: number; - memoryReservation?: number; - privileged?: boolean; - readonlyRootFilesystem?: boolean; - user?: string; - workingDirectory?: string -} -// environment?: list of key-value; -// extraHosts?: hostEntry[]; -// healthCheck?: healthCheck; -// linuxParameters: linuxParam[]; -// mountPoints?: mountPoint[]; -// portMappings?: portMapping[]; -// ulimits?: ulimit[]; -// volumesFrom?: volumeFrom[]; - export interface PlacementConstraint { expression?: string; type: string; // PlacementConstraintType; @@ -250,8 +219,3 @@ export interface Volume { export interface Host { sourcePath?: string; } - -export interface LogConfiguration { - logDriver: string; - // options?: -} From ae881c2a9a9f4980fd924f80bca353c52d22b35f Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 28 Sep 2018 15:31:35 +0200 Subject: [PATCH 16/97] Turning ContainerDefinition into a construct --- .../hello-cdk-ecs/index.ts | 25 +- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 23 +- .../aws-ecs/lib/container-definition.ts | 300 +++++++++++++++++- .../@aws-cdk/aws-ecs/lib/container-image.ts | 21 ++ packages/@aws-cdk/aws-ecs/lib/index.ts | 4 + .../@aws-cdk/aws-ecs/lib/linux-parameters.ts | 16 + .../aws-ecs/lib/log-drivers/aws-log-driver.ts | 89 +++++- .../aws-ecs/lib/log-drivers/log-driver.ts | 12 + .../@aws-cdk/aws-ecs/lib/task-definition.ts | 71 +---- packages/@aws-cdk/aws-ecs/package.json | 3 +- 10 files changed, 464 insertions(+), 100 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/container-image.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/log-drivers/log-driver.ts diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 08f3ba4220012..a51b9688b233e 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -31,27 +31,22 @@ class BonjourECS extends cdk.Stack { // with the application container itself // - autoscaling - application autoscaling (Fargate focused?) - const taskDef = new ecs.TaskDefinition(this, "MyTD", { + const taskDefinition = new ecs.TaskDefinition(this, "MyTD", { family: "ecs-task-definition", placementConstraints: [{ type: "distinctInstance" }], - containerDefinitions: [ - { - name: "web", - image: "amazon/amazon-ecs-sample", - cpu: 1024, - memory: 512, - essential: true - } - ] }); - new ecs.Service(this, "Service", { - cluster: cluster.clusterName, - taskDefinition: taskDef.taskDefinitionArn, - desiredCount: 1, - }); + taskDefinition.addContainer(new ecs.ContainerDefinition(this, 'Def', { + name: "web", + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + cpu: 1024, + memoryMiB: 512, + essential: true + })); + + cluster.runService(taskDefinition); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index efea48df8b428..1ad78b021a536 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -2,6 +2,8 @@ import autoscaling = require('@aws-cdk/aws-autoscaling'); import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { cloudformation } from './ecs.generated'; +import { Service } from './service'; +import { TaskDefinition } from './task-definition'; export interface ClusterProps { /** @@ -49,12 +51,12 @@ export class Cluster extends cdk.Construct { constructor(parent: cdk.Construct, name: string, props: ClusterProps) { super(parent, name); - const cluster = new cloudformation.ClusterResource(this, "Resource", {clusterName: props.clusterName}); + const cluster = new cloudformation.ClusterResource(this, 'Resource', {clusterName: props.clusterName}); this.clusterArn = cluster.clusterArn; this.clusterName = cluster.ref; - const fleet = new autoscaling.AutoScalingGroup(this, 'MyASG', { + const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'AutoScalingGroup', { vpc: props.vpc, instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), machineImage: new EcsOptimizedAmi(), @@ -62,13 +64,13 @@ export class Cluster extends cdk.Construct { }); // Tie instances to cluster - fleet.addUserData(`echo ECS_CLUSTER=${this.clusterName} >> /etc/ecs/ecs.config`); + autoScalingGroup.addUserData(`echo ECS_CLUSTER=${this.clusterName} >> /etc/ecs/ecs.config`); if (!props.containersAccessInstanceRole) { // Deny containers access to instance metadata service // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html - fleet.addUserData('sudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP'); - fleet.addUserData('sudo service iptables save'); + autoScalingGroup.addUserData('sudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP'); + autoScalingGroup.addUserData('sudo service iptables save'); } // Note: if the fleet doesn't launch or doesn't register itself with @@ -90,7 +92,7 @@ export class Cluster extends cdk.Construct { // ECS instances must be able to do these things // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html - fleet.addToRolePolicy(new cdk.PolicyStatement().addActions( + autoScalingGroup.addToRolePolicy(new cdk.PolicyStatement().addActions( "ecs:CreateCluster", "ecs:DeregisterContainerInstance", "ecs:DiscoverPollEndpoint", @@ -106,7 +108,14 @@ export class Cluster extends cdk.Construct { "logs:PutLogEvents" ).addAllResources()); // Conceivably we might do better than all resources and add targeted ARNs - this.fleet = fleet; + this.fleet = autoScalingGroup; + } + public runService(taskDefinition: TaskDefinition): Service { + return new Service(this, `${taskDefinition.family}Service`, { + cluster: this, + taskDefinition, + // FIXME: additional props? Or set on Service object? + }); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 6e571fe6a72c0..de97fe38048e6 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -1,37 +1,307 @@ -export interface ContainerDefinition { +import cdk = require('@aws-cdk/cdk'); +import { ContainerImage } from './container-image'; +import { cloudformation } from './ecs.generated'; +import { LinuxParameters } from './linux-parameters'; +import { LogDriver } from './log-drivers/log-driver'; + +export interface ContainerDefinitionProps { + /** + * A name for the container. + */ name: string; - image: string; + /** + * The image to use for a container. + * + * You can use images in the Docker Hub registry or specify other + * repositories (repository-url/image:tag). + */ + image: ContainerImage; + + /** + * The CMD value to pass to the container. + * + * @default CMD value built into container image + */ command?: string[]; + + /** + * The minimum number of CPU units to reserve for the container. + */ cpu?: number; + + /** + * Indicates whether networking is disabled within the container. + * + * @default false + */ disableNetworking?: boolean; + + /** + * A list of DNS search domains that are provided to the container. + * + * @default No search domains + */ dnsSearchDomains?: string[]; + + /** + * A list of DNS servers that Amazon ECS provides to the container. + * + * @default Default DNS servers + */ dnsServers?: string[]; - dockerLabels?: string[]; + + /** + * A key-value map of labels for the container. + * + * @default No labels + */ + dockerLabels?: {[key: string]: string }; + + /** + * A list of custom labels for SELinux and AppArmor multi-level security systems. + * + * @default No security labels + */ dockerSecurityOptions?: string[]; + + /** + * The ENTRYPOINT value to pass to the container. + * + * @see https://docs.docker.com/engine/reference/builder/#entrypoint + * @default Entry point configured in container + */ entryPoint?: string[]; + + /** + * The environment variables to pass to the container. + * + * @default No environment variables + */ + environment?: {[key: string]: string}; + + /** + * Indicates whether the task stops if this container fails. + * + * If you specify true and the container fails, all other containers in the + * task stop. If you specify false and the container fails, none of the other + * containers in the task is affected. This value is true by default. + * + * You must have at least one essential container in a task. + * + * @default false + */ essential?: boolean; + + /** + * A list of hostnames and IP address mappings to append to the /etc/hosts file on the container. + * + * @default No extra hosts + */ + extraHosts?: {[name: string]: string}; + + /** + * Container health check. + * + * @default Health check configuration from container + */ + healthCheck?: HealthCheck; + + /** + * The name that Docker uses for the container hostname. + * + * @default Automatic hostname + */ hostname?: string; - links?: string[]; - logConfiguration?: LogConfiguration[]; - memory?: number; - memoryReservation?: 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. + */ + memoryMiB?: 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. + */ + memoryReservationMiB?: number; + + /** + * Indicates whether the container is given full access to the host container instance. + * + * @default false + */ privileged?: boolean; + + /** + * Indicates whether the container's root file system is mounted as read only. + * + * @default false + */ readonlyRootFilesystem?: boolean; + + /** + * The user name to use inside the container. + * + * @default root + */ user?: string; - workingDirectory?: string + + /** + * The working directory in the container to run commands in. + * + * @default / + */ + workingDirectory?: string; + + /** + * Configures a custom log driver for the container. + */ + logging?: LogDriver; } -export interface LogConfiguration { - logDriver: string; - // options?: +export class ContainerDefinition extends cdk.Construct { + public readonly name: string; + + public readonly linuxParameters = new LinuxParameters(); + + private readonly links = new Array(); + + constructor(parent: cdk.Construct, id: string, private readonly props: ContainerDefinitionProps) { + super(parent, id); + this.name = props.name; + props.image.bind(this); + } + + public addLink(container: ContainerDefinition, alias?: string) { + if (alias !== undefined) { + this.links.push(`${container.name}:${alias}`); + } else { + this.links.push(`${container.name}`); + } + } + + public toContainerDefinitionJson(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { + return { + command: this.props.command, + cpu: this.props.cpu, + disableNetworking: this.props.disableNetworking, + dnsSearchDomains: this.props.dnsSearchDomains, + dnsServers: this.props.dnsServers, + dockerLabels: this.props.dockerLabels, + dockerSecurityOptions: this.props.dockerSecurityOptions, + entryPoint: this.props.entryPoint, + essential: this.props.essential, + hostname: this.props.hostname, + image: this.props.image.imageName, + memory: this.props.memoryMiB, + memoryReservation: this.props.memoryReservationMiB, + mountPoints: [], // FIXME + name: this.props.name, + portMappings: [], // FIXME + privileged: this.props.privileged, + readonlyRootFilesystem: this.props.readonlyRootFilesystem, + repositoryCredentials: undefined, // FIXME + ulimits: [], // FIXME + user: this.props.user, + volumesFrom: [], // FIXME + workingDirectory: this.props.workingDirectory, + logConfiguration: this.props.logging && this.props.logging.toLogDriverJson(), + environment: this.props.environment && renderKV(this.props.environment, 'name', 'value'), + extraHosts: this.props.extraHosts && renderKV(this.props.extraHosts, 'hostname', 'ipAddress'), + healthCheck: this.props.healthCheck && renderHealthCheck(this.props.healthCheck), + links: this.links, + linuxParameters: this.linuxParameters.toLinuxParametersJson(), + }; + } +} + +/** + * Container health check configuration + */ +export interface HealthCheck { + /** + * Command to run, as the binary path and arguments. + * + * If you use this form, you do not have to quote command-line arguments. + * + * Exactly one of command and shellCommand must be supplied. + */ + command?: string[]; + + /** + * Command to run, as a shell command + * + * Exactly one of command and shellCommand must be supplied. + */ + shellCommand?: string; + + /** + * Time period in seconds between each health check execution. + * + * You may specify between 5 and 300 seconds. + * + * @default 30 + */ + intervalSeconds?: number; + + /** + * Number of times to retry a failed health check before the container is considered unhealthy. + * + * You may specify between 1 and 10 retries. + * + * @default 3 + */ + retries?: number; + + /** + * Grace period after startup before failed health checks count. + * + * You may specify between 0 and 300 seconds. + * + * @default No start period + */ + startPeriod?: number; + + /** + * The time period in seconds to wait for a health check to succeed before it is considered a failure. + * + * You may specify between 2 and 60 seconds. + * + * @default 5 + */ + timeout?: number; } -// environment?: list of key-value; -// extraHosts?: hostEntry[]; -// healthCheck?: healthCheck; -// linuxParameters: linuxParam[]; // mountPoints?: mountPoint[]; // portMappings?: portMapping[]; // ulimits?: ulimit[]; // volumesFrom?: volumeFrom[]; + +function renderKV(env: {[key: string]: string}, keyName: string, valueName: string): any { + const ret = []; + for (const [key, value] of Object.entries(env)) { + ret.push({ [keyName]: key, [valueName]: value }); + } + return ret; +} + +function renderHealthCheck(hc: HealthCheck): cloudformation.TaskDefinitionResource.HealthCheckProperty { + if ((hc.command === undefined) === (hc.shellCommand === undefined)) { + throw new Error(`Exactly one of 'command' and 'shellCommand' must be supplied.`); + } + + return { + command: hc.command !== undefined ? ['CMD'].concat(hc.command) : ['CMD-SHELL', hc.shellCommand!], + interval: hc.intervalSeconds, + retries: hc.retries, + startPeriod: hc.startPeriod, + timeout: hc.timeout + }; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/container-image.ts b/packages/@aws-cdk/aws-ecs/lib/container-image.ts new file mode 100644 index 0000000000000..25ecc9fb5590a --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/container-image.ts @@ -0,0 +1,21 @@ +import { ContainerDefinition } from './container-definition'; + +export abstract class ContainerImage { + public abstract readonly imageName: string; + public abstract bind(containerDefinition: ContainerDefinition): void; +} + +export class DockerHub { + public static image(name: string): ContainerImage { + return new DockerHubImage(name); + } +} + +class DockerHubImage { + constructor(public readonly imageName: string) { + } + + public bind(_containerDefinition: ContainerDefinition): void { + // Nothing + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 9658da2d11cde..58a43c2e84d3f 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -2,6 +2,10 @@ export * from './cluster'; export * from './service'; export * from './task-definition'; export * from './container-definition'; +export * from './container-image'; +export * from './linux-parameters'; + +export * from './log-drivers/log-driver'; export * from './log-drivers/aws-log-driver'; // AWS::ECS CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts new file mode 100644 index 0000000000000..b9ea35bb61317 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -0,0 +1,16 @@ +import { cloudformation } from './ecs.generated'; + +export class LinuxParameters { + + public addCapability() { + // FIXME + } + + public dropCapability() { + // FIXME + } + + public toLinuxParametersJson(): cloudformation.TaskDefinitionResource.LinuxParametersProperty { + return {}; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts b/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts index ed581cac5580a..65b34e4b51737 100644 --- a/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts +++ b/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts @@ -1,4 +1,91 @@ +import logs = require('@aws-cdk/aws-logs'); +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from '../ecs.generated'; +import { LogDriver } from "./log-driver"; -export class AwsLogDriver { +/** + * Properties for defining a new AWS Log Driver + */ +export interface AwsLogDriverProps { + /** + * Prefix for the log streams + * + * The awslogs-stream-prefix option allows you to associate a log stream + * with the specified prefix, the container name, and the ID of the Amazon + * ECS task to which the container belongs. If you specify a prefix with + * this option, then the log stream takes the following format: + * + * prefix-name/container-name/ecs-task-id + */ + streamPrefix: string; + /** + * The log group to log to + * + * @default A log group is automatically created + */ + logGroup?: logs.LogGroupRef; + + /** + * This option defines a multiline start pattern in Python strftime format. + * + * A log message consists of a line that matches the pattern and any + * following lines that don’t match the pattern. Thus the matched line is + * the delimiter between log messages. + */ + datetimeFormat?: string; + + /** + * This option defines a multiline start pattern using a regular expression. + * + * A log message consists of a line that matches the pattern and any + * following lines that don’t match the pattern. Thus the matched line is + * the delimiter between log messages. + */ + multilinePattern?: string; +} + +/** + * A log driver that will log to an AWS Log Group + */ +export class AwsLogDriver extends LogDriver { + /** + * The log group that the logs will be sent to + */ + public readonly logGroup: logs.LogGroupRef; + + constructor(parent: cdk.Construct, id: string, private readonly props: AwsLogDriverProps) { + super(parent, id); + this.logGroup = props.logGroup || new logs.LogGroup(this, 'LogGroup', { + retentionDays: 365, + }); + } + + /** + * Return the log driver CloudFormation JSON + */ + public toLogDriverJson(): cloudformation.TaskDefinitionResource.LogConfigurationProperty { + return { + logDriver: 'awslogs', + options: removeEmpty({ + 'awslogs-group': this.logGroup.logGroupName, + 'awslogs-stream-prefix': this.props.streamPrefix, + 'awslogs-region': `${new cdk.AwsRegion()}`, + 'awslogs-datetime-format': this.props.datetimeFormat, + 'awslogs-multiline-pattern': this.props.multilinePattern, + }), + }; + } +} + +/** + * Remove undefined values from a dictionary + */ +function removeEmpty(x: {[key: string]: (T | undefined)}): {[key: string]: T} { + for (const key of Object.keys(x)) { + if (!x[key]) { + delete x[key]; + } + } + return x as any; } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/log-drivers/log-driver.ts b/packages/@aws-cdk/aws-ecs/lib/log-drivers/log-driver.ts new file mode 100644 index 0000000000000..2b8e9c5882d2f --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/log-drivers/log-driver.ts @@ -0,0 +1,12 @@ +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from '../ecs.generated'; + +/** + * Base class for log drivers + */ +export abstract class LogDriver extends cdk.Construct { + /** + * Return the log driver CloudFormation JSON + */ + public abstract toLogDriverJson(): cloudformation.TaskDefinitionResource.LogConfigurationProperty; +} diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index 97113604a63cd..b6a2f77ea1054 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -3,9 +3,6 @@ import { ContainerDefinition } from './container-definition'; import { cloudformation } from './ecs.generated'; export interface TaskDefinitionProps { - - containerDefinitions: ContainerDefinition[]; - /** * The number of cpu units used by the task. If using the EC2 launch type, * this field is optional. Supported values are between 128 CPU units @@ -35,7 +32,7 @@ export interface TaskDefinitionProps { /** * Namespace for task definition versions * - * @default CloudFormation-generated name + * @default Automatically generated name */ family?: string; @@ -58,7 +55,7 @@ export interface TaskDefinitionProps { * * @default 512 */ - memory?: string; + memoryMiB?: string; /** * The Docker networking mode to use for the containers in the task, such as none, bridge, or host. @@ -98,15 +95,16 @@ export interface TaskDefinitionProps { } export class TaskDefinition extends cdk.Construct { + public readonly family: string; public readonly taskDefinitionArn: string; - private readonly containerDefinitions: cloudformation.TaskDefinitionResource.ContainerDefinitionProperty[] = []; + private readonly containerDefinitions = new Array(); private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; constructor(parent: cdk.Construct, name: string, props: TaskDefinitionProps) { super(parent, name); - props.containerDefinitions.forEach(cd => this.addContainer(cd)); + this.family = props.family || this.uniqueId; if (props.placementConstraints) { props.placementConstraints.forEach(pc => this.addPlacementConstraint(pc)); @@ -117,11 +115,11 @@ export class TaskDefinition extends cdk.Construct { } const taskDef = new cloudformation.TaskDefinitionResource(this, "TaskDef", { - containerDefinitions: new cdk.Token(() => this.containerDefinitions), + containerDefinitions: new cdk.Token(() => this.containerDefinitions.map(x => x.toContainerDefinitionJson())), cpu: props.cpu, executionRoleArn: props.executionRoleArn, - family: props.family, - memory: props.memory, + family: this.family, + memory: props.memoryMiB, networkMode: props.networkMode, placementConstraints: new cdk.Token(() => this.placementConstraints), taskRoleArn: props.taskRoleArn @@ -130,9 +128,8 @@ export class TaskDefinition extends cdk.Construct { this.taskDefinitionArn = taskDef.ref; } - private addContainer(definition: ContainerDefinition) { - const cd = this.renderContainerDefininition(definition); - this.containerDefinitions.push(cd); + public addContainer(container: ContainerDefinition) { + this.containerDefinitions.push(container); } private addPlacementConstraint(constraint: PlacementConstraint) { @@ -145,60 +142,12 @@ export class TaskDefinition extends cdk.Construct { this.volumes.push(volume); } - // Populates task definition with container definition - private renderContainerDefininition(definition: ContainerDefinition): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { - // const logConfigs = this.renderLogConfiguration(definition.logConfiguration); // what to do if undefined? - - return { - name: definition.name, - image: definition.image, - command: definition.command, - cpu: definition.cpu, - disableNetworking: definition.disableNetworking, - dnsSearchDomains: definition.dnsSearchDomains, - dnsServers: definition.dnsServers, - // dockerLabels: definition.dockerLabels, - dockerSecurityOptions: definition.dockerSecurityOptions, - entryPoint: definition.entryPoint, - essential: definition.essential, - hostname: definition.hostname, - links: definition.links, - // logConfiguration: logConfigs, // only set if passed in? - memory: definition.memory, - memoryReservation: definition.memoryReservation, - privileged: definition.privileged, - readonlyRootFilesystem: definition.readonlyRootFilesystem, - user: definition.user, - workingDirectory: definition.workingDirectory - }; - } - private renderPlacementConstraint(pc: PlacementConstraint): cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty { return { type: pc.type, expression: pc.expression }; } - - // private renderLogConfiguration(lc: LogConfiguration[]): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty.LogConfiguration[] { - // return { - // logDriver: lc.logDriver, - // options: lc.options - // }; - // } - - // private renderVolume(volume: Volume): cloudformation.TaskDefinitionResource.VolumeProperty { - // return { - // host: this.renderHost(volume.host), - // name: volume.name - // }; - // } - - // private renderHost(host: Host): cloudformation.TaskDefinitionResource.VolumeProperty.Host { - // return { - // sourcePath: host.sourcePath - // } - // } } export interface PlacementConstraint { diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 87faffc994ed5..2b5d170a1a2be 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -60,7 +60,8 @@ "dependencies": { "@aws-cdk/cdk": "^0.10.0", "@aws-cdk/aws-autoscaling": "^0.10.0", - "@aws-cdk/aws-ec2": "^0.10.0" + "@aws-cdk/aws-ec2": "^0.10.0", + "@aws-cdk/aws-logs": "^0.10.0" }, "homepage": "https://github.com/awslabs/aws-cdk" } From 0672e6a06bb90ecddcff1cbb45826596b43ee463 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 1 Oct 2018 10:10:17 +0200 Subject: [PATCH 17/97] Add enum for placement constraints --- .../cdk-examples-typescript/hello-cdk-ecs/index.ts | 2 +- packages/@aws-cdk/aws-ecs/lib/task-definition.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index a51b9688b233e..188097118d87d 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -34,7 +34,7 @@ class BonjourECS extends cdk.Stack { const taskDefinition = new ecs.TaskDefinition(this, "MyTD", { family: "ecs-task-definition", placementConstraints: [{ - type: "distinctInstance" + type: PlacementConstraintType.DistinctInstance }], }); diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index b6a2f77ea1054..2626360778ec2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -152,13 +152,13 @@ export class TaskDefinition extends cdk.Construct { export interface PlacementConstraint { expression?: string; - type: string; // PlacementConstraintType; + type: PlacementConstraintType; } -// enum PlacementConstraintType{ -// DistinctInstance = "distinctInstance", -// MemberOf = "memberOf" -// } +export enum PlacementConstraintType { + DistinctInstance = "distinctInstance", + MemberOf = "memberOf" +} export interface Volume { host?: Host; From 4f93a5b16cb25bc19fc93c804c609a7a9690b848 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 1 Oct 2018 10:20:40 +0200 Subject: [PATCH 18/97] Rename fleet to autoScalingGroup --- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 1ad78b021a536..4dd934f97b2a0 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -46,7 +46,7 @@ export class Cluster extends cdk.Construct { public readonly clusterName: string; - public readonly fleet: autoscaling.AutoScalingGroup; + public readonly autoScalingGroup: autoscaling.AutoScalingGroup; constructor(parent: cdk.Construct, name: string, props: ClusterProps) { super(parent, name); @@ -73,7 +73,7 @@ export class Cluster extends cdk.Construct { autoScalingGroup.addUserData('sudo service iptables save'); } - // Note: if the fleet doesn't launch or doesn't register itself with + // Note: if the ASG doesn't launch or doesn't register itself with // ECS, *Cluster* stabilization will fail after timing our for an hour // or so, because the *Service* doesn't have any running instances. // During this time, you CANNOT DO ANYTHING ELSE WITH YOUR STACK. @@ -108,7 +108,7 @@ export class Cluster extends cdk.Construct { "logs:PutLogEvents" ).addAllResources()); // Conceivably we might do better than all resources and add targeted ARNs - this.fleet = autoScalingGroup; + this.autoScalingGroup = autoScalingGroup; } public runService(taskDefinition: TaskDefinition): Service { From 9b61b89328886289fc2494760fe35bd1d2060f98 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 1 Oct 2018 10:38:53 +0200 Subject: [PATCH 19/97] Qualify "PlacementConstraintType" usage with package name --- examples/cdk-examples-typescript/hello-cdk-ecs/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 188097118d87d..3a2a0340b56e5 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -34,7 +34,7 @@ class BonjourECS extends cdk.Stack { const taskDefinition = new ecs.TaskDefinition(this, "MyTD", { family: "ecs-task-definition", placementConstraints: [{ - type: PlacementConstraintType.DistinctInstance + type: ecs.PlacementConstraintType.DistinctInstance }], }); From 98bf81ba0bb5e1fd3ce892768d1ccdece6b4b9f2 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 1 Oct 2018 12:05:38 +0200 Subject: [PATCH 20/97] ECR can return itself as container image --- .../@aws-cdk/aws-ecr/lib/repository-ref.ts | 21 +++++++++++++++++++ packages/@aws-cdk/aws-ecr/package.json | 1 + 2 files changed, 22 insertions(+) diff --git a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts index 90b2dfd7e744d..8a40bcc2fe238 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts @@ -1,3 +1,4 @@ +import ecs = require('@aws-cdk/aws-ecs'); import cdk = require('@aws-cdk/cdk'); /** @@ -43,6 +44,13 @@ export abstract class RepositoryRef extends cdk.Construct { const parts = cdk.ArnUtils.parse(this.repositoryArn); return `${parts.account}.dkr.ecr.${parts.region}.amazonaws.com/${parts.resourceName}`; } + + /** + * Refer to a particular image tag from this repository + */ + public getImage(tag: string = "latest"): ecs.ContainerImage { + return new EcrImage(this, tag); + } } export interface RepositoryRefProps { @@ -66,3 +74,16 @@ class ImportedRepository extends RepositoryRef { // FIXME: Add annotation about policy we dropped on the floor } } + +class EcrImage extends ecs.ContainerImage { + public readonly imageName: string; + + constructor(repository: RepositoryRef, tag: string) { + super(); + this.imageName = `${repository.repositoryUri}:${tag}`; + } + + public bind(_containerDefinition: ecs.ContainerDefinition): void { + // Nothing, for now + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/package.json b/packages/@aws-cdk/aws-ecr/package.json index f719c74e6cc9c..dee3612c30041 100644 --- a/packages/@aws-cdk/aws-ecr/package.json +++ b/packages/@aws-cdk/aws-ecr/package.json @@ -53,6 +53,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "^0.10.0", + "@aws-cdk/aws-ecs": "^0.10.0", "cdk-build-tools": "^0.10.0", "cdk-integ-tools": "^0.10.0", "cfn2ts": "^0.10.0", From ae1d525095c1c5d8cdd245c3cc75bce1e1c245af Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 1 Oct 2018 12:25:09 +0200 Subject: [PATCH 21/97] Automatically generate execution and task roles --- .../@aws-cdk/aws-ecr/lib/repository-ref.ts | 4 +- .../aws-ecs/lib/container-definition.ts | 13 ++++ packages/@aws-cdk/aws-ecs/lib/index.ts | 1 + .../@aws-cdk/aws-ecs/lib/task-definition.ts | 72 ++++++++++++++----- packages/@aws-cdk/aws-ecs/package.json | 1 + 5 files changed, 72 insertions(+), 19 deletions(-) diff --git a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts index 8a40bcc2fe238..a38d332e60700 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts @@ -83,7 +83,7 @@ class EcrImage extends ecs.ContainerImage { this.imageName = `${repository.repositoryUri}:${tag}`; } - public bind(_containerDefinition: ecs.ContainerDefinition): void { - // Nothing, for now + public bind(containerDefinition: ecs.ContainerDefinition): void { + containerDefinition.useEcrImage(); } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index de97fe38048e6..7cff4163a37ff 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -173,6 +173,8 @@ export class ContainerDefinition extends cdk.Construct { private readonly links = new Array(); + private _usesEcrImages: boolean = false; + constructor(parent: cdk.Construct, id: string, private readonly props: ContainerDefinitionProps) { super(parent, id); this.name = props.name; @@ -187,6 +189,17 @@ export class ContainerDefinition extends cdk.Construct { } } + /** + * Mark this ContainerDefinition as using an ECR image + */ + public useEcrImage() { + this._usesEcrImages = true; + } + + public get usesEcrImages() { + return this._usesEcrImages; + } + public toContainerDefinitionJson(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { return { command: this.props.command, diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 58a43c2e84d3f..a4729352cf1a8 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -9,4 +9,5 @@ export * from './log-drivers/log-driver'; export * from './log-drivers/aws-log-driver'; // AWS::ECS CloudFormation Resources: +// export * from './ecs.generated'; diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index 2626360778ec2..18f9ceb991705 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -1,3 +1,4 @@ +import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { ContainerDefinition } from './container-definition'; import { cloudformation } from './ecs.generated'; @@ -20,15 +21,6 @@ export interface TaskDefinitionProps { */ cpu?: string; - /** - * The Amazon Resource Name (ARN) of the task execution role that - * containers in this task can assume. All containers in this task are - * granted the permissions that are specified in this role. - * - * Needed in Fargate to communicate with Cloudwatch Logs and ECR. - */ - executionRoleArn?: string; - /** * Namespace for task definition versions * @@ -82,11 +74,21 @@ export interface TaskDefinitionProps { // requiresCompatibilities?: string[]; // FARGATE or EC2 -- set on ECS TD vs FG TD /** - * The Amazon Resource Name (ARN) of an AWS Identity and Access Management - * (IAM) role that grants containers in the task permission to call AWS - * APIs on your behalf + * The IAM role assumed by the ECS agent. + * + * The role will be used to retrieve container images from ECR and + * create CloudWatch log groups. + * + * @default An execution role will be automatically created if you use ECR images in your task definition + */ + executionRole?: iam.Role; + + /** + * The IAM role assumable by your application code running inside the container + * + * @default A task role is automatically created for you */ - taskRoleArn?: string; + taskRole?: iam.Role; /** * See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide//task_definition_parameters.html#volumes @@ -100,6 +102,8 @@ export class TaskDefinition extends cdk.Construct { private readonly containerDefinitions = new Array(); private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; + private executionRole?: iam.Role; + private readonly taskRole: iam.Role; constructor(parent: cdk.Construct, name: string, props: TaskDefinitionProps) { super(parent, name); @@ -114,24 +118,46 @@ export class TaskDefinition extends cdk.Construct { props.volumes.forEach(v => this.addVolume(v)); } - const taskDef = new cloudformation.TaskDefinitionResource(this, "TaskDef", { + this.executionRole = props.executionRole; + + this.taskRole = props.taskRole || new iam.Role(this, 'TaskRole', { + assumedBy: new cdk.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + const taskDef = new cloudformation.TaskDefinitionResource(this, 'Resource', { containerDefinitions: new cdk.Token(() => this.containerDefinitions.map(x => x.toContainerDefinitionJson())), cpu: props.cpu, - executionRoleArn: props.executionRoleArn, + executionRoleArn: new cdk.Token(() => this.executionRole && this.executionRole.roleArn), family: this.family, memory: props.memoryMiB, networkMode: props.networkMode, placementConstraints: new cdk.Token(() => this.placementConstraints), - taskRoleArn: props.taskRoleArn + taskRoleArn: this.taskRole.roleArn }); - this.taskDefinitionArn = taskDef.ref; + this.taskDefinitionArn = taskDef.taskDefinitionArn; } + /** + * Add a policy statement to the Task Role + */ + public addToRolePolicy(statement: cdk.PolicyStatement) { + this.taskRole.addToPolicy(statement); + } + + /** + * Add a container to this task + */ public addContainer(container: ContainerDefinition) { this.containerDefinitions.push(container); + if (container.usesEcrImages) { + this.generateExecutionRole(); + } } + /** + * Constrain where this task can be placed + */ private addPlacementConstraint(constraint: PlacementConstraint) { const pc = this.renderPlacementConstraint(constraint); this.placementConstraints.push(pc); @@ -148,6 +174,18 @@ export class TaskDefinition extends cdk.Construct { expression: pc.expression }; } + + /** + * Generate a default execution role that allows pulling from ECR + */ + private generateExecutionRole() { + if (!this.executionRole) { + this.executionRole = new iam.Role(this, 'ExecutionRole', { + assumedBy: new cdk.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + this.executionRole.attachManagedPolicy(new iam.AwsManagedPolicy("service-role/AmazonECSTaskExecutionRolePolicy").policyArn); + } + } } export interface PlacementConstraint { diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 2b5d170a1a2be..fd0c9c2813625 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -61,6 +61,7 @@ "@aws-cdk/cdk": "^0.10.0", "@aws-cdk/aws-autoscaling": "^0.10.0", "@aws-cdk/aws-ec2": "^0.10.0", + "@aws-cdk/aws-iam": "^0.10.0", "@aws-cdk/aws-logs": "^0.10.0" }, "homepage": "https://github.com/awslabs/aws-cdk" From 23072a978820f2ef0b7a2b0487f08b929ab01b6b Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 1 Oct 2018 12:28:32 +0200 Subject: [PATCH 22/97] Make taskRole public --- packages/@aws-cdk/aws-ecs/lib/task-definition.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index 18f9ceb991705..fad04125f54ba 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -99,11 +99,11 @@ export interface TaskDefinitionProps { export class TaskDefinition extends cdk.Construct { public readonly family: string; public readonly taskDefinitionArn: string; + public readonly taskRole: iam.Role; private readonly containerDefinitions = new Array(); private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; private executionRole?: iam.Role; - private readonly taskRole: iam.Role; constructor(parent: cdk.Construct, name: string, props: TaskDefinitionProps) { super(parent, name); From e5cc7f253243c9122cd73cb0a25f6e5130ebec30 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 1 Oct 2018 14:29:17 +0200 Subject: [PATCH 23/97] Remove shellCommand from healthCheck --- .../aws-ecs/lib/container-definition.ts | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 7cff4163a37ff..d789d3c0d2932 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -15,12 +15,15 @@ export interface ContainerDefinitionProps { * * You can use images in the Docker Hub registry or specify other * repositories (repository-url/image:tag). + * TODO: Update these to specify using classes of ContainerImage */ image: ContainerImage; /** * The CMD value to pass to the container. * + * If you provide a shell command as a single string, you have to quote command-line arguments. + * * @default CMD value built into container image */ command?: string[]; @@ -242,18 +245,9 @@ export interface HealthCheck { /** * Command to run, as the binary path and arguments. * - * If you use this form, you do not have to quote command-line arguments. - * - * Exactly one of command and shellCommand must be supplied. - */ - command?: string[]; - - /** - * Command to run, as a shell command - * - * Exactly one of command and shellCommand must be supplied. + * If you provide a shell command as a single string, you have to quote command-line arguments. */ - shellCommand?: string; + command: string[]; /** * Time period in seconds between each health check execution. @@ -306,15 +300,31 @@ function renderKV(env: {[key: string]: string}, keyName: string, valueName: stri } function renderHealthCheck(hc: HealthCheck): cloudformation.TaskDefinitionResource.HealthCheckProperty { - if ((hc.command === undefined) === (hc.shellCommand === undefined)) { - throw new Error(`Exactly one of 'command' and 'shellCommand' must be supplied.`); - } - return { - command: hc.command !== undefined ? ['CMD'].concat(hc.command) : ['CMD-SHELL', hc.shellCommand!], + command: getHealthCheckCommand(hc), interval: hc.intervalSeconds, retries: hc.retries, startPeriod: hc.startPeriod, timeout: hc.timeout }; -} \ No newline at end of file +} + +function getHealthCheckCommand(hc: HealthCheck): string[] { + let cmd = hc.command; + const hcCommand = new Array(); + + if (cmd.length === 0) { + throw new Error(`At least one argument must be supplied for health check command.`); + } + + if (cmd.length === 1) { + hcCommand.push('CMD-SHELL', cmd[0]); + return hcCommand; + } + + if (cmd[0] !== "CMD" || cmd[0] !== 'CMD-SHELL') { + hcCommand.push('CMD') + } + + return hcCommand.concat(cmd); +} From 047eed81b7fead441bf348c8b7f09bab90abc934 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 1 Oct 2018 14:32:59 +0200 Subject: [PATCH 24/97] Rename toContainerDefinitionJson to renderContainerDefinition --- packages/@aws-cdk/aws-ecs/lib/container-definition.ts | 2 +- packages/@aws-cdk/aws-ecs/lib/task-definition.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index d789d3c0d2932..f19cbf78a1f38 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -203,7 +203,7 @@ export class ContainerDefinition extends cdk.Construct { return this._usesEcrImages; } - public toContainerDefinitionJson(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { + public renderContainerDefinition(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { return { command: this.props.command, cpu: this.props.cpu, diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index 18f9ceb991705..d42326fd42ae9 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -125,7 +125,7 @@ export class TaskDefinition extends cdk.Construct { }); const taskDef = new cloudformation.TaskDefinitionResource(this, 'Resource', { - containerDefinitions: new cdk.Token(() => this.containerDefinitions.map(x => x.toContainerDefinitionJson())), + containerDefinitions: new cdk.Token(() => this.containerDefinitions.map(x => x.renderContainerDefinition())), cpu: props.cpu, executionRoleArn: new cdk.Token(() => this.executionRole && this.executionRole.roleArn), family: this.family, From 65d3f790037c385a04f68512796c5b0e339c137a Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 1 Oct 2018 15:36:06 +0200 Subject: [PATCH 25/97] Some work on Service --- packages/@aws-cdk/aws-ecr/package.json | 4 +- .../aws-ecs/lib/container-definition.ts | 13 ++- packages/@aws-cdk/aws-ecs/lib/service.ts | 104 ++++++++++++++++-- .../@aws-cdk/aws-ecs/lib/task-definition.ts | 59 ++++++++-- packages/@aws-cdk/aws-ecs/package.json | 2 + 5 files changed, 161 insertions(+), 21 deletions(-) diff --git a/packages/@aws-cdk/aws-ecr/package.json b/packages/@aws-cdk/aws-ecr/package.json index dee3612c30041..f83da66b34989 100644 --- a/packages/@aws-cdk/aws-ecr/package.json +++ b/packages/@aws-cdk/aws-ecr/package.json @@ -53,14 +53,14 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "^0.10.0", - "@aws-cdk/aws-ecs": "^0.10.0", "cdk-build-tools": "^0.10.0", "cdk-integ-tools": "^0.10.0", "cfn2ts": "^0.10.0", "pkglint": "^0.10.0" }, "dependencies": { - "@aws-cdk/cdk": "^0.10.0" + "@aws-cdk/cdk": "^0.10.0", + "@aws-cdk/aws-ecs": "^0.10.0" }, "homepage": "https://github.com/awslabs/aws-cdk" } diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 7cff4163a37ff..f8612b384764b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -85,11 +85,11 @@ export interface ContainerDefinitionProps { * * If you specify true and the container fails, all other containers in the * task stop. If you specify false and the container fails, none of the other - * containers in the task is affected. This value is true by default. + * containers in the task is affected. * * You must have at least one essential container in a task. * - * @default false + * @default true */ essential?: boolean; @@ -171,6 +171,8 @@ export class ContainerDefinition extends cdk.Construct { public readonly linuxParameters = new LinuxParameters(); + public readonly essential: boolean; + private readonly links = new Array(); private _usesEcrImages: boolean = false; @@ -178,6 +180,7 @@ export class ContainerDefinition extends cdk.Construct { constructor(parent: cdk.Construct, id: string, private readonly props: ContainerDefinitionProps) { super(parent, id); this.name = props.name; + this.essential = props.essential !== undefined ? props.essential : true; props.image.bind(this); } @@ -200,6 +203,10 @@ export class ContainerDefinition extends cdk.Construct { return this._usesEcrImages; } + public loadBalancerPort(_classicLB: boolean): number { + return 0; + } + public toContainerDefinitionJson(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { return { command: this.props.command, @@ -210,7 +217,7 @@ export class ContainerDefinition extends cdk.Construct { dockerLabels: this.props.dockerLabels, dockerSecurityOptions: this.props.dockerSecurityOptions, entryPoint: this.props.entryPoint, - essential: this.props.essential, + essential: this.essential, hostname: this.props.hostname, image: this.props.image.imageName, memory: this.props.memoryMiB, diff --git a/packages/@aws-cdk/aws-ecs/lib/service.ts b/packages/@aws-cdk/aws-ecs/lib/service.ts index 843532dc07c8d..d03876bf485a5 100644 --- a/packages/@aws-cdk/aws-ecs/lib/service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/service.ts @@ -1,3 +1,7 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import elb = require('@aws-cdk/aws-elasticloadbalancing'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { Cluster } from './cluster'; import { cloudformation } from './ecs.generated'; @@ -33,7 +37,7 @@ export interface ServiceProps { * * @default EC2 */ - launchType?: string; // maybe unnecessary if we have different ECS vs FG service + launchType?: LaunchType; /** * The maximum number of tasks, specified as a percentage of the Amazon ECS @@ -54,11 +58,18 @@ export interface ServiceProps { minimumHealthyPercent?: number; /** - * The name or ARN of an AWS Identity and Access Management (IAM) role that - * allows your Amazon ECS container agent to make calls to your load - * balancer. + * Role used by ECS agent to register containers with the Load Balancer + * + * @default A role will be created for you */ - role?: string; + role?: iam.Role; + + /** + * Time after startup to ignore unhealthy load balancer checks. + * + * @default ??? + */ + healthCheckGracePeriodSeconds?: number; ///////// TBD /////////////////////////////// // healthCheckGracePeriodSeconds?: number; // only needed with load balancers @@ -72,21 +83,98 @@ export interface ServiceProps { //////////////////////////////////////////// } -export class Service extends cdk.Construct { +export class Service extends cdk.Construct implements elb.ILoadBalancerTarget, + elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { + public readonly dependencyElements: cdk.IDependable[]; + public readonly connections: ec2.Connections; + private loadBalancers = new Array(); + private readonly taskDefinition: TaskDefinition; + private role?: iam.Role; + private readonly resource: cloudformation.ServiceResource; + constructor(parent: cdk.Construct, name: string, props: ServiceProps) { super(parent, name); - new cloudformation.ServiceResource(this, "Service", { + this.connections = new ec2.Connections({ + securityGroupRule: { + canInlineRule: false, + toEgressRuleJSON() { return {}; }, + toIngressRuleJSON() { return {}; }, + uniqueId: '' + }, + }); + + this.taskDefinition = props.taskDefinition; + if (!this.taskDefinition.defaultContainer) { + throw new Error('A TaskDefinition must have at least one essential container'); + } + + this.role = props.role; + + this.resource = new cloudformation.ServiceResource(this, "Service", { cluster: props.cluster.clusterName, taskDefinition: props.taskDefinition.taskDefinitionArn, desiredCount: props.desiredCount, serviceName: props.serviceName, launchType: props.launchType, + loadBalancers: new cdk.Token(() => this.loadBalancers), deploymentConfiguration: { maximumPercent: props.maximumPercent, minimumHealthyPercent: props.minimumHealthyPercent }, - role: props.role, + role: new cdk.Token(() => this.role && this.role.roleArn), }); + + this.dependencyElements = [this.resource]; } + + public attachToClassicLB(loadBalancer: elb.LoadBalancer): void { + // FIXME: If Fargate then throw a helpful error + // FIXME: Security Groups + this.loadBalancers.push({ + loadBalancerName: loadBalancer.loadBalancerName, + containerName: this.taskDefinition.defaultContainer!.name, + containerPort: this.taskDefinition.defaultContainer!.loadBalancerPort(true), + }); + this.createLoadBalancerRole(); + } + + public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { + // FIXME: Security Groups + this.loadBalancers.push({ + targetGroupArn: targetGroup.targetGroupArn, + containerName: this.taskDefinition.defaultContainer!.name, + containerPort: this.taskDefinition.defaultContainer!.loadBalancerPort(false), + }); + this.createLoadBalancerRole(); + + return { targetType: elbv2.TargetType.SelfRegistering }; + } + + public attachToNetworkTargetGroup(targetGroup: elbv2.NetworkTargetGroup): elbv2.LoadBalancerTargetProps { + this.loadBalancers.push({ + targetGroupArn: targetGroup.targetGroupArn, + containerName: this.taskDefinition.defaultContainer!.name, + containerPort: this.taskDefinition.defaultContainer!.loadBalancerPort(false), + }); + this.createLoadBalancerRole(); + + return { targetType: elbv2.TargetType.SelfRegistering }; + } + + public createLoadBalancerRole() { + if (!this.role) { + this.role = new iam.Role(this, 'Role', { + assumedBy: new cdk.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + this.role.attachManagedPolicy(new iam.AwsManagedPolicy('service-role/AmazonEC2ContainerServiceRole').policyArn); + this.resource.addDependency(this.role); + } + } + } + +export enum LaunchType { + EC2 = 'EC2', + Fargate = 'FARGATE' +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index fad04125f54ba..77dde2643560b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -50,12 +50,13 @@ export interface TaskDefinitionProps { memoryMiB?: string; /** - * The Docker networking mode to use for the containers in the task, such as none, bridge, or host. + * The Docker networking mode to use for the containers in the task. + * * For Fargate or to use task networking, "awsvpc" mode is required. * - * @default bridge + * @default NetworkMode.Bridge */ - networkMode?: string; + networkMode?: NetworkMode; /** * An array of placement constraint objects to use for the task. You can @@ -100,7 +101,17 @@ export class TaskDefinition extends cdk.Construct { public readonly family: string; public readonly taskDefinitionArn: string; public readonly taskRole: iam.Role; - private readonly containerDefinitions = new Array(); + public readonly networkMode: NetworkMode; + + /** + * Default container for this task + * + * Load balancers will send traffic to this container. The first + * essential container that is added to this task will become the default + * container. + */ + public defaultContainer?: ContainerDefinition; + private readonly containers = new Array(); private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; private executionRole?: iam.Role; @@ -109,6 +120,7 @@ export class TaskDefinition extends cdk.Construct { super(parent, name); this.family = props.family || this.uniqueId; + this.networkMode = props.networkMode || NetworkMode.Bridge; if (props.placementConstraints) { props.placementConstraints.forEach(pc => this.addPlacementConstraint(pc)); @@ -125,12 +137,12 @@ export class TaskDefinition extends cdk.Construct { }); const taskDef = new cloudformation.TaskDefinitionResource(this, 'Resource', { - containerDefinitions: new cdk.Token(() => this.containerDefinitions.map(x => x.toContainerDefinitionJson())), + containerDefinitions: new cdk.Token(() => this.containers.map(x => x.toContainerDefinitionJson())), cpu: props.cpu, executionRoleArn: new cdk.Token(() => this.executionRole && this.executionRole.roleArn), family: this.family, memory: props.memoryMiB, - networkMode: props.networkMode, + networkMode: this.networkMode, placementConstraints: new cdk.Token(() => this.placementConstraints), taskRoleArn: this.taskRole.roleArn }); @@ -149,10 +161,13 @@ export class TaskDefinition extends cdk.Construct { * Add a container to this task */ public addContainer(container: ContainerDefinition) { - this.containerDefinitions.push(container); + this.containers.push(container); if (container.usesEcrImages) { this.generateExecutionRole(); } + if (this.defaultContainer === undefined && container.essential) { + this.defaultContainer = container; + } } /** @@ -188,6 +203,34 @@ export class TaskDefinition extends cdk.Construct { } } +/** + * The Docker networking mode to use for the containers in the task. + */ +export enum NetworkMode { + /** + * The task's containers do not have external connectivity and port mappings can't be specified in the container definition. + */ + None = 'none', + + /** + * The task utilizes Docker's built-in virtual network which runs inside each container instance. + */ + Bridge = 'bridge', + + /** + * The task is allocated an elastic network interface. + */ + AwsVpc = 'awsvpc', + + /** + * The task bypasses Docker's built-in virtual network and maps container ports directly to the EC2 instance's network interface directly. + * + * In this mode, you can't run multiple instantiations of the same task on a + * single container instance when port mappings are used. + */ + Host = 'host', +} + export interface PlacementConstraint { expression?: string; type: PlacementConstraintType; @@ -205,4 +248,4 @@ export interface Volume { export interface Host { sourcePath?: string; -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index fd0c9c2813625..e6c82d1e6e64a 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -61,6 +61,8 @@ "@aws-cdk/cdk": "^0.10.0", "@aws-cdk/aws-autoscaling": "^0.10.0", "@aws-cdk/aws-ec2": "^0.10.0", + "@aws-cdk/aws-elasticloadbalancing": "^0.10.0", + "@aws-cdk/aws-elasticloadbalancingv2": "^0.10.0", "@aws-cdk/aws-iam": "^0.10.0", "@aws-cdk/aws-logs": "^0.10.0" }, From fb1753258edcd1ec9546a46ecb7565a80575c8a4 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 1 Oct 2018 15:58:53 +0200 Subject: [PATCH 26/97] Separate Fargate and ECS clusters --- .../@aws-cdk/aws-ecs/lib/base/base-cluster.ts | 33 +++++++++ .../lib/{cluster.ts => ecs/ecs-cluster.ts} | 74 +++++++------------ .../aws-ecs/lib/fargate/fargate-cluster.ts | 12 +++ packages/@aws-cdk/aws-ecs/lib/index.ts | 4 +- 4 files changed, 73 insertions(+), 50 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts rename packages/@aws-cdk/aws-ecs/lib/{cluster.ts => ecs/ecs-cluster.ts} (76%) create mode 100644 packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts new file mode 100644 index 0000000000000..f416cf46351dd --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts @@ -0,0 +1,33 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from '../ecs.generated'; + +export interface BaseClusterProps { + /** + * A name for the cluster. + * + * @default CloudFormation-generated name + */ + clusterName?: string; + + /** + * The VPC where your ECS instances will be running + */ + vpc: ec2.VpcNetworkRef; +} + +export class BaseCluster extends cdk.Construct { + + public readonly clusterArn: string; + + public readonly clusterName: string; + + constructor(parent: cdk.Construct, name: string, props: BaseClusterProps) { + super(parent, name); + + const cluster = new cloudformation.ClusterResource(this, 'Resource', {clusterName: props.clusterName}); + + this.clusterArn = cluster.clusterArn; + this.clusterName = cluster.ref; + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts similarity index 76% rename from packages/@aws-cdk/aws-ecs/lib/cluster.ts rename to packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index 4dd934f97b2a0..a4231f12b07e1 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -1,23 +1,9 @@ import autoscaling = require('@aws-cdk/aws-autoscaling'); import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); -import { cloudformation } from './ecs.generated'; -import { Service } from './service'; -import { TaskDefinition } from './task-definition'; - -export interface ClusterProps { - /** - * A name for the cluster. - * - * @default CloudFormation-generated name - */ - clusterName?: string; - - /** - * The VPC where your ECS instances will be running - */ - vpc: ec2.VpcNetworkRef; +import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; +export interface EcsClusterProps extends BaseClusterProps { /** * Whether or not the containers can access the instance role * @@ -26,35 +12,11 @@ export interface ClusterProps { containersAccessInstanceRole?: boolean; } -/** - * Construct a Linux machine image from the latest ECS Optimized AMI published in SSM - */ -export class EcsOptimizedAmi implements ec2.IMachineImageSource { - private static AmiParamterName = "/aws/service/ecs/optimized-ami/amazon-linux/recommended"; - - public getImage(parent: cdk.Construct): ec2.MachineImage { - const ssmProvider = new cdk.SSMParameterProvider(parent); - - const ami = ssmProvider.getString(EcsOptimizedAmi.AmiParamterName); - return new ec2.MachineImage(ami, new ec2.LinuxOS()); - } -} - -export class Cluster extends cdk.Construct { - - public readonly clusterArn: string; - - public readonly clusterName: string; - +export class EcsCluster extends BaseCluster { public readonly autoScalingGroup: autoscaling.AutoScalingGroup; - constructor(parent: cdk.Construct, name: string, props: ClusterProps) { - super(parent, name); - - const cluster = new cloudformation.ClusterResource(this, 'Resource', {clusterName: props.clusterName}); - - this.clusterArn = cluster.clusterArn; - this.clusterName = cluster.ref; + constructor(parent: cdk.Construct, name: string, props: EcsClusterProps) { + super(parent, name, props); const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'AutoScalingGroup', { vpc: props.vpc, @@ -111,11 +73,25 @@ export class Cluster extends cdk.Construct { this.autoScalingGroup = autoScalingGroup; } - public runService(taskDefinition: TaskDefinition): Service { - return new Service(this, `${taskDefinition.family}Service`, { - cluster: this, - taskDefinition, - // FIXME: additional props? Or set on Service object? - }); + // public runService(taskDefinition: EcsTaskDefinition): EcsService { + // return new Service(this, `${taskDefinition.family}Service`, { + // cluster: this, + // taskDefinition, + // // FIXME: additional props? Or set on Service object? + // }); + // } +} + +/** + * Construct a Linux machine image from the latest ECS Optimized AMI published in SSM + */ +export class EcsOptimizedAmi implements ec2.IMachineImageSource { + private static AmiParamterName = "/aws/service/ecs/optimized-ami/amazon-linux/recommended"; + + public getImage(parent: cdk.Construct): ec2.MachineImage { + const ssmProvider = new cdk.SSMParameterProvider(parent); + + const ami = ssmProvider.getString(EcsOptimizedAmi.AmiParamterName); + return new ec2.MachineImage(ami, new ec2.LinuxOS()); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts new file mode 100644 index 0000000000000..b928fb8be616f --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts @@ -0,0 +1,12 @@ +import cdk = require('@aws-cdk/cdk'); +import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; + +// tslint:disable-next-line:no-empty-interface +export interface FargateClusterProps extends BaseClusterProps { +} + +export class FargateCluster extends BaseCluster { + constructor(parent: cdk.Construct, name: string, props: FargateClusterProps) { + super(parent, name, props); + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index a4729352cf1a8..ec49b26c2e0b9 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -1,4 +1,6 @@ -export * from './cluster'; +export * from './base/base-cluster'; +export * from './ecs/ecs-cluster'; +// export * from './fargate/fargate-cluster'; export * from './service'; export * from './task-definition'; export * from './container-definition'; From be8cb86d9ae04aeaa924e9d8e129f9e901181256 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 1 Oct 2018 16:53:04 +0200 Subject: [PATCH 27/97] Separate ECS and Fargate Service --- .../lib/{service.ts => base/base-service.ts} | 70 ++++--------------- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 48 +++++++++++++ .../aws-ecs/lib/fargate/fargate-service.ts | 37 ++++++++++ packages/@aws-cdk/aws-ecs/lib/index.ts | 7 +- 4 files changed, 104 insertions(+), 58 deletions(-) rename packages/@aws-cdk/aws-ecs/lib/{service.ts => base/base-service.ts} (62%) create mode 100644 packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts similarity index 62% rename from packages/@aws-cdk/aws-ecs/lib/service.ts rename to packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index d03876bf485a5..7e50462ae510b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -1,23 +1,11 @@ import ec2 = require('@aws-cdk/aws-ec2'); -import elb = require('@aws-cdk/aws-elasticloadbalancing'); import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); -import { Cluster } from './cluster'; -import { cloudformation } from './ecs.generated'; -import { TaskDefinition } from './task-definition'; - -export interface ServiceProps { - /** - * Cluster where service will be deployed - */ - cluster: Cluster; // should be required? do we assume 'default' exists? - - /** - * Task Definition used for running tasks in the service - */ - taskDefinition: TaskDefinition; +import { BaseTaskDefinition } from '../base/base-task-definition'; +import { cloudformation } from '../ecs.generated'; +export interface BaseServiceProps { /** * Number of desired copies of running tasks * @@ -32,13 +20,6 @@ export interface ServiceProps { */ serviceName?: string; - /** - * Whether the service is hosted in EC2 or Fargate - * - * @default EC2 - */ - launchType?: LaunchType; - /** * The maximum number of tasks, specified as a percentage of the Amazon ECS * service's DesiredCount value, that can run in a service during a @@ -83,16 +64,15 @@ export interface ServiceProps { //////////////////////////////////////////// } -export class Service extends cdk.Construct implements elb.ILoadBalancerTarget, - elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { +export abstract class BaseService extends cdk.Construct implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { public readonly dependencyElements: cdk.IDependable[]; public readonly connections: ec2.Connections; - private loadBalancers = new Array(); - private readonly taskDefinition: TaskDefinition; + protected loadBalancers = new Array(); + protected readonly abstract taskDef: BaseTaskDefinition; private role?: iam.Role; private readonly resource: cloudformation.ServiceResource; - constructor(parent: cdk.Construct, name: string, props: ServiceProps) { + constructor(parent: cdk.Construct, name: string, props: BaseServiceProps, additionalProps: any) { super(parent, name); this.connections = new ec2.Connections({ @@ -104,47 +84,31 @@ export class Service extends cdk.Construct implements elb.ILoadBalancerTarget, }, }); - this.taskDefinition = props.taskDefinition; - if (!this.taskDefinition.defaultContainer) { - throw new Error('A TaskDefinition must have at least one essential container'); - } + // this.taskDefinition = props.taskDefinition; this.role = props.role; this.resource = new cloudformation.ServiceResource(this, "Service", { - cluster: props.cluster.clusterName, - taskDefinition: props.taskDefinition.taskDefinitionArn, desiredCount: props.desiredCount, serviceName: props.serviceName, - launchType: props.launchType, loadBalancers: new cdk.Token(() => this.loadBalancers), deploymentConfiguration: { maximumPercent: props.maximumPercent, minimumHealthyPercent: props.minimumHealthyPercent }, role: new cdk.Token(() => this.role && this.role.roleArn), + ...additionalProps }); this.dependencyElements = [this.resource]; } - public attachToClassicLB(loadBalancer: elb.LoadBalancer): void { - // FIXME: If Fargate then throw a helpful error - // FIXME: Security Groups - this.loadBalancers.push({ - loadBalancerName: loadBalancer.loadBalancerName, - containerName: this.taskDefinition.defaultContainer!.name, - containerPort: this.taskDefinition.defaultContainer!.loadBalancerPort(true), - }); - this.createLoadBalancerRole(); - } - public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { // FIXME: Security Groups this.loadBalancers.push({ targetGroupArn: targetGroup.targetGroupArn, - containerName: this.taskDefinition.defaultContainer!.name, - containerPort: this.taskDefinition.defaultContainer!.loadBalancerPort(false), + containerName: this.taskDef.defaultContainer!.name, + containerPort: this.taskDef.defaultContainer!.loadBalancerPort(false), }); this.createLoadBalancerRole(); @@ -154,15 +118,15 @@ export class Service extends cdk.Construct implements elb.ILoadBalancerTarget, public attachToNetworkTargetGroup(targetGroup: elbv2.NetworkTargetGroup): elbv2.LoadBalancerTargetProps { this.loadBalancers.push({ targetGroupArn: targetGroup.targetGroupArn, - containerName: this.taskDefinition.defaultContainer!.name, - containerPort: this.taskDefinition.defaultContainer!.loadBalancerPort(false), + containerName: this.taskDef.defaultContainer!.name, + containerPort: this.taskDef.defaultContainer!.loadBalancerPort(false), }); this.createLoadBalancerRole(); return { targetType: elbv2.TargetType.SelfRegistering }; } - public createLoadBalancerRole() { + protected createLoadBalancerRole() { if (!this.role) { this.role = new iam.Role(this, 'Role', { assumedBy: new cdk.ServicePrincipal('ecs-tasks.amazonaws.com'), @@ -171,10 +135,4 @@ export class Service extends cdk.Construct implements elb.ILoadBalancerTarget, this.resource.addDependency(this.role); } } - } - -export enum LaunchType { - EC2 = 'EC2', - Fargate = 'FARGATE' -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts new file mode 100644 index 0000000000000..682a5290dd6af --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -0,0 +1,48 @@ +import elb = require('@aws-cdk/aws-elasticloadbalancing'); +import cdk = require('@aws-cdk/cdk'); +import { BaseService, BaseServiceProps } from '../base/base-service'; +import { BaseTaskDefinition } from '../base/base-task-definition'; +import { EcsCluster } from './ecs-cluster'; +import { EcsTaskDefinition } from './ecs-task-definition'; + +export interface EcsServiceProps extends BaseServiceProps { + /** + * Cluster where service will be deployed + */ + cluster: EcsCluster; // should be required? do we assume 'default' exists? + + /** + * Task Definition used for running tasks in the service + */ + taskDefinition: EcsTaskDefinition; +} + +export class EcsService extends BaseService implements elb.ILoadBalancerTarget { + protected readonly taskDef: BaseTaskDefinition; + private readonly taskDefinition: EcsTaskDefinition; + + constructor(parent: cdk.Construct, name: string, props: EcsServiceProps) { + super(parent, name, props, { + cluster: props.cluster.clusterName, + taskDefinition: props.taskDefinition.taskDefinitionArn, + launchType: 'EC2' + }); + + if (!this.taskDefinition.defaultContainer) { + throw new Error('A TaskDefinition must have at least one essential container'); + } + + this.taskDefinition = props.taskDefinition; + this.taskDef = props.taskDefinition; + } + + public attachToClassicLB(loadBalancer: elb.LoadBalancer): void { + // FIXME: Security Groups + this.loadBalancers.push({ + loadBalancerName: loadBalancer.loadBalancerName, + containerName: this.taskDefinition.defaultContainer!.name, + containerPort: this.taskDefinition.defaultContainer!.loadBalancerPort(true), + }); + this.createLoadBalancerRole(); + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts new file mode 100644 index 0000000000000..dab41cc3c70df --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -0,0 +1,37 @@ +import cdk = require('@aws-cdk/cdk'); +import { BaseService, BaseServiceProps } from '../base/base-service'; +import { BaseTaskDefinition } from '../base/base-task-definition'; +import { FargateCluster } from './fargate-cluster'; +import { FargateTaskDefinition } from './fargate-task-definition'; + +export interface FargateServiceProps extends BaseServiceProps { + /** + * Cluster where service will be deployed + */ + cluster: FargateCluster; // should be required? do we assume 'default' exists? + + /** + * Task Definition used for running tasks in the service + */ + taskDefinition: FargateTaskDefinition; +} + +export class FargateService extends BaseService { + protected readonly taskDef: BaseTaskDefinition; + private readonly taskDefinition: FargateTaskDefinition; + + constructor(parent: cdk.Construct, name: string, props: FargateServiceProps) { + super(parent, name, props, { + cluster: props.cluster.clusterName, + taskDefinition: props.taskDefinition.taskDefinitionArn, + launchType: 'FARGATE' + }); + + if (!this.taskDefinition.defaultContainer) { + throw new Error('A TaskDefinition must have at least one essential container'); + } + + this.taskDefinition = props.taskDefinition; + this.taskDef = props.taskDefinition; + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index ec49b26c2e0b9..bc7efd9f0b8b8 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -1,7 +1,10 @@ export * from './base/base-cluster'; export * from './ecs/ecs-cluster'; -// export * from './fargate/fargate-cluster'; -export * from './service'; +export * from './fargate/fargate-cluster'; + +export * from './base/base-service'; +export * from './ecs/ecs-service'; +export * from './fargate/fargate-service'; export * from './task-definition'; export * from './container-definition'; export * from './container-image'; From b8cce3aeb65ba3a2c41a236181b68e362d291498 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 1 Oct 2018 18:59:41 +0200 Subject: [PATCH 28/97] Separate ECS/Fargate TaskDefinition --- .../base-task-definition.ts} | 113 ++---------------- .../aws-ecs/lib/ecs/ecs-task-definition.ts | 81 +++++++++++++ .../lib/fargate/fargate-task-definition.ts | 48 ++++++++ packages/@aws-cdk/aws-ecs/lib/index.ts | 5 +- 4 files changed, 143 insertions(+), 104 deletions(-) rename packages/@aws-cdk/aws-ecs/lib/{task-definition.ts => base/base-task-definition.ts} (51%) create mode 100644 packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts similarity index 51% rename from packages/@aws-cdk/aws-ecs/lib/task-definition.ts rename to packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts index d41d74679f5de..46b99085c8b15 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts @@ -1,26 +1,9 @@ import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); -import { ContainerDefinition } from './container-definition'; -import { cloudformation } from './ecs.generated'; - -export interface TaskDefinitionProps { - /** - * The number of cpu units used by the task. If using the EC2 launch type, - * this field is optional. Supported values are between 128 CPU units - * (0.125 vCPUs) and 10240 CPU units (10 vCPUs). If you are using the - * Fargate launch type, this field is required and you must use one of the - * following 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 - * - * @default 256 - */ - cpu?: string; +import { ContainerDefinition } from '../container-definition'; +import { cloudformation } from '../ecs.generated'; +export interface BaseTaskDefinitionProps { /** * Namespace for task definition versions * @@ -28,52 +11,6 @@ export interface TaskDefinitionProps { */ family?: string; - /** - * The amount (in MiB) of memory used by the task. If using the EC2 launch - * type, this field is optional and any value can be used. If you are using - * the Fargate launch type, 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) - * - * @default 512 - */ - memoryMiB?: string; - - /** - * The Docker networking mode to use for the containers in the task. - * - * For Fargate or to use task networking, "awsvpc" mode is required. - * - * @default NetworkMode.Bridge - */ - networkMode?: NetworkMode; - - /** - * An array of placement constraint objects to use for the task. You can - * specify a maximum of 10 constraints per task (this limit includes - * constraints in the task definition and those specified at run time). - * - * Not supported in Fargate. - */ - placementConstraints?: PlacementConstraint[]; - - /** - * Valid values include EC2 and FARGATE. - * - * @default EC2 - */ - // requiresCompatibilities?: string[]; // FARGATE or EC2 -- set on ECS TD vs FG TD - /** * The IAM role assumed by the ECS agent. * @@ -97,11 +34,10 @@ export interface TaskDefinitionProps { volumes?: Volume[]; } -export class TaskDefinition extends cdk.Construct { +export class BaseTaskDefinition extends cdk.Construct { public readonly family: string; public readonly taskDefinitionArn: string; public readonly taskRole: iam.Role; - public readonly networkMode: NetworkMode; /** * Default container for this task @@ -112,19 +48,13 @@ export class TaskDefinition extends cdk.Construct { */ public defaultContainer?: ContainerDefinition; private readonly containers = new Array(); - private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; private executionRole?: iam.Role; - constructor(parent: cdk.Construct, name: string, props: TaskDefinitionProps) { + constructor(parent: cdk.Construct, name: string, props: BaseTaskDefinitionProps, additionalProps: any) { super(parent, name); this.family = props.family || this.uniqueId; - this.networkMode = props.networkMode || NetworkMode.Bridge; - - if (props.placementConstraints) { - props.placementConstraints.forEach(pc => this.addPlacementConstraint(pc)); - } if (props.volumes) { props.volumes.forEach(v => this.addVolume(v)); @@ -138,13 +68,10 @@ export class TaskDefinition extends cdk.Construct { const taskDef = new cloudformation.TaskDefinitionResource(this, 'Resource', { containerDefinitions: new cdk.Token(() => this.containers.map(x => x.renderContainerDefinition())), - cpu: props.cpu, executionRoleArn: new cdk.Token(() => this.executionRole && this.executionRole.roleArn), family: this.family, - memory: props.memoryMiB, - networkMode: this.networkMode, - placementConstraints: new cdk.Token(() => this.placementConstraints), - taskRoleArn: this.taskRole.roleArn + taskRoleArn: this.taskRole.roleArn, + ...additionalProps }); this.taskDefinitionArn = taskDef.taskDefinitionArn; @@ -170,26 +97,11 @@ export class TaskDefinition extends cdk.Construct { } } - /** - * Constrain where this task can be placed - */ - private addPlacementConstraint(constraint: PlacementConstraint) { - const pc = this.renderPlacementConstraint(constraint); - this.placementConstraints.push(pc); - } - private addVolume(volume: Volume) { // const v = this.renderVolume(volume); this.volumes.push(volume); } - private renderPlacementConstraint(pc: PlacementConstraint): cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty { - return { - type: pc.type, - expression: pc.expression - }; - } - /** * Generate a default execution role that allows pulling from ECR */ @@ -231,14 +143,9 @@ export enum NetworkMode { Host = 'host', } -export interface PlacementConstraint { - expression?: string; - type: PlacementConstraintType; -} - -export enum PlacementConstraintType { - DistinctInstance = "distinctInstance", - MemberOf = "memberOf" +export enum Compatibilities { + Ec2 = "EC2", + Fargate = "FARGATE" } export interface Volume { diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts new file mode 100644 index 0000000000000..ca82b837bf634 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts @@ -0,0 +1,81 @@ +import cdk = require('@aws-cdk/cdk'); +import { BaseTaskDefinition, BaseTaskDefinitionProps, Compatibilities, NetworkMode } from '../base/base-task-definition'; +import { cloudformation } from '../ecs.generated'; + +export interface EcsTaskDefinitionProps extends BaseTaskDefinitionProps { + /** + * The number of cpu units used by the task. If using the EC2 launch type, + * this field is optional. Supported values are between 128 CPU units + * (0.125 vCPUs) and 10240 CPU units (10 vCPUs). + * + * @default 256 + */ + cpu?: string; + + /** + * The amount (in MiB) of memory used by the task. If using the EC2 launch type, this field is optional and any value + * can be used. + * + * @default 512 + */ + memoryMiB?: string; + + /** + * The Docker networking mode to use for the containers in the task. + * + * @default NetworkMode.Bridge + */ + networkMode?: NetworkMode; + + /** + * An array of placement constraint objects to use for the task. You can + * specify a maximum of 10 constraints per task (this limit includes + * constraints in the task definition and those specified at run time). + * + * Not supported in Fargate. + */ + placementConstraints?: PlacementConstraint[]; +} + +export class EcsTaskDefinition extends BaseTaskDefinition { + private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; + + constructor(parent: cdk.Construct, name: string, props: EcsTaskDefinitionProps) { + super(parent, name, props, { + cpu: props.cpu, + memoryMiB: props.memoryMiB, + networkMode: props.networkMode || NetworkMode.Bridge, + requiresCompatibilities: Compatibilities.Ec2, + placementConstraints: new cdk.Token(() => this.placementConstraints) + }); + + if (props.placementConstraints) { + props.placementConstraints.forEach(pc => this.addPlacementConstraint(pc)); + } + } + + /** + * Constrain where tasks can be placed + */ + private addPlacementConstraint(constraint: PlacementConstraint) { + const pc = this.renderPlacementConstraint(constraint); + this.placementConstraints.push(pc); + } + + private renderPlacementConstraint(pc: PlacementConstraint): cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty { + return { + type: pc.type, + expression: pc.expression + }; + } +} + +export interface PlacementConstraint { + expression?: string; + type: PlacementConstraintType; +} + +export enum PlacementConstraintType { + DistinctInstance = "distinctInstance", + MemberOf = "memberOf" +} diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts new file mode 100644 index 0000000000000..d366c28ba2cf1 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts @@ -0,0 +1,48 @@ +import cdk = require('@aws-cdk/cdk'); +import { BaseTaskDefinition, BaseTaskDefinitionProps, Compatibilities, NetworkMode } from '../base/base-task-definition'; + +export interface FargateTaskDefinitionProps extends BaseTaskDefinitionProps { + /** + * 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 + * + * @default 256 + */ + 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) + * + * @default 512 + */ + memoryMiB: string; +} + +export class FargateTaskDefinition extends BaseTaskDefinition { + constructor(parent: cdk.Construct, name: string, props: FargateTaskDefinitionProps) { + super(parent, name, props, { + cpu: props.cpu, + memoryMiB: props.memoryMiB, + networkMode: NetworkMode.AwsVpc, + requiresCompatibilities: Compatibilities.Fargate + }); + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index bc7efd9f0b8b8..5c3170c2fe003 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -5,7 +5,10 @@ export * from './fargate/fargate-cluster'; export * from './base/base-service'; export * from './ecs/ecs-service'; export * from './fargate/fargate-service'; -export * from './task-definition'; + +export * from './base/base-task-definition'; +export * from './ecs/ecs-task-definition'; +export * from './fargate/fargate-task-definition'; export * from './container-definition'; export * from './container-image'; export * from './linux-parameters'; From 274160811aa4ebb73dd3ff81697ba1e38eda5cc6 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 1 Oct 2018 19:00:05 +0200 Subject: [PATCH 29/97] Fix lint errors --- packages/@aws-cdk/aws-ecs/lib/container-definition.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 98a9a50ec63a0..95080b18a446d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -317,7 +317,7 @@ function renderHealthCheck(hc: HealthCheck): cloudformation.TaskDefinitionResour } function getHealthCheckCommand(hc: HealthCheck): string[] { - let cmd = hc.command; + const cmd = hc.command; const hcCommand = new Array(); if (cmd.length === 0) { @@ -330,7 +330,7 @@ function getHealthCheckCommand(hc: HealthCheck): string[] { } if (cmd[0] !== "CMD" || cmd[0] !== 'CMD-SHELL') { - hcCommand.push('CMD') + hcCommand.push('CMD'); } return hcCommand.concat(cmd); From a901912f4bbf7961d98a84d0fef405aa486ca8c8 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 1 Oct 2018 19:00:05 +0200 Subject: [PATCH 30/97] Fix lint errors --- packages/@aws-cdk/aws-ecs/lib/base/base-service.ts | 3 ++- packages/@aws-cdk/aws-ecs/lib/container-definition.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 7e50462ae510b..cb9be35c47ce8 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -64,7 +64,8 @@ export interface BaseServiceProps { //////////////////////////////////////////// } -export abstract class BaseService extends cdk.Construct implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { +export abstract class BaseService extends cdk.Construct + implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { public readonly dependencyElements: cdk.IDependable[]; public readonly connections: ec2.Connections; protected loadBalancers = new Array(); diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 98a9a50ec63a0..95080b18a446d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -317,7 +317,7 @@ function renderHealthCheck(hc: HealthCheck): cloudformation.TaskDefinitionResour } function getHealthCheckCommand(hc: HealthCheck): string[] { - let cmd = hc.command; + const cmd = hc.command; const hcCommand = new Array(); if (cmd.length === 0) { @@ -330,7 +330,7 @@ function getHealthCheckCommand(hc: HealthCheck): string[] { } if (cmd[0] !== "CMD" || cmd[0] !== 'CMD-SHELL') { - hcCommand.push('CMD') + hcCommand.push('CMD'); } return hcCommand.concat(cmd); From 51adff56276ff003d6d908ac7b8e310670ddce4e Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 11:09:59 +0200 Subject: [PATCH 31/97] Fix bugs in service and task def --- packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 6 +++--- packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts | 2 +- .../@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index 682a5290dd6af..cd3ff5b7be29f 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -28,12 +28,12 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { launchType: 'EC2' }); + this.taskDefinition = props.taskDefinition; + this.taskDef = props.taskDefinition; + if (!this.taskDefinition.defaultContainer) { throw new Error('A TaskDefinition must have at least one essential container'); } - - this.taskDefinition = props.taskDefinition; - this.taskDef = props.taskDefinition; } public attachToClassicLB(loadBalancer: elb.LoadBalancer): void { diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts index ca82b837bf634..02d9f3de2b894 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts @@ -45,7 +45,7 @@ export class EcsTaskDefinition extends BaseTaskDefinition { cpu: props.cpu, memoryMiB: props.memoryMiB, networkMode: props.networkMode || NetworkMode.Bridge, - requiresCompatibilities: Compatibilities.Ec2, + requiresCompatibilities: [Compatibilities.Ec2], placementConstraints: new cdk.Token(() => this.placementConstraints) }); diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts index d366c28ba2cf1..a1ac36c29b47c 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts @@ -42,7 +42,7 @@ export class FargateTaskDefinition extends BaseTaskDefinition { cpu: props.cpu, memoryMiB: props.memoryMiB, networkMode: NetworkMode.AwsVpc, - requiresCompatibilities: Compatibilities.Fargate + requiresCompatibilities: [Compatibilities.Fargate] }); } } From 70ae6827879bc8abbf2695281e914d8a64fd5eb9 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 11:26:10 +0200 Subject: [PATCH 32/97] Fix AMI ID getter for ECS clusters --- .gitignore | 1 + packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 5a03c2b774932..7d57ce2c8dfe0 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ coverage .nyc_output .LAST_BUILD *.swp +examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index a4231f12b07e1..a35a54c29ccb2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -86,12 +86,14 @@ export class EcsCluster extends BaseCluster { * Construct a Linux machine image from the latest ECS Optimized AMI published in SSM */ export class EcsOptimizedAmi implements ec2.IMachineImageSource { - private static AmiParamterName = "/aws/service/ecs/optimized-ami/amazon-linux/recommended"; + private static AmiParameterName = "/aws/service/ecs/optimized-ami/amazon-linux/recommended"; public getImage(parent: cdk.Construct): ec2.MachineImage { const ssmProvider = new cdk.SSMParameterProvider(parent); - const ami = ssmProvider.getString(EcsOptimizedAmi.AmiParamterName); + const json = ssmProvider.getString(EcsOptimizedAmi.AmiParameterName); + const ami = JSON.parse(json).image_id; + return new ec2.MachineImage(ami, new ec2.LinuxOS()); } } From 298e0a796daef9f61588bf718b7ab6f69e7b91bc Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 11:26:29 +0200 Subject: [PATCH 33/97] Add Capabilities in Linux Parameters on ContainerDefs --- .../hello-cdk-ecs/index.ts | 99 ++++++++++--------- .../@aws-cdk/aws-ecs/lib/linux-parameters.ts | 80 +++++++++++++-- 2 files changed, 128 insertions(+), 51 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 3a2a0340b56e5..d520648fbc19f 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -3,51 +3,60 @@ 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, 'DemoCluster', { - vpc - }); - - // name, image, cpu, memory, port (with default) - // - // Include in constructs: - // - networking - include SD, ALB - // - logging - cloudwatch logs integration? talk to nathan about 3rd - // party integrations - aggregated logging across the service - // (instead of per task). Probably prometheus or elk? - // - tracing aws-xray-fargate - CNCF opentracing standard - jaeger, - // zipkin. - // - so x-ray is a container that is hooked up to sidecars that come - // with the application container itself - // - autoscaling - application autoscaling (Fargate focused?) - - const taskDefinition = new ecs.TaskDefinition(this, "MyTD", { - family: "ecs-task-definition", - placementConstraints: [{ - type: ecs.PlacementConstraintType.DistinctInstance - }], - }); - - taskDefinition.addContainer(new ecs.ContainerDefinition(this, 'Def', { - name: "web", - image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), - cpu: 1024, - memoryMiB: 512, - essential: true - })); - - cluster.runService(taskDefinition); - } + 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.EcsCluster(this, 'EcsCluster', { + vpc + }); + + // name, image, cpu, memory, port (with default) + // + // Include in constructs: + // - networking - include SD, ALB + // - logging - cloudwatch logs integration? talk to nathan about 3rd + // party integrations - aggregated logging across the service + // (instead of per task). Probably prometheus or elk? + // - tracing aws-xray-fargate - CNCF opentracing standard - jaeger, + // zipkin. + // - so x-ray is a container that is hooked up to sidecars that come + // with the application container itself + // - autoscaling - application autoscaling (Fargate focused?) + + const taskDefinition = new ecs.EcsTaskDefinition(this, "EcsTD", { + family: "ecs-task-definition", + placementConstraints: [{ + type: ecs.PlacementConstraintType.DistinctInstance + }], + }); + + const container = new ecs.ContainerDefinition(this, 'Container', { + name: "web", + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + cpu: 1024, + memoryMiB: 512, + essential: true + }); + + container.linuxParameters.addCapability(ecs.Capability.All); + + taskDefinition.addContainer(container); + + new ecs.EcsService(this, "EcsService", { + cluster, + taskDefinition, + desiredCount: 1, + }); + // cluster.runService(taskDefinition); + } } const app = new cdk.App(process.argv); diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts index b9ea35bb61317..1ef970fcf3e19 100644 --- a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -1,16 +1,84 @@ import { cloudformation } from './ecs.generated'; export class LinuxParameters { + public initProcessEnabled?: boolean; - public addCapability() { - // FIXME + public sharedMemorySize?: number; + + private readonly addCapabilities: Capability[] = []; + + private readonly dropCapabilities: Capability[] = []; + + // private readonly devices: Device[] = []; + + // private readonly tmpfs: Tmpfs[] = []; + + /** + * Only works with EC2 launch type + */ + public addCapability(...cap: Capability[]) { + this.addCapabilities.push(...cap); } - public dropCapability() { - // FIXME + public dropCapability(...cap: Capability[]) { + this.dropCapabilities.push(...cap); } public toLinuxParametersJson(): cloudformation.TaskDefinitionResource.LinuxParametersProperty { - return {}; + return { + initProcessEnabled: this.initProcessEnabled, + sharedMemorySize: this.sharedMemorySize, + capabilities: { + add: this.addCapabilities, + drop: this.dropCapabilities, + } + }; } -} \ No newline at end of file +} + +// export interface Device { +// } + +// export interface Tmpfs { +// } + +export enum Capability { + All = "ALL", + AuditControl = "AUDIT_CONTROL", + AuditWrite = "AUDIT_WRITE", + BlockSuspend = "BLOCK_SUSPEND", + Chown = "CHOWN", + DacOverride = "DAC_OVERRIDE", + DacReadSearch = "DAC_READ_SEARCH", + Fowner = "FOWNER", + Fsetid = "FSETID", + IpcLock = "IPC_LOCK", + IpcOwner = "IPC_OWNER", + Kill = "KILL", + Lease = "LEASE", + LinuxImmutable = "LINUX_IMMUTABLE", + MacAdmin = "MAC_ADMIN", + MacOverride = "MAC_OVERRIDE", + Mknod = "MKNOD", + NetAdmin = "NET_ADMIN", + NetBindService = "NET_BIND_SERVICE", + NetBroadcast = "NET_BROADCAST", + NetRaw = "NET_RAW", + Setfcap = "SETFCAP", + Setgid = "SETGID", + Setpcap = "SETPCAP", + Setuid = "SETUID", + SysAdmin = "SYS_ADMIN", + SysBoot = "SYS_BOOT", + SysChroot = "SYS_CHROOT", + SysModule = "SYS_MODULE", + SysNice = "SYS_NICE", + SysPacct = "SYS_PACCT", + SysPtrace = "SYS_PTRACE", + SysRawio = "SYS_RAWIO", + SysResource = "SYS_RESOURCE", + SysTime = "SYS_TIME", + SysTtyConfig = "SYS_TTY_CONFIG", + Syslog = "SYSLOG", + WakeAlarm = "WAKE_ALARM" +} From 1541edd645e65c2f5fdbf7cd9b790a427027e571 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 11:26:29 +0200 Subject: [PATCH 34/97] Add Capabilities in Linux Parameters on ContainerDefs --- .../hello-cdk-ecs/index.ts | 99 ++++++++++--------- .../@aws-cdk/aws-ecs/lib/linux-parameters.ts | 80 +++++++++++++-- 2 files changed, 128 insertions(+), 51 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 3a2a0340b56e5..d520648fbc19f 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -3,51 +3,60 @@ 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, 'DemoCluster', { - vpc - }); - - // name, image, cpu, memory, port (with default) - // - // Include in constructs: - // - networking - include SD, ALB - // - logging - cloudwatch logs integration? talk to nathan about 3rd - // party integrations - aggregated logging across the service - // (instead of per task). Probably prometheus or elk? - // - tracing aws-xray-fargate - CNCF opentracing standard - jaeger, - // zipkin. - // - so x-ray is a container that is hooked up to sidecars that come - // with the application container itself - // - autoscaling - application autoscaling (Fargate focused?) - - const taskDefinition = new ecs.TaskDefinition(this, "MyTD", { - family: "ecs-task-definition", - placementConstraints: [{ - type: ecs.PlacementConstraintType.DistinctInstance - }], - }); - - taskDefinition.addContainer(new ecs.ContainerDefinition(this, 'Def', { - name: "web", - image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), - cpu: 1024, - memoryMiB: 512, - essential: true - })); - - cluster.runService(taskDefinition); - } + 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.EcsCluster(this, 'EcsCluster', { + vpc + }); + + // name, image, cpu, memory, port (with default) + // + // Include in constructs: + // - networking - include SD, ALB + // - logging - cloudwatch logs integration? talk to nathan about 3rd + // party integrations - aggregated logging across the service + // (instead of per task). Probably prometheus or elk? + // - tracing aws-xray-fargate - CNCF opentracing standard - jaeger, + // zipkin. + // - so x-ray is a container that is hooked up to sidecars that come + // with the application container itself + // - autoscaling - application autoscaling (Fargate focused?) + + const taskDefinition = new ecs.EcsTaskDefinition(this, "EcsTD", { + family: "ecs-task-definition", + placementConstraints: [{ + type: ecs.PlacementConstraintType.DistinctInstance + }], + }); + + const container = new ecs.ContainerDefinition(this, 'Container', { + name: "web", + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + cpu: 1024, + memoryMiB: 512, + essential: true + }); + + container.linuxParameters.addCapability(ecs.Capability.All); + + taskDefinition.addContainer(container); + + new ecs.EcsService(this, "EcsService", { + cluster, + taskDefinition, + desiredCount: 1, + }); + // cluster.runService(taskDefinition); + } } const app = new cdk.App(process.argv); diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts index b9ea35bb61317..1be5327c704ab 100644 --- a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -1,16 +1,84 @@ import { cloudformation } from './ecs.generated'; export class LinuxParameters { + public initProcessEnabled?: boolean; - public addCapability() { - // FIXME + public sharedMemorySize?: number; + + private readonly addCapabilities: Capability[] = []; + + private readonly dropCapabilities: Capability[] = []; + + // private readonly devices: Device[] = []; + + // private readonly tmpfs: Tmpfs[] = []; + + /** + * AddCapability only works with EC2 launch type + */ + public addCapability(...cap: Capability[]) { + this.addCapabilities.push(...cap); } - public dropCapability() { - // FIXME + public dropCapability(...cap: Capability[]) { + this.dropCapabilities.push(...cap); } public toLinuxParametersJson(): cloudformation.TaskDefinitionResource.LinuxParametersProperty { - return {}; + return { + initProcessEnabled: this.initProcessEnabled, + sharedMemorySize: this.sharedMemorySize, + capabilities: { + add: this.addCapabilities, + drop: this.dropCapabilities, + } + }; } -} \ No newline at end of file +} + +// export interface Device { +// } + +// export interface Tmpfs { +// } + +export enum Capability { + All = "ALL", + AuditControl = "AUDIT_CONTROL", + AuditWrite = "AUDIT_WRITE", + BlockSuspend = "BLOCK_SUSPEND", + Chown = "CHOWN", + DacOverride = "DAC_OVERRIDE", + DacReadSearch = "DAC_READ_SEARCH", + Fowner = "FOWNER", + Fsetid = "FSETID", + IpcLock = "IPC_LOCK", + IpcOwner = "IPC_OWNER", + Kill = "KILL", + Lease = "LEASE", + LinuxImmutable = "LINUX_IMMUTABLE", + MacAdmin = "MAC_ADMIN", + MacOverride = "MAC_OVERRIDE", + Mknod = "MKNOD", + NetAdmin = "NET_ADMIN", + NetBindService = "NET_BIND_SERVICE", + NetBroadcast = "NET_BROADCAST", + NetRaw = "NET_RAW", + Setfcap = "SETFCAP", + Setgid = "SETGID", + Setpcap = "SETPCAP", + Setuid = "SETUID", + SysAdmin = "SYS_ADMIN", + SysBoot = "SYS_BOOT", + SysChroot = "SYS_CHROOT", + SysModule = "SYS_MODULE", + SysNice = "SYS_NICE", + SysPacct = "SYS_PACCT", + SysPtrace = "SYS_PTRACE", + SysRawio = "SYS_RAWIO", + SysResource = "SYS_RESOURCE", + SysTime = "SYS_TIME", + SysTtyConfig = "SYS_TTY_CONFIG", + Syslog = "SYSLOG", + WakeAlarm = "WAKE_ALARM" +} From a154d0935eeb2bd37aebea5890f23decee390a50 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 11:40:53 +0200 Subject: [PATCH 35/97] Add default arg to SSMProvider.getString --- .gitignore | 2 +- examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json | 3 ++- packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts | 2 +- packages/@aws-cdk/cdk/lib/context.ts | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 7d57ce2c8dfe0..bd75b435a1ac4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ coverage .nyc_output .LAST_BUILD *.swp -examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json +./examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json b/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json index 841c948f0b918..eb5f700d36513 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json @@ -36,6 +36,7 @@ "eu-west-1a", "eu-west-1b", "eu-west-1c" - ] + ], + "ssm:794715269151:us-west-2:/aws/service/ecs/optimized-ami/amazon-linux/recommended": "{\"schema_version\":1,\"image_name\":\"amzn-ami-2018.03.g-amazon-ecs-optimized\",\"image_id\":\"ami-00430184c7bb49914\",\"os\":\"Amazon Linux\",\"ecs_runtime_version\":\"Docker version 18.06.1-ce\",\"ecs_agent_version\":\"1.20.3\"}" } } diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index a35a54c29ccb2..20928345a8f00 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -91,7 +91,7 @@ export class EcsOptimizedAmi implements ec2.IMachineImageSource { public getImage(parent: cdk.Construct): ec2.MachineImage { const ssmProvider = new cdk.SSMParameterProvider(parent); - const json = ssmProvider.getString(EcsOptimizedAmi.AmiParameterName); + const json = ssmProvider.getString(EcsOptimizedAmi.AmiParameterName, "{\"image_id\": \"\"}"); const ami = JSON.parse(json).image_id; return new ec2.MachineImage(ami, new ec2.LinuxOS()); diff --git a/packages/@aws-cdk/cdk/lib/context.ts b/packages/@aws-cdk/cdk/lib/context.ts index 3c17890b6e129..e63e6aa58d201 100644 --- a/packages/@aws-cdk/cdk/lib/context.ts +++ b/packages/@aws-cdk/cdk/lib/context.ts @@ -151,9 +151,9 @@ export class SSMParameterProvider { /** * Return the SSM parameter string with the indicated key */ - public getString(parameterName: string): any { + public getString(parameterName: string, defaultValue: string = "dummy"): any { const scope = this.provider.accountRegionScope('SSMParameterProvider'); - return this.provider.getStringValue(SSM_PARAMETER_PROVIDER, scope, [parameterName], 'dummy'); + return this.provider.getStringValue(SSM_PARAMETER_PROVIDER, scope, [parameterName], defaultValue); } } From 4b466853ad3703334e3effa71a96c8f895d13a50 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 14:00:35 +0200 Subject: [PATCH 36/97] Add Devices to LinuxParameters --- .../hello-cdk-ecs/index.ts | 15 +++++++--- .../@aws-cdk/aws-ecs/lib/linux-parameters.ts | 30 ++++++++++++++++--- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index d520648fbc19f..e1095d97a30e3 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -33,9 +33,10 @@ class BonjourECS extends cdk.Stack { const taskDefinition = new ecs.EcsTaskDefinition(this, "EcsTD", { family: "ecs-task-definition", - placementConstraints: [{ - type: ecs.PlacementConstraintType.DistinctInstance - }], + // placementConstraints: [{ + // type: ecs.PlacementConstraintType.MemberOf, + // expression: "attribute:ecs.instance-type =~ t2.*" + // }], }); const container = new ecs.ContainerDefinition(this, 'Container', { @@ -48,6 +49,12 @@ class BonjourECS extends cdk.Stack { container.linuxParameters.addCapability(ecs.Capability.All); + container.linuxParameters.addDevice({ + containerPath: "/pudding", + hostPath: "/dev/sda", + permissions: [ecs.DevicePermission.Read] + }); + taskDefinition.addContainer(container); new ecs.EcsService(this, "EcsService", { @@ -61,6 +68,6 @@ class BonjourECS extends cdk.Stack { const app = new cdk.App(process.argv); -new BonjourECS(app, 'GoedeMorgen'); +new BonjourECS(app, 'Bonjour'); process.stdout.write(app.run()); diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts index 1be5327c704ab..b973e646182e1 100644 --- a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -9,7 +9,7 @@ export class LinuxParameters { private readonly dropCapabilities: Capability[] = []; - // private readonly devices: Device[] = []; + private readonly devices: Device[] = []; // private readonly tmpfs: Tmpfs[] = []; @@ -24,6 +24,10 @@ export class LinuxParameters { this.dropCapabilities.push(...cap); } + public addDevice(...device: Device[]) { + this.devices.push(...device); + } + public toLinuxParametersJson(): cloudformation.TaskDefinitionResource.LinuxParametersProperty { return { initProcessEnabled: this.initProcessEnabled, @@ -31,13 +35,25 @@ export class LinuxParameters { capabilities: { add: this.addCapabilities, drop: this.dropCapabilities, - } + }, + devices: this.devices.map(renderDevice) }; } } -// export interface Device { -// } +export interface Device { + containerPath?: string, + hostPath: string, + permissions?: DevicePermission[] +} + +function renderDevice(device: Device): cloudformation.TaskDefinitionResource.DeviceProperty { + return { + containerPath: device.containerPath, + hostPath: device.hostPath, + permissions: device.permissions + } +} // export interface Tmpfs { // } @@ -82,3 +98,9 @@ export enum Capability { Syslog = "SYSLOG", WakeAlarm = "WAKE_ALARM" } + +export enum DevicePermission { + Read = "read", + Write = "write", + Mknod = "mknod", +} From 9875c6e5b7d0aac370f446ca4d34849320fca737 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 14:20:50 +0200 Subject: [PATCH 37/97] Add Tmpfs to LinuxParameters --- .../hello-cdk-ecs/index.ts | 6 ++ .../aws-ecs/lib/container-definition.ts | 2 +- .../@aws-cdk/aws-ecs/lib/linux-parameters.ts | 68 +++++++++++++++++-- 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index e1095d97a30e3..2a054531fee2d 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -55,6 +55,12 @@ class BonjourECS extends cdk.Stack { permissions: [ecs.DevicePermission.Read] }); + container.linuxParameters.addTmpfs({ + containerPath: "/pudding", + size: 12345, + mountOptions: [ecs.TmpfsMountOption.Ro] + }); + taskDefinition.addContainer(container); new ecs.EcsService(this, "EcsService", { diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 95080b18a446d..f3827fc5be7d8 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -240,7 +240,7 @@ export class ContainerDefinition extends cdk.Construct { extraHosts: this.props.extraHosts && renderKV(this.props.extraHosts, 'hostname', 'ipAddress'), healthCheck: this.props.healthCheck && renderHealthCheck(this.props.healthCheck), links: this.links, - linuxParameters: this.linuxParameters.toLinuxParametersJson(), + linuxParameters: this.linuxParameters.renderLinuxParameters(), }; } } diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts index b973e646182e1..21a149310b65b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -11,7 +11,7 @@ export class LinuxParameters { private readonly devices: Device[] = []; - // private readonly tmpfs: Tmpfs[] = []; + private readonly tmpfs: Tmpfs[] = []; /** * AddCapability only works with EC2 launch type @@ -28,7 +28,11 @@ export class LinuxParameters { this.devices.push(...device); } - public toLinuxParametersJson(): cloudformation.TaskDefinitionResource.LinuxParametersProperty { + public addTmpfs(...tmpfs: Tmpfs[]) { + this.tmpfs.push(...tmpfs); + } + + public renderLinuxParameters(): cloudformation.TaskDefinitionResource.LinuxParametersProperty { return { initProcessEnabled: this.initProcessEnabled, sharedMemorySize: this.sharedMemorySize, @@ -36,7 +40,8 @@ export class LinuxParameters { add: this.addCapabilities, drop: this.dropCapabilities, }, - devices: this.devices.map(renderDevice) + devices: this.devices.map(renderDevice), + tmpfs: this.tmpfs.map(renderTmpfs) }; } } @@ -55,8 +60,19 @@ function renderDevice(device: Device): cloudformation.TaskDefinitionResource.Dev } } -// export interface Tmpfs { -// } +export interface Tmpfs { + containerPath: string, + size: number, + mountOptions?: TmpfsMountOption[], +} + +function renderTmpfs(tmpfs: Tmpfs): cloudformation.TaskDefinitionResource.TmpfsProperty { + return { + containerPath: tmpfs.containerPath, + size: tmpfs.size, + mountOptions: tmpfs.mountOptions + } +} export enum Capability { All = "ALL", @@ -104,3 +120,45 @@ export enum DevicePermission { Write = "write", Mknod = "mknod", } + +export enum TmpfsMountOption { + Defaults = "defaults", + Ro = "ro", + Rw = "rw", + Suid = "suid", + Nosuid = "nosuid", + Dev = "dev", + Nodev = "nodev", + Exec = "exec", + Noexec = "noexec", + Sync = "sync", + Async = "async", + Dirsync = "dirsync", + Remount = "remount", + Mand = "mand", + Nomand = "nomand", + Atime = "atime", + Noatime = "noatime", + Diratime = "diratime", + Nodiratime = "nodiratime", + Bind = "bind", + Rbind = "rbind", + Unbindable = "unbindable", + Runbindable = "runbindable", + Private = "private", + Rprivate = "rprivate", + Shared = "shared", + Rshared = "rshared", + Slave = "slave", + Rslave = "rslave", + Relatime = "relatime", + Norelatime = "norelatime", + Strictatime = "strictatime", + Nostrictatime = "nostrictatime", + Mode = "mode", + Uid = "uid", + Gid = "gid", + NrInodes = "nr_inodes", + NrBlocks = "nr_blocks", + Mpol = "mpol" +} From 1044d1c4e3a3564008fcfc0ac80e34b78735145e Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 14:26:05 +0200 Subject: [PATCH 38/97] Update ecs demo with full Linux Parameters example --- examples/cdk-examples-typescript/hello-cdk-ecs/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 2a054531fee2d..ef757482685b1 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -61,6 +61,9 @@ class BonjourECS extends cdk.Stack { mountOptions: [ecs.TmpfsMountOption.Ro] }); + container.linuxParameters.sharedMemorySize = 65535 + container.linuxParameters.initProcessEnabled = true + taskDefinition.addContainer(container); new ecs.EcsService(this, "EcsService", { From e7f8000706e558d57408d9e0237694f4b4c31295 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 14:50:30 +0200 Subject: [PATCH 39/97] Rename renderLogDriver --- packages/@aws-cdk/aws-ecs/lib/container-definition.ts | 3 +-- packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index f3827fc5be7d8..f6ab4dfe0c576 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -235,7 +235,7 @@ export class ContainerDefinition extends cdk.Construct { user: this.props.user, volumesFrom: [], // FIXME workingDirectory: this.props.workingDirectory, - logConfiguration: this.props.logging && this.props.logging.toLogDriverJson(), + logConfiguration: this.props.logging && this.props.logging.renderLogDriver(), environment: this.props.environment && renderKV(this.props.environment, 'name', 'value'), extraHosts: this.props.extraHosts && renderKV(this.props.extraHosts, 'hostname', 'ipAddress'), healthCheck: this.props.healthCheck && renderHealthCheck(this.props.healthCheck), @@ -295,7 +295,6 @@ export interface HealthCheck { // mountPoints?: mountPoint[]; // portMappings?: portMapping[]; -// ulimits?: ulimit[]; // volumesFrom?: volumeFrom[]; function renderKV(env: {[key: string]: string}, keyName: string, valueName: string): any { diff --git a/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts b/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts index 65b34e4b51737..8e8caa8f5e8e1 100644 --- a/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts +++ b/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts @@ -64,7 +64,7 @@ export class AwsLogDriver extends LogDriver { /** * Return the log driver CloudFormation JSON */ - public toLogDriverJson(): cloudformation.TaskDefinitionResource.LogConfigurationProperty { + public renderLogDriver(): cloudformation.TaskDefinitionResource.LogConfigurationProperty { return { logDriver: 'awslogs', options: removeEmpty({ @@ -88,4 +88,4 @@ function removeEmpty(x: {[key: string]: (T | undefined)}): {[key: string]: T} } } return x as any; -} \ No newline at end of file +} From e7f7b7d824c5289be4a24d6ceeb16ebeb2e6eaf8 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 2 Oct 2018 14:54:08 +0200 Subject: [PATCH 40/97] Do most of Service --- .../@aws-cdk/aws-ecs/lib/base/base-cluster.ts | 5 + .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 94 ++++++--- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 188 +++++++++++++++++- .../aws-ecs/lib/ecs/ecs-task-definition.ts | 10 +- .../aws-ecs/lib/fargate/fargate-service.ts | 32 ++- 5 files changed, 298 insertions(+), 31 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts index f416cf46351dd..b9944cf4e1344 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts @@ -17,6 +17,10 @@ export interface BaseClusterProps { } export class BaseCluster extends cdk.Construct { + /** + * The VPC this cluster was created in + */ + public readonly vpc: ec2.VpcNetworkRef; public readonly clusterArn: string; @@ -27,6 +31,7 @@ export class BaseCluster extends cdk.Construct { const cluster = new cloudformation.ClusterResource(this, 'Resource', {clusterName: props.clusterName}); + this.vpc = props.vpc; this.clusterArn = cluster.clusterArn; this.clusterName = cluster.ref; } diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index cb9be35c47ce8..1205f66642164 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -52,41 +52,31 @@ export interface BaseServiceProps { */ healthCheckGracePeriodSeconds?: number; - ///////// TBD /////////////////////////////// - // healthCheckGracePeriodSeconds?: number; // only needed with load balancers - // loadBalancers?: LoadBalancer[]; - // placementConstraints?: PlacementConstraint[]; - // placementStrategies?: PlacementStrategy[]; - // networkConfiguration?: NetworkConfiguration; - // serviceRegistries?: ServiceRegistry[]; - // - // platformVersion?: string; // FARGATE ONLY. default is LATEST. Other options: 1.2.0, 1.1.0, 1.0.0 - //////////////////////////////////////////// + /** + * Fargate platform version to run this service on + * + * Unless you have specific compatibility requirements, you don't need to + * specify this. + * + * @default Latest + */ + platformVersion?: FargatePlatformVersion; } export abstract class BaseService extends cdk.Construct implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { public readonly dependencyElements: cdk.IDependable[]; - public readonly connections: ec2.Connections; + public abstract readonly connections: ec2.Connections; protected loadBalancers = new Array(); + protected networkConfiguration?: cloudformation.ServiceResource.NetworkConfigurationProperty; protected readonly abstract taskDef: BaseTaskDefinition; + protected _securityGroup?: ec2.SecurityGroupRef; private role?: iam.Role; private readonly resource: cloudformation.ServiceResource; constructor(parent: cdk.Construct, name: string, props: BaseServiceProps, additionalProps: any) { super(parent, name); - this.connections = new ec2.Connections({ - securityGroupRule: { - canInlineRule: false, - toEgressRuleJSON() { return {}; }, - toIngressRuleJSON() { return {}; }, - uniqueId: '' - }, - }); - - // this.taskDefinition = props.taskDefinition; - this.role = props.role; this.resource = new cloudformation.ServiceResource(this, "Service", { @@ -98,6 +88,8 @@ export abstract class BaseService extends cdk.Construct minimumHealthyPercent: props.minimumHealthyPercent }, role: new cdk.Token(() => this.role && this.role.roleArn), + networkConfiguration: this.networkConfiguration, + platformVersion: props.platformVersion, ...additionalProps }); @@ -105,7 +97,6 @@ export abstract class BaseService extends cdk.Construct } public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { - // FIXME: Security Groups this.loadBalancers.push({ targetGroupArn: targetGroup.targetGroupArn, containerName: this.taskDef.defaultContainer!.name, @@ -127,6 +118,10 @@ export abstract class BaseService extends cdk.Construct return { targetType: elbv2.TargetType.SelfRegistering }; } + public get securityGroup(): ec2.SecurityGroupRef { + return this._securityGroup!; + } + protected createLoadBalancerRole() { if (!this.role) { this.role = new iam.Role(this, 'Role', { @@ -136,4 +131,57 @@ export abstract class BaseService extends cdk.Construct this.resource.addDependency(this.role); } } + + // tslint:disable-next-line:max-line-length + protected configureAwsVpcNetworking(vpc: ec2.VpcNetworkRef, assignPublicIp?: boolean, vpcPlacement?: ec2.VpcPlacementStrategy, securityGroup?: ec2.SecurityGroupRef) { + if (vpcPlacement === undefined) { + vpcPlacement = { subnetsToUse: assignPublicIp ? ec2.SubnetType.Public : ec2.SubnetType.Private }; + } + if (securityGroup === undefined) { + securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { vpc }); + } + const subnets = vpc.subnets(vpcPlacement); + this._securityGroup = securityGroup; + + this.networkConfiguration = { + awsvpcConfiguration: { + assignPublicIp : assignPublicIp ? 'ENABLED' : 'DISABLED', + subnets: subnets.map(x => x.subnetId), + securityGroups: new cdk.Token(() => [securityGroup!.securityGroupId]), + } + }; + } } + +/** + * Fargate platform version + * + * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/platform_versions.html + */ +export enum FargatePlatformVersion { + /** + * The latest, recommended platform version + */ + Latest = 'LATEST', + + /** + * Version 1.2 + * + * Supports private registries. + */ + Version12 = '1.2.0', + + /** + * Version 1.1.0 + * + * Supports task metadata, health checks, service discovery. + */ + Version11 = '1.1.0', + + /** + * Initial release + * + * Based on Amazon Linux 2017.09. + */ + Version10 = '1.0.0', +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index cd3ff5b7be29f..c1df9645850b3 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -1,7 +1,9 @@ +import ec2 = require('@aws-cdk/aws-ec2'); import elb = require('@aws-cdk/aws-elasticloadbalancing'); import cdk = require('@aws-cdk/cdk'); import { BaseService, BaseServiceProps } from '../base/base-service'; -import { BaseTaskDefinition } from '../base/base-task-definition'; +import { BaseTaskDefinition, NetworkMode } from '../base/base-task-definition'; +import { cloudformation } from '../ecs.generated'; import { EcsCluster } from './ecs-cluster'; import { EcsTaskDefinition } from './ecs-task-definition'; @@ -15,29 +17,156 @@ export interface EcsServiceProps extends BaseServiceProps { * Task Definition used for running tasks in the service */ taskDefinition: EcsTaskDefinition; + + /** + * In what subnets to place the task's ENIs + * + * (Only applicable in case the TaskDefinition is configured for AwsVpc networking) + * + * @default Private subnets + */ + vpcPlacement?: ec2.VpcPlacementStrategy; + + /** + * Existing security group to use for the task's ENIs + * + * (Only applicable in case the TaskDefinition is configured for AwsVpc networking) + * + * @default A new security group is created + */ + securityGroup?: ec2.SecurityGroupRef; + + /** + * Whether to start services on distinct instances + * + * @default true + */ + placeOnDistinctInstances?: boolean; + + /** + * Deploy exactly one task on each instance in your cluster. + * + * When using this strategy, do not specify a desired number of tasks or any + * task placement strategies. + * + * @default false + */ + daemon?: boolean; } export class EcsService extends BaseService implements elb.ILoadBalancerTarget { + public readonly connections: ec2.Connections; protected readonly taskDef: BaseTaskDefinition; private readonly taskDefinition: EcsTaskDefinition; + private readonly constraints: cloudformation.ServiceResource.PlacementConstraintProperty[]; + private readonly strategies: cloudformation.ServiceResource.PlacementStrategyProperty[]; + private readonly daemon: boolean; constructor(parent: cdk.Construct, name: string, props: EcsServiceProps) { + if (props.daemon && props.desiredCount !== undefined) { + throw new Error('Daemon mode launches one task on every instance. Don\'t supply desiredCount.'); + } + super(parent, name, props, { cluster: props.cluster.clusterName, taskDefinition: props.taskDefinition.taskDefinitionArn, - launchType: 'EC2' + launchType: 'EC2', + placementConstraints: new cdk.Token(() => this.constraints), + placementStrategies: new cdk.Token(() => this.strategies), + schedulingStrategy: props.daemon ? 'DAEMON' : 'REPLICA', }); + this.constraints = []; + this.strategies = []; + this.daemon = props.daemon || false; + + if (props.taskDefinition.networkMode === NetworkMode.AwsVpc) { + this.configureAwsVpcNetworking(props.cluster.vpc, false, props.vpcPlacement, props.securityGroup); + } else { + // Either None, Bridge or Host networking. Copy SecurityGroup from ASG. + validateNoNetworkingProps(props); + this._securityGroup = props.cluster.autoScalingGroup.connections.securityGroup!; + } + + this.connections = new ec2.Connections({ securityGroup: this.securityGroup }); this.taskDefinition = props.taskDefinition; this.taskDef = props.taskDefinition; + if (props.placeOnDistinctInstances) { + this.constraints.push({ type: 'distinctInstance' }); + } + if (!this.taskDefinition.defaultContainer) { throw new Error('A TaskDefinition must have at least one essential container'); } } + /** + * Place services only on instances matching the given query expression + * + * You can specify multiple expressions in one call. The tasks will only + * be placed on instances matching all expressions. + * + * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cluster-query-language.html + */ + public placeOnMemberOf(...expressions: string[]) { + for (const expression of expressions) { + this.constraints.push({ type: 'memberOf', expression }); + } + } + + /** + * Try to place tasks spread across instance attributes. + * + * You can use one of the built-in attributes found on `BuiltInAttributes` + * or supply your own custom instance attributes. If more than one attribute + * is supplied, spreading is done in order. + * + * @default attributes instanceId + */ + public placeSpreadAcross(...fields: string[]) { + if (this.daemon) { + throw new Error("Can't configure spreading placement for a service with daemon=true"); + } + + if (fields.length === 0) { + fields = [BuiltInAttributes.InstanceId]; + } + for (const field of fields) { + this.strategies.push({ type: 'spread', field }); + } + } + + /** + * Try to place tasks on instances with the least amount of indicated resource available + * + * This ensures the total consumption of this resource is lowest. + */ + public placePackedBy(resource: BinPackResource) { + if (this.daemon) { + throw new Error("Can't configure packing placement for a service with daemon=true"); + } + + this.strategies.push({ type: 'binpack', field: resource }); + } + + /** + * Place tasks randomly across the available instances. + */ + public placeRandomly() { + if (this.daemon) { + throw new Error("Can't configure random placement for a service with daemon=true"); + } + + this.strategies.push({ type: 'random' }); + } + + /** + * Register this service as the target of a Classic Load Balancer + * + * Don't call this. Call `loadBalancer.addTarget()` instead. + */ public attachToClassicLB(loadBalancer: elb.LoadBalancer): void { - // FIXME: Security Groups this.loadBalancers.push({ loadBalancerName: loadBalancer.loadBalancerName, containerName: this.taskDefinition.defaultContainer!.name, @@ -46,3 +175,56 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { this.createLoadBalancerRole(); } } + +function validateNoNetworkingProps(props: EcsServiceProps) { + if (props.vpcPlacement !== undefined || props.securityGroup !== undefined) { + throw new Error('vpcPlacement and securityGroup can only be used in AwsVpc networking mode'); + } +} + +/** + * Built-in container instance attributes + */ +export class BuiltInAttributes { + /** + * The Instance ID of the instance + */ + public static readonly InstanceId = 'instanceId'; + + /** + * The AZ where the instance is running + */ + public static readonly AvailabilityZone = 'attribute:ecs.availability-zone'; + + /** + * The AMI ID of the instance + */ + public static readonly AmiId = 'attribute:ecs.ami-id'; + + /** + * The instance type + */ + public static readonly InstanceType = 'attribute:ecs.instance-type'; + + /** + * The OS type + * + * Either 'linux' or 'windows'. + */ + public static readonly OsType = 'attribute:ecs.os-type'; +} + +/** + * Instance resource used for bin packing + */ +export enum BinPackResource { + /** + * Fill up hosts' CPU allocations first + */ + Cpu = 'cpu', + + /** + * Fill up hosts' memory allocations first + */ + Memory = 'memory', +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts index 02d9f3de2b894..5c70660c1e698 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts @@ -38,17 +38,23 @@ export interface EcsTaskDefinitionProps extends BaseTaskDefinitionProps { } export class EcsTaskDefinition extends BaseTaskDefinition { - private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; + public readonly networkMode: NetworkMode; + private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[]; constructor(parent: cdk.Construct, name: string, props: EcsTaskDefinitionProps) { + const networkMode = props.networkMode || NetworkMode.Bridge; + super(parent, name, props, { cpu: props.cpu, memoryMiB: props.memoryMiB, - networkMode: props.networkMode || NetworkMode.Bridge, + networkMode, requiresCompatibilities: [Compatibilities.Ec2], placementConstraints: new cdk.Token(() => this.placementConstraints) }); + this.networkMode = networkMode; + this.placementConstraints = []; + if (props.placementConstraints) { props.placementConstraints.forEach(pc => this.addPlacementConstraint(pc)); } diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index dab41cc3c70df..d1a490c859c2b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -1,3 +1,4 @@ +import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { BaseService, BaseServiceProps } from '../base/base-service'; import { BaseTaskDefinition } from '../base/base-task-definition'; @@ -14,20 +15,45 @@ export interface FargateServiceProps extends BaseServiceProps { * Task Definition used for running tasks in the service */ taskDefinition: FargateTaskDefinition; + + /** + * Assign public IP addresses to each task + * + * @default false + */ + assignPublicIp?: boolean; + + /** + * In what subnets to place the task's ENIs + * + * @default Public subnet if assignPublicIp, private subnets otherwise + */ + vpcPlacement?: ec2.VpcPlacementStrategy; + + /** + * Existing security group to use for the tasks + * + * @default A new security group is created + */ + securityGroup?: ec2.SecurityGroupRef; } export class FargateService extends BaseService { + public readonly connections: ec2.Connections; + public readonly taskDefinition: FargateTaskDefinition; protected readonly taskDef: BaseTaskDefinition; - private readonly taskDefinition: FargateTaskDefinition; constructor(parent: cdk.Construct, name: string, props: FargateServiceProps) { super(parent, name, props, { cluster: props.cluster.clusterName, taskDefinition: props.taskDefinition.taskDefinitionArn, - launchType: 'FARGATE' + launchType: 'FARGATE', }); - if (!this.taskDefinition.defaultContainer) { + this.configureAwsVpcNetworking(props.cluster.vpc, props.assignPublicIp, props.vpcPlacement, props.securityGroup); + this.connections = new ec2.Connections({ securityGroup: this.securityGroup }); + + if (!props.taskDefinition.defaultContainer) { throw new Error('A TaskDefinition must have at least one essential container'); } From 97083dc98b7b2cf8b4e02b883fa954ab4002c219 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 15:01:58 +0200 Subject: [PATCH 41/97] Add Ulimits --- .../hello-cdk-ecs/index.ts | 6 +++ .../aws-ecs/lib/container-definition.ts | 45 ++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 6a0f170c15f2c..1300b0fc15bc6 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -60,6 +60,12 @@ class BonjourECS extends cdk.Stack { container.linuxParameters.sharedMemorySize = 65535 container.linuxParameters.initProcessEnabled = true + container.addUlimits({ + name: ecs.UlimitName.Core, + softLimit: 1234, + hardLimit: 1234, + }); + taskDefinition.addContainer(container); new ecs.EcsService(this, "EcsService", { diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index f6ab4dfe0c576..5abc092fd9c1e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -174,6 +174,8 @@ export class ContainerDefinition extends cdk.Construct { public readonly linuxParameters = new LinuxParameters(); + public readonly ulimits = new Array(); + public readonly essential: boolean; private readonly links = new Array(); @@ -195,6 +197,10 @@ export class ContainerDefinition extends cdk.Construct { } } + public addUlimits(...ulimits: Ulimits) { + this.ulimits.push(...ulimits); + } + /** * Mark this ContainerDefinition as using an ECR image */ @@ -231,7 +237,7 @@ export class ContainerDefinition extends cdk.Construct { privileged: this.props.privileged, readonlyRootFilesystem: this.props.readonlyRootFilesystem, repositoryCredentials: undefined, // FIXME - ulimits: [], // FIXME + ulimits: this.ulimits.map(renderUlimit), user: this.props.user, volumesFrom: [], // FIXME workingDirectory: this.props.workingDirectory, @@ -334,3 +340,40 @@ function getHealthCheckCommand(hc: HealthCheck): string[] { return hcCommand.concat(cmd); } + +/** + * Container ulimits. Correspond to ulimits options on docker run. + * + * NOTE: Does not work for Windows containers. + */ +export interface Ulimits { + name: UlimitName, + softLimit: number, + hardLimit: number, +} + +export enum UlimitName { + Core = "core", + Cpu = "cpu", + Data = "data", + Fsize = "fsize", + Locks = "locks", + Memlock = "memlock", + Msgqueue = "msgqueue", + Nice = "nice", + Nofile = "nofile", + Nproc = "nproc", + Rss = "rss", + Rtprio = "rtprio", + Rttime = "rttime", + Sigpending = "sigpending", + Stack = "stack" +} + +function renderUlimit(ulimit: Ulimit): cloudformation.TaskDefinitionResource.UlimitProperty { + return { + name: ulimit.name, + softLimit: ulimit.softLimit, + hardLimit: ulimit.hardLimit, + }; +} From a3e361562e4e8a4eaa3ce6dbaeff05a7269ab2ac Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 15:29:16 +0200 Subject: [PATCH 42/97] Fix log driver --- packages/@aws-cdk/aws-ecs/lib/log-drivers/log-driver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/log-drivers/log-driver.ts b/packages/@aws-cdk/aws-ecs/lib/log-drivers/log-driver.ts index 2b8e9c5882d2f..eb7c1344b5dda 100644 --- a/packages/@aws-cdk/aws-ecs/lib/log-drivers/log-driver.ts +++ b/packages/@aws-cdk/aws-ecs/lib/log-drivers/log-driver.ts @@ -8,5 +8,5 @@ export abstract class LogDriver extends cdk.Construct { /** * Return the log driver CloudFormation JSON */ - public abstract toLogDriverJson(): cloudformation.TaskDefinitionResource.LogConfigurationProperty; + public abstract renderLogDriver(): cloudformation.TaskDefinitionResource.LogConfigurationProperty; } From b913f0eac4bb0ad8ca1680a7e6e21d7bd456293e Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 15:38:15 +0200 Subject: [PATCH 43/97] Make add* functions consistently plural Except for `addTmpfs`, bc `addTmpfses` is just silly --- .../hello-cdk-ecs/index.ts | 5 +++-- .../aws-ecs/lib/container-definition.ts | 6 +++--- .../@aws-cdk/aws-ecs/lib/linux-parameters.ts | 18 +++++++++--------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 1300b0fc15bc6..1a19d8a1c3f09 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -43,9 +43,10 @@ class BonjourECS extends cdk.Stack { essential: true }); - container.linuxParameters.addCapability(ecs.Capability.All); + container.linuxParameters.addCapabilities(ecs.Capability.All); + container.linuxParameters.dropCapabilities(ecs.Capability.Chown); - container.linuxParameters.addDevice({ + container.linuxParameters.addDevices({ containerPath: "/pudding", hostPath: "/dev/sda", permissions: [ecs.DevicePermission.Read] diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 5abc092fd9c1e..6f91984e9b469 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -174,7 +174,7 @@ export class ContainerDefinition extends cdk.Construct { public readonly linuxParameters = new LinuxParameters(); - public readonly ulimits = new Array(); + public readonly ulimits = new Array(); public readonly essential: boolean; @@ -197,7 +197,7 @@ export class ContainerDefinition extends cdk.Construct { } } - public addUlimits(...ulimits: Ulimits) { + public addUlimits(...ulimits: Ulimit[]) { this.ulimits.push(...ulimits); } @@ -346,7 +346,7 @@ function getHealthCheckCommand(hc: HealthCheck): string[] { * * NOTE: Does not work for Windows containers. */ -export interface Ulimits { +export interface Ulimit { name: UlimitName, softLimit: number, hardLimit: number, diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts index 21a149310b65b..d7d0dabd36781 100644 --- a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -5,9 +5,9 @@ export class LinuxParameters { public sharedMemorySize?: number; - private readonly addCapabilities: Capability[] = []; + private readonly capAdd: Capability[] = []; - private readonly dropCapabilities: Capability[] = []; + private readonly capDrop: Capability[] = []; private readonly devices: Device[] = []; @@ -16,15 +16,15 @@ export class LinuxParameters { /** * AddCapability only works with EC2 launch type */ - public addCapability(...cap: Capability[]) { - this.addCapabilities.push(...cap); + public addCapabilities(...cap: Capability[]) { + this.capAdd.push(...cap); } - public dropCapability(...cap: Capability[]) { - this.dropCapabilities.push(...cap); + public dropCapabilities(...cap: Capability[]) { + this.capDrop.push(...cap); } - public addDevice(...device: Device[]) { + public addDevices(...device: Device[]) { this.devices.push(...device); } @@ -37,8 +37,8 @@ export class LinuxParameters { initProcessEnabled: this.initProcessEnabled, sharedMemorySize: this.sharedMemorySize, capabilities: { - add: this.addCapabilities, - drop: this.dropCapabilities, + add: this.capAdd, + drop: this.capDrop, }, devices: this.devices.map(renderDevice), tmpfs: this.tmpfs.map(renderTmpfs) From fd21c0df07574dac724cc0738520c37b20985acc Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 2 Oct 2018 15:48:10 +0200 Subject: [PATCH 44/97] Make load balancers respect network mode --- .../aws-autoscaling/lib/auto-scaling-group.ts | 4 +- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 55 +++++++++++++------ .../aws-ecs/lib/base/base-task-definition.ts | 3 +- .../aws-ecs/lib/container-definition.ts | 5 +- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 9 ++- .../aws-elasticloadbalancingv2/README.md | 12 ++-- .../lib/shared/base-target-group.ts | 13 ++--- .../lib/shared/enums.ts | 5 -- .../test/helpers.ts | 4 +- 9 files changed, 66 insertions(+), 44 deletions(-) diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index acf23e55ec05f..c10e8b6f0edad 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -276,7 +276,7 @@ export class AutoScalingGroup extends cdk.Construct implements cdk.ITaggable, el public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { this.targetGroupArns.push(targetGroup.targetGroupArn); targetGroup.registerConnectable(this); - return { targetType: elbv2.TargetType.SelfRegistering }; + return { targetType: elbv2.TargetType.Instance }; } /** @@ -284,7 +284,7 @@ export class AutoScalingGroup extends cdk.Construct implements cdk.ITaggable, el */ public attachToNetworkTargetGroup(targetGroup: elbv2.NetworkTargetGroup): elbv2.LoadBalancerTargetProps { this.targetGroupArns.push(targetGroup.targetGroupArn); - return { targetType: elbv2.TargetType.SelfRegistering }; + return { targetType: elbv2.TargetType.Instance }; } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 1205f66642164..c00b0b40c0dd5 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -2,7 +2,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); -import { BaseTaskDefinition } from '../base/base-task-definition'; +import { BaseTaskDefinition, NetworkMode } from '../base/base-task-definition'; import { cloudformation } from '../ecs.generated'; export interface BaseServiceProps { @@ -97,25 +97,19 @@ export abstract class BaseService extends cdk.Construct } public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { - this.loadBalancers.push({ - targetGroupArn: targetGroup.targetGroupArn, - containerName: this.taskDef.defaultContainer!.name, - containerPort: this.taskDef.defaultContainer!.loadBalancerPort(false), - }); - this.createLoadBalancerRole(); + const ret = this.attachToELBv2(targetGroup); + + // Open up security groups. For dynamic port mapping, we won't know the port range + // in advance so we need to open up all ports. + const port = this.instancePort; + const portRange = port === 0 ? EPHEMERAL_PORT_RANGE : new ec2.TcpPort(port); + targetGroup.registerConnectable(this, portRange); - return { targetType: elbv2.TargetType.SelfRegistering }; + return ret; } public attachToNetworkTargetGroup(targetGroup: elbv2.NetworkTargetGroup): elbv2.LoadBalancerTargetProps { - this.loadBalancers.push({ - targetGroupArn: targetGroup.targetGroupArn, - containerName: this.taskDef.defaultContainer!.name, - containerPort: this.taskDef.defaultContainer!.loadBalancerPort(false), - }); - this.createLoadBalancerRole(); - - return { targetType: elbv2.TargetType.SelfRegistering }; + return this.attachToELBv2(targetGroup); } public get securityGroup(): ec2.SecurityGroupRef { @@ -151,8 +145,37 @@ export abstract class BaseService extends cdk.Construct } }; } + + private attachToELBv2(targetGroup: elbv2.ITargetGroup): elbv2.LoadBalancerTargetProps { + if (this.taskDef.networkMode === NetworkMode.None) { + throw new Error("Cannot use a load balancer if NetworkMode is None. Use Host or AwsVpc instead."); + } + + this.loadBalancers.push({ + targetGroupArn: targetGroup.targetGroupArn, + containerName: this.taskDef.defaultContainer!.name, + containerPort: this.instancePort, + }); + this.createLoadBalancerRole(); + + return { targetType: elbv2.TargetType.Ip }; + } + + /** + * Return the port on which the instance will be listening + * + * Returns 0 if the networking mode implies dynamic port allocation. + */ + private get instancePort() { + return this.taskDef.networkMode === NetworkMode.Bridge ? 0 : this.taskDef.defaultContainer!.instancePort; + } } +/** + * The port range to open up for dynamic port mapping + */ +const EPHEMERAL_PORT_RANGE = new ec2.TcpPortRange(32768, 65535); + /** * Fargate platform version * diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts index 46b99085c8b15..91fdc843eec8d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts @@ -34,10 +34,11 @@ export interface BaseTaskDefinitionProps { volumes?: Volume[]; } -export class BaseTaskDefinition extends cdk.Construct { +export abstract class BaseTaskDefinition extends cdk.Construct { public readonly family: string; public readonly taskDefinitionArn: string; public readonly taskRole: iam.Role; + public abstract readonly networkMode: NetworkMode; /** * Default container for this task diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index f6ab4dfe0c576..a92e404c9a5b0 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -206,7 +206,10 @@ export class ContainerDefinition extends cdk.Construct { return this._usesEcrImages; } - public loadBalancerPort(_classicLB: boolean): number { + /** + * Return the instance port that the container will be listening on + */ + public get instancePort(): number { return 0; } diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index c1df9645850b3..9daf768230766 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -167,10 +167,17 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { * Don't call this. Call `loadBalancer.addTarget()` instead. */ public attachToClassicLB(loadBalancer: elb.LoadBalancer): void { + if (this.taskDefinition.networkMode === NetworkMode.Bridge) { + throw new Error("Cannot use a Classic Load Balancer if NetworkMode is Bridge. Use Host or AwsVpc instead."); + } + if (this.taskDefinition.networkMode === NetworkMode.None) { + throw new Error("Cannot use a load balancer if NetworkMode is None. Use Host or AwsVpc instead."); + } + this.loadBalancers.push({ loadBalancerName: loadBalancer.loadBalancerName, containerName: this.taskDefinition.defaultContainer!.name, - containerPort: this.taskDefinition.defaultContainer!.loadBalancerPort(true), + containerPort: this.taskDefinition.defaultContainer!.instancePort, }); this.createLoadBalancerRole(); } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md index b7110d5ca5875..18d6cab616d5f 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md @@ -179,18 +179,14 @@ load balancing target: public attachToApplicationTargetGroup(targetGroup: ApplicationTargetGroup): LoadBalancerTargetProps { targetGroup.registerConnectable(...); return { - targetType: TargetType.Instance | TargetType.Ip | TargetType.SelfRegistering, + targetType: TargetType.Instance | TargetType.Ip targetJson: { id: ..., port: ... }, }; } ``` - -`targetType` should be one of `Instance` or `Ip` if the target can be directly -added to the target group, or `SelfRegistering` if the target will register new -instances with the load balancer at some later point. - -If the `targetType` is `Instance` or `Ip`, `targetJson` should contain the `id` -of the target (either instance ID or IP address depending on the type) and +`targetType` should be one of `Instance` or `Ip`. If the target can be +directly added to the target group, `targetJson` should contain the `id` of +the target (either instance ID or IP address depending on the type) and optionally a `port` or `availabilityZone` override. Application load balancer targets can call `registerConnectable()` on the diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts index 696ce8108d865..356456de0ab26 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts @@ -238,15 +238,10 @@ export abstract class BaseTargetGroup extends cdk.Construct implements ITargetGr * Register the given load balancing target as part of this group */ protected addLoadBalancerTarget(props: LoadBalancerTargetProps) { - if ((props.targetType === TargetType.SelfRegistering) !== (props.targetJson === undefined)) { - throw new Error('Load balancing target should specify targetJson if and only if TargetType is not SelfRegistering'); - } - if (props.targetType !== TargetType.SelfRegistering) { - if (this.targetType !== undefined && this.targetType !== props.targetType) { - throw new Error(`Already have a of type '${this.targetType}', adding '${props.targetType}'; make all targets the same type.`); - } - this.targetType = props.targetType; + if (this.targetType !== undefined && this.targetType !== props.targetType) { + throw new Error(`Already have a of type '${this.targetType}', adding '${props.targetType}'; make all targets the same type.`); } + this.targetType = props.targetType; if (props.targetJson) { this.targetsJson.push(props.targetJson); @@ -290,6 +285,8 @@ export interface LoadBalancerTargetProps { /** * JSON representing the target's direct addition to the TargetGroup list + * + * May be omitted if the target is going to register itself later. */ targetJson?: any; } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts index 8888c23e0e949..f043272ab139d 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts @@ -109,9 +109,4 @@ export enum TargetType { * Targets identified by IP address */ Ip = 'ip', - - /** - * A target that will register itself with the target group - */ - SelfRegistering = 'self-registering', } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/helpers.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/helpers.ts index 75d35f74da063..77fec08c04824 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/helpers.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/helpers.ts @@ -17,10 +17,10 @@ export class FakeSelfRegisteringTarget extends cdk.Construct implements elbv2.IA public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { targetGroup.registerConnectable(this); - return { targetType: elbv2.TargetType.SelfRegistering }; + return { targetType: elbv2.TargetType.Instance }; } public attachToNetworkTargetGroup(_targetGroup: elbv2.NetworkTargetGroup): elbv2.LoadBalancerTargetProps { - return { targetType: elbv2.TargetType.SelfRegistering }; + return { targetType: elbv2.TargetType.Instance }; } } From 187f62b18a807a2e5914dc1be1974da8b4563510 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 15:38:15 +0200 Subject: [PATCH 45/97] Make add* functions consistently plural Except for `addTmpfs`, bc `addTmpfses` is just silly --- .../hello-cdk-ecs/index.ts | 5 +++-- .../aws-ecs/lib/container-definition.ts | 6 +++--- .../@aws-cdk/aws-ecs/lib/linux-parameters.ts | 20 +++++++++---------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 1300b0fc15bc6..1a19d8a1c3f09 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -43,9 +43,10 @@ class BonjourECS extends cdk.Stack { essential: true }); - container.linuxParameters.addCapability(ecs.Capability.All); + container.linuxParameters.addCapabilities(ecs.Capability.All); + container.linuxParameters.dropCapabilities(ecs.Capability.Chown); - container.linuxParameters.addDevice({ + container.linuxParameters.addDevices({ containerPath: "/pudding", hostPath: "/dev/sda", permissions: [ecs.DevicePermission.Read] diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 5abc092fd9c1e..6f91984e9b469 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -174,7 +174,7 @@ export class ContainerDefinition extends cdk.Construct { public readonly linuxParameters = new LinuxParameters(); - public readonly ulimits = new Array(); + public readonly ulimits = new Array(); public readonly essential: boolean; @@ -197,7 +197,7 @@ export class ContainerDefinition extends cdk.Construct { } } - public addUlimits(...ulimits: Ulimits) { + public addUlimits(...ulimits: Ulimit[]) { this.ulimits.push(...ulimits); } @@ -346,7 +346,7 @@ function getHealthCheckCommand(hc: HealthCheck): string[] { * * NOTE: Does not work for Windows containers. */ -export interface Ulimits { +export interface Ulimit { name: UlimitName, softLimit: number, hardLimit: number, diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts index 21a149310b65b..2d3af2136fd7c 100644 --- a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -5,26 +5,26 @@ export class LinuxParameters { public sharedMemorySize?: number; - private readonly addCapabilities: Capability[] = []; + private readonly capAdd: Capability[] = []; - private readonly dropCapabilities: Capability[] = []; + private readonly capDrop: Capability[] = []; private readonly devices: Device[] = []; private readonly tmpfs: Tmpfs[] = []; /** - * AddCapability only works with EC2 launch type + * AddCapabilities only works with EC2 launch type */ - public addCapability(...cap: Capability[]) { - this.addCapabilities.push(...cap); + public addCapabilities(...cap: Capability[]) { + this.capAdd.push(...cap); } - public dropCapability(...cap: Capability[]) { - this.dropCapabilities.push(...cap); + public dropCapabilities(...cap: Capability[]) { + this.capDrop.push(...cap); } - public addDevice(...device: Device[]) { + public addDevices(...device: Device[]) { this.devices.push(...device); } @@ -37,8 +37,8 @@ export class LinuxParameters { initProcessEnabled: this.initProcessEnabled, sharedMemorySize: this.sharedMemorySize, capabilities: { - add: this.addCapabilities, - drop: this.dropCapabilities, + add: this.capAdd, + drop: this.capDrop, }, devices: this.devices.map(renderDevice), tmpfs: this.tmpfs.map(renderTmpfs) From 6912e3f484fb32a0ec887c32b740bb6777118dbf Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 16:18:49 +0200 Subject: [PATCH 46/97] Add portMappings to ContainerDefinition --- .../hello-cdk-ecs/index.ts | 12 ++++++--- .../aws-ecs/lib/container-definition.ts | 27 ++++++++++++++++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 1a19d8a1c3f09..7a2d45258be08 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -47,13 +47,13 @@ class BonjourECS extends cdk.Stack { container.linuxParameters.dropCapabilities(ecs.Capability.Chown); container.linuxParameters.addDevices({ - containerPath: "/pudding", - hostPath: "/dev/sda", + containerPath: "/dev/pudding", + hostPath: "/dev/clyde", permissions: [ecs.DevicePermission.Read] }); container.linuxParameters.addTmpfs({ - containerPath: "/pudding", + containerPath: "/dev/sda", size: 12345, mountOptions: [ecs.TmpfsMountOption.Ro] }); @@ -67,6 +67,12 @@ class BonjourECS extends cdk.Stack { hardLimit: 1234, }); + container.addPortMappings({ + containerPort: 80 + hostPort: 80, + protocol: ecs.Protocol.Tcp, + }); + taskDefinition.addContainer(container); new ecs.EcsService(this, "EcsService", { diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 6f91984e9b469..d73bb18845b5d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -174,6 +174,8 @@ export class ContainerDefinition extends cdk.Construct { public readonly linuxParameters = new LinuxParameters(); + public readonly portMappings = new Array(); + public readonly ulimits = new Array(); public readonly essential: boolean; @@ -197,6 +199,10 @@ export class ContainerDefinition extends cdk.Construct { } } + public addPortMappings(...portMappings: PortMapping[]) { + this.portMappings.push(...portMappings); + } + public addUlimits(...ulimits: Ulimit[]) { this.ulimits.push(...ulimits); } @@ -233,7 +239,7 @@ export class ContainerDefinition extends cdk.Construct { memoryReservation: this.props.memoryReservationMiB, mountPoints: [], // FIXME name: this.props.name, - portMappings: [], // FIXME + portMappings: this.portMappings.map(renderPortMapping), privileged: this.props.privileged, readonlyRootFilesystem: this.props.readonlyRootFilesystem, repositoryCredentials: undefined, // FIXME @@ -377,3 +383,22 @@ function renderUlimit(ulimit: Ulimit): cloudformation.TaskDefinitionResource.Uli hardLimit: ulimit.hardLimit, }; } + +export interface PortMapping { + containerPort?: number, + hostPort?: number, + protocol: Protocol +} + +export enum Protocol { + Tcp = "tcp", + Udp = "udp", +} + +function renderPortMapping(pm: PortMapping): cloudformation.TaskDefinitionResource.PortMappingProperty { + return { + containerPort: pm.containerPort, + hostPort: pm.hostPort, + protocol: pm.protocol, + }; +} From 2bf2d57c3c32b4836e861e67469e46344e6cd093 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 16:19:17 +0200 Subject: [PATCH 47/97] Missing semicolons --- packages/@aws-cdk/aws-ecs/lib/container-definition.ts | 1 + packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index d73bb18845b5d..28646d85a9951 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -384,6 +384,7 @@ function renderUlimit(ulimit: Ulimit): cloudformation.TaskDefinitionResource.Uli }; } +// TODO: add default? export interface PortMapping { containerPort?: number, hostPort?: number, diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts index 2d3af2136fd7c..bdc7c9a7c0089 100644 --- a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -57,7 +57,7 @@ function renderDevice(device: Device): cloudformation.TaskDefinitionResource.Dev containerPath: device.containerPath, hostPath: device.hostPath, permissions: device.permissions - } + }; } export interface Tmpfs { @@ -71,7 +71,7 @@ function renderTmpfs(tmpfs: Tmpfs): cloudformation.TaskDefinitionResource.TmpfsP containerPath: tmpfs.containerPath, size: tmpfs.size, mountOptions: tmpfs.mountOptions - } + }; } export enum Capability { From f477d8d37bd65c32242a8ca055f3f4f30009218f Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 16:37:06 +0200 Subject: [PATCH 48/97] Add MountPoints to ContainerDefinition --- .../hello-cdk-ecs/index.ts | 13 ++++++++++- .../aws-ecs/lib/container-definition.ts | 22 ++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 7a2d45258be08..fbaf875d56d43 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -68,11 +68,22 @@ class BonjourECS extends cdk.Stack { }); container.addPortMappings({ - containerPort: 80 + containerPort: 80, hostPort: 80, protocol: ecs.Protocol.Tcp, }); + container.addMountPoints({ + containerPath: '/tmp/cache', + sourceVolume: 'volume-1', + readOnly: true, + }, { + containerPath: './cache', + sourceVolume: 'volume-2', + readOnly: true, + }); + + taskDefinition.addContainer(container); new ecs.EcsService(this, "EcsService", { diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 28646d85a9951..7e8fcdc04a63d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -174,6 +174,8 @@ export class ContainerDefinition extends cdk.Construct { public readonly linuxParameters = new LinuxParameters(); + public readonly mountPoints = new Array(); + public readonly portMappings = new Array(); public readonly ulimits = new Array(); @@ -199,6 +201,10 @@ export class ContainerDefinition extends cdk.Construct { } } + public addMountPoints(...mountPoints: MountPoint[]) { + this.mountPoints.push(...mountPoints); + } + public addPortMappings(...portMappings: PortMapping[]) { this.portMappings.push(...portMappings); } @@ -237,7 +243,7 @@ export class ContainerDefinition extends cdk.Construct { image: this.props.image.imageName, memory: this.props.memoryMiB, memoryReservation: this.props.memoryReservationMiB, - mountPoints: [], // FIXME + mountPoints: this.mountPoints.map(renderMountPoint), name: this.props.name, portMappings: this.portMappings.map(renderPortMapping), privileged: this.props.privileged, @@ -403,3 +409,17 @@ function renderPortMapping(pm: PortMapping): cloudformation.TaskDefinitionResour protocol: pm.protocol, }; } + +export interface MountPoint { + containerPath: string, + readOnly: boolean, + sourceVolume: string, +} + +function renderMountPoint(mp: MountPoint): cloudformation.TaskDefinitionResource.MountPointProperty { + return { + containerPath: mp.containerPath, + readOnly: mp.readOnly, + sourceVolume: mp.sourceVolume, + }; +} From b947f5114a490997a53cbd28cea14f7c33269404 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 2 Oct 2018 16:50:51 +0200 Subject: [PATCH 49/97] Change 'addContainer' to take the props and return ContainerDefinition. Add (start of) integ test. --- .../hello-cdk-ecs/index.ts | 9 ++--- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 2 +- .../aws-ecs/lib/base/base-task-definition.ts | 6 ++-- .../aws-ecs/lib/container-definition.ts | 22 +++++------- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 2 +- .../aws-ecs/lib/ecs/ecs-task-definition.ts | 21 +----------- .../lib/fargate/fargate-task-definition.ts | 12 ++++--- .../@aws-cdk/aws-ecs/lib/linux-parameters.ts | 4 +-- .../test/fargate/integ.lb-bridge-nw.ts | 34 +++++++++++++++++++ 9 files changed, 62 insertions(+), 50 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 1a19d8a1c3f09..2ed542a83ac6c 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -35,8 +35,7 @@ class BonjourECS extends cdk.Stack { family: "ecs-task-definition", }); - const container = new ecs.ContainerDefinition(this, 'Container', { - name: "web", + const container = taskDefinition.addContainer('web', { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), cpu: 1024, memoryMiB: 512, @@ -58,8 +57,8 @@ class BonjourECS extends cdk.Stack { mountOptions: [ecs.TmpfsMountOption.Ro] }); - container.linuxParameters.sharedMemorySize = 65535 - container.linuxParameters.initProcessEnabled = true + container.linuxParameters.sharedMemorySize = 65535; + container.linuxParameters.initProcessEnabled = true; container.addUlimits({ name: ecs.UlimitName.Core, @@ -67,8 +66,6 @@ class BonjourECS extends cdk.Stack { hardLimit: 1234, }); - taskDefinition.addContainer(container); - new ecs.EcsService(this, "EcsService", { cluster, taskDefinition, diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index c00b0b40c0dd5..c06e03cfd0d61 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -153,7 +153,7 @@ export abstract class BaseService extends cdk.Construct this.loadBalancers.push({ targetGroupArn: targetGroup.targetGroupArn, - containerName: this.taskDef.defaultContainer!.name, + containerName: this.taskDef.defaultContainer!.id, containerPort: this.instancePort, }); this.createLoadBalancerRole(); diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts index 91fdc843eec8d..7b4159a226048 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts @@ -1,6 +1,6 @@ import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); -import { ContainerDefinition } from '../container-definition'; +import { ContainerDefinition, ContainerDefinitionProps } from '../container-definition'; import { cloudformation } from '../ecs.generated'; export interface BaseTaskDefinitionProps { @@ -88,7 +88,8 @@ export abstract class BaseTaskDefinition extends cdk.Construct { /** * Add a container to this task */ - public addContainer(container: ContainerDefinition) { + public addContainer(id: string, props: ContainerDefinitionProps) { + const container = new ContainerDefinition(this, id, props); this.containers.push(container); if (container.usesEcrImages) { this.generateExecutionRole(); @@ -96,6 +97,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { if (this.defaultContainer === undefined && container.essential) { this.defaultContainer = container; } + return container; } private addVolume(volume: Volume) { diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index a0921ea407584..22442085b73ec 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -5,11 +5,6 @@ import { LinuxParameters } from './linux-parameters'; import { LogDriver } from './log-drivers/log-driver'; export interface ContainerDefinitionProps { - /** - * A name for the container. - */ - name: string; - /** * The image to use for a container. * @@ -122,8 +117,10 @@ export interface ContainerDefinitionProps { * * If your container attempts to exceed the allocated memory, the container * is terminated. + * + * At least one of memoryLimitMiB and memoryReservationMiB is required. */ - memoryMiB?: number; + memoryLimitMiB?: number; /** * The soft limit (in MiB) of memory to reserve for the container. @@ -132,6 +129,8 @@ export interface ContainerDefinitionProps { * 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. */ memoryReservationMiB?: number; @@ -170,8 +169,6 @@ export interface ContainerDefinitionProps { } export class ContainerDefinition extends cdk.Construct { - public readonly name: string; - public readonly linuxParameters = new LinuxParameters(); public readonly ulimits = new Array(); @@ -184,16 +181,15 @@ export class ContainerDefinition extends cdk.Construct { constructor(parent: cdk.Construct, id: string, private readonly props: ContainerDefinitionProps) { super(parent, id); - this.name = props.name; this.essential = props.essential !== undefined ? props.essential : true; props.image.bind(this); } public addLink(container: ContainerDefinition, alias?: string) { if (alias !== undefined) { - this.links.push(`${container.name}:${alias}`); + this.links.push(`${container.id}:${alias}`); } else { - this.links.push(`${container.name}`); + this.links.push(`${container.id}`); } } @@ -232,10 +228,10 @@ export class ContainerDefinition extends cdk.Construct { essential: this.essential, hostname: this.props.hostname, image: this.props.image.imageName, - memory: this.props.memoryMiB, + memory: this.props.memoryLimitMiB, memoryReservation: this.props.memoryReservationMiB, mountPoints: [], // FIXME - name: this.props.name, + name: this.id, portMappings: [], // FIXME privileged: this.props.privileged, readonlyRootFilesystem: this.props.readonlyRootFilesystem, diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index 9daf768230766..f9f9309558212 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -176,7 +176,7 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { this.loadBalancers.push({ loadBalancerName: loadBalancer.loadBalancerName, - containerName: this.taskDefinition.defaultContainer!.name, + containerName: this.taskDefinition.defaultContainer!.id, containerPort: this.taskDefinition.defaultContainer!.instancePort, }); this.createLoadBalancerRole(); diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts index 5c70660c1e698..19659836781d9 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts @@ -3,23 +3,6 @@ import { BaseTaskDefinition, BaseTaskDefinitionProps, Compatibilities, NetworkMo import { cloudformation } from '../ecs.generated'; export interface EcsTaskDefinitionProps extends BaseTaskDefinitionProps { - /** - * The number of cpu units used by the task. If using the EC2 launch type, - * this field is optional. Supported values are between 128 CPU units - * (0.125 vCPUs) and 10240 CPU units (10 vCPUs). - * - * @default 256 - */ - cpu?: string; - - /** - * The amount (in MiB) of memory used by the task. If using the EC2 launch type, this field is optional and any value - * can be used. - * - * @default 512 - */ - memoryMiB?: string; - /** * The Docker networking mode to use for the containers in the task. * @@ -41,12 +24,10 @@ export class EcsTaskDefinition extends BaseTaskDefinition { public readonly networkMode: NetworkMode; private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[]; - constructor(parent: cdk.Construct, name: string, props: EcsTaskDefinitionProps) { + constructor(parent: cdk.Construct, name: string, props: EcsTaskDefinitionProps = {}) { const networkMode = props.networkMode || NetworkMode.Bridge; super(parent, name, props, { - cpu: props.cpu, - memoryMiB: props.memoryMiB, networkMode, requiresCompatibilities: [Compatibilities.Ec2], placementConstraints: new cdk.Token(() => this.placementConstraints) diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts index a1ac36c29b47c..7e445d1ad2efe 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts @@ -13,7 +13,7 @@ export interface FargateTaskDefinitionProps extends BaseTaskDefinitionProps { * * @default 256 */ - cpu: string; + cpu?: string; /** * The amount (in MiB) of memory used by the task. @@ -33,14 +33,16 @@ export interface FargateTaskDefinitionProps extends BaseTaskDefinitionProps { * * @default 512 */ - memoryMiB: string; + memoryMiB?: string; } export class FargateTaskDefinition extends BaseTaskDefinition { - constructor(parent: cdk.Construct, name: string, props: FargateTaskDefinitionProps) { + public readonly networkMode = NetworkMode.AwsVpc; + + constructor(parent: cdk.Construct, name: string, props: FargateTaskDefinitionProps = {}) { super(parent, name, props, { - cpu: props.cpu, - memoryMiB: props.memoryMiB, + cpu: props.cpu || '256', + memoryMiB: props.memoryMiB || '512', networkMode: NetworkMode.AwsVpc, requiresCompatibilities: [Compatibilities.Fargate] }); diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts index d7d0dabd36781..80b46b5d0eb9b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -57,7 +57,7 @@ function renderDevice(device: Device): cloudformation.TaskDefinitionResource.Dev containerPath: device.containerPath, hostPath: device.hostPath, permissions: device.permissions - } + }; } export interface Tmpfs { @@ -71,7 +71,7 @@ function renderTmpfs(tmpfs: Tmpfs): cloudformation.TaskDefinitionResource.TmpfsP containerPath: tmpfs.containerPath, size: tmpfs.size, mountOptions: tmpfs.mountOptions - } + }; } export enum Capability { diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts new file mode 100644 index 0000000000000..76be274f5d3f5 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts @@ -0,0 +1,34 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +// import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); +// import ecs = require('../../lib'); + +const app = new cdk.App(process.argv); +const stack = new cdk.Stack(app, 'aws-ecs-integ'); + +const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); +Array.isArray(vpc); +/* +const cluster = new ecs.FargateCluster(stack, 'EcsCluster', { vpc }); + +const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef'); +taskDefinition.addContainer('web', { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + cpu: 256, + memoryMiB: 512, +}); + +const service = new ecs.FargateService(stack, "Service", { + cluster, + taskDefinition, +}); + +const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); +const listener = lb.addListener('PublicListener', { port: 80 }); +listener.addTargets('ECS', { + port: 80, + targets: [service] +}); +*/ + +process.stdout.write(app.run()); \ No newline at end of file From 30cb925f2123027962d11aca641b738631b8861d Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 16:57:14 +0200 Subject: [PATCH 50/97] Add VolumesFrom to ContainerDefinition --- .../hello-cdk-ecs/index.ts | 4 ++++ .../aws-ecs/lib/container-definition.ts | 24 +++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index ad387476873e2..97ba362de1b9c 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -82,6 +82,10 @@ class BonjourECS extends cdk.Stack { readOnly: true, }); + container.addVolumesFrom({ + sourceContainer: 'web', + readOnly: true, + }); new ecs.EcsService(this, "EcsService", { cluster, diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index c73c198fa6d49..3342499326f12 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -175,6 +175,8 @@ export class ContainerDefinition extends cdk.Construct { public readonly portMappings = new Array(); + public readonly volumesFrom = new Array(); + public readonly ulimits = new Array(); public readonly essential: boolean; @@ -209,6 +211,10 @@ export class ContainerDefinition extends cdk.Construct { this.ulimits.push(...ulimits); } + public addVolumesFrom(...volumesFrom: VolumeFrom[]) { + this.volumesFrom.push(...volumesFrom); + } + /** * Mark this ContainerDefinition as using an ECR image */ @@ -250,7 +256,7 @@ export class ContainerDefinition extends cdk.Construct { repositoryCredentials: undefined, // FIXME ulimits: this.ulimits.map(renderUlimit), user: this.props.user, - volumesFrom: [], // FIXME + volumesFrom: this.volumesFrom.map(renderVolumeFrom), workingDirectory: this.props.workingDirectory, logConfiguration: this.props.logging && this.props.logging.renderLogDriver(), environment: this.props.environment && renderKV(this.props.environment, 'name', 'value'), @@ -310,10 +316,6 @@ export interface HealthCheck { timeout?: number; } -// mountPoints?: mountPoint[]; -// portMappings?: portMapping[]; -// volumesFrom?: volumeFrom[]; - function renderKV(env: {[key: string]: string}, keyName: string, valueName: string): any { const ret = []; for (const [key, value] of Object.entries(env)) { @@ -422,3 +424,15 @@ function renderMountPoint(mp: MountPoint): cloudformation.TaskDefinitionResource sourceVolume: mp.sourceVolume, }; } + +export interface VolumeFrom { + sourceContainer: string, + readOnly: boolean, +} + +function renderVolumeFrom(vf: VolumeFrom): cloudformation.TaskDefinitionResource.VolumeFromProperty { + return { + sourceContainer: vf.sourceContainer, + readOnly: vf.readOnly, + }; +} From c64e184a35c0a938beb5e54510afe32b6499e5ac Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 3 Oct 2018 12:06:06 +0200 Subject: [PATCH 51/97] Add fargate example for demo purposes --- .../hello-cdk-fargate/index.ts | 51 +++++++++++++++++++ examples/cdk-examples-typescript/package.json | 1 + 2 files changed, 52 insertions(+) create mode 100644 examples/cdk-examples-typescript/hello-cdk-fargate/index.ts diff --git a/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts new file mode 100644 index 0000000000000..3561b026fb7b6 --- /dev/null +++ b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts @@ -0,0 +1,51 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import ecs = require('@aws-cdk/aws-ecs'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); + +class BonjourFargate extends cdk.Stack { + constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { + super(parent, name, props); + const vpc = new ec2.VpcNetwork(this, 'VPC'); + const cluster = new ecs.FargateCluster(this, 'Cluster', { + vpc + }); + + const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', { + cpu: '512', + memoryMiB: '2GB' + }); + + taskDefinition.addContainer('WebApp', { + // image: new ecs.ImageFromSource('./my-webapp-source'), + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + // portMappings: [{ containerPort: 8080 }], + }); + + const service = new ecs.FargateService(this, 'Service', { + cluster, + taskDefinition + }); + + const loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'LB', { + vpc, + internetFacing: true, + }); + + const listener = loadBalancer.addListener('Listener', { + port: 80, + open: true, + }); + + listener.addTargets('DefaultTargets', { + targets: [service], + protocol: elbv2.ApplicationProtocol.Http + }); + } +} + +const app = new cdk.App(process.argv); + +new BonjourFargate(app, 'Bonjour'); + +process.stdout.write(app.run()); diff --git a/examples/cdk-examples-typescript/package.json b/examples/cdk-examples-typescript/package.json index c3b2eae3dc1c4..79b2fd677a62a 100644 --- a/examples/cdk-examples-typescript/package.json +++ b/examples/cdk-examples-typescript/package.json @@ -30,6 +30,7 @@ "@aws-cdk/aws-ec2": "^0.10.0", "@aws-cdk/aws-ecs": "^0.10.0", "@aws-cdk/aws-elasticloadbalancing": "^0.10.0", + "@aws-cdk/aws-elasticloadbalancingv2": "^0.10.0", "@aws-cdk/aws-iam": "^0.10.0", "@aws-cdk/aws-lambda": "^0.10.0", "@aws-cdk/aws-neptune": "^0.10.0", From 566aac3e5d362c6696b00683fffbeed43ba16ae3 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 3 Oct 2018 12:24:15 +0200 Subject: [PATCH 52/97] Remove Service Role, fix DesiredCount default, make task memory definition work --- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 26 ++----------------- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 1 - .../lib/fargate/fargate-task-definition.ts | 2 +- .../test/fargate/integ.lb-bridge-nw.ts | 17 ++++++------ 4 files changed, 12 insertions(+), 34 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index c06e03cfd0d61..72599bad8b7ee 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -1,6 +1,5 @@ import ec2 = require('@aws-cdk/aws-ec2'); import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); -import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { BaseTaskDefinition, NetworkMode } from '../base/base-task-definition'; import { cloudformation } from '../ecs.generated'; @@ -38,13 +37,6 @@ export interface BaseServiceProps { */ minimumHealthyPercent?: number; - /** - * Role used by ECS agent to register containers with the Load Balancer - * - * @default A role will be created for you - */ - role?: iam.Role; - /** * Time after startup to ignore unhealthy load balancer checks. * @@ -71,23 +63,20 @@ export abstract class BaseService extends cdk.Construct protected networkConfiguration?: cloudformation.ServiceResource.NetworkConfigurationProperty; protected readonly abstract taskDef: BaseTaskDefinition; protected _securityGroup?: ec2.SecurityGroupRef; - private role?: iam.Role; private readonly resource: cloudformation.ServiceResource; constructor(parent: cdk.Construct, name: string, props: BaseServiceProps, additionalProps: any) { super(parent, name); - this.role = props.role; - this.resource = new cloudformation.ServiceResource(this, "Service", { - desiredCount: props.desiredCount, + desiredCount: props.desiredCount || 1, serviceName: props.serviceName, loadBalancers: new cdk.Token(() => this.loadBalancers), deploymentConfiguration: { maximumPercent: props.maximumPercent, minimumHealthyPercent: props.minimumHealthyPercent }, - role: new cdk.Token(() => this.role && this.role.roleArn), + /* role: never specified, supplanted by Service Linked Role */ networkConfiguration: this.networkConfiguration, platformVersion: props.platformVersion, ...additionalProps @@ -116,16 +105,6 @@ export abstract class BaseService extends cdk.Construct return this._securityGroup!; } - protected createLoadBalancerRole() { - if (!this.role) { - this.role = new iam.Role(this, 'Role', { - assumedBy: new cdk.ServicePrincipal('ecs-tasks.amazonaws.com'), - }); - this.role.attachManagedPolicy(new iam.AwsManagedPolicy('service-role/AmazonEC2ContainerServiceRole').policyArn); - this.resource.addDependency(this.role); - } - } - // tslint:disable-next-line:max-line-length protected configureAwsVpcNetworking(vpc: ec2.VpcNetworkRef, assignPublicIp?: boolean, vpcPlacement?: ec2.VpcPlacementStrategy, securityGroup?: ec2.SecurityGroupRef) { if (vpcPlacement === undefined) { @@ -156,7 +135,6 @@ export abstract class BaseService extends cdk.Construct containerName: this.taskDef.defaultContainer!.id, containerPort: this.instancePort, }); - this.createLoadBalancerRole(); return { targetType: elbv2.TargetType.Ip }; } diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index f9f9309558212..1808ab1f04590 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -179,7 +179,6 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { containerName: this.taskDefinition.defaultContainer!.id, containerPort: this.taskDefinition.defaultContainer!.instancePort, }); - this.createLoadBalancerRole(); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts index 7e445d1ad2efe..4dd95a84319a2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts @@ -42,7 +42,7 @@ export class FargateTaskDefinition extends BaseTaskDefinition { constructor(parent: cdk.Construct, name: string, props: FargateTaskDefinitionProps = {}) { super(parent, name, props, { cpu: props.cpu || '256', - memoryMiB: props.memoryMiB || '512', + memory: props.memoryMiB || '512', networkMode: NetworkMode.AwsVpc, requiresCompatibilities: [Compatibilities.Fargate] }); diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts index 76be274f5d3f5..1395a36192efb 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts @@ -1,21 +1,21 @@ import ec2 = require('@aws-cdk/aws-ec2'); -// import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); -// import ecs = require('../../lib'); +import ecs = require('../../lib'); const app = new cdk.App(process.argv); const stack = new cdk.Stack(app, 'aws-ecs-integ'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); -Array.isArray(vpc); -/* const cluster = new ecs.FargateCluster(stack, 'EcsCluster', { vpc }); -const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef'); +const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { + memoryMiB: '1GB', + cpu: '512' +}); taskDefinition.addContainer('web', { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), - cpu: 256, - memoryMiB: 512, + memoryLimitMiB: 1024, }); const service = new ecs.FargateService(stack, "Service", { @@ -29,6 +29,7 @@ listener.addTargets('ECS', { port: 80, targets: [service] }); -*/ + +new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName }); process.stdout.write(app.run()); \ No newline at end of file From 48cd8d4591a7934cc79947c42abc07a7e7eb99c3 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 3 Oct 2018 13:26:21 +0200 Subject: [PATCH 53/97] Starting on port mapping defaults --- .../aws-ecs/lib/container-definition.ts | 36 +++++++++++++++---- .../test/fargate/integ.lb-bridge-nw.ts | 6 +++- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 3342499326f12..b683fc7fde655 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -230,7 +230,10 @@ export class ContainerDefinition extends cdk.Construct { * Return the instance port that the container will be listening on */ public get instancePort(): number { - return 0; + if (this.portMappings.length === 0) { + throw new Error(`Container ${this.id} hasn't defined any ports`); + } + return this.portMappings[0].hostPort || this.portMappings[0].containerPort; } public renderContainerDefinition(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { @@ -391,11 +394,32 @@ function renderUlimit(ulimit: Ulimit): cloudformation.TaskDefinitionResource.Uli }; } -// TODO: add default? +/** + * Map a host port to a container port + */ export interface PortMapping { - containerPort?: number, - hostPort?: number, - protocol: Protocol + /** + * Port inside the container + */ + containerPort: number; + + /** + * Port on the host + * + * In AwsVpc or Host networking mode, leave this out or set it to the + * same value as containerPort. + * + * In Bridge networking mode, leave this out or set it to non-reserved + * non-ephemeral port. + */ + hostPort?: number; + + /** + * Protocol + * + * @default Tcp + */ + protocol?: Protocol } export enum Protocol { @@ -407,7 +431,7 @@ function renderPortMapping(pm: PortMapping): cloudformation.TaskDefinitionResour return { containerPort: pm.containerPort, hostPort: pm.hostPort, - protocol: pm.protocol, + protocol: pm.protocol || Protocol.Tcp, }; } diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts index 1395a36192efb..542975396d13b 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts @@ -13,10 +13,14 @@ const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { memoryMiB: '1GB', cpu: '512' }); -taskDefinition.addContainer('web', { +const container = taskDefinition.addContainer('web', { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), memoryLimitMiB: 1024, }); +container.addPortMappings({ + containerPort: 8080, + protocol: ecs.Protocol.Tcp +}); const service = new ecs.FargateService(stack, "Service", { cluster, From e311293ec2c1ebe3077c07427181ae2a0d6b5688 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 3 Oct 2018 14:56:14 +0200 Subject: [PATCH 54/97] Add validation on port mappings with tests --- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 2 +- .../aws-ecs/lib/base/base-task-definition.ts | 3 +- .../aws-ecs/lib/container-definition.ts | 15 +- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 2 +- .../aws-ecs/test/test.container-definition.ts | 131 ++++++++++++++++++ 5 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/test.container-definition.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 72599bad8b7ee..3b5778edded73 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -145,7 +145,7 @@ export abstract class BaseService extends cdk.Construct * Returns 0 if the networking mode implies dynamic port allocation. */ private get instancePort() { - return this.taskDef.networkMode === NetworkMode.Bridge ? 0 : this.taskDef.defaultContainer!.instancePort; + return this.taskDef.networkMode === NetworkMode.Bridge ? 0 : this.taskDef.defaultContainer!.ingressPort; } } diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts index 7b4159a226048..4c71d62a2a0d5 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts @@ -89,7 +89,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { * Add a container to this task */ public addContainer(id: string, props: ContainerDefinitionProps) { - const container = new ContainerDefinition(this, id, props); + const container = new ContainerDefinition(this, id, this, props); this.containers.push(container); if (container.usesEcrImages) { this.generateExecutionRole(); @@ -97,6 +97,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { if (this.defaultContainer === undefined && container.essential) { this.defaultContainer = container; } + return container; } diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index b683fc7fde655..bdcf18cd59f68 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -1,4 +1,5 @@ import cdk = require('@aws-cdk/cdk'); +import { BaseTaskDefinition, NetworkMode } from './base/base-task-definition'; import { ContainerImage } from './container-image'; import { cloudformation } from './ecs.generated'; import { LinuxParameters } from './linux-parameters'; @@ -183,11 +184,14 @@ export class ContainerDefinition extends cdk.Construct { private readonly links = new Array(); + private readonly taskDefinition: BaseTaskDefinition; + private _usesEcrImages: boolean = false; - constructor(parent: cdk.Construct, id: string, private readonly props: ContainerDefinitionProps) { + constructor(parent: cdk.Construct, id: string, taskDefinition: BaseTaskDefinition, private readonly props: ContainerDefinitionProps) { super(parent, id); this.essential = props.essential !== undefined ? props.essential : true; + this.taskDefinition = taskDefinition; props.image.bind(this); } @@ -204,6 +208,13 @@ export class ContainerDefinition extends cdk.Construct { } public addPortMappings(...portMappings: PortMapping[]) { + for (const pm of portMappings) { + if (this.taskDefinition.networkMode === NetworkMode.AwsVpc || this.taskDefinition.networkMode === NetworkMode.Host) { + if (pm.containerPort !== pm.hostPort && pm.hostPort !== undefined) { + throw new Error(`Host port ${pm.hostPort} does not match container port ${pm.containerPort}.`); + } + } + } this.portMappings.push(...portMappings); } @@ -229,7 +240,7 @@ export class ContainerDefinition extends cdk.Construct { /** * Return the instance port that the container will be listening on */ - public get instancePort(): number { + public get ingressPort(): number { if (this.portMappings.length === 0) { throw new Error(`Container ${this.id} hasn't defined any ports`); } diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index 1808ab1f04590..f389d4b0b45a4 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -177,7 +177,7 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { this.loadBalancers.push({ loadBalancerName: loadBalancer.loadBalancerName, containerName: this.taskDefinition.defaultContainer!.id, - containerPort: this.taskDefinition.defaultContainer!.instancePort, + containerPort: this.taskDefinition.defaultContainer!.ingressPort, }); } } diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts new file mode 100644 index 0000000000000..ef88fecaa9596 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -0,0 +1,131 @@ +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../lib'); + +export = { + "When creating a Task Definition": { + // Validating portMapping inputs + "With network mode AwsVpc": { + "Host port should be the same as container port"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + // WHEN + test.throws(() => { + container.addPortMappings({ + containerPort: 8080, + hostPort: 8081 + }); + }); + // THEN + test.done(); + }, + + "Host port can be empty "(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + // WHEN + container.addPortMappings({ + containerPort: 8080, + }); + + // THEN no excpetion raised + test.done(); + }, + }, + "With network mode Host ": { + "Host port should be the same as container port"(test: Test) { + test.done(); + }, + "Host port can be empty "(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + // WHEN + container.addPortMappings({ + containerPort: 8080, + }); + + // THEN no excpetion raised + test.done(); + }, + }, + "With network mode Bridge": { + "Host port should not be lower than 1024"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + test.throws(() => { + container.addPortMappings({ + containerPort: 8080, + hostPort: 1, + }); + }); + + // THEN + test.done(); + }, + }, + + // "With health check": { + // "healthCheck.command is a single string"(test: Test) { + // const stack = new cdk.Stack(); + // const taskDefinition = new TaskDefinition(stack, 'TaskDef'); + // const containerDefinition = taskDefinition.ContainerDefinition[0]; + // test.deepEqual(resolve(vpc.vpcId), {Ref: 'TheVPC92636AB0' } ); + // test.done(); + // }, + // } + }, + "Ingress Port": { + "With network mode AwsVpc": { + "Ingress port should be the same as container port"(test: Test) { + test.done(); + }, + }, + "With network mode Host ": { + "Ingress port should be the same as container port"(test: Test) { + test.done(); + }, + }, + "With network mode Bridge": { + "Ingress port should be the same as host port if supplied"(test: Test) { + test.done(); + }, + "Ingress port should be the 0 if not supplied"(test: Test) { + test.done(); + }, + }, + }, +}; From 4ffed0922a2b8bb7e9f1f0be45b700b343af35b9 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 3 Oct 2018 15:19:28 +0200 Subject: [PATCH 55/97] Implement ingressPort method --- .../aws-ecs/lib/container-definition.ts | 9 +- .../aws-ecs/test/test.container-definition.ts | 90 +++++++++++++++++-- 2 files changed, 92 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index bdcf18cd59f68..ed1af1f3727dc 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -244,7 +244,14 @@ export class ContainerDefinition extends cdk.Construct { if (this.portMappings.length === 0) { throw new Error(`Container ${this.id} hasn't defined any ports`); } - return this.portMappings[0].hostPort || this.portMappings[0].containerPort; + const defaultPortMapping = this.portMappings[0]; + if (defaultPortMapping.hostPort !== undefined) { + return defaultPortMapping.hostPort; + } + if (this.taskDefinition.networkMode === NetworkMode.Bridge) { + return 0; + } + return defaultPortMapping.containerPort; } public renderContainerDefinition(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts index ef88fecaa9596..06476589066a4 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -17,14 +17,13 @@ export = { image: ecs.DockerHub.image("/aws/aws-example-app"), memoryLimitMiB: 2048, }); - // WHEN + // THEN test.throws(() => { container.addPortMappings({ containerPort: 8080, hostPort: 8081 }); }); - // THEN test.done(); }, @@ -68,7 +67,7 @@ export = { containerPort: 8080, }); - // THEN no excpetion raised + // THEN no exception raised test.done(); }, }, @@ -85,15 +84,13 @@ export = { memoryLimitMiB: 2048, }); - // WHEN + // THEN test.throws(() => { container.addPortMappings({ containerPort: 8080, hostPort: 1, }); }); - - // THEN test.done(); }, }, @@ -111,19 +108,100 @@ export = { "Ingress Port": { "With network mode AwsVpc": { "Ingress port should be the same as container port"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + container.addPortMappings({ + containerPort: 8080, + }); + const actual = container.ingressPort; + + // THEN + const expected = 8080; + test.equal(actual, expected, "Ingress port should be the same as container port"); test.done(); }, }, "With network mode Host ": { "Ingress port should be the same as container port"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.Host, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + container.addPortMappings({ + containerPort: 8080, + }); + const actual = container.ingressPort; + + // THEN + const expected = 8080; + test.equal(actual, expected); test.done(); }, }, "With network mode Bridge": { "Ingress port should be the same as host port if supplied"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.Bridge, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + container.addPortMappings({ + containerPort: 8081, + hostPort: 8080, + }); + const actual = container.ingressPort; + + // THEN + const expected = 8080; + test.equal(actual, expected); test.done(); }, "Ingress port should be the 0 if not supplied"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.Bridge, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + container.addPortMappings({ + containerPort: 8081, + }); + const actual = container.ingressPort; + + // THEN + const expected = 0; + test.equal(actual, expected); test.done(); }, }, From 97d3db3e73c31ae9bbd5a7a1a80dd5ae2bc2359d Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 4 Oct 2018 11:09:12 +0200 Subject: [PATCH 56/97] Make ALB listener ACTUALLY default to 'true' --- .../aws-elasticloadbalancingv2/lib/alb/application-listener.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index e488c7ac7901d..18f5f8d803542 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -134,7 +134,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis (props.defaultTargetGroups || []).forEach(this.addDefaultTargetGroup.bind(this)); - if (props.open) { + if (props.open !== false) { this.connections.allowDefaultPortFrom(new ec2.AnyIPv4(), `Allow from anyone on port ${port}`); } } From 4f9b54818fa07c62a673309887f74329f4b8f27d Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 4 Oct 2018 11:10:33 +0200 Subject: [PATCH 57/97] Add dependency on listener This ensures that the dependency resolution by CFN works properly when attaching a load balancer to an ECS service --- .../lib/alb/application-listener.ts | 3 ++- .../lib/alb/application-target-group.ts | 7 ++++++- .../lib/nlb/network-listener.ts | 4 +++- .../lib/nlb/network-target-group.ts | 14 ++++++++++++++ .../lib/shared/base-listener.ts | 4 +++- .../lib/shared/base-target-group.ts | 19 ++++++++++++++++++- .../lib/shared/util.ts | 13 +++++++++++++ 7 files changed, 59 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index e488c7ac7901d..599b5bc0c7309 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -258,7 +258,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis /** * Properties to reference an existing listener */ -export interface IApplicationListener extends ec2.IConnectable { +export interface IApplicationListener extends ec2.IConnectable, cdk.IDependable { /** * ARN of the listener */ @@ -319,6 +319,7 @@ export interface ApplicationListenerRefProps { } class ImportedApplicationListener extends cdk.Construct implements IApplicationListener { + public readonly dependencyElements: cdk.IDependable[] = []; public readonly connections: ec2.Connections; /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts index b6f21689df72f..2454f856af895 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts @@ -3,7 +3,7 @@ import cdk = require('@aws-cdk/cdk'); import { BaseTargetGroup, BaseTargetGroupProps, ITargetGroup, LoadBalancerTargetProps, TargetGroupRefProps } from '../shared/base-target-group'; import { ApplicationProtocol } from '../shared/enums'; import { BaseImportedTargetGroup } from '../shared/imported'; -import { determineProtocolAndPort } from '../shared/util'; +import { determineProtocolAndPort, LazyDependable } from '../shared/util'; import { IApplicationListener } from './application-listener'; /** @@ -144,6 +144,7 @@ export class ApplicationTargetGroup extends BaseTargetGroup { listener.registerConnectable(member.connectable, member.portRange); } this.listeners.push(listener); + this.dependableListeners.push(listener); } } @@ -181,6 +182,10 @@ class ImportedApplicationTargetGroup extends BaseImportedTargetGroup implements public registerListener(_listener: IApplicationListener) { // Nothing to do, we know nothing of our members } + + public listenerDependency(): cdk.IDependable { + return new LazyDependable([]); + } } /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts index e72e8f1c951df..99c89342fa7a2 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts @@ -113,7 +113,7 @@ export class NetworkListener extends BaseListener implements INetworkListener { /** * Properties to reference an existing listener */ -export interface INetworkListener { +export interface INetworkListener extends cdk.IDependable { /** * ARN of the listener */ @@ -134,6 +134,8 @@ export interface NetworkListenerRefProps { * An imported Network Listener */ class ImportedNetworkListener extends cdk.Construct implements INetworkListener { + public readonly dependencyElements: cdk.IDependable[] = []; + /** * ARN of the listener */ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts index 5dc2253d84f35..4cb59fbdfad2e 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts @@ -2,6 +2,8 @@ import cdk = require('@aws-cdk/cdk'); import { BaseTargetGroup, BaseTargetGroupProps, ITargetGroup, LoadBalancerTargetProps, TargetGroupRefProps } from '../shared/base-target-group'; import { Protocol } from '../shared/enums'; import { BaseImportedTargetGroup } from '../shared/imported'; +import { LazyDependable } from '../shared/util'; +import { INetworkListener } from './network-listener'; /** * Properties for a new Network Target Group @@ -62,6 +64,15 @@ export class NetworkTargetGroup extends BaseTargetGroup { this.addLoadBalancerTarget(result); } } + + /** + * Register a listener that is load balancing to this target group. + * + * Don't call this directly. It will be called by listeners. + */ + public registerListener(listener: INetworkListener) { + this.dependableListeners.push(listener); + } } /** @@ -75,6 +86,9 @@ export interface INetworkTargetGroup extends ITargetGroup { * An imported network target group */ class ImportedNetworkTargetGroup extends BaseImportedTargetGroup implements INetworkTargetGroup { + public listenerDependency(): cdk.IDependable { + return new LazyDependable([]); + } } /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts index 6bc12566cb6f4..af212853acea7 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -5,7 +5,8 @@ import { ITargetGroup } from './base-target-group'; /** * Base class for listeners */ -export abstract class BaseListener extends cdk.Construct { +export abstract class BaseListener extends cdk.Construct implements cdk.IDependable { + public readonly dependencyElements: cdk.IDependable[]; public readonly listenerArn: string; private readonly defaultActions: any[] = []; @@ -17,6 +18,7 @@ export abstract class BaseListener extends cdk.Construct { defaultActions: new cdk.Token(() => this.defaultActions), }); + this.dependencyElements = [resource]; this.listenerArn = resource.ref; } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts index 356456de0ab26..280b2ab25bd82 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts @@ -2,7 +2,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { cloudformation } from '../elasticloadbalancingv2.generated'; import { Protocol, TargetType } from './enums'; -import { Attributes, renderAttributes } from './util'; +import { Attributes, LazyDependable, renderAttributes } from './util'; /** * Basic properties of both Application and Network Target Groups @@ -145,6 +145,11 @@ export abstract class BaseTargetGroup extends cdk.Construct implements ITargetGr */ protected readonly defaultPort: string; + /** + * List of listeners routing to this target group + */ + protected readonly dependableListeners = new Array(); + /** * Attributes of this target group */ @@ -234,6 +239,13 @@ export abstract class BaseTargetGroup extends cdk.Construct implements ITargetGr this.resource.addDependency(...other); } + /** + * Return an object to depend on the listeners added to this target group + */ + public listenerDependency(): cdk.IDependable { + return new LazyDependable(this.dependableListeners); + } + /** * Register the given load balancing target as part of this group */ @@ -272,6 +284,11 @@ export interface ITargetGroup { * ARN of the target group */ readonly targetGroupArn: string; + + /** + * Return an object to depend on the listeners added to this target group + */ + listenerDependency(): cdk.IDependable; } /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts index d992d156a3b2f..808a8f0e2c33e 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts @@ -1,3 +1,4 @@ +import cdk = require('@aws-cdk/cdk'); import { ApplicationProtocol } from "./enums"; export type Attributes = {[key: string]: string | undefined}; @@ -67,3 +68,15 @@ export function determineProtocolAndPort(protocol: ApplicationProtocol | undefin export function ifUndefined(x: T | undefined, def: T) { return x !== undefined ? x : def; } + +/** + * Allow lazy evaluation of a list of dependables + */ +export class LazyDependable implements cdk.IDependable { + constructor(private readonly depList: cdk.IDependable[]) { + } + + public get dependencyElements(): cdk.IDependable[] { + return this.depList; + } +} \ No newline at end of file From e8aa186b0b8beaab1ae1dfeffbab9d75faca1c77 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 4 Oct 2018 11:51:43 +0200 Subject: [PATCH 58/97] Integ test for Fargate service Notes: - Loadbalancer needs to be specified to be "internetFacing" - Cannot modify LoadBalancer on service once service has been created; can get around this by modifying ID of LB - Listener must be set to open --- packages/@aws-cdk/aws-ecs/test/fargate/cdk.json | 9 +++++++++ .../{integ.lb-bridge-nw.ts => integ.lb-awsvpc-nw.ts} | 9 +++++---- 2 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/fargate/cdk.json rename packages/@aws-cdk/aws-ecs/test/fargate/{integ.lb-bridge-nw.ts => integ.lb-awsvpc-nw.ts} (78%) diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json b/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json new file mode 100644 index 0000000000000..64fd3b2dafe6e --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json @@ -0,0 +1,9 @@ +{ + "context": { + "availability-zones:794715269151:eu-west-1": [ + "eu-west-1a", + "eu-west-1b", + "eu-west-1c" + ] + } +} diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts similarity index 78% rename from packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts rename to packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts index 542975396d13b..c9db123d8a7e2 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts @@ -7,6 +7,7 @@ const app = new cdk.App(process.argv); const stack = new cdk.Stack(app, 'aws-ecs-integ'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); + const cluster = new ecs.FargateCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { @@ -18,7 +19,7 @@ const container = taskDefinition.addContainer('web', { memoryLimitMiB: 1024, }); container.addPortMappings({ - containerPort: 8080, + containerPort: 80, protocol: ecs.Protocol.Tcp }); @@ -27,13 +28,13 @@ const service = new ecs.FargateService(stack, "Service", { taskDefinition, }); -const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); -const listener = lb.addListener('PublicListener', { port: 80 }); +const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc, internetFacing: true }); +const listener = lb.addListener('PublicListener', { port: 80, open: true }); listener.addTargets('ECS', { port: 80, targets: [service] }); -new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName }); +new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName, }); process.stdout.write(app.run()); \ No newline at end of file From e06a3ec08ccd2fcb2fec04e5a3f70c70df0618d1 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 16:57:14 +0200 Subject: [PATCH 59/97] Add VolumesFrom to ContainerDefinition --- .../hello-cdk-ecs/index.ts | 4 ++++ .../aws-ecs/lib/container-definition.ts | 24 +++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index ad387476873e2..97ba362de1b9c 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -82,6 +82,10 @@ class BonjourECS extends cdk.Stack { readOnly: true, }); + container.addVolumesFrom({ + sourceContainer: 'web', + readOnly: true, + }); new ecs.EcsService(this, "EcsService", { cluster, diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index c73c198fa6d49..3342499326f12 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -175,6 +175,8 @@ export class ContainerDefinition extends cdk.Construct { public readonly portMappings = new Array(); + public readonly volumesFrom = new Array(); + public readonly ulimits = new Array(); public readonly essential: boolean; @@ -209,6 +211,10 @@ export class ContainerDefinition extends cdk.Construct { this.ulimits.push(...ulimits); } + public addVolumesFrom(...volumesFrom: VolumeFrom[]) { + this.volumesFrom.push(...volumesFrom); + } + /** * Mark this ContainerDefinition as using an ECR image */ @@ -250,7 +256,7 @@ export class ContainerDefinition extends cdk.Construct { repositoryCredentials: undefined, // FIXME ulimits: this.ulimits.map(renderUlimit), user: this.props.user, - volumesFrom: [], // FIXME + volumesFrom: this.volumesFrom.map(renderVolumeFrom), workingDirectory: this.props.workingDirectory, logConfiguration: this.props.logging && this.props.logging.renderLogDriver(), environment: this.props.environment && renderKV(this.props.environment, 'name', 'value'), @@ -310,10 +316,6 @@ export interface HealthCheck { timeout?: number; } -// mountPoints?: mountPoint[]; -// portMappings?: portMapping[]; -// volumesFrom?: volumeFrom[]; - function renderKV(env: {[key: string]: string}, keyName: string, valueName: string): any { const ret = []; for (const [key, value] of Object.entries(env)) { @@ -422,3 +424,15 @@ function renderMountPoint(mp: MountPoint): cloudformation.TaskDefinitionResource sourceVolume: mp.sourceVolume, }; } + +export interface VolumeFrom { + sourceContainer: string, + readOnly: boolean, +} + +function renderVolumeFrom(vf: VolumeFrom): cloudformation.TaskDefinitionResource.VolumeFromProperty { + return { + sourceContainer: vf.sourceContainer, + readOnly: vf.readOnly, + }; +} From 95b8d848b0b7e2501fae4a7e4157a0c9899a03ff Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 3 Oct 2018 12:06:06 +0200 Subject: [PATCH 60/97] Add fargate example for demo purposes --- .../hello-cdk-fargate/index.ts | 51 +++++++++++++++++++ examples/cdk-examples-typescript/package.json | 1 + 2 files changed, 52 insertions(+) create mode 100644 examples/cdk-examples-typescript/hello-cdk-fargate/index.ts diff --git a/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts new file mode 100644 index 0000000000000..3561b026fb7b6 --- /dev/null +++ b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts @@ -0,0 +1,51 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import ecs = require('@aws-cdk/aws-ecs'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); + +class BonjourFargate extends cdk.Stack { + constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { + super(parent, name, props); + const vpc = new ec2.VpcNetwork(this, 'VPC'); + const cluster = new ecs.FargateCluster(this, 'Cluster', { + vpc + }); + + const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', { + cpu: '512', + memoryMiB: '2GB' + }); + + taskDefinition.addContainer('WebApp', { + // image: new ecs.ImageFromSource('./my-webapp-source'), + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + // portMappings: [{ containerPort: 8080 }], + }); + + const service = new ecs.FargateService(this, 'Service', { + cluster, + taskDefinition + }); + + const loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'LB', { + vpc, + internetFacing: true, + }); + + const listener = loadBalancer.addListener('Listener', { + port: 80, + open: true, + }); + + listener.addTargets('DefaultTargets', { + targets: [service], + protocol: elbv2.ApplicationProtocol.Http + }); + } +} + +const app = new cdk.App(process.argv); + +new BonjourFargate(app, 'Bonjour'); + +process.stdout.write(app.run()); diff --git a/examples/cdk-examples-typescript/package.json b/examples/cdk-examples-typescript/package.json index c3b2eae3dc1c4..79b2fd677a62a 100644 --- a/examples/cdk-examples-typescript/package.json +++ b/examples/cdk-examples-typescript/package.json @@ -30,6 +30,7 @@ "@aws-cdk/aws-ec2": "^0.10.0", "@aws-cdk/aws-ecs": "^0.10.0", "@aws-cdk/aws-elasticloadbalancing": "^0.10.0", + "@aws-cdk/aws-elasticloadbalancingv2": "^0.10.0", "@aws-cdk/aws-iam": "^0.10.0", "@aws-cdk/aws-lambda": "^0.10.0", "@aws-cdk/aws-neptune": "^0.10.0", From 55957074c1f694a8c43128f5d348a929576d005e Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 3 Oct 2018 13:26:21 +0200 Subject: [PATCH 61/97] Starting on port mapping defaults --- .../aws-ecs/lib/container-definition.ts | 36 +++++++++++++++---- .../test/fargate/integ.lb-bridge-nw.ts | 6 +++- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 3342499326f12..b683fc7fde655 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -230,7 +230,10 @@ export class ContainerDefinition extends cdk.Construct { * Return the instance port that the container will be listening on */ public get instancePort(): number { - return 0; + if (this.portMappings.length === 0) { + throw new Error(`Container ${this.id} hasn't defined any ports`); + } + return this.portMappings[0].hostPort || this.portMappings[0].containerPort; } public renderContainerDefinition(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { @@ -391,11 +394,32 @@ function renderUlimit(ulimit: Ulimit): cloudformation.TaskDefinitionResource.Uli }; } -// TODO: add default? +/** + * Map a host port to a container port + */ export interface PortMapping { - containerPort?: number, - hostPort?: number, - protocol: Protocol + /** + * Port inside the container + */ + containerPort: number; + + /** + * Port on the host + * + * In AwsVpc or Host networking mode, leave this out or set it to the + * same value as containerPort. + * + * In Bridge networking mode, leave this out or set it to non-reserved + * non-ephemeral port. + */ + hostPort?: number; + + /** + * Protocol + * + * @default Tcp + */ + protocol?: Protocol } export enum Protocol { @@ -407,7 +431,7 @@ function renderPortMapping(pm: PortMapping): cloudformation.TaskDefinitionResour return { containerPort: pm.containerPort, hostPort: pm.hostPort, - protocol: pm.protocol, + protocol: pm.protocol || Protocol.Tcp, }; } diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts index 1395a36192efb..542975396d13b 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts @@ -13,10 +13,14 @@ const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { memoryMiB: '1GB', cpu: '512' }); -taskDefinition.addContainer('web', { +const container = taskDefinition.addContainer('web', { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), memoryLimitMiB: 1024, }); +container.addPortMappings({ + containerPort: 8080, + protocol: ecs.Protocol.Tcp +}); const service = new ecs.FargateService(stack, "Service", { cluster, From a1b78f471a62d739498d9fbec1caed8cd42c7bbf Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 4 Oct 2018 11:09:12 +0200 Subject: [PATCH 62/97] Make ALB listener ACTUALLY default to 'true' --- .../aws-elasticloadbalancingv2/lib/alb/application-listener.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index e488c7ac7901d..18f5f8d803542 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -134,7 +134,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis (props.defaultTargetGroups || []).forEach(this.addDefaultTargetGroup.bind(this)); - if (props.open) { + if (props.open !== false) { this.connections.allowDefaultPortFrom(new ec2.AnyIPv4(), `Allow from anyone on port ${port}`); } } From 6f08406df67be77bb00b50ef94fa85361a8b0041 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 3 Oct 2018 14:56:14 +0200 Subject: [PATCH 63/97] Add validation on port mappings with tests --- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 2 +- .../aws-ecs/lib/base/base-task-definition.ts | 3 +- .../aws-ecs/lib/container-definition.ts | 15 +- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 2 +- .../aws-ecs/test/test.container-definition.ts | 131 ++++++++++++++++++ 5 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/test.container-definition.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 72599bad8b7ee..3b5778edded73 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -145,7 +145,7 @@ export abstract class BaseService extends cdk.Construct * Returns 0 if the networking mode implies dynamic port allocation. */ private get instancePort() { - return this.taskDef.networkMode === NetworkMode.Bridge ? 0 : this.taskDef.defaultContainer!.instancePort; + return this.taskDef.networkMode === NetworkMode.Bridge ? 0 : this.taskDef.defaultContainer!.ingressPort; } } diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts index 7b4159a226048..4c71d62a2a0d5 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts @@ -89,7 +89,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { * Add a container to this task */ public addContainer(id: string, props: ContainerDefinitionProps) { - const container = new ContainerDefinition(this, id, props); + const container = new ContainerDefinition(this, id, this, props); this.containers.push(container); if (container.usesEcrImages) { this.generateExecutionRole(); @@ -97,6 +97,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { if (this.defaultContainer === undefined && container.essential) { this.defaultContainer = container; } + return container; } diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index b683fc7fde655..bdcf18cd59f68 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -1,4 +1,5 @@ import cdk = require('@aws-cdk/cdk'); +import { BaseTaskDefinition, NetworkMode } from './base/base-task-definition'; import { ContainerImage } from './container-image'; import { cloudformation } from './ecs.generated'; import { LinuxParameters } from './linux-parameters'; @@ -183,11 +184,14 @@ export class ContainerDefinition extends cdk.Construct { private readonly links = new Array(); + private readonly taskDefinition: BaseTaskDefinition; + private _usesEcrImages: boolean = false; - constructor(parent: cdk.Construct, id: string, private readonly props: ContainerDefinitionProps) { + constructor(parent: cdk.Construct, id: string, taskDefinition: BaseTaskDefinition, private readonly props: ContainerDefinitionProps) { super(parent, id); this.essential = props.essential !== undefined ? props.essential : true; + this.taskDefinition = taskDefinition; props.image.bind(this); } @@ -204,6 +208,13 @@ export class ContainerDefinition extends cdk.Construct { } public addPortMappings(...portMappings: PortMapping[]) { + for (const pm of portMappings) { + if (this.taskDefinition.networkMode === NetworkMode.AwsVpc || this.taskDefinition.networkMode === NetworkMode.Host) { + if (pm.containerPort !== pm.hostPort && pm.hostPort !== undefined) { + throw new Error(`Host port ${pm.hostPort} does not match container port ${pm.containerPort}.`); + } + } + } this.portMappings.push(...portMappings); } @@ -229,7 +240,7 @@ export class ContainerDefinition extends cdk.Construct { /** * Return the instance port that the container will be listening on */ - public get instancePort(): number { + public get ingressPort(): number { if (this.portMappings.length === 0) { throw new Error(`Container ${this.id} hasn't defined any ports`); } diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index 1808ab1f04590..f389d4b0b45a4 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -177,7 +177,7 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { this.loadBalancers.push({ loadBalancerName: loadBalancer.loadBalancerName, containerName: this.taskDefinition.defaultContainer!.id, - containerPort: this.taskDefinition.defaultContainer!.instancePort, + containerPort: this.taskDefinition.defaultContainer!.ingressPort, }); } } diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts new file mode 100644 index 0000000000000..ef88fecaa9596 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -0,0 +1,131 @@ +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../lib'); + +export = { + "When creating a Task Definition": { + // Validating portMapping inputs + "With network mode AwsVpc": { + "Host port should be the same as container port"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + // WHEN + test.throws(() => { + container.addPortMappings({ + containerPort: 8080, + hostPort: 8081 + }); + }); + // THEN + test.done(); + }, + + "Host port can be empty "(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + // WHEN + container.addPortMappings({ + containerPort: 8080, + }); + + // THEN no excpetion raised + test.done(); + }, + }, + "With network mode Host ": { + "Host port should be the same as container port"(test: Test) { + test.done(); + }, + "Host port can be empty "(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + // WHEN + container.addPortMappings({ + containerPort: 8080, + }); + + // THEN no excpetion raised + test.done(); + }, + }, + "With network mode Bridge": { + "Host port should not be lower than 1024"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + test.throws(() => { + container.addPortMappings({ + containerPort: 8080, + hostPort: 1, + }); + }); + + // THEN + test.done(); + }, + }, + + // "With health check": { + // "healthCheck.command is a single string"(test: Test) { + // const stack = new cdk.Stack(); + // const taskDefinition = new TaskDefinition(stack, 'TaskDef'); + // const containerDefinition = taskDefinition.ContainerDefinition[0]; + // test.deepEqual(resolve(vpc.vpcId), {Ref: 'TheVPC92636AB0' } ); + // test.done(); + // }, + // } + }, + "Ingress Port": { + "With network mode AwsVpc": { + "Ingress port should be the same as container port"(test: Test) { + test.done(); + }, + }, + "With network mode Host ": { + "Ingress port should be the same as container port"(test: Test) { + test.done(); + }, + }, + "With network mode Bridge": { + "Ingress port should be the same as host port if supplied"(test: Test) { + test.done(); + }, + "Ingress port should be the 0 if not supplied"(test: Test) { + test.done(); + }, + }, + }, +}; From 531236286b9adf83c5960e906a9e70e0681da1c4 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 3 Oct 2018 15:19:28 +0200 Subject: [PATCH 64/97] Implement ingressPort method --- .../aws-ecs/lib/container-definition.ts | 9 +- .../aws-ecs/test/test.container-definition.ts | 90 +++++++++++++++++-- 2 files changed, 92 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index bdcf18cd59f68..ed1af1f3727dc 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -244,7 +244,14 @@ export class ContainerDefinition extends cdk.Construct { if (this.portMappings.length === 0) { throw new Error(`Container ${this.id} hasn't defined any ports`); } - return this.portMappings[0].hostPort || this.portMappings[0].containerPort; + const defaultPortMapping = this.portMappings[0]; + if (defaultPortMapping.hostPort !== undefined) { + return defaultPortMapping.hostPort; + } + if (this.taskDefinition.networkMode === NetworkMode.Bridge) { + return 0; + } + return defaultPortMapping.containerPort; } public renderContainerDefinition(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts index ef88fecaa9596..06476589066a4 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -17,14 +17,13 @@ export = { image: ecs.DockerHub.image("/aws/aws-example-app"), memoryLimitMiB: 2048, }); - // WHEN + // THEN test.throws(() => { container.addPortMappings({ containerPort: 8080, hostPort: 8081 }); }); - // THEN test.done(); }, @@ -68,7 +67,7 @@ export = { containerPort: 8080, }); - // THEN no excpetion raised + // THEN no exception raised test.done(); }, }, @@ -85,15 +84,13 @@ export = { memoryLimitMiB: 2048, }); - // WHEN + // THEN test.throws(() => { container.addPortMappings({ containerPort: 8080, hostPort: 1, }); }); - - // THEN test.done(); }, }, @@ -111,19 +108,100 @@ export = { "Ingress Port": { "With network mode AwsVpc": { "Ingress port should be the same as container port"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + container.addPortMappings({ + containerPort: 8080, + }); + const actual = container.ingressPort; + + // THEN + const expected = 8080; + test.equal(actual, expected, "Ingress port should be the same as container port"); test.done(); }, }, "With network mode Host ": { "Ingress port should be the same as container port"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.Host, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + container.addPortMappings({ + containerPort: 8080, + }); + const actual = container.ingressPort; + + // THEN + const expected = 8080; + test.equal(actual, expected); test.done(); }, }, "With network mode Bridge": { "Ingress port should be the same as host port if supplied"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.Bridge, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + container.addPortMappings({ + containerPort: 8081, + hostPort: 8080, + }); + const actual = container.ingressPort; + + // THEN + const expected = 8080; + test.equal(actual, expected); test.done(); }, "Ingress port should be the 0 if not supplied"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.Bridge, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + container.addPortMappings({ + containerPort: 8081, + }); + const actual = container.ingressPort; + + // THEN + const expected = 0; + test.equal(actual, expected); test.done(); }, }, From 6f42026f22017b7f05daa57ae6084be0c2e5b597 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 4 Oct 2018 11:10:33 +0200 Subject: [PATCH 65/97] Add dependency on listener This ensures that the dependency resolution by CFN works properly when attaching a load balancer to an ECS service --- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 4 +++- .../lib/alb/application-listener.ts | 3 ++- .../lib/alb/application-target-group.ts | 7 ++++++- .../lib/nlb/network-listener.ts | 4 +++- .../lib/nlb/network-target-group.ts | 14 ++++++++++++++ .../lib/shared/base-listener.ts | 4 +++- .../lib/shared/base-target-group.ts | 19 ++++++++++++++++++- .../lib/shared/util.ts | 13 +++++++++++++ 8 files changed, 62 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 3b5778edded73..67d8cf58e23c3 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -136,6 +136,8 @@ export abstract class BaseService extends cdk.Construct containerPort: this.instancePort, }); + this.resource.addDependency(targetGroup.listenerDependency()); + return { targetType: elbv2.TargetType.Ip }; } @@ -185,4 +187,4 @@ export enum FargatePlatformVersion { * Based on Amazon Linux 2017.09. */ Version10 = '1.0.0', -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index 18f5f8d803542..217db667db327 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -258,7 +258,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis /** * Properties to reference an existing listener */ -export interface IApplicationListener extends ec2.IConnectable { +export interface IApplicationListener extends ec2.IConnectable, cdk.IDependable { /** * ARN of the listener */ @@ -319,6 +319,7 @@ export interface ApplicationListenerRefProps { } class ImportedApplicationListener extends cdk.Construct implements IApplicationListener { + public readonly dependencyElements: cdk.IDependable[] = []; public readonly connections: ec2.Connections; /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts index b6f21689df72f..2454f856af895 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts @@ -3,7 +3,7 @@ import cdk = require('@aws-cdk/cdk'); import { BaseTargetGroup, BaseTargetGroupProps, ITargetGroup, LoadBalancerTargetProps, TargetGroupRefProps } from '../shared/base-target-group'; import { ApplicationProtocol } from '../shared/enums'; import { BaseImportedTargetGroup } from '../shared/imported'; -import { determineProtocolAndPort } from '../shared/util'; +import { determineProtocolAndPort, LazyDependable } from '../shared/util'; import { IApplicationListener } from './application-listener'; /** @@ -144,6 +144,7 @@ export class ApplicationTargetGroup extends BaseTargetGroup { listener.registerConnectable(member.connectable, member.portRange); } this.listeners.push(listener); + this.dependableListeners.push(listener); } } @@ -181,6 +182,10 @@ class ImportedApplicationTargetGroup extends BaseImportedTargetGroup implements public registerListener(_listener: IApplicationListener) { // Nothing to do, we know nothing of our members } + + public listenerDependency(): cdk.IDependable { + return new LazyDependable([]); + } } /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts index e72e8f1c951df..99c89342fa7a2 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts @@ -113,7 +113,7 @@ export class NetworkListener extends BaseListener implements INetworkListener { /** * Properties to reference an existing listener */ -export interface INetworkListener { +export interface INetworkListener extends cdk.IDependable { /** * ARN of the listener */ @@ -134,6 +134,8 @@ export interface NetworkListenerRefProps { * An imported Network Listener */ class ImportedNetworkListener extends cdk.Construct implements INetworkListener { + public readonly dependencyElements: cdk.IDependable[] = []; + /** * ARN of the listener */ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts index 5dc2253d84f35..4cb59fbdfad2e 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts @@ -2,6 +2,8 @@ import cdk = require('@aws-cdk/cdk'); import { BaseTargetGroup, BaseTargetGroupProps, ITargetGroup, LoadBalancerTargetProps, TargetGroupRefProps } from '../shared/base-target-group'; import { Protocol } from '../shared/enums'; import { BaseImportedTargetGroup } from '../shared/imported'; +import { LazyDependable } from '../shared/util'; +import { INetworkListener } from './network-listener'; /** * Properties for a new Network Target Group @@ -62,6 +64,15 @@ export class NetworkTargetGroup extends BaseTargetGroup { this.addLoadBalancerTarget(result); } } + + /** + * Register a listener that is load balancing to this target group. + * + * Don't call this directly. It will be called by listeners. + */ + public registerListener(listener: INetworkListener) { + this.dependableListeners.push(listener); + } } /** @@ -75,6 +86,9 @@ export interface INetworkTargetGroup extends ITargetGroup { * An imported network target group */ class ImportedNetworkTargetGroup extends BaseImportedTargetGroup implements INetworkTargetGroup { + public listenerDependency(): cdk.IDependable { + return new LazyDependable([]); + } } /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts index 6bc12566cb6f4..af212853acea7 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -5,7 +5,8 @@ import { ITargetGroup } from './base-target-group'; /** * Base class for listeners */ -export abstract class BaseListener extends cdk.Construct { +export abstract class BaseListener extends cdk.Construct implements cdk.IDependable { + public readonly dependencyElements: cdk.IDependable[]; public readonly listenerArn: string; private readonly defaultActions: any[] = []; @@ -17,6 +18,7 @@ export abstract class BaseListener extends cdk.Construct { defaultActions: new cdk.Token(() => this.defaultActions), }); + this.dependencyElements = [resource]; this.listenerArn = resource.ref; } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts index 356456de0ab26..280b2ab25bd82 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts @@ -2,7 +2,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { cloudformation } from '../elasticloadbalancingv2.generated'; import { Protocol, TargetType } from './enums'; -import { Attributes, renderAttributes } from './util'; +import { Attributes, LazyDependable, renderAttributes } from './util'; /** * Basic properties of both Application and Network Target Groups @@ -145,6 +145,11 @@ export abstract class BaseTargetGroup extends cdk.Construct implements ITargetGr */ protected readonly defaultPort: string; + /** + * List of listeners routing to this target group + */ + protected readonly dependableListeners = new Array(); + /** * Attributes of this target group */ @@ -234,6 +239,13 @@ export abstract class BaseTargetGroup extends cdk.Construct implements ITargetGr this.resource.addDependency(...other); } + /** + * Return an object to depend on the listeners added to this target group + */ + public listenerDependency(): cdk.IDependable { + return new LazyDependable(this.dependableListeners); + } + /** * Register the given load balancing target as part of this group */ @@ -272,6 +284,11 @@ export interface ITargetGroup { * ARN of the target group */ readonly targetGroupArn: string; + + /** + * Return an object to depend on the listeners added to this target group + */ + listenerDependency(): cdk.IDependable; } /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts index d992d156a3b2f..808a8f0e2c33e 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts @@ -1,3 +1,4 @@ +import cdk = require('@aws-cdk/cdk'); import { ApplicationProtocol } from "./enums"; export type Attributes = {[key: string]: string | undefined}; @@ -67,3 +68,15 @@ export function determineProtocolAndPort(protocol: ApplicationProtocol | undefin export function ifUndefined(x: T | undefined, def: T) { return x !== undefined ? x : def; } + +/** + * Allow lazy evaluation of a list of dependables + */ +export class LazyDependable implements cdk.IDependable { + constructor(private readonly depList: cdk.IDependable[]) { + } + + public get dependencyElements(): cdk.IDependable[] { + return this.depList; + } +} \ No newline at end of file From 1983858b0ebfaf337a4fd118dcff3740a9bbea27 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 4 Oct 2018 11:51:43 +0200 Subject: [PATCH 66/97] Integ test for Fargate service Notes: - Loadbalancer needs to be specified to be "internetFacing" - Cannot modify LoadBalancer on service once service has been created; can get around this by modifying ID of LB - Listener must be set to open --- packages/@aws-cdk/aws-ecs/test/fargate/cdk.json | 9 +++++++++ .../{integ.lb-bridge-nw.ts => integ.lb-awsvpc-nw.ts} | 12 ++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/fargate/cdk.json rename packages/@aws-cdk/aws-ecs/test/fargate/{integ.lb-bridge-nw.ts => integ.lb-awsvpc-nw.ts} (72%) diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json b/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json new file mode 100644 index 0000000000000..64fd3b2dafe6e --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json @@ -0,0 +1,9 @@ +{ + "context": { + "availability-zones:794715269151:eu-west-1": [ + "eu-west-1a", + "eu-west-1b", + "eu-west-1c" + ] + } +} diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts similarity index 72% rename from packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts rename to packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts index 542975396d13b..fa7e847112bbf 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts @@ -4,9 +4,10 @@ import cdk = require('@aws-cdk/cdk'); import ecs = require('../../lib'); const app = new cdk.App(process.argv); -const stack = new cdk.Stack(app, 'aws-ecs-integ'); +const stack = new cdk.Stack(app, 'aws-ecs-integ-fargate'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); + const cluster = new ecs.FargateCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { @@ -15,10 +16,9 @@ const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { }); const container = taskDefinition.addContainer('web', { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), - memoryLimitMiB: 1024, }); container.addPortMappings({ - containerPort: 8080, + containerPort: 80, protocol: ecs.Protocol.Tcp }); @@ -27,13 +27,13 @@ const service = new ecs.FargateService(stack, "Service", { taskDefinition, }); -const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); -const listener = lb.addListener('PublicListener', { port: 80 }); +const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc, internetFacing: true }); +const listener = lb.addListener('PublicListener', { port: 80, open: true }); listener.addTargets('ECS', { port: 80, targets: [service] }); -new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName }); +new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName, }); process.stdout.write(app.run()); \ No newline at end of file From 4ea6b8b70cb9d343e6288ce93686fc8fbe80c073 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 4 Oct 2018 14:03:54 +0200 Subject: [PATCH 67/97] hello-cdk-fargate example works --- .../hello-cdk-fargate/cdk.json | 47 +++++++++++++++++++ .../hello-cdk-fargate/index.ts | 16 +++++-- 2 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 examples/cdk-examples-typescript/hello-cdk-fargate/cdk.json diff --git a/examples/cdk-examples-typescript/hello-cdk-fargate/cdk.json b/examples/cdk-examples-typescript/hello-cdk-fargate/cdk.json new file mode 100644 index 0000000000000..65f81e1c0d3ee --- /dev/null +++ b/examples/cdk-examples-typescript/hello-cdk-fargate/cdk.json @@ -0,0 +1,47 @@ +{ + "app": "node index", + "context": { + "availability-zones:585695036304:us-east-1": [ + "us-east-1a", + "us-east-1b", + "us-east-1c", + "us-east-1d", + "us-east-1e", + "us-east-1f" + ], + "ssm:585695036304:us-east-1:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-14c5486b", + "availability-zones:585695036304:eu-west-2": [ + "eu-west-2a", + "eu-west-2b", + "eu-west-2c" + ], + "ssm:585695036304:eu-west-2:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-a36f8dc4", + "availability-zones:585695036304:eu-west-1": [ + "eu-west-1a", + "eu-west-1b", + "eu-west-1c" + ], + "ssm:585695036304:eu-west-1:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-ca0135b3", + "availability-zones:794715269151:us-west-2": [ + "us-west-2a", + "us-west-2b", + "us-west-2c" + ], + "availability-zones:993655754359:us-west-2": [ + "us-west-2a", + "us-west-2b", + "us-west-2c" + ], + "availability-zones:993655754359:eu-west-1": [ + "eu-west-1a", + "eu-west-1b", + "eu-west-1c" + ], + "ssm:794715269151:us-west-2:/aws/service/ecs/optimized-ami/amazon-linux/recommended": "{\"schema_version\":1,\"image_name\":\"amzn-ami-2018.03.g-amazon-ecs-optimized\",\"image_id\":\"ami-00430184c7bb49914\",\"os\":\"Amazon Linux\",\"ecs_runtime_version\":\"Docker version 18.06.1-ce\",\"ecs_agent_version\":\"1.20.3\"}", + "availability-zones:794715269151:eu-west-1": [ + "eu-west-1a", + "eu-west-1b", + "eu-west-1c" + ] + } +} diff --git a/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts index 3561b026fb7b6..83b8e3a0916b0 100644 --- a/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts @@ -6,7 +6,10 @@ import cdk = require('@aws-cdk/cdk'); class BonjourFargate extends cdk.Stack { constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { super(parent, name, props); - const vpc = new ec2.VpcNetwork(this, 'VPC'); + const vpc = new ec2.VpcNetwork(this, 'MyVpc', { + maxAZs: 2 // Just to limit the number of resources created and avoid reaching quotas + }); + const cluster = new ecs.FargateCluster(this, 'Cluster', { vpc }); @@ -16,10 +19,14 @@ class BonjourFargate extends cdk.Stack { memoryMiB: '2GB' }); - taskDefinition.addContainer('WebApp', { + const container = taskDefinition.addContainer('WebApp', { // image: new ecs.ImageFromSource('./my-webapp-source'), image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), - // portMappings: [{ containerPort: 8080 }], + }); + + container.addPortMappings({ + containerPort: 80, + protocol: ecs.Protocol.Tcp }); const service = new ecs.FargateService(this, 'Service', { @@ -41,6 +48,9 @@ class BonjourFargate extends cdk.Stack { targets: [service], protocol: elbv2.ApplicationProtocol.Http }); + + // This outputs for the DNS where you can access your service + new cdk.Output(this, 'LoadBalancerDNS', { value: loadBalancer.dnsName }); } } From e34d515ea95e6083320e5749d18b991b1d105ad1 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 4 Oct 2018 16:05:03 +0200 Subject: [PATCH 68/97] Add lazy evaluation of network configuration property --- packages/@aws-cdk/aws-ecs/lib/base/base-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 67d8cf58e23c3..9d9b8b7e491e8 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -77,7 +77,7 @@ export abstract class BaseService extends cdk.Construct minimumHealthyPercent: props.minimumHealthyPercent }, /* role: never specified, supplanted by Service Linked Role */ - networkConfiguration: this.networkConfiguration, + networkConfiguration: new cdk.Token(() => this.networkConfiguration), platformVersion: props.platformVersion, ...additionalProps }); From bf90c27c0baa93136bca7b6c9f0a289d287b5970 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 4 Oct 2018 16:07:56 +0200 Subject: [PATCH 69/97] Add LoadBalancedFargateService L3 construct --- .../hello-cdk-fargate/index.ts | 52 ++------ packages/@aws-cdk/aws-ecs/lib/index.ts | 1 + .../lib/load-balanced-fargate-service.ts | 114 ++++++++++++++++++ 3 files changed, 125 insertions(+), 42 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts diff --git a/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts index 83b8e3a0916b0..03456cf32280f 100644 --- a/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts @@ -1,56 +1,24 @@ import ec2 = require('@aws-cdk/aws-ec2'); import ecs = require('@aws-cdk/aws-ecs'); -import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); class BonjourFargate extends cdk.Stack { constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { super(parent, name, props); - const vpc = new ec2.VpcNetwork(this, 'MyVpc', { - maxAZs: 2 // Just to limit the number of resources created and avoid reaching quotas - }); - - const cluster = new ecs.FargateCluster(this, 'Cluster', { - vpc - }); - - const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', { - cpu: '512', - memoryMiB: '2GB' - }); - - const container = taskDefinition.addContainer('WebApp', { - // image: new ecs.ImageFromSource('./my-webapp-source'), - image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), - }); - container.addPortMappings({ - containerPort: 80, - protocol: ecs.Protocol.Tcp - }); + // 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.FargateCluster(this, 'Cluster', { vpc }); - const service = new ecs.FargateService(this, 'Service', { + // Instantiate Fargate Service with just cluster and image + const fargateService = new ecs.LoadBalancedFargateService(this, "FargateService", { cluster, - taskDefinition - }); - - const loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'LB', { - vpc, - internetFacing: true, - }); - - const listener = loadBalancer.addListener('Listener', { - port: 80, - open: true, - }); - - listener.addTargets('DefaultTargets', { - targets: [service], - protocol: elbv2.ApplicationProtocol.Http + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), }); - // This outputs for the DNS where you can access your service - new cdk.Output(this, 'LoadBalancerDNS', { value: loadBalancer.dnsName }); + // Output the DNS where you can access your service + new cdk.Output(this, 'LoadBalancerDNS', { value: fargateService.loadBalancer.dnsName }); } } @@ -58,4 +26,4 @@ const app = new cdk.App(process.argv); new BonjourFargate(app, 'Bonjour'); -process.stdout.write(app.run()); +process.stdout.write(app.run()); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 5c3170c2fe003..892ddfba70418 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -12,6 +12,7 @@ export * from './fargate/fargate-task-definition'; export * from './container-definition'; export * from './container-image'; export * from './linux-parameters'; +export * from './load-balanced-fargate-service'; export * from './log-drivers/log-driver'; export * from './log-drivers/aws-log-driver'; diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts new file mode 100644 index 0000000000000..8d02f178f5c54 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts @@ -0,0 +1,114 @@ +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); +import { ContainerImage } from './container-image'; +import { FargateCluster } from './fargate/fargate-cluster'; +import { FargateService } from './fargate/fargate-service'; +import { FargateTaskDefinition } from './fargate/fargate-task-definition'; + +export interface LoadBalancedFargateServiceProps { + /** + * The cluster where your Fargate service will be deployed + */ + cluster: FargateCluster; + + image: ContainerImage; + /** + * 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 + */ + 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 + */ + memoryMiB?: string; + + /** + * The container port of the application load balancer attached to your Fargate service. Corresponds to container port mapping. + * + * @default 80 + */ + containerPort?: number; + + /** + * Determines whether the Application Load Balancer will be internet-facing + * + * @default true + */ + publicLoadBalancer?: boolean; + + /** + * Determines whether your Fargate Service will be assigned a public IP address. + * + * @default false + */ + publicTasks?: boolean; +} + +export class LoadBalancedFargateService extends cdk.Construct { + public readonly loadBalancer: elbv2.ApplicationLoadBalancer; + + constructor(parent: cdk.Construct, id: string, props: LoadBalancedFargateServiceProps) { + super(parent, id); + + const taskDefinition = new FargateTaskDefinition(this, 'TaskDef', { + memoryMiB: props.memoryMiB, + cpu: props.cpu + }); + + const container = taskDefinition.addContainer('web', { + image: props.image, + }); + + container.addPortMappings({ + containerPort: props.containerPort || 80, + }); + + const assignPublicIp = props.publicTasks !== undefined ? props.publicTasks : false; + const service = new FargateService(this, "Service", { + cluster: props.cluster, + taskDefinition, + assignPublicIp + }); + + const internetFacing = props.publicLoadBalancer !== undefined ? props.publicLoadBalancer : true; + const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { + vpc: props.cluster.vpc, + internetFacing + }); + + this.loadBalancer = lb; + + const listener = lb.addListener('PublicListener', { port: 80, open: true }); + listener.addTargets('ECS', { + port: 80, + targets: [service] + }); + } +} \ No newline at end of file From ffbad9f29ffacb09844206af0489b4f209fd617c Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 4 Oct 2018 16:42:07 +0200 Subject: [PATCH 70/97] Start LoadBalancedFargateServiceApplet --- .../fargate-service.yml | 5 ++ .../load-balanced-fargate-service-applet.ts | 86 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml create mode 100644 packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml b/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml new file mode 100644 index 0000000000000..5148b20ae12a1 --- /dev/null +++ b/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml @@ -0,0 +1,5 @@ +# applet is loaded from the local ./test-applet.js file +applet: cdk-ecs:LoadBalancedFargateServiceApplet +image: 'amazon/amazon-ecs-sample' +cpu: 2048 +memoryMiB: 1024 diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts new file mode 100644 index 0000000000000..23fea36a3729c --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts @@ -0,0 +1,86 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { DockerHub } from './container-image'; +import { FargateCluster } from './fargate/fargate-cluster'; +import { LoadBalancedFargateService } from './load-balanced-fargate-service'; + +export interface LoadBalancedFargateServiceAppletProps extends cdk.StackProps { + image: string; + /** + * 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 + */ + 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 + */ + memoryMiB?: string; + + /** + * The container port of the application load balancer attached to your Fargate service. Corresponds to container port mapping. + * + * @default 80 + */ + containerPort?: number; + + /** + * Determines whether the Application Load Balancer will be internet-facing + * + * @default true + */ + publicLoadBalancer?: boolean; + + /** + * Determines whether your Fargate Service will be assigned a public IP address. + * + * @default false + */ + publicTasks?: boolean; +} + +export class LoadBalancedFargateServiceApplet extends cdk.Stack { + constructor(parent: cdk.App, id: string, props: LoadBalancedFargateServiceAppletProps) { + super(parent, id, props); + + const vpc = new ec2.VpcNetwork(this, 'MyVpc', { maxAZs: 2 }); + const cluster = new FargateCluster(this, 'Cluster', { vpc }); + + // Instantiate Fargate Service with just cluster and image + new LoadBalancedFargateService(this, "FargateService", { + cluster, + cpu: props.cpu, + containerPort: props.containerPort, + memoryMiB: props.memoryMiB, + publicLoadBalancer: props.publicLoadBalancer, + publicTasks: props.publicTasks, + image: DockerHub.image(props.image), + }); + } +} \ No newline at end of file From d1b22bd29cdbf8b9e6352d5a441a9fe1e5d1cff6 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 4 Oct 2018 16:42:18 +0200 Subject: [PATCH 71/97] Application AutoScaling WIP --- .../lib/base-scaling-policy.ts | 54 +++++ .../aws-applicationautoscaling/lib/index.ts | 4 + .../lib/scalable-target.ts | 189 ++++++++++++++++++ .../lib/step-scaling-policy.ts | 130 ++++++++++++ .../aws-applicationautoscaling/package.json | 3 +- 5 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk/aws-applicationautoscaling/lib/base-scaling-policy.ts create mode 100644 packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts create mode 100644 packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/base-scaling-policy.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/base-scaling-policy.ts new file mode 100644 index 0000000000000..3aca090aab883 --- /dev/null +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/base-scaling-policy.ts @@ -0,0 +1,54 @@ +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from './applicationautoscaling.generated'; +import { IScalableTarget } from './scalable-target'; + +/** + * Properties for a scaling policy + */ +export interface BaseScalingPolicyProps { + /** + * A name for the scaling policy + * + * @default Automatically generated name + */ + policyName?: string; + + /** + * The scalable target + */ + scalingTarget: IScalableTarget; +} + +export abstract class BaseScalingPolicy extends cdk.Construct { + public readonly scalingPolicyArn: string; + + constructor(parent: cdk.Construct, id: string, props: BaseScalingPolicyProps, additionalProps: any) { + super(parent, id); + + // scalingTargetId == "" means look at the other properties + const scalingTargetId = props.scalingTarget.scalableTargetId !== "" ? props.scalingTarget.scalableTargetId : undefined; + let resourceId; + let scalableDimension; + let serviceNamespace; + if (scalingTargetId === undefined) { + if (props.scalingTarget.scalableDimension === "" || props.scalingTarget.resourceId === "" + || props.scalingTarget.serviceNamespace === "") { + throw new Error(`A scaling target requires either a 'scalableTargetId' or all of 'resourceId', 'scalableDimension', 'serviceNamespace'`); + } + resourceId = props.scalingTarget.resourceId; + scalableDimension = props.scalingTarget.scalableDimension; + serviceNamespace = props.scalingTarget.serviceNamespace; + } + + const resource = new cloudformation.ScalingPolicyResource(this, 'Resource', { + policyName: props.policyName || this.uniqueId, + scalingTargetId, + resourceId, + scalableDimension, + serviceNamespace, + ...additionalProps + }); + + this.scalingPolicyArn = resource.scalingPolicyArn; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/index.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/index.ts index 5f831ad820f97..40dc603cd0132 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/index.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/index.ts @@ -1,2 +1,6 @@ // AWS::ApplicationAutoScaling CloudFormation Resources: export * from './applicationautoscaling.generated'; + +export * from './scalable-target'; +export * from './base-scaling-policy'; +export * from './step-scaling-policy'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts new file mode 100644 index 0000000000000..3609d0343d148 --- /dev/null +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts @@ -0,0 +1,189 @@ +import iam = require('@aws-cdk/aws-iam'); +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from './applicationautoscaling.generated'; + +/** + * Properties for a scalable target + */ +export interface ScalableTargetProps { + /** + * The minimum value that Application Auto Scaling can use to scale a target during a scaling activity. + */ + minCapacity: number; + + /** + * The maximum value that Application Auto Scaling can use to scale a target during a scaling activity. + */ + maxCapacity: number; + + /** + * Role that allows Application Auto Scaling to modify your scalable target. + * + * If not supplied, a service-linked role is used. Some resources require a + * concrete role. + * + * @default A service-linked role is used + */ + role?: iam.Role; + + /** + * The resource identifier to associate with this scalable target. + * + * This string consists of the resource type and unique identifier. + * + * @example service/ecsStack-MyECSCluster-AB12CDE3F4GH/ecsStack-MyECSService-AB12CDE3F4GH + * @see https://docs.aws.amazon.com/autoscaling/application/APIReference/API_RegisterScalableTarget.html + */ + resourceId: string; + + /** + * The scalable dimension that's associated with the scalable target. + * + * Specify the service namespace, resource type, and scaling property. + * + * @example ecs:service:DesiredCount + * @see https://docs.aws.amazon.com/autoscaling/application/APIReference/API_ScalingPolicy.html + */ + scalableDimension: string; + + /** + * The namespace of the AWS service that provides the resource or + * custom-resource for a resource provided by your own application or + * service. + * + * For valid AWS service namespace values, see the RegisterScalableTarget + * action in the Application Auto Scaling API Reference. + * + * @see https://docs.aws.amazon.com/autoscaling/application/APIReference/API_RegisterScalableTarget.html + */ + serviceNamespace: string; +} + +/** + * Define a scalable target + */ +export class ScalableTarget extends cdk.Construct implements IScalableTarget { + /** + * ID of the Scalable Target + * + * @example service/ecsStack-MyECSCluster-AB12CDE3F4GH/ecsStack-MyECSService-AB12CDE3F4GH|ecs:service:DesiredCount|ecs + */ + public readonly scalableTargetId: string; + + public readonly resourceId: string; + public readonly scalableDimension: string; + public readonly serviceNamespace: string; + + private readonly actions = new Array(); + + constructor(parent: cdk.Construct, id: string, props: ScalableTargetProps) { + super(parent, id); + + const resource = new cloudformation.ScalableTargetResource(this, 'Resource', { + maxCapacity: props.maxCapacity, + minCapacity: props.minCapacity, + resourceId: props.resourceId, + // Schema says roleArn is required but docs say it's not. Override typechecking. + roleArn: (props.role && props.role.roleArn) as any, + scalableDimension: props.scalableDimension, + scheduledActions: this.actions, + serviceNamespace: props.serviceNamespace + }); + + this.resourceId = props.resourceId; + this.scalableDimension = props.scalableDimension; + this.serviceNamespace = props.serviceNamespace; + this.scalableTargetId = resource.scalableTargetId; + } + + /** + * Schedule an action for this scalable target + */ + public addScheduledAction(action: ScheduledAction) { + if (action.minCapacity === undefined && action.maxCapacity === undefined) { + throw new Error('You must supply at least one of minCapacity or maxCapacity'); + } + this.actions.push({ + scheduledActionName: action.name, + schedule: action.schedule, + startTime: action.startTime, + endTime: action.endTime, + scalableTargetAction: { + maxCapacity: action.maxCapacity, + minCapacity: action.minCapacity + }, + }); + } +} + +export interface ScheduledAction { + /** + * A name for the scheduled action + */ + name: string; + + /** + * When to perform this action. + * + * Support formats: + * - at(yyyy-mm-ddThh:mm:ss) + * - rate(value unit) + * - cron(fields) + * + * "At" expressions are useful for one-time schedules. Specify the time in + * UTC. + * + * For "rate" expressions, value is a positive integer, and unit is minute, + * minutes, hour, hours, day, or days. + * + * For more information about cron expressions, see https://en.wikipedia.org/wiki/Cron. + * + * @example rate(12 hours) + */ + schedule: string; + + /** + * When this scheduled action becomes active. + * + * @default The rule is activate immediately + */ + startTime?: Date + + /** + * When this scheduled action expires. + * + * @default The rule never expires. + */ + endTime?: Date; + + /** + * The new minimum capacity. + * + * During the scheduled time, if the current capacity is below the minimum + * capacity, Application Auto Scaling scales out to the minimum capacity. + * + * At least one of maxCapacity and minCapacity must be supplied. + * + * @default No new minimum capacity + */ + minCapacity?: number; + + /** + * The new maximum capacity. + * + * During the scheduled time, the current capacity is above the maximum + * capacity, Application Auto Scaling scales in to the maximum capacity. + * + * At least one of maxCapacity and minCapacity must be supplied. + * + * @default No new maximum capacity + */ + maxCapacity?: number; +} + +export interface IScalableTarget { + readonly scalableTargetId: string; + readonly resourceId: string; + readonly scalableDimension: string; + readonly serviceNamespace: string; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts new file mode 100644 index 0000000000000..1b3ea03fd4e55 --- /dev/null +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts @@ -0,0 +1,130 @@ +import cloudwatch = require('@aws-cdk/aws-cloudwatch'); +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from './applicationautoscaling.generated'; +import { BaseScalingPolicy, BaseScalingPolicyProps } from "./base-scaling-policy"; + +/** + * Properties for a scaling policy + */ +export interface StepScalingPolicyProps extends BaseScalingPolicyProps { + /** + * How the adjustment numbers are interpreted + * + * @default ChangeInCapacity + */ + adjustmentType?: AdjustmentType; + + /** + * Grace period after scaling activity. + * + * For scale out policies, multiple scale outs during the cooldown period are + * squashed so that only the biggest scale out happens. + * + * For scale in policies, subsequent scale ins during the cooldown period are + * ignored. + * + * @see https://docs.aws.amazon.com/autoscaling/application/APIReference/API_StepScalingPolicyConfiguration.html + * @default No cooldown period + */ + cooldownSeconds?: number; + + /** + * Minimum absolute number to adjust capacity with as result of percentage scaling. + * + * Only when using AdjustmentType = PercentChangeInCapacity, this number controls + * the minimum absolute effect size. + * + * @default No minimum scaling effect + */ + minAdjustmentMagnitude?: number; + + /** + * The aggregation type for the CloudWatch metrics. + * + * @default Average + */ + metricAggregationType?: MetricAggregationType; +} + +/** + * Define a step scaling policy + * + * This kind of scaling policy adjusts the target capacity in configurable + * steps. The size of the step is configurable based on the metric's distance + * to its alarm threshold. + */ +export class StepScalingPolicy extends BaseScalingPolicy implements cloudwatch.IAlarmAction { + public readonly alarmActionArn: string; + private readonly adjustments = new Array(); + + constructor(parent: cdk.Construct, id: string, props: StepScalingPolicyProps) { + super(parent, id, props, { + policyType: 'StepScaling', + stepScalingPolicyConfiguration: { + adjustmentType: props.adjustmentType, + cooldown: props.cooldownSeconds, + minAdjustmentMagnitude: props.minAdjustmentMagnitude, + metricAggregationType: props.metricAggregationType, + stepAdjustments: new cdk.Token(() => this.adjustments), + } as cloudformation.ScalingPolicyResource.StepScalingPolicyConfigurationProperty + }); + this.alarmActionArn = this.scalingPolicyArn; + } +} + +/** + * How adjustment numbers are interpreted + */ +export enum AdjustmentType { + /** + * Add the adjustment number to the current capacity. + * + * A positive number increases capacity, a negative number decreases capacity. + */ + ChangeInCapacity = 'ChangeInCapacity', + + /** + * Add this percentage of the current capacity to itself. + * + * The number must be between -100 and 100; a positive number increases + * capacity and a negative number decreases it. + */ + PercentChangeInCapacity = 'PercentChangeInCapacity', + + /** + * Make the capacity equal to the exact number given. + */ + ExactCapacity = 'ExactCapacity', +} + +/** + * How the scaling metric is going to be aggregated + */ +export enum MetricAggregationType { + /** + * Average + */ + Average = 'Average', + + /** + * Minimum + */ + Minimum = 'Minimum', + + /** + * Maximum + */ + Maximum = 'Maximum' +} + +export interface ITierRange { + scaleBy(adjustment: number): ITierMark; +} + +export interface ITierMark { + at(threshold: number): ITierRange; + + endAt0(): StepScalingPolicy; + + endAtInfinity(): StepScalingPolicy; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-applicationautoscaling/package.json b/packages/@aws-cdk/aws-applicationautoscaling/package.json index cd5a071eaf1e5..5de930f16c18b 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/package.json +++ b/packages/@aws-cdk/aws-applicationautoscaling/package.json @@ -58,7 +58,8 @@ "pkglint": "^0.10.0" }, "dependencies": { - "@aws-cdk/cdk": "^0.10.0" + "@aws-cdk/cdk": "^0.10.0", + "@aws-cdk/aws-cloudwatch": "^0.10.0" }, "homepage": "https://github.com/awslabs/aws-cdk" } From d1ec640c5ce8368f274f485c7d2e204dbe376d62 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 4 Oct 2018 16:51:18 +0200 Subject: [PATCH 72/97] Make declarative example work --- .../hello-cdk-ecs-declarative/cdk.json | 3 +++ .../hello-cdk-ecs-declarative/fargate-service.yml | 6 +++--- examples/cdk-examples-typescript/package.json | 1 + packages/@aws-cdk/aws-ecs/lib/index.ts | 1 + 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 examples/cdk-examples-typescript/hello-cdk-ecs-declarative/cdk.json diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/cdk.json b/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/cdk.json new file mode 100644 index 0000000000000..01cdce7b82735 --- /dev/null +++ b/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "../node_modules/.bin/cdk-applet-js fargate-service.yml" +} diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml b/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml index 5148b20ae12a1..b00e3c4b0eb4c 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml +++ b/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml @@ -1,5 +1,5 @@ # applet is loaded from the local ./test-applet.js file -applet: cdk-ecs:LoadBalancedFargateServiceApplet +applet: @aws-cdk/aws-ecs:LoadBalancedFargateServiceApplet image: 'amazon/amazon-ecs-sample' -cpu: 2048 -memoryMiB: 1024 +cpu: "2048" +memoryMiB: "1024" diff --git a/examples/cdk-examples-typescript/package.json b/examples/cdk-examples-typescript/package.json index 79b2fd677a62a..08c5b33d82334 100644 --- a/examples/cdk-examples-typescript/package.json +++ b/examples/cdk-examples-typescript/package.json @@ -23,6 +23,7 @@ "pkglint": "^0.10.0" }, "dependencies": { + "@aws-cdk/applet-js": "^0.10.0", "@aws-cdk/aws-autoscaling": "^0.10.0", "@aws-cdk/aws-cloudformation": "^0.10.0", "@aws-cdk/aws-cognito": "^0.10.0", diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 892ddfba70418..0c06c5155f709 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -13,6 +13,7 @@ export * from './container-definition'; export * from './container-image'; export * from './linux-parameters'; export * from './load-balanced-fargate-service'; +export * from './load-balanced-fargate-service-applet'; export * from './log-drivers/log-driver'; export * from './log-drivers/aws-log-driver'; From 36e1ba281ad367e6fb3925d2cab0a36991d9a178 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 5 Oct 2018 12:17:51 +0200 Subject: [PATCH 73/97] Add import exports to Cluster --- .../@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts | 54 ++++++++++++++++--- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 6 +-- .../aws-ecs/lib/fargate/fargate-cluster.ts | 39 +++++++++++++- .../aws-ecs/lib/fargate/fargate-service.ts | 4 +- .../lib/load-balanced-fargate-service.ts | 4 +- 5 files changed, 91 insertions(+), 16 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index 20928345a8f00..b180cc8003838 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -12,8 +12,13 @@ export interface EcsClusterProps extends BaseClusterProps { containersAccessInstanceRole?: boolean; } -export class EcsCluster extends BaseCluster { +export class EcsCluster extends BaseCluster implements IEcsCluster { + public static import(parent: cdk.Construct, name: string, props: ImportedEcsClusterProps): IEcsCluster { + return new ImportedEcsCluster(parent, name, props); + } + public readonly autoScalingGroup: autoscaling.AutoScalingGroup; + public readonly securityGroup: ec2.SecurityGroupRef; constructor(parent: cdk.Construct, name: string, props: EcsClusterProps) { super(parent, name, props); @@ -25,6 +30,8 @@ export class EcsCluster extends BaseCluster { updateType: autoscaling.UpdateType.ReplacingUpdate }); + this.securityGroup = autoScalingGroup.connections.securityGroup!; + // Tie instances to cluster autoScalingGroup.addUserData(`echo ECS_CLUSTER=${this.clusterName} >> /etc/ecs/ecs.config`); @@ -73,13 +80,16 @@ export class EcsCluster extends BaseCluster { this.autoScalingGroup = autoScalingGroup; } - // public runService(taskDefinition: EcsTaskDefinition): EcsService { - // return new Service(this, `${taskDefinition.family}Service`, { - // cluster: this, - // taskDefinition, - // // FIXME: additional props? Or set on Service object? - // }); - // } + /** + * Export the EcsCluster + */ + public export(): ImportedEcsClusterProps { + return { + clusterName: new cdk.Output(this, 'ClusterName', { value: this.clusterName }).makeImportValue().toString(), + vpc: this.vpc.export(), + securityGroup: this.securityGroup.export(), + }; + } } /** @@ -97,3 +107,31 @@ export class EcsOptimizedAmi implements ec2.IMachineImageSource { return new ec2.MachineImage(ami, new ec2.LinuxOS()); } } + +export interface IEcsCluster { + readonly clusterName: string; + readonly vpc: ec2.VpcNetworkRef; + readonly securityGroup: ec2.SecurityGroupRef; +} + +export interface ImportedEcsClusterProps { + readonly clusterName: string; + readonly vpc: ec2.VpcNetworkRefProps; + readonly securityGroup: ec2.SecurityGroupRefProps; +} + +// /** +// * A EcsCluster that has been imported +// */ +class ImportedEcsCluster extends cdk.Construct implements IEcsCluster { + public readonly clusterName: string; + public readonly vpc: ec2.VpcNetworkRef; + public readonly securityGroup: ec2.SecurityGroupRef; + + constructor(parent: cdk.Construct, name: string, props: ImportedEcsClusterProps) { + super(parent, name); + this.clusterName = props.clusterName; + this.vpc = ec2.VpcNetworkRef.import(this, "vpc", props.vpc); + this.securityGroup = ec2.SecurityGroupRef.import(this, "securityGroup", props.securityGroup); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index f389d4b0b45a4..49a30ca38f3fe 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -4,14 +4,14 @@ import cdk = require('@aws-cdk/cdk'); import { BaseService, BaseServiceProps } from '../base/base-service'; import { BaseTaskDefinition, NetworkMode } from '../base/base-task-definition'; import { cloudformation } from '../ecs.generated'; -import { EcsCluster } from './ecs-cluster'; +import { IEcsCluster } from './ecs-cluster'; import { EcsTaskDefinition } from './ecs-task-definition'; export interface EcsServiceProps extends BaseServiceProps { /** * Cluster where service will be deployed */ - cluster: EcsCluster; // should be required? do we assume 'default' exists? + cluster: IEcsCluster; // should be required? do we assume 'default' exists? /** * Task Definition used for running tasks in the service @@ -85,7 +85,7 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { } else { // Either None, Bridge or Host networking. Copy SecurityGroup from ASG. validateNoNetworkingProps(props); - this._securityGroup = props.cluster.autoScalingGroup.connections.securityGroup!; + this._securityGroup = props.cluster.securityGroup!; } this.connections = new ec2.Connections({ securityGroup: this.securityGroup }); diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts index b928fb8be616f..a92f990657592 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts @@ -1,3 +1,4 @@ +import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; @@ -5,8 +6,44 @@ import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; export interface FargateClusterProps extends BaseClusterProps { } -export class FargateCluster extends BaseCluster { +export class FargateCluster extends BaseCluster implements IFargateCluster { + public static import(parent: cdk.Construct, name: string, props: ImportedFargateClusterProps): IFargateCluster { + return new ImportedFargateCluster(parent, name, props); + } constructor(parent: cdk.Construct, name: string, props: FargateClusterProps) { super(parent, name, props); } + /** + * Export the FargateCluster + */ + public export(): ImportedFargateClusterProps { + return { + clusterName: new cdk.Output(this, 'ClusterName', { value: this.clusterName }).makeImportValue().toString(), + vpc: this.vpc.export(), + }; + } } + +export interface IFargateCluster { + clusterName: string; + vpc: ec2.VpcNetworkRef; +} + +export interface ImportedFargateClusterProps { + readonly clusterName: string; + readonly vpc: ec2.VpcNetworkRefProps; +} + +// /** +// * A FargateCluster that has been imported +// */ +class ImportedFargateCluster extends cdk.Construct implements IFargateCluster { + public readonly clusterName: string; + public readonly vpc: ec2.VpcNetworkRef; + + constructor(parent: cdk.Construct, name: string, props: ImportedFargateClusterProps) { + super(parent, name); + this.clusterName = props.clusterName; + this.vpc = ec2.VpcNetworkRef.import(this, "vpc", props.vpc); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index d1a490c859c2b..8144ae913607e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -2,14 +2,14 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { BaseService, BaseServiceProps } from '../base/base-service'; import { BaseTaskDefinition } from '../base/base-task-definition'; -import { FargateCluster } from './fargate-cluster'; +import { IFargateCluster } from './fargate-cluster'; import { FargateTaskDefinition } from './fargate-task-definition'; export interface FargateServiceProps extends BaseServiceProps { /** * Cluster where service will be deployed */ - cluster: FargateCluster; // should be required? do we assume 'default' exists? + cluster: IFargateCluster; // should be required? do we assume 'default' exists? /** * Task Definition used for running tasks in the service diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts index 8d02f178f5c54..04cae422cc05d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts @@ -1,7 +1,7 @@ import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); import { ContainerImage } from './container-image'; -import { FargateCluster } from './fargate/fargate-cluster'; +import { IFargateCluster } from './fargate/fargate-cluster'; import { FargateService } from './fargate/fargate-service'; import { FargateTaskDefinition } from './fargate/fargate-task-definition'; @@ -9,7 +9,7 @@ export interface LoadBalancedFargateServiceProps { /** * The cluster where your Fargate service will be deployed */ - cluster: FargateCluster; + cluster: IFargateCluster; image: ContainerImage; /** From 178898f6f8ff8e2c6c6f5e12236420dcdcad7e8c Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 5 Oct 2018 15:11:56 +0200 Subject: [PATCH 74/97] Fix portMappings/ingressPort --- .../hello-cdk-ecs/index.ts | 2 +- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 15 +++++++-------- .../@aws-cdk/aws-ecs/lib/container-definition.ts | 14 ++++++++------ packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts | 7 ++++++- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 97ba362de1b9c..a433860efef58 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -68,7 +68,7 @@ class BonjourECS extends cdk.Stack { container.addPortMappings({ containerPort: 80, - hostPort: 80, + // hostPort: 80, protocol: ecs.Protocol.Tcp, }); diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 9d9b8b7e491e8..a26a49030fe43 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -90,7 +90,7 @@ export abstract class BaseService extends cdk.Construct // Open up security groups. For dynamic port mapping, we won't know the port range // in advance so we need to open up all ports. - const port = this.instancePort; + const port = this.containerPort; const portRange = port === 0 ? EPHEMERAL_PORT_RANGE : new ec2.TcpPort(port); targetGroup.registerConnectable(this, portRange); @@ -133,21 +133,20 @@ export abstract class BaseService extends cdk.Construct this.loadBalancers.push({ targetGroupArn: targetGroup.targetGroupArn, containerName: this.taskDef.defaultContainer!.id, - containerPort: this.instancePort, + containerPort: this.containerPort, }); this.resource.addDependency(targetGroup.listenerDependency()); - return { targetType: elbv2.TargetType.Ip }; + const targetType = this.taskDef.networkMode === NetworkMode.AwsVpc ? elbv2.TargetType.Ip : elbv2.TargetType.Instance; + return { targetType }; } /** - * Return the port on which the instance will be listening - * - * Returns 0 if the networking mode implies dynamic port allocation. + * Return the port on which the load balancer will be listening */ - private get instancePort() { - return this.taskDef.networkMode === NetworkMode.Bridge ? 0 : this.taskDef.defaultContainer!.ingressPort; + private get containerPort() { + return this.taskDef.defaultContainer!.ingressPort; } } diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index ed1af1f3727dc..1146ade212515 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -214,6 +214,11 @@ export class ContainerDefinition extends cdk.Construct { throw new Error(`Host port ${pm.hostPort} does not match container port ${pm.containerPort}.`); } } + if (this.taskDefinition.networkMode === NetworkMode.Bridge) { + if (pm.hostPort === undefined) { + pm.hostPort = 0; + } + } } this.portMappings.push(...portMappings); } @@ -245,12 +250,9 @@ export class ContainerDefinition extends cdk.Construct { throw new Error(`Container ${this.id} hasn't defined any ports`); } const defaultPortMapping = this.portMappings[0]; - if (defaultPortMapping.hostPort !== undefined) { - return defaultPortMapping.hostPort; - } - if (this.taskDefinition.networkMode === NetworkMode.Bridge) { - return 0; - } + // if (defaultPortMapping.hostPort !== undefined && defaultPortMapping.hostPort !== 0) { + // return defaultPortMapping.hostPort; + // } return defaultPortMapping.containerPort; } diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index b180cc8003838..f82ea5b5f513b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -10,6 +10,11 @@ export interface EcsClusterProps extends BaseClusterProps { * @default false */ containersAccessInstanceRole?: boolean; + + /** + * The type of EC2 instance to launch into your Autoscaling Group + */ + instanceType?: ec2.InstanceType; } export class EcsCluster extends BaseCluster implements IEcsCluster { @@ -25,7 +30,7 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'AutoScalingGroup', { vpc: props.vpc, - instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), + instanceType: props.instanceType || new ec2.InstanceTypePair(ec2.InstanceClass.T2, ec2.InstanceSize.Micro), machineImage: new EcsOptimizedAmi(), updateType: autoscaling.UpdateType.ReplacingUpdate }); From b73bdad4fed4e831114826291c2cc88e05986403 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 5 Oct 2018 15:13:11 +0200 Subject: [PATCH 75/97] Clean up fargate integ test --- .../aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts index 54d49109ae3b5..4b7cb5f6ca163 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts @@ -4,23 +4,21 @@ import cdk = require('@aws-cdk/cdk'); import ecs = require('../../lib'); const app = new cdk.App(process.argv); -const stack = new cdk.Stack(app, 'aws-ecs-integ-fargate'); +const stack = new cdk.Stack(app, 'aws-ecs-integ'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); -const cluster = new ecs.FargateCluster(stack, 'EcsCluster', { vpc }); +const cluster = new ecs.FargateCluster(stack, 'FargateCluster', { vpc }); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { memoryMiB: '1GB', cpu: '512' }); + const container = taskDefinition.addContainer('web', { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), }); -container.addPortMappings({ - containerPort: 80, - protocol: ecs.Protocol.Tcp -}); + container.addPortMappings({ containerPort: 80, protocol: ecs.Protocol.Tcp @@ -33,7 +31,7 @@ const service = new ecs.FargateService(stack, "Service", { const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc, internetFacing: true }); const listener = lb.addListener('PublicListener', { port: 80, open: true }); -listener.addTargets('ECS', { +listener.addTargets('Fargate', { port: 80, targets: [service] }); From 049557ebbea8ecdd7a28116b819b5d16b70b61de Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 5 Oct 2018 15:14:27 +0200 Subject: [PATCH 76/97] Add integ test for load balanced service in bridge mode --- .../aws-ecs/test/ecs/integ.lb-bridge-nw.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts new file mode 100644 index 0000000000000..8769e055699ea --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts @@ -0,0 +1,43 @@ + +import ec2 = require('@aws-cdk/aws-ec2'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); +import ecs = require('../../lib'); + +const app = new cdk.App(process.argv); +const stack = new cdk.Stack(app, 'aws-ecs-integ-ecs'); + +const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); + +const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + +const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + // networkMode defaults to "bridge" + // memoryMiB: '1GB', + // cpu: '512' +}); + +const container = taskDefinition.addContainer('web', { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 1024, +}); +container.addPortMappings({ + containerPort: 80, + protocol: ecs.Protocol.Tcp +}); + +const service = new ecs.EcsService(stack, "Service", { + cluster, + taskDefinition, +}); + +const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc, internetFacing: true }); +const listener = lb.addListener('PublicListener', { port: 80, open: true }); +listener.addTargets('ECS', { + port: 80, + targets: [service] +}); + +new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName, }); + +process.stdout.write(app.run()); \ No newline at end of file From 2295ef253da4cf43e7c2c5d0e4851aeada935ffd Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 5 Oct 2018 15:54:03 +0200 Subject: [PATCH 77/97] Add integ test for awsvpc network mode on EcsCluster --- .../aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts new file mode 100644 index 0000000000000..a81d9c8932999 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts @@ -0,0 +1,42 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); +import ecs = require('../../lib'); +import { NetworkMode } from '../../lib'; + +const app = new cdk.App(process.argv); +const stack = new cdk.Stack(app, 'aws-ecs-integ'); + +const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); + +const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + +const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: NetworkMode.AwsVpc +}); + +const container = taskDefinition.addContainer('web', { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 256, +}); + +container.addPortMappings({ + containerPort: 80, + protocol: ecs.Protocol.Tcp +}); + +const service = new ecs.EcsService(stack, "Service", { + cluster, + taskDefinition, +}); + +const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc, internetFacing: true }); +const listener = lb.addListener('PublicListener', { port: 80, open: true }); +listener.addTargets('ECS', { + port: 80, + targets: [service] +}); + +new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName, }); + +process.stdout.write(app.run()); \ No newline at end of file From f34c23caffbb190fd160039d9574ef09179d446d Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 5 Oct 2018 16:54:00 +0200 Subject: [PATCH 78/97] Fix ingress/containerPort on LBs Allows bridge network mode to work --- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 11 ++-------- .../aws-ecs/lib/container-definition.ts | 20 +++++++++++++++---- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 2 +- .../aws-ecs/test/ecs/integ.lb-bridge-nw.ts | 2 +- .../aws-ecs/test/test.container-definition.ts | 8 ++++---- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index a26a49030fe43..170890b3df45e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -90,7 +90,7 @@ export abstract class BaseService extends cdk.Construct // Open up security groups. For dynamic port mapping, we won't know the port range // in advance so we need to open up all ports. - const port = this.containerPort; + const port = this.taskDef.defaultContainer!.ingressPort; const portRange = port === 0 ? EPHEMERAL_PORT_RANGE : new ec2.TcpPort(port); targetGroup.registerConnectable(this, portRange); @@ -133,7 +133,7 @@ export abstract class BaseService extends cdk.Construct this.loadBalancers.push({ targetGroupArn: targetGroup.targetGroupArn, containerName: this.taskDef.defaultContainer!.id, - containerPort: this.containerPort, + containerPort: this.taskDef.defaultContainer!.containerPort, }); this.resource.addDependency(targetGroup.listenerDependency()); @@ -141,13 +141,6 @@ export abstract class BaseService extends cdk.Construct const targetType = this.taskDef.networkMode === NetworkMode.AwsVpc ? elbv2.TargetType.Ip : elbv2.TargetType.Instance; return { targetType }; } - - /** - * Return the port on which the load balancer will be listening - */ - private get containerPort() { - return this.taskDef.defaultContainer!.ingressPort; - } } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 1146ade212515..9d8f563fd4d5c 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -242,17 +242,29 @@ export class ContainerDefinition extends cdk.Construct { return this._usesEcrImages; } + public get ingressPort(): number { + if (this.portMappings.length === 0) { + throw new Error(`Container ${this.id} hasn't defined any ports`); + } + const defaultPortMapping = this.portMappings[0]; + + if (defaultPortMapping.hostPort !== undefined && defaultPortMapping.hostPort !== 0) { + return defaultPortMapping.hostPort; + } + + if (this.taskDefinition.networkMode === NetworkMode.Bridge) { + return 0; + } + return defaultPortMapping.containerPort; + } /** * Return the instance port that the container will be listening on */ - public get ingressPort(): number { + public get containerPort(): number { if (this.portMappings.length === 0) { throw new Error(`Container ${this.id} hasn't defined any ports`); } const defaultPortMapping = this.portMappings[0]; - // if (defaultPortMapping.hostPort !== undefined && defaultPortMapping.hostPort !== 0) { - // return defaultPortMapping.hostPort; - // } return defaultPortMapping.containerPort; } diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index 49a30ca38f3fe..33630da2fb207 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -177,7 +177,7 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { this.loadBalancers.push({ loadBalancerName: loadBalancer.loadBalancerName, containerName: this.taskDefinition.defaultContainer!.id, - containerPort: this.taskDefinition.defaultContainer!.ingressPort, + containerPort: this.taskDefinition.defaultContainer!.containerPort, }); } } diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts index 8769e055699ea..52e548daf620b 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts @@ -19,7 +19,7 @@ const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { const container = taskDefinition.addContainer('web', { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), - memoryLimitMiB: 1024, + memoryLimitMiB: 256, }); container.addPortMappings({ containerPort: 80, diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts index 06476589066a4..d91ec616f9c3c 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -123,7 +123,7 @@ export = { container.addPortMappings({ containerPort: 8080, }); - const actual = container.ingressPort; + const actual = container.containerPort; // THEN const expected = 8080; @@ -148,7 +148,7 @@ export = { container.addPortMappings({ containerPort: 8080, }); - const actual = container.ingressPort; + const actual = container.containerPort; // THEN const expected = 8080; @@ -174,7 +174,7 @@ export = { containerPort: 8081, hostPort: 8080, }); - const actual = container.ingressPort; + const actual = container.containerPort; // THEN const expected = 8080; @@ -197,7 +197,7 @@ export = { container.addPortMappings({ containerPort: 8081, }); - const actual = container.ingressPort; + const actual = container.containerPort; // THEN const expected = 0; From c491a1d9abb75ed77c576ab9e18ae68f657b5886 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 5 Oct 2018 16:55:56 +0200 Subject: [PATCH 79/97] Cleanup example --- examples/cdk-examples-typescript/hello-cdk-ecs/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index a433860efef58..0c1bc12c58fd3 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -92,7 +92,6 @@ class BonjourECS extends cdk.Stack { taskDefinition, desiredCount: 1, }); - // cluster.runService(taskDefinition); } } From 430f81ed4ae39ba25bf0196f38cc47e45024d41c Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 5 Oct 2018 18:11:18 +0200 Subject: [PATCH 80/97] Add cloudwatch metrics --- .../@aws-cdk/aws-ecs/lib/base/base-cluster.ts | 15 ++++++++- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 17 +++++++++- .../@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts | 31 +++++++++++++++++ .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 33 +++++++++++++++++++ packages/@aws-cdk/aws-ecs/package.json | 1 + 5 files changed, 95 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts index b9944cf4e1344..fe2d923994951 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts @@ -1,3 +1,4 @@ +import cloudwatch = require ('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { cloudformation } from '../ecs.generated'; @@ -35,4 +36,16 @@ export class BaseCluster extends cdk.Construct { this.clusterArn = cluster.clusterArn; this.clusterName = cluster.ref; } -} + + /** + * Return the given named metric for this Cluster + */ + public metric(metricName: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/ECS', + metricName, + dimensions: { ClusterName: this.clusterName }, + ...props + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 170890b3df45e..978fd6bf74619 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -1,3 +1,4 @@ +import cloudwatch = require ('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); @@ -59,6 +60,7 @@ export abstract class BaseService extends cdk.Construct implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { public readonly dependencyElements: cdk.IDependable[]; public abstract readonly connections: ec2.Connections; + public readonly serviceName: string; protected loadBalancers = new Array(); protected networkConfiguration?: cloudformation.ServiceResource.NetworkConfigurationProperty; protected readonly abstract taskDef: BaseTaskDefinition; @@ -83,6 +85,7 @@ export abstract class BaseService extends cdk.Construct }); this.dependencyElements = [this.resource]; + this.serviceName = this.resource.serviceName; } public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { @@ -105,6 +108,18 @@ export abstract class BaseService extends cdk.Construct return this._securityGroup!; } + /** + * Return the given named metric for this Service + */ + public metric(metricName: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/ECS', + metricName, + dimensions: { ServiceName: this.serviceName }, + ...props + }); + } + // tslint:disable-next-line:max-line-length protected configureAwsVpcNetworking(vpc: ec2.VpcNetworkRef, assignPublicIp?: boolean, vpcPlacement?: ec2.VpcPlacementStrategy, securityGroup?: ec2.SecurityGroupRef) { if (vpcPlacement === undefined) { @@ -179,4 +194,4 @@ export enum FargatePlatformVersion { * Based on Amazon Linux 2017.09. */ Version10 = '1.0.0', -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index f82ea5b5f513b..324d02c123221 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -1,4 +1,5 @@ import autoscaling = require('@aws-cdk/aws-autoscaling'); +import cloudwatch = require ('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; @@ -95,6 +96,36 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { securityGroup: this.securityGroup.export(), }; } + + /** + * Metric for cluster CPU reservation + * + * @default average over 5 minutes + */ + public metricCpuReservation(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('CPUReservation', props); + } + + /** + * Metric for cluster Memory reservation + * + * @default average over 5 minutes + */ + public metricMemoryReservation(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('MemoryReservation', props ); + } + + /** + * Return the given named metric for this Cluster + */ + public metric(metricName: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/ECS', + metricName, + dimensions: { ClusterName: this.clusterName }, + ...props + }); + } } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index 33630da2fb207..2821d23c8da27 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -1,3 +1,4 @@ +import cloudwatch = require ('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); import elb = require('@aws-cdk/aws-elasticloadbalancing'); import cdk = require('@aws-cdk/cdk'); @@ -56,6 +57,7 @@ export interface EcsServiceProps extends BaseServiceProps { export class EcsService extends BaseService implements elb.ILoadBalancerTarget { public readonly connections: ec2.Connections; + public readonly clusterName: string; protected readonly taskDef: BaseTaskDefinition; private readonly taskDefinition: EcsTaskDefinition; private readonly constraints: cloudformation.ServiceResource.PlacementConstraintProperty[]; @@ -76,6 +78,7 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { schedulingStrategy: props.daemon ? 'DAEMON' : 'REPLICA', }); + this.clusterName = props.cluster.clusterName; this.constraints = []; this.strategies = []; this.daemon = props.daemon || false; @@ -180,6 +183,36 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { containerPort: this.taskDefinition.defaultContainer!.containerPort, }); } + + /** + * Return the given named metric for this Service + */ + public metric(metricName: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/ECS', + metricName, + dimensions: { ClusterName: this.clusterName, ServiceName: this.serviceName }, + ...props + }); + } + + /** + * Metric for cluster Memory utilization + * + * @default average over 5 minutes + */ + public metricMemoryUtilization(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('MemoryUtilization', props ); + } + + /** + * Metric for cluster CPU utilization + * + * @default average over 5 minutes + */ + public metricCpuUtilization(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('CPUUtilization', props); + } } function validateNoNetworkingProps(props: EcsServiceProps) { diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index e6c82d1e6e64a..a95f312f7caf9 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -60,6 +60,7 @@ "dependencies": { "@aws-cdk/cdk": "^0.10.0", "@aws-cdk/aws-autoscaling": "^0.10.0", + "@aws-cdk/aws-cloudwatch": "^0.10.0", "@aws-cdk/aws-ec2": "^0.10.0", "@aws-cdk/aws-elasticloadbalancing": "^0.10.0", "@aws-cdk/aws-elasticloadbalancingv2": "^0.10.0", From dca69727b56f7ce90bfcfd0e15ba7b529c7c7436 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 5 Oct 2018 18:12:33 +0200 Subject: [PATCH 81/97] Bridge mode works with host port specified --- packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts index 52e548daf620b..b28ee603455a7 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts @@ -23,6 +23,7 @@ const container = taskDefinition.addContainer('web', { }); container.addPortMappings({ containerPort: 80, + hostPort: 8080, protocol: ecs.Protocol.Tcp }); From ef51786019e10ef19ec0b59146794be9b0fe7050 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 12 Oct 2018 12:15:32 +0200 Subject: [PATCH 82/97] Fix libs to be in line with latest API changes from master --- packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts | 6 +++--- packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts | 3 ++- .../@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts index 4c71d62a2a0d5..d56e17cb3c61d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts @@ -64,7 +64,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { this.executionRole = props.executionRole; this.taskRole = props.taskRole || new iam.Role(this, 'TaskRole', { - assumedBy: new cdk.ServicePrincipal('ecs-tasks.amazonaws.com'), + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), }); const taskDef = new cloudformation.TaskDefinitionResource(this, 'Resource', { @@ -81,7 +81,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { /** * Add a policy statement to the Task Role */ - public addToRolePolicy(statement: cdk.PolicyStatement) { + public addToRolePolicy(statement: iam.PolicyStatement) { this.taskRole.addToPolicy(statement); } @@ -112,7 +112,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { private generateExecutionRole() { if (!this.executionRole) { this.executionRole = new iam.Role(this, 'ExecutionRole', { - assumedBy: new cdk.ServicePrincipal('ecs-tasks.amazonaws.com'), + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), }); this.executionRole.attachManagedPolicy(new iam.AwsManagedPolicy("service-role/AmazonECSTaskExecutionRolePolicy").policyArn); } diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index 324d02c123221..aea6ba2d3b53e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -1,6 +1,7 @@ import autoscaling = require('@aws-cdk/aws-autoscaling'); import cloudwatch = require ('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); +import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; @@ -67,7 +68,7 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { // ECS instances must be able to do these things // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html - autoScalingGroup.addToRolePolicy(new cdk.PolicyStatement().addActions( + autoScalingGroup.addToRolePolicy(new iam.PolicyStatement().addActions( "ecs:CreateCluster", "ecs:DeregisterContainerInstance", "ecs:DiscoverPollEndpoint", diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts index 4b7cb5f6ca163..87ed17d1bab93 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts @@ -3,7 +3,7 @@ import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); import ecs = require('../../lib'); -const app = new cdk.App(process.argv); +const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-ecs-integ'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); @@ -38,4 +38,4 @@ listener.addTargets('Fargate', { new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName, }); -process.stdout.write(app.run()); \ No newline at end of file +app.run(); From 64b6d066cb8528ab431f1d77d19d54ae9377d329 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 17 Oct 2018 10:51:54 +0200 Subject: [PATCH 83/97] Fix build errors --- examples/cdk-examples-typescript/hello-cdk-ecs/index.ts | 4 ++-- .../cdk-examples-typescript/hello-cdk-fargate/index.ts | 4 ++-- packages/@aws-cdk/aws-applicationautoscaling/package.json | 1 + packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts | 4 ++-- packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts | 4 ++-- .../lib/shared/base-target-group.ts | 7 ------- 6 files changed, 9 insertions(+), 15 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 0c1bc12c58fd3..c392b053ba36a 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -95,8 +95,8 @@ class BonjourECS extends cdk.Stack { } } -const app = new cdk.App(process.argv); +const app = new cdk.App(); new BonjourECS(app, 'Bonjour'); -process.stdout.write(app.run()); +app.run(); diff --git a/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts index 03456cf32280f..2d545c1b3cb9a 100644 --- a/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts @@ -22,8 +22,8 @@ class BonjourFargate extends cdk.Stack { } } -const app = new cdk.App(process.argv); +const app = new cdk.App(); new BonjourFargate(app, 'Bonjour'); -process.stdout.write(app.run()); \ No newline at end of file +app.run(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-applicationautoscaling/package.json b/packages/@aws-cdk/aws-applicationautoscaling/package.json index 6225e611434ae..9be6e16a0a485 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/package.json +++ b/packages/@aws-cdk/aws-applicationautoscaling/package.json @@ -59,6 +59,7 @@ }, "dependencies": { "@aws-cdk/cdk": "^0.12.0", + "@aws-cdk/aws-iam": "^0.12.0", "@aws-cdk/aws-cloudwatch": "^0.12.0" }, "homepage": "https://github.com/awslabs/aws-cdk" diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts index a81d9c8932999..205372b0e36ad 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts @@ -4,7 +4,7 @@ import cdk = require('@aws-cdk/cdk'); import ecs = require('../../lib'); import { NetworkMode } from '../../lib'; -const app = new cdk.App(process.argv); +const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-ecs-integ'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); @@ -39,4 +39,4 @@ listener.addTargets('ECS', { new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName, }); -process.stdout.write(app.run()); \ No newline at end of file +app.run(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts index b28ee603455a7..dbd8c8e798c30 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts @@ -4,7 +4,7 @@ import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); import ecs = require('../../lib'); -const app = new cdk.App(process.argv); +const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-ecs-integ-ecs'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); @@ -41,4 +41,4 @@ listener.addTargets('ECS', { new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName, }); -process.stdout.write(app.run()); \ No newline at end of file +app.run(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts index 37d57290cd54f..265b65c934aa8 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts @@ -254,13 +254,6 @@ export abstract class BaseTargetGroup extends cdk.Construct implements ITargetGr return new LazyDependable(this.dependableListeners); } - /** - * Return an object to depend on the listeners added to this target group - */ - public listenerDependency(): cdk.IDependable { - return new LazyDependable(this.dependableListeners); - } - /** * Register the given load balancing target as part of this group */ From 51097fde6a5cf8fa709cc5908668e78831720430 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Sun, 21 Oct 2018 20:30:25 -0700 Subject: [PATCH 84/97] Fix ingressPort/containerPort tests --- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 9 ++++++--- .../@aws-cdk/aws-ecs/lib/container-definition.ts | 5 ++++- .../aws-ecs/test/test.container-definition.ts | 16 ++++++++-------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 978fd6bf74619..81ef233c2d12e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -1,4 +1,4 @@ -import cloudwatch = require ('@aws-cdk/aws-cloudwatch'); +import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); @@ -57,7 +57,7 @@ export interface BaseServiceProps { } export abstract class BaseService extends cdk.Construct - implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { + implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { public readonly dependencyElements: cdk.IDependable[]; public abstract readonly connections: ec2.Connections; public readonly serviceName: string; @@ -88,6 +88,9 @@ export abstract class BaseService extends cdk.Construct this.serviceName = this.resource.serviceName; } + /** + * FIXME How to reconcile this with the fact ECS registers service with target group automatically? + */ public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { const ret = this.attachToELBv2(targetGroup); @@ -133,7 +136,7 @@ export abstract class BaseService extends cdk.Construct this.networkConfiguration = { awsvpcConfiguration: { - assignPublicIp : assignPublicIp ? 'ENABLED' : 'DISABLED', + assignPublicIp: assignPublicIp ? 'ENABLED' : 'DISABLED', subnets: subnets.map(x => x.subnetId), securityGroups: new cdk.Token(() => [securityGroup!.securityGroupId]), } diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 9d8f563fd4d5c..eac5e4935a8d5 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -242,6 +242,9 @@ export class ContainerDefinition extends cdk.Construct { return this._usesEcrImages; } + /** + * Ingress Port is needed to set the security group ingress for the task/service. + */ public get ingressPort(): number { if (this.portMappings.length === 0) { throw new Error(`Container ${this.id} hasn't defined any ports`); @@ -258,7 +261,7 @@ export class ContainerDefinition extends cdk.Construct { return defaultPortMapping.containerPort; } /** - * Return the instance port that the container will be listening on + * Return the port that the container will be listening on by default */ public get containerPort(): number { if (this.portMappings.length === 0) { diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts index d91ec616f9c3c..afd6ab836f168 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -123,7 +123,7 @@ export = { container.addPortMappings({ containerPort: 8080, }); - const actual = container.containerPort; + const actual = container.ingressPort; // THEN const expected = 8080; @@ -148,7 +148,7 @@ export = { container.addPortMappings({ containerPort: 8080, }); - const actual = container.containerPort; + const actual = container.ingressPort; // THEN const expected = 8080; @@ -171,17 +171,17 @@ export = { // WHEN container.addPortMappings({ - containerPort: 8081, - hostPort: 8080, + containerPort: 8080, + hostPort: 8081, }); - const actual = container.containerPort; + const actual = container.ingressPort; // THEN - const expected = 8080; + const expected = 8081; test.equal(actual, expected); test.done(); }, - "Ingress port should be the 0 if not supplied"(test: Test) { + "Ingress port should be 0 if not supplied"(test: Test) { // GIVEN const stack = new cdk.Stack(); const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { @@ -197,7 +197,7 @@ export = { container.addPortMappings({ containerPort: 8081, }); - const actual = container.containerPort; + const actual = container.ingressPort; // THEN const expected = 0; From 7b19dd2bb3107e64aa4e8d5c5ffa24f640f13f95 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 22 Oct 2018 13:48:09 -0700 Subject: [PATCH 85/97] Add LoadBalancedEcsService L3 Construct --- .../hello-cdk-ecs/index.ts | 162 +++++++++--------- packages/@aws-cdk/aws-ecs/lib/index.ts | 1 + .../aws-ecs/lib/load-balanced-ecs-service.ts | 95 ++++++++++ 3 files changed, 180 insertions(+), 78 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index c392b053ba36a..8d022e90561b5 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -10,88 +10,18 @@ class BonjourECS extends cdk.Stack { // 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.EcsCluster(this, 'EcsCluster', { - vpc - }); + const vpc = new ec2.VpcNetwork(this, 'MyVpc', { maxAZs: 2 }); + const cluster = new ecs.EcsCluster(this, 'EcsCluster', { vpc }); - // name, image, cpu, memory, port (with default) - // - // Include in constructs: - // - networking - include SD, ALB - // - logging - cloudwatch logs integration? talk to nathan about 3rd - // party integrations - aggregated logging across the service - // (instead of per task). Probably prometheus or elk? - // - tracing aws-xray-fargate - CNCF opentracing standard - jaeger, - // zipkin. - // - so x-ray is a container that is hooked up to sidecars that come - // with the application container itself - // - autoscaling - application autoscaling (Fargate focused?) - - const taskDefinition = new ecs.EcsTaskDefinition(this, "EcsTD", { - family: "ecs-task-definition", - }); - - const container = taskDefinition.addContainer('web', { - image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), - cpu: 1024, + // Instantiate ECS Service with just cluster and image + const ecsService = new ecs.LoadBalancedEcsService(this, "EcsService", { + cluster, memoryLimitMiB: 512, - essential: true - }); - - container.linuxParameters.addCapabilities(ecs.Capability.All); - container.linuxParameters.dropCapabilities(ecs.Capability.Chown); - - container.linuxParameters.addDevices({ - containerPath: "/dev/pudding", - hostPath: "/dev/clyde", - permissions: [ecs.DevicePermission.Read] - }); - - container.linuxParameters.addTmpfs({ - containerPath: "/dev/sda", - size: 12345, - mountOptions: [ecs.TmpfsMountOption.Ro] - }); - - container.linuxParameters.sharedMemorySize = 65535; - container.linuxParameters.initProcessEnabled = true; - - container.addUlimits({ - name: ecs.UlimitName.Core, - softLimit: 1234, - hardLimit: 1234, - }); - - container.addPortMappings({ - containerPort: 80, - // hostPort: 80, - protocol: ecs.Protocol.Tcp, - }); - - container.addMountPoints({ - containerPath: '/tmp/cache', - sourceVolume: 'volume-1', - readOnly: true, - }, { - containerPath: './cache', - sourceVolume: 'volume-2', - readOnly: true, - }); - - container.addVolumesFrom({ - sourceContainer: 'web', - readOnly: true, + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), }); - new ecs.EcsService(this, "EcsService", { - cluster, - taskDefinition, - desiredCount: 1, - }); + // Output the DNS where you can access your service + new cdk.Output(this, 'LoadBalancerDNS', { value: ecsService.loadBalancer.dnsName }); } } @@ -100,3 +30,79 @@ const app = new cdk.App(); new BonjourECS(app, 'Bonjour'); app.run(); + +// name, image, cpu, memory, port (with default) +// +// Include in constructs: +// - networking - include SD, ALB +// - logging - cloudwatch logs integration? talk to nathan about 3rd +// party integrations - aggregated logging across the service +// (instead of per task). Probably prometheus or elk? +// - tracing aws-xray-fargate - CNCF opentracing standard - jaeger, +// zipkin. +// - so x-ray is a container that is hooked up to sidecars that come +// with the application container itself +// - autoscaling - application autoscaling (Fargate focused?) + +// const taskDefinition = new ecs.EcsTaskDefinition(this, "EcsTD", { +// family: "ecs-task-definition", +// }); + +// const container = taskDefinition.addContainer('web', { +// image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), +// cpu: 1024, +// memoryLimitMiB: 512, +// essential: true +// }); + +// container.linuxParameters.addCapabilities(ecs.Capability.All); +// container.linuxParameters.dropCapabilities(ecs.Capability.Chown); + +// container.linuxParameters.addDevices({ +// containerPath: "/dev/pudding", +// hostPath: "/dev/clyde", +// permissions: [ecs.DevicePermission.Read] +// }); + +// container.linuxParameters.addTmpfs({ +// containerPath: "/dev/sda", +// size: 12345, +// mountOptions: [ecs.TmpfsMountOption.Ro] +// }); + +// container.linuxParameters.sharedMemorySize = 65535; +// container.linuxParameters.initProcessEnabled = true; + +// container.addUlimits({ +// name: ecs.UlimitName.Core, +// softLimit: 1234, +// hardLimit: 1234, +// }); + +// container.addPortMappings({ +// containerPort: 80, +// // hostPort: 80, +// protocol: ecs.Protocol.Tcp, +// }); + +// container.addMountPoints({ +// containerPath: '/tmp/cache', +// sourceVolume: 'volume-1', +// readOnly: true, +// }, { +// containerPath: './cache', +// sourceVolume: 'volume-2', +// readOnly: true, +// }); + +// container.addVolumesFrom({ +// sourceContainer: 'web', +// readOnly: true, +// }); + +// new ecs.EcsService(this, "EcsService", { +// cluster, +// taskDefinition, +// desiredCount: 1, +// }); +// } diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 0c06c5155f709..b163abbafa24b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -12,6 +12,7 @@ export * from './fargate/fargate-task-definition'; export * from './container-definition'; export * from './container-image'; export * from './linux-parameters'; +export * from './load-balanced-ecs-service'; export * from './load-balanced-fargate-service'; export * from './load-balanced-fargate-service-applet'; diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts new file mode 100644 index 0000000000000..839263d6e927e --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts @@ -0,0 +1,95 @@ +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); +import { ContainerImage } from './container-image'; +import { IEcsCluster } from './ecs/ecs-cluster'; +import { EcsService } from './ecs/ecs-service'; +import { EcsTaskDefinition } from './ecs/ecs-task-definition'; + +export interface LoadBalancedEcsServiceProps { + /** + * The cluster where your Fargate service will be deployed + */ + cluster: IEcsCluster; + + /** + * The image to use for a container. + * + * You can use images in the Docker Hub registry or specify other + * repositories (repository-url/image:tag). + */ + image: ContainerImage; + + /** + * 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. + */ + 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. + */ + memoryReservationMiB?: number; + + /** + * The container port of the application load balancer attached to your Fargate service. Corresponds to container port mapping. + * + * @default 80 + */ + containerPort?: number; + + /** + * Determines whether the Application Load Balancer will be internet-facing + * + * @default true + */ + publicLoadBalancer?: boolean; +} + +export class LoadBalancedEcsService extends cdk.Construct { + public readonly loadBalancer: elbv2.ApplicationLoadBalancer; + + constructor(parent: cdk.Construct, id: string, props: LoadBalancedEcsServiceProps) { + super(parent, id); + + const taskDefinition = new EcsTaskDefinition(this, 'TaskDef', {}); + + const container = taskDefinition.addContainer('web', { + image: props.image, + memoryLimitMiB: props.memoryLimitMiB, + }); + + container.addPortMappings({ + containerPort: props.containerPort || 80, + }); + + const service = new EcsService(this, "Service", { + cluster: props.cluster, + taskDefinition, + }); + + const internetFacing = props.publicLoadBalancer !== undefined ? props.publicLoadBalancer : true; + const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { + vpc: props.cluster.vpc, + internetFacing + }); + + this.loadBalancer = lb; + + const listener = lb.addListener('PublicListener', { port: 80, open: true }); + listener.addTargets('ECS', { + port: 80, + targets: [service] + }); + } +} From 9bea6bee8eb8a729749a464346376f6c6a1b1153 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 24 Oct 2018 15:53:38 -0700 Subject: [PATCH 86/97] Add ability to specify cluster size Sets maxSize and desiredCapabity on ASG for ECS Cluster. NOTE: Should we set minSize to 0 or 1? See ecs-cli default) --- .../cdk-examples-typescript/hello-cdk-ecs/index.ts | 7 ++++++- packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 8d022e90561b5..7d823061cd580 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -1,4 +1,5 @@ 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'); @@ -11,7 +12,11 @@ class BonjourECS extends cdk.Stack { // 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.EcsCluster(this, 'EcsCluster', { vpc }); + const cluster = new ecs.EcsCluster(this, 'EcsCluster', { + vpc, + size: 3, + instanceType: new InstanceType("t2.xlarge") + }); // Instantiate ECS Service with just cluster and image const ecsService = new ecs.LoadBalancedEcsService(this, "EcsService", { diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index aea6ba2d3b53e..be5e41cbefd67 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -17,6 +17,13 @@ export interface EcsClusterProps extends BaseClusterProps { * The type of EC2 instance to launch into your Autoscaling Group */ instanceType?: ec2.InstanceType; + + /** + * Number of container instances registered in your ECS Cluster + * + * @default 1 + */ + size?: number; } export class EcsCluster extends BaseCluster implements IEcsCluster { @@ -34,7 +41,10 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { vpc: props.vpc, instanceType: props.instanceType || new ec2.InstanceTypePair(ec2.InstanceClass.T2, ec2.InstanceSize.Micro), machineImage: new EcsOptimizedAmi(), - updateType: autoscaling.UpdateType.ReplacingUpdate + updateType: autoscaling.UpdateType.ReplacingUpdate, + minSize: 0, // NOTE: This differs from default of 1 in ASG construct lib -- also does not appear to work? + maxSize: props.size || 1, + desiredCapacity: props.size || 1 }); this.securityGroup = autoScalingGroup.connections.securityGroup!; @@ -171,4 +181,4 @@ class ImportedEcsCluster extends cdk.Construct implements IEcsCluster { this.vpc = ec2.VpcNetworkRef.import(this, "vpc", props.vpc); this.securityGroup = ec2.SecurityGroupRef.import(this, "securityGroup", props.securityGroup); } -} \ No newline at end of file +} From be9f4471bacab69f9c90de72eb7ce716075e9d51 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 26 Oct 2018 11:05:10 +0200 Subject: [PATCH 87/97] feat(aws-ecs): add Task AutoScaling to Service This uses the @aws-cdk/aws-applicationautoscaling library to add Task AutoScaling facilities to ECS and Fargate services. --- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 44 +++++++- .../aws-ecs/lib/base/scalable-task-count.ts | 103 ++++++++++++++++++ .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 2 +- .../aws-ecs/lib/fargate/fargate-service.ts | 2 +- packages/@aws-cdk/aws-ecs/lib/index.ts | 21 ++-- packages/@aws-cdk/aws-ecs/package.json | 1 + .../@aws-cdk/aws-ecs/test/fargate/cdk.json | 16 +++ .../test/fargate/integ.lb-awsvpc-nw.ts | 4 + 8 files changed, 179 insertions(+), 14 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/base/scalable-task-count.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 81ef233c2d12e..52e9ded54fa20 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -1,9 +1,12 @@ +import appscaling = require('@aws-cdk/aws-applicationautoscaling'); import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { BaseTaskDefinition, NetworkMode } from '../base/base-task-definition'; import { cloudformation } from '../ecs.generated'; +import { ScalableTaskCount } from './scalable-task-count'; export interface BaseServiceProps { /** @@ -60,14 +63,17 @@ export abstract class BaseService extends cdk.Construct implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { public readonly dependencyElements: cdk.IDependable[]; public abstract readonly connections: ec2.Connections; + public readonly serviceArn: string; public readonly serviceName: string; + public readonly clusterName: string; protected loadBalancers = new Array(); protected networkConfiguration?: cloudformation.ServiceResource.NetworkConfigurationProperty; protected readonly abstract taskDef: BaseTaskDefinition; protected _securityGroup?: ec2.SecurityGroupRef; private readonly resource: cloudformation.ServiceResource; + private scalableTaskCount?: ScalableTaskCount; - constructor(parent: cdk.Construct, name: string, props: BaseServiceProps, additionalProps: any) { + constructor(parent: cdk.Construct, name: string, props: BaseServiceProps, additionalProps: any, clusterName: string) { super(parent, name); this.resource = new cloudformation.ServiceResource(this, "Service", { @@ -83,9 +89,10 @@ export abstract class BaseService extends cdk.Construct platformVersion: props.platformVersion, ...additionalProps }); - - this.dependencyElements = [this.resource]; + this.serviceArn = this.resource.serviceArn; this.serviceName = this.resource.serviceName; + this.dependencyElements = [this.resource]; + this.clusterName = clusterName; } /** @@ -111,6 +118,23 @@ export abstract class BaseService extends cdk.Construct return this._securityGroup!; } + /** + * Enable autoscaling for the number of tasks in this service + */ + public autoScaleTaskCount(props: appscaling.EnableScalingProps) { + if (this.scalableTaskCount) { + throw new Error('AutoScaling of task count already enabled for this service'); + } + + return this.scalableTaskCount = new ScalableTaskCount(this, 'TaskCount', { + serviceNamespace: appscaling.ServiceNamespace.Ecs, + resourceId: `service/${this.clusterName}/${this.resource.serviceName}`, + dimension: 'ecs:service:DesiredCount', + role: this.makeAutoScalingRole(), + ...props + }); + } + /** * Return the given named metric for this Service */ @@ -159,6 +183,20 @@ export abstract class BaseService extends cdk.Construct const targetType = this.taskDef.networkMode === NetworkMode.AwsVpc ? elbv2.TargetType.Ip : elbv2.TargetType.Instance; return { targetType }; } + + /** + * Generate the role that will be used for autoscaling this service + */ + private makeAutoScalingRole(): iam.IRole { + // Use a Service Linked Role. + return iam.Role.import(this, 'ScalingRole', { + roleArn: cdk.ArnUtils.fromComponents({ + service: 'iam', + resource: 'role/aws-service-role/ecs.application-autoscaling.amazonaws.com', + resourceName: 'AWSServiceRoleForApplicationAutoScaling_ECSService', + }) + }); + } } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/base/scalable-task-count.ts b/packages/@aws-cdk/aws-ecs/lib/base/scalable-task-count.ts new file mode 100644 index 0000000000000..f63e42a18d1a3 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/base/scalable-task-count.ts @@ -0,0 +1,103 @@ +import appscaling = require('@aws-cdk/aws-applicationautoscaling'); +import cloudwatch = require('@aws-cdk/aws-cloudwatch'); + +/** + * Scalable attribute representing task count + */ +export class ScalableTaskCount extends appscaling.BaseScalableAttribute { + /** + * Scale out or in based on time + */ + public scaleOnSchedule(id: string, props: appscaling.ScalingSchedule) { + return super.scaleOnSchedule(id, props); + } + + /** + * Scale out or in based on a metric value + */ + public scaleOnMetric(id: string, props: appscaling.BasicStepScalingPolicyProps) { + return super.scaleOnMetric(id, props); + } + + /** + * Scale out or in to achieve a target CPU utilization + */ + public scaleOnCpuUtilization(id: string, props: CpuUtilizationScalingProps) { + return super.scaleToTrackMetric(id, { + predefinedMetric: appscaling.PredefinedMetric.ECSServiceAverageCPUUtilization, + policyName: props.policyName, + disableScaleIn: props.disableScaleIn, + targetValue: props.targetUtilizationPercent, + scaleInCooldownSec: props.scaleInCooldownSec, + scaleOutCooldownSec: props.scaleOutCooldownSec, + }); + } + + /** + * Scale out or in to achieve a target memory utilization utilization + */ + public scaleOnMemoryUtilization(id: string, props: CpuUtilizationScalingProps) { + return super.scaleToTrackMetric(id, { + predefinedMetric: appscaling.PredefinedMetric.ECSServiceAverageMemoryUtilization, + targetValue: props.targetUtilizationPercent, + policyName: props.policyName, + disableScaleIn: props.disableScaleIn, + scaleInCooldownSec: props.scaleInCooldownSec, + scaleOutCooldownSec: props.scaleOutCooldownSec, + }); + } + + /** + * Scale out or in to track a custom metric + */ + public scaleToTrackCustomMetric(id: string, props: TrackCustomMetricProps) { + return super.scaleToTrackMetric(id, { + customMetric: props.metric, + targetValue: props.targetValue, + policyName: props.policyName, + disableScaleIn: props.disableScaleIn, + scaleInCooldownSec: props.scaleInCooldownSec, + scaleOutCooldownSec: props.scaleOutCooldownSec, + }); + } +} + +/** + * Properties for enabling scaling based on CPU utilization + */ +export interface CpuUtilizationScalingProps extends appscaling.BaseTargetTrackingProps { + /** + * Target average CPU utilization across the task + */ + targetUtilizationPercent: number; +} + +/** + * Properties for enabling scaling based on memory utilization + */ +export interface MemoryUtilizationScalingProps extends appscaling.BaseTargetTrackingProps { + /** + * Target average memory utilization across the task + */ + targetUtilizationPercent: number; +} + +/** + * Properties to target track a custom metric + */ +export interface TrackCustomMetricProps extends appscaling.BaseTargetTrackingProps { + /** + * Metric to track + * + * The metric must represent utilization; that is, you will always get the following behavior: + * + * - metric > targetValue => scale out + * - metric < targetValue => scale in + */ + metric: cloudwatch.Metric; + + /** + * The target value to achieve for the metric + */ + targetValue: number; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index 2821d23c8da27..f0c6c1a80abe1 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -76,7 +76,7 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { placementConstraints: new cdk.Token(() => this.constraints), placementStrategies: new cdk.Token(() => this.strategies), schedulingStrategy: props.daemon ? 'DAEMON' : 'REPLICA', - }); + }, props.cluster.clusterName); this.clusterName = props.cluster.clusterName; this.constraints = []; diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index 8144ae913607e..fa622fd71df98 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -48,7 +48,7 @@ export class FargateService extends BaseService { cluster: props.cluster.clusterName, taskDefinition: props.taskDefinition.taskDefinitionArn, launchType: 'FARGATE', - }); + }, props.cluster.clusterName); this.configureAwsVpcNetworking(props.cluster.vpc, props.assignPublicIp, props.vpcPlacement, props.securityGroup); this.connections = new ec2.Connections({ securityGroup: this.securityGroup }); diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index b163abbafa24b..4031b3a6078fc 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -1,23 +1,26 @@ export * from './base/base-cluster'; -export * from './ecs/ecs-cluster'; -export * from './fargate/fargate-cluster'; - export * from './base/base-service'; -export * from './ecs/ecs-service'; -export * from './fargate/fargate-service'; - export * from './base/base-task-definition'; -export * from './ecs/ecs-task-definition'; -export * from './fargate/fargate-task-definition'; +export * from './base/scalable-task-count'; + export * from './container-definition'; export * from './container-image'; + +export * from './ecs/ecs-cluster'; +export * from './ecs/ecs-service'; +export * from './ecs/ecs-task-definition'; + +export * from './fargate/fargate-cluster'; +export * from './fargate/fargate-service'; +export * from './fargate/fargate-task-definition'; + export * from './linux-parameters'; export * from './load-balanced-ecs-service'; export * from './load-balanced-fargate-service'; export * from './load-balanced-fargate-service-applet'; -export * from './log-drivers/log-driver'; export * from './log-drivers/aws-log-driver'; +export * from './log-drivers/log-driver'; // AWS::ECS CloudFormation Resources: // diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 1374f28487a85..07f57276c30c9 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -58,6 +58,7 @@ "pkglint": "^0.13.0" }, "dependencies": { + "@aws-cdk/aws-applicationautoscaling": "^0.13.0", "@aws-cdk/aws-autoscaling": "^0.13.0", "@aws-cdk/aws-cloudwatch": "^0.13.0", "@aws-cdk/aws-ec2": "^0.13.0", diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json b/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json index 64fd3b2dafe6e..be30e8b227170 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json +++ b/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json @@ -4,6 +4,22 @@ "eu-west-1a", "eu-west-1b", "eu-west-1c" + ], + "availability-zones:993655754359:us-east-1": [ + "us-east-1a", + "us-east-1b", + "us-east-1c", + "us-east-1d", + "us-east-1e", + "us-east-1f" + ], + "availability-zones:435784268886:us-east-1": [ + "us-east-1a", + "us-east-1b", + "us-east-1c", + "us-east-1d", + "us-east-1e", + "us-east-1f" ] } } diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts index 87ed17d1bab93..b3c6f9401d46a 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts @@ -29,6 +29,10 @@ const service = new ecs.FargateService(stack, "Service", { taskDefinition, }); +const scaling = service.autoScaleTaskCount({ maxCapacity: 10 }); +// Quite low to try and force it to scale +scaling.scaleOnCpuUtilization('ReasonableCpu', { targetUtilizationPercent: 10 }); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc, internetFacing: true }); const listener = lb.addListener('PublicListener', { port: 80, open: true }); listener.addTargets('Fargate', { From 2eab5135066a9d4db8b17e268e57b998bc8874b9 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 26 Oct 2018 13:02:39 -0700 Subject: [PATCH 88/97] Add unit tests for EcsCluster --- .../aws-ecs/test/ecs/test.ecs-cluster.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts new file mode 100644 index 0000000000000..a427c54577892 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts @@ -0,0 +1,45 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../../lib'); + +export = { + "When creating an ECS Cluster": { + "with only required properties set, it correctly sets default properties"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + new ecs.EcsCluster(stack, 'Cluster', { + vpc, + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::Cluster', { + // Type: "AWS::EC2::VPC", + })); + // Properties: { + // CidrBlock: '10.10.0.0/16', + // EnableDnsHostnames: true, + // EnableDnsSupport: true, + // InstanceTenancy: ec2.DefaultInstanceTenancy.Default, + // Tags: [ + // { + // Key: "Name", + // Value: "MyVpc" + // } + // ] + // } + // })); + // expect(stack).toMatch({ + // Resources: { + // Cluster: { + // Type: 'AWS::ECS::Cluster' + // } + // } + // }, MatchStyle.SUPERSET);​ + + test.done(); + }, + } +}; From 4cf23fc23c0f2fe0a97569814925eed2cce30de4 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 29 Oct 2018 11:18:34 -0700 Subject: [PATCH 89/97] Unit tests for Cluster --- .../@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts | 4 +- .../aws-ecs/test/ecs/test.ecs-cluster.ts | 200 +++++++++++++++--- .../test/fargate/test.fargate-cluster.ts | 41 ++++ 3 files changed, 218 insertions(+), 27 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-cluster.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index 2f9be92c8587f..577169eaceb2c 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -42,7 +42,7 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { instanceType: props.instanceType || new ec2.InstanceTypePair(ec2.InstanceClass.T2, ec2.InstanceSize.Micro), machineImage: new EcsOptimizedAmi(), updateType: autoscaling.UpdateType.ReplacingUpdate, - minSize: 0, // NOTE: This differs from default of 1 in ASG construct lib -- also does not appear to work? + minSize: 0, maxSize: props.size || 1, desiredCapacity: props.size || 1 }); @@ -97,6 +97,8 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { this.autoScalingGroup = autoScalingGroup; } + // TODO Add cluster scaling API + /** * Export the EcsCluster */ diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts index a427c54577892..ccf2801f83f11 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts @@ -1,5 +1,6 @@ import { expect, haveResource } from '@aws-cdk/assert'; import ec2 = require('@aws-cdk/aws-ec2'); +import { InstanceType } from '@aws-cdk/aws-ec2'; import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import ecs = require('../../lib'); @@ -10,36 +11,183 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - new ecs.EcsCluster(stack, 'Cluster', { + new ecs.EcsCluster(stack, 'EcsCluster', { vpc, }); - // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { - // Type: "AWS::EC2::VPC", + expect(stack).to(haveResource("AWS::ECS::Cluster")); + + expect(stack).to(haveResource("AWS::EC2::VPC", { + CidrBlock: '10.0.0.0/16', + EnableDnsHostnames: true, + EnableDnsSupport: true, + InstanceTenancy: ec2.DefaultInstanceTenancy.Default, + Tags: [ + { + Key: "Name", + Value: "MyVpc" + } + ] + })); + + expect(stack).to(haveResource("AWS::AutoScaling::LaunchConfiguration", { + ImageId: "", // Should this not be the latest image ID? + InstanceType: "t2.micro", + IamInstanceProfile: { + Ref: "EcsClusterAutoScalingGroupInstanceProfile77D897B8" + }, + SecurityGroups: [ + { + "Fn::GetAtt": [ + "EcsClusterAutoScalingGroupInstanceSecurityGroupBFB09B50", + "GroupId" + ] + } + ], + UserData: { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\necho ECS_CLUSTER=", + { + Ref: "EcsCluster97242B84" + }, + // tslint:disable-next-line:max-line-length + " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save" + ] + ] + } + } + })); + + expect(stack).to(haveResource("AWS::AutoScaling::AutoScalingGroup", { + MaxSize: "1", + MinSize: "0", + DesiredCapacity: "1", + LaunchConfigurationName: { + Ref: "EcsClusterAutoScalingGroupLaunchConfig965E00BD" + }, + Tags: [ + { + Key: "Name", + PropagateAtLaunch: true, + Value: "EcsCluster/AutoScalingGroup" + } + ], + VPCZoneIdentifier: [ + { + Ref: "MyVpcPrivateSubnet1Subnet5057CF7E" + }, + { + Ref: "MyVpcPrivateSubnet2Subnet0040C983" + }, + { + Ref: "MyVpcPrivateSubnet3Subnet772D6AD7" + } + ] + })); + + expect(stack).to(haveResource("AWS::EC2::SecurityGroup", { + GroupDescription: "EcsCluster/AutoScalingGroup/InstanceSecurityGroup", + SecurityGroupEgress: [ + { + CidrIp: "0.0.0.0/0", + Description: "Allow all outbound traffic by default", + IpProtocol: "-1" + } + ], + SecurityGroupIngress: [], + Tags: [ + { + Key: "Name", + Value: "EcsCluster/AutoScalingGroup" + } + ], + VpcId: { + Ref: "MyVpcF9F0CA6F" + } + })); + + expect(stack).to(haveResource("AWS::IAM::Role", { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: "sts:AssumeRole", + Effect: "Allow", + Principal: { + Service: "ec2.amazonaws.com" + } + } + ], + Version: "2012-10-17" + } + })); + + expect(stack).to(haveResource("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "ecs:CreateCluster", + "ecs:DeregisterContainerInstance", + "ecs:DiscoverPollEndpoint", + "ecs:Poll", + "ecs:RegisterContainerInstance", + "ecs:StartTelemetrySession", + "ecs:Submit*", + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + Effect: "Allow", + Resource: "*" + } + ], + Version: "2012-10-17" + } })); - // Properties: { - // CidrBlock: '10.10.0.0/16', - // EnableDnsHostnames: true, - // EnableDnsSupport: true, - // InstanceTenancy: ec2.DefaultInstanceTenancy.Default, - // Tags: [ - // { - // Key: "Name", - // Value: "MyVpc" - // } - // ] - // } - // })); - // expect(stack).toMatch({ - // Resources: { - // Cluster: { - // Type: 'AWS::ECS::Cluster' - // } - // } - // }, MatchStyle.SUPERSET);​ test.done(); }, - } -}; + }, + + "allows specifying instance type"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + + new ecs.EcsCluster(stack, 'EcsCluster', { + vpc, + instanceType: new InstanceType("m3.large") + }); + + // THEN + expect(stack).to(haveResource("AWS::AutoScaling::LaunchConfiguration", { + InstanceType: "m3.large" + })); + + test.done(); + }, + + "allows specifying cluster size"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + + new ecs.EcsCluster(stack, 'EcsCluster', { + vpc, + size: 3 + }); + + // THEN + expect(stack).to(haveResource("AWS::AutoScaling::AutoScalingGroup", { + MaxSize: "3" + })); + + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-cluster.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-cluster.ts new file mode 100644 index 0000000000000..f25bcaf95985e --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-cluster.ts @@ -0,0 +1,41 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../../lib'); + +export = { + "When creating a Fargate Cluster": { + "with only required properties set, it correctly sets default properties"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + new ecs.FargateCluster(stack, 'FargateCluster', { + vpc, + }); + + expect(stack).to(haveResource("AWS::ECS::Cluster")); + + expect(stack).to(haveResource("AWS::EC2::VPC", { + CidrBlock: '10.0.0.0/16', + EnableDnsHostnames: true, + EnableDnsSupport: true, + InstanceTenancy: ec2.DefaultInstanceTenancy.Default, + Tags: [ + { + Key: "Name", + Value: "MyVpc" + } + ] + })); + + expect(stack).notTo(haveResource("AWS::EC2::SecurityGroup")); + expect(stack).notTo(haveResource("AWS::AutoScaling::LaunchConfiguration")); + expect(stack).notTo(haveResource("AWS::AutoScaling::AutoScalingGroup")); + expect(stack).notTo(haveResource("AWS::IAM::Role")); + expect(stack).notTo(haveResource("AWS::IAM::Policy")); + + test.done(); + }, + } +}; From 37d3c4e863e0faae3577885727016f4cb5462fdd Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 29 Oct 2018 19:11:16 -0700 Subject: [PATCH 90/97] Set volumes on TaskDefinition --- packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts index d56e17cb3c61d..3c253973275af 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts @@ -69,6 +69,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { const taskDef = new cloudformation.TaskDefinitionResource(this, 'Resource', { containerDefinitions: new cdk.Token(() => this.containers.map(x => x.renderContainerDefinition())), + volumes: new cdk.Token(() => this.volumes), executionRoleArn: new cdk.Token(() => this.executionRole && this.executionRole.roleArn), family: this.family, taskRoleArn: this.taskRole.roleArn, @@ -87,6 +88,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { /** * Add a container to this task + * FIXME pass in actual container instead of container props? */ public addContainer(id: string, props: ContainerDefinitionProps) { const container = new ContainerDefinition(this, id, this, props); @@ -102,7 +104,6 @@ export abstract class BaseTaskDefinition extends cdk.Construct { } private addVolume(volume: Volume) { - // const v = this.renderVolume(volume); this.volumes.push(volume); } @@ -152,9 +153,11 @@ export enum Compatibilities { Fargate = "FARGATE" } +// FIXME separate Volume from InstanceVolume (Host not supported in Fargate) export interface Volume { host?: Host; name?: string; + // FIXME add dockerVolumeConfiguration } export interface Host { From 1ad3669f7e34ed0389e6504025536ba2a1653adf Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 29 Oct 2018 22:38:45 -0700 Subject: [PATCH 91/97] Clarify doc string for memoryLimit and memoryReservation --- packages/@aws-cdk/aws-ecs/lib/container-definition.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index eac5e4935a8d5..d5820a2169e39 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -119,7 +119,7 @@ export interface ContainerDefinitionProps { * If your container attempts to exceed the allocated memory, the container * is terminated. * - * At least one of memoryLimitMiB and memoryReservationMiB is required. + * At least one of memoryLimitMiB and memoryReservationMiB is required for non-Fargate services. */ memoryLimitMiB?: number; @@ -131,7 +131,7 @@ export interface ContainerDefinitionProps { * 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. + * At least one of memoryLimitMiB and memoryReservationMiB is required for non-Fargate services. */ memoryReservationMiB?: number; @@ -302,6 +302,7 @@ export class ContainerDefinition extends cdk.Construct { healthCheck: this.props.healthCheck && renderHealthCheck(this.props.healthCheck), links: this.links, linuxParameters: this.linuxParameters.renderLinuxParameters(), + }; } } From 78d2a49597fe7dd687b628d95f61fa3ec0fd2adc Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 29 Oct 2018 19:11:53 -0700 Subject: [PATCH 92/97] WIP Unit tests on EcsTaskDefinition TaskExecutionRole not being set -- lazy evaluation of executionRole on TaskDef not working on private call to generateExecutionRole? Also weird issue with importing aws-iam --- .../test/ecs/test.ecs-task-definition.ts | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-task-definition.ts diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-task-definition.ts b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-task-definition.ts new file mode 100644 index 0000000000000..d5a3b2b7c0da3 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-task-definition.ts @@ -0,0 +1,209 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { Protocol } from '@aws-cdk/aws-ec2'; +// import iam = require('@aws-cdk/aws-iam'); // importing this is throwing a really weird error in line 11? +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../../lib'); + +export = { + "When creating an ECS TaskDefinition": { + "with only required properties set, it correctly sets default properties"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + // THEN + expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + Family: "EcsTaskDef", + ContainerDefinitions: [], + PlacementConstraints: [], + Volumes: [], + NetworkMode: ecs.NetworkMode.Bridge, + RequiresCompatibilities: [ecs.Compatibilities.Ec2] + })); + + // test error if no container defs? + test.done(); + }, + + "correctly sets network mode"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + new ecs.EcsTaskDefinition(stack, 'EcsTaskDef', { + networkMode: ecs.NetworkMode.AwsVpc + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + NetworkMode: ecs.NetworkMode.AwsVpc, + })); + + test.done(); + }, + + "correctly sets containers"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + const container = taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 // add validation? + }); + + // TODO test other containerDefinition methods + container.addPortMappings({ + containerPort: 3000 + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + Family: "EcsTaskDef", + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: "amazon/amazon-ecs-sample", + Links: [], + LinuxParameters: { + Capabilities: { + Add: [], + Drop: [] + }, + Devices: [], + Tmpfs: [] + }, + MountPoints: [], + Name: "web", + PortMappings: [{ + ContainerPort: 3000, + HostPort: 0, + Protocol: Protocol.Tcp + }], + Ulimits: [], + VolumesFrom: [] + }], + })); + + test.done(); + }, + + "correctly sets volumes"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const volume = { + host: { + sourcePath: "/tmp/cache", + }, + name: "scratch" + }; + + // Adding volumes via props is a bit clunky + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef', { + volumes: [volume] + }); + + const container = taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + // this needs to be a better API -- should auto-add volumes + container.addMountPoints({ + containerPath: "./cache", + readOnly: true, + sourceVolume: "scratch", + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + Family: "EcsTaskDef", + ContainerDefinitions: [{ + MountPoints: [ + { + ContainerPath: "./cache", + ReadOnly: true, + SourceVolume: "scratch" + } + ] + }], + Volumes: [{ + Host: { + SourcePath: "/tmp/cache" + }, + Name: "scratch" + }] + })); + + test.done(); + }, + + "correctly sets placement constraints"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef', { + placementConstraints: [{ + expression: "attribute:ecs.instance-type =~ t2.*", + type: ecs.PlacementConstraintType.MemberOf + }] + }); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample") + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + PlacementConstraints: [ + { + Expression: "attribute:ecs.instance-type =~ t2.*", + Type: "memberOf" + } + ] + })); + + test.done(); + }, + + // "correctly sets taskRole"(test: Test) { + // // GIVEN + // const stack = new cdk.Stack(); + // const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef', { + // taskRole: new iam.Role(this, 'TaskRole', { + // assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + // }) + // }); + + // taskDefinition.addContainer("web", { + // image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + // memoryLimitMiB: 512 + // }); + + // // THEN + // expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + // TaskRole: "roleArn" + // })); + + // test.done(); + // }, + + // "correctly sets taskExecutionRole if containerDef uses ECR"(test: Test) { + // // GIVEN + // const stack = new cdk.Stack(); + // const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef', {}); + // const container = taskDefinition.addContainer("web", { + // image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + // memoryLimitMiB: 512 // add validation? + // }); + + // container.useEcrImage(); + + // // THEN + // expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + // TaskExecutionRole: "roleArn" + // })); + + // test.done(); + // }, + } +}; \ No newline at end of file From ca61a7473ae0c18e70f0460f85871d88772b7c94 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 29 Oct 2018 23:29:36 -0700 Subject: [PATCH 93/97] WIP Unit tests for EcsService Add default deployment configuration --- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 6 +- .../aws-ecs/test/ecs/test.ecs-service.ts | 391 ++++++++++++++++++ 2 files changed, 394 insertions(+), 3 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 81ef233c2d12e..2bbc3799c317d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -41,7 +41,7 @@ export interface BaseServiceProps { /** * Time after startup to ignore unhealthy load balancer checks. * - * @default ??? + * @default ??? FIXME */ healthCheckGracePeriodSeconds?: number; @@ -75,8 +75,8 @@ export abstract class BaseService extends cdk.Construct serviceName: props.serviceName, loadBalancers: new cdk.Token(() => this.loadBalancers), deploymentConfiguration: { - maximumPercent: props.maximumPercent, - minimumHealthyPercent: props.minimumHealthyPercent + maximumPercent: props.maximumPercent || 200, + minimumHealthyPercent: props.minimumHealthyPercent || 50 }, /* role: never specified, supplanted by Service Linked Role */ networkConfiguration: new cdk.Token(() => this.networkConfiguration), diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts new file mode 100644 index 0000000000000..3b2c29c76065f --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts @@ -0,0 +1,391 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../../lib'); +import { BinPackResource, BuiltInAttributes, NetworkMode } from '../../lib'; + +export = { + "When creating an ECS Service": { + "with only required properties set, it correctly sets default properties"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + TaskDefinition: { + Ref: "EcsTaskDefA3440FB6" + }, + Cluster: { + Ref: "EcsCluster97242B84" + }, + DeploymentConfiguration: { + MaximumPercent: 200, + MinimumHealthyPercent: 50 + }, + DesiredCount: 1, + LaunchType: "EC2", + LoadBalancers: [], + PlacementConstraints: [], + PlacementStrategies: [], + SchedulingStrategy: "REPLICA" + })); + + test.done(); + }, + + "errors if daemon and desiredCount both specified"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + // THEN + test.throws(() => { + new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition, + daemon: true, + desiredCount: 2 + }); + }); + + test.done(); + }, + + "errors if no container definitions"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + // THEN + test.throws(() => { + new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition, + }); + }); + + test.done(); + }, + + "sets daemon scheduling strategy"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition, + daemon: true + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + SchedulingStrategy: "DAEMON" + })); + + test.done(); + }, + + "with a TaskDefinition with AwsVpc network mode": { + "it creates a security group for the service"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef', { + networkMode: NetworkMode.AwsVpc + }); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + NetworkConfiguration: { + AwsvpcConfiguration: { + AssignPublicIp: "DISABLED", + SecurityGroups: [ + { + "Fn::GetAtt": [ + "EcsServiceSecurityGroup8FDFD52F", + "GroupId" + ] + } + ], + Subnets: [ + { + Ref: "MyVpcPrivateSubnet1Subnet5057CF7E" + }, + { + Ref: "MyVpcPrivateSubnet2Subnet0040C983" + }, + { + Ref: "MyVpcPrivateSubnet3Subnet772D6AD7" + } + ] + } + } + })); + + test.done(); + } + }, + + "with distinctInstance placement constraint"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition, + placeOnDistinctInstances: true + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + PlacementConstraints: [{ + Type: "distinctInstance" + }] + })); + + test.done(); + }, + + "with memberOf placement constraints"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + const service = new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition + }); + + service.placeOnMemberOf("attribute:ecs.instance-type =~ t2.*"); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + PlacementConstraints: [{ + Expression: "attribute:ecs.instance-type =~ t2.*", + Type: "memberOf" + }] + })); + + test.done(); + }, + + "with placeSpreadAcross placement strategy"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + const service = new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition + }); + + service.placeSpreadAcross(BuiltInAttributes.AvailabilityZone); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + PlacementStrategies: [{ + Field: "attribute:ecs.availability-zone", + Type: "spread" + }] + })); + + test.done(); + }, + + "errors with placeSpreadAcross placement strategy if daemon specified"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + const service = new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition, + daemon: true + }); + + // THEN + test.throws(() => { + service.placeSpreadAcross(BuiltInAttributes.AvailabilityZone); + }); + + test.done(); + }, + + "with placeRandomly placement strategy"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc'); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + const service = new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition + }); + + service.placeRandomly(); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + PlacementStrategies: [{ + Type: "random" + }] + })); + + test.done(); + }, + + "errors with placeRandomly placement strategy if daemon specified"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc'); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + const service = new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition, + daemon: true + }); + + // THEN + test.throws(() => { + service.placeRandomly(); + }); + + test.done(); + }, + + "with placePackedBy placement strategy"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + const service = new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition + }); + + service.placePackedBy(BinPackResource.Memory); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + PlacementStrategies: [{ + Field: "memory", + Type: "binpack" + }] + })); + + test.done(); + }, + + "errors with placePackedBy placement strategy if daemon specified"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + const service = new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition, + daemon: true + }); + + // THEN + test.throws(() => { + service.placePackedBy(BinPackResource.Memory); + }); + + test.done(); + } + } +}; \ No newline at end of file From 98345b3a6ee8f1934a6b60713b4e7f100e928dfc Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 30 Oct 2018 01:03:19 -0700 Subject: [PATCH 94/97] WIP Unit tests for FargateService --- .../aws-ecs/lib/fargate/fargate-service.ts | 2 +- .../test/fargate/test.fargate-service.ts | 114 ++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index 8144ae913607e..b000237d689d2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -26,7 +26,7 @@ export interface FargateServiceProps extends BaseServiceProps { /** * In what subnets to place the task's ENIs * - * @default Public subnet if assignPublicIp, private subnets otherwise + * @default Private subnet if assignPublicIp, public subnets otherwise */ vpcPlacement?: ec2.VpcPlacementStrategy; diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts new file mode 100644 index 0000000000000..9eaa639f394bf --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts @@ -0,0 +1,114 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../../lib'); + +export = { + "When creating a Fargate Service": { + "with only required properties set, it correctly sets default properties"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.FargateCluster(stack, 'FargateCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + }); + + new ecs.FargateService(stack, "FargateService", { + cluster, + taskDefinition + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + TaskDefinition: { + Ref: "FargateTaskDefC6FB60B4" + }, + Cluster: { + Ref: "FargateCluster7CCD5F93" + }, + DeploymentConfiguration: { + MaximumPercent: 200, + MinimumHealthyPercent: 50 + }, + DesiredCount: 1, + LaunchType: "FARGATE", + LoadBalancers: [], + NetworkConfiguration: { + AwsvpcConfiguration: { + AssignPublicIp: "DISABLED", + SecurityGroups: [ + { + "Fn::GetAtt": [ + "FargateServiceSecurityGroup0A0E79CB", + "GroupId" + ] + } + ], + Subnets: [ + { + Ref: "MyVpcPrivateSubnet1Subnet5057CF7E" + }, + { + Ref: "MyVpcPrivateSubnet2Subnet0040C983" + }, + { + Ref: "MyVpcPrivateSubnet3Subnet772D6AD7" + } + ] + } + } + })); + + expect(stack).to(haveResource("AWS::EC2::SecurityGroup", { + GroupDescription: "FargateService/SecurityGroup", + SecurityGroupEgress: [ + { + CidrIp: "0.0.0.0/0", + Description: "Allow all outbound traffic by default", + IpProtocol: "-1" + } + ], + SecurityGroupIngress: [], + VpcId: { + Ref: "MyVpcF9F0CA6F" + } + })); + + test.done(); + }, + + "allows specifying assignPublicIP as enabled"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.FargateCluster(stack, 'FargateCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + }); + + new ecs.FargateService(stack, "FargateService", { + cluster, + taskDefinition, + assignPublicIp: true + + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + NetworkConfiguration: { + AwsvpcConfiguration: { + AssignPublicIp: "ENABLED", + } + } + })); + + test.done(); + }, + } +}; \ No newline at end of file From 35ebd9ca8e3785f5bb771f27ab9f7a947463601e Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 30 Oct 2018 01:44:04 -0700 Subject: [PATCH 95/97] WIP unit tests Fargate Task Definition --- .../fargate/test.fargate-task-definition.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-task-definition.ts diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-task-definition.ts new file mode 100644 index 0000000000000..daff091baa022 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-task-definition.ts @@ -0,0 +1,28 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../../lib'); + +export = { + "When creating an Fargate TaskDefinition": { + "with only required properties set, it correctly sets default properties"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + + // THEN + expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + Family: "FargateTaskDef", + ContainerDefinitions: [], + Volumes: [], + NetworkMode: ecs.NetworkMode.AwsVpc, + RequiresCompatibilities: [ecs.Compatibilities.Fargate], + Cpu: "256", + Memory: "512", + })); + + // test error if no container defs? + test.done(); + }, + } +}; \ No newline at end of file From 2fd2e7c33d3d2ffb1b4b2c700c1e43863f4fa46f Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 30 Oct 2018 16:01:15 +0100 Subject: [PATCH 96/97] Add docs and readme and a couple of tests --- packages/@aws-cdk/aws-ecs/README.md | 144 +++++++++++++++++- .../@aws-cdk/aws-ecs/lib/base/base-cluster.ts | 18 ++- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 47 +++++- .../aws-ecs/lib/base/base-task-definition.ts | 68 ++++++++- .../aws-ecs/lib/container-definition.ts | 103 ++++++++++++- .../@aws-cdk/aws-ecs/lib/container-image.ts | 19 +++ .../@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts | 89 ++++++++--- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 20 ++- .../aws-ecs/lib/ecs/ecs-task-definition.ts | 40 ++++- .../aws-ecs/lib/fargate/fargate-cluster.ts | 52 ++++++- .../aws-ecs/lib/fargate/fargate-service.ts | 13 ++ .../lib/fargate/fargate-task-definition.ts | 9 ++ .../@aws-cdk/aws-ecs/lib/linux-parameters.ts | 91 ++++++++++- .../aws-ecs/lib/load-balanced-ecs-service.ts | 16 +- .../load-balanced-fargate-service-applet.ts | 10 ++ .../lib/load-balanced-fargate-service.ts | 12 ++ packages/@aws-cdk/aws-ecs/package.json | 1 + .../aws-ecs/test/ecs/test.ecs-service.ts | 33 ++++ .../aws-ecs/test/test.container-definition.ts | 31 ++++ packages/@aws-cdk/aws-ecs/test/test.ecs.ts | 8 - packages/@aws-cdk/aws-ecs/test/test.l3s.ts | 43 ++++++ tools/cdk-integ-tools/lib/integ-helpers.ts | 26 +++- 22 files changed, 828 insertions(+), 65 deletions(-) delete mode 100644 packages/@aws-cdk/aws-ecs/test/test.ecs.ts create mode 100644 packages/@aws-cdk/aws-ecs/test/test.l3s.ts diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 0b281e1184740..9943a85cedd00 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -1,2 +1,142 @@ -## The CDK Construct Library for AWS Elastic Container Service (ECS) -This module is part of the [AWS Cloud Development Kit](https://github.com/awslabs/aws-cdk) project. +## AWS Elastic Container Service (ECS) Construct Library + +This package contains constructs for working with **AWS Elastic Container +Service** (ECS). The simplest example of using this library looks like this: + +```ts +// Create an ECS cluster (backed by an AutoScaling group) +const cluster = new ecs.EcsCluster(this, 'Cluster', { + vpc, + size: 3, + instanceType: new InstanceType("t2.xlarge") +}); + +// Instantiate ECS Service with an automatic load balancer +const ecsService = new ecs.LoadBalancedEcsService(this, 'Service', { + cluster, + memoryLimitMiB: 512, + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), +}); +``` + +### Fargate vs ECS + +There are two sets of constructs in this library; one to run tasks on ECS and +one to run Tasks on fargate. + +- Use the `EcsCluster`, `EcsTaskDefinition` and `EcsService` constructs to + run tasks on EC2 instances running in your account. +- Use the `FargateCluster`, `FargateTaskDefinition` and `FargateService` + constructs to run tasks on instances that are managed for you by AWS. + +## Cluster + +An `EcsCluster` or `FargateCluster` defines a set of instances to run your +tasks on. If you create an ECS cluster, an AutoScalingGroup of EC2 instances +running the right AMI will implicitly be created for you. + +You can run many tasks on a single cluster. + +To create a cluster, go: + +```ts +const cluster = new ecs.FargateCluster(this, 'Cluster', { + vpc: vpc +}); +``` + +## TaskDefinition + +A `TaskDefinition` describes what a single copy of a **Task** should look like. +A task definition has one or more containers; typically, it has one +main container (the *default container* is the first one that's added +to the task definition, and it will be marked *essential*) and optionally +some supporting containers which are used to support the main container, +doings things like upload logs or metrics to monitoring services. + +To add containers to a `TaskDefinition`, call `addContainer()`: + +```ts +const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', { + memoryMiB: '512' + cpu: 256 +}); + +taskDefinition.addContainer('main', { + // Use an image from DockerHub + image: ecs.DockerHub.image('amazon/amazon-ecs-sample') +}); +``` + +### Images + +Images supply the software that runs inside the container. Images can be +obtained from either DockerHub or from ECR repositories: + +* `ecs.DockerHub.image(imageName)`: use an publicly available image from + DockerHub. +* `repository.getImage(tag)`: use the given ECR repository as the image + to start. + +## Service + +A `Service` instantiates a `TaskDefinition` on a `Cluster` a given number of +times, optionally associating them with a load balnacer. Tasks that fail will +automatically be restarted. + +```ts +const taskDefinition; + +const service = new ecs.EcsService(this, 'Service', { + cluster, + taskDefinition, + desiredCount: 5 +}); +``` + +### Include a load balancer + +`Services` are load balancing targets and can be directly attached to load +balancers: + +```ts +const service = new ecs.FargateService(this, 'Service', { /* ... */ }); + +const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { vpc, internetFacing: true }); +const listener = lb.addListener('Listener', { port: 80 }); +listener.addTargets('ECS', { + port: 80, + targets: [service] +}); +``` + +There are two higher-level constructs available which include a load balancer for you: + +* `LoadBalancedFargateService` +* `LoadBalancedEcsService` + +## Task AutoScaling + +You can configure the task count of a service to match demand. Task AutoScaling is +configured by calling `autoScaleTaskCount()`: + +```ts +const scaling = service.autoScaleTaskCount({ maxCapacity: 10 }); +scaling.scaleOnCpuUtilization('CpuScaling', { + targetUtilizationPercent: 50 +}); +``` + +Task AutoScaling is powered by *Application AutoScaling*. Refer to that for +more information. + +## Instance AutoScaling + +If you're running on Fargate, AWS will manage the physical machines that your +containers are running on for you. If you're running an ECS cluster however, +your EC2 instances might fill up as your number of Tasks goes up. + +To avoid placement errors, you will want to configure AutoScaling for your +EC2 instance group so that your instance count scales with demand. + +TO BE IMPLEMENTED \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts index fe2d923994951..823ed7ceae715 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts @@ -3,6 +3,9 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { cloudformation } from '../ecs.generated'; +/** + * Basic cluster properties + */ export interface BaseClusterProps { /** * A name for the cluster. @@ -12,19 +15,28 @@ export interface BaseClusterProps { clusterName?: string; /** - * The VPC where your ECS instances will be running + * The VPC where your ECS instances will be running or your ENIs will be deployed */ vpc: ec2.VpcNetworkRef; } -export class BaseCluster extends cdk.Construct { +/** + * Base class for Ecs and Fargate clusters + */ +export abstract class BaseCluster extends cdk.Construct { /** - * The VPC this cluster was created in + * The VPC this cluster was created in. */ public readonly vpc: ec2.VpcNetworkRef; + /** + * The ARN of this cluster + */ public readonly clusterArn: string; + /** + * The name of this cluster + */ public readonly clusterName: string; constructor(parent: cdk.Construct, name: string, props: BaseClusterProps) { diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index dc332831af377..4ed25d03085fc 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -8,6 +8,9 @@ import { BaseTaskDefinition, NetworkMode } from '../base/base-task-definition'; import { cloudformation } from '../ecs.generated'; import { ScalableTaskCount } from './scalable-task-count'; +/** + * Basic service properties + */ export interface BaseServiceProps { /** * Number of desired copies of running tasks @@ -59,13 +62,37 @@ export interface BaseServiceProps { platformVersion?: FargatePlatformVersion; } +/** + * Base class for Ecs and Fargate services + */ export abstract class BaseService extends cdk.Construct implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { + + /** + * CloudFormation resources generated by this service + */ public readonly dependencyElements: cdk.IDependable[]; + + /** + * Manage allowed network traffic for this construct + */ public abstract readonly connections: ec2.Connections; + + /** + * ARN of this service + */ public readonly serviceArn: string; + + /** + * Name of this service + */ public readonly serviceName: string; + + /** + * Name of this service's cluster + */ public readonly clusterName: string; + protected loadBalancers = new Array(); protected networkConfiguration?: cloudformation.ServiceResource.NetworkConfigurationProperty; protected readonly abstract taskDef: BaseTaskDefinition; @@ -96,7 +123,10 @@ export abstract class BaseService extends cdk.Construct } /** - * FIXME How to reconcile this with the fact ECS registers service with target group automatically? + * Called when the service is attached to an ALB + * + * Don't call this function directly. Instead, call listener.addTarget() + * to add this service to a load balancer. */ public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { const ret = this.attachToELBv2(targetGroup); @@ -110,10 +140,19 @@ export abstract class BaseService extends cdk.Construct return ret; } + /** + * Called when the service is attached to an NLB + * + * Don't call this function directly. Instead, call listener.addTarget() + * to add this service to a load balancer. + */ public attachToNetworkTargetGroup(targetGroup: elbv2.NetworkTargetGroup): elbv2.LoadBalancerTargetProps { return this.attachToELBv2(targetGroup); } + /** + * SecurityGroup of this service + */ public get securityGroup(): ec2.SecurityGroupRef { return this._securityGroup!; } @@ -147,6 +186,9 @@ export abstract class BaseService extends cdk.Construct }); } + /** + * Set up AWSVPC networking for this construct + */ // tslint:disable-next-line:max-line-length protected configureAwsVpcNetworking(vpc: ec2.VpcNetworkRef, assignPublicIp?: boolean, vpcPlacement?: ec2.VpcPlacementStrategy, securityGroup?: ec2.SecurityGroupRef) { if (vpcPlacement === undefined) { @@ -167,6 +209,9 @@ export abstract class BaseService extends cdk.Construct }; } + /** + * Shared logic for attaching to an ELBv2 + */ private attachToELBv2(targetGroup: elbv2.ITargetGroup): elbv2.LoadBalancerTargetProps { if (this.taskDef.networkMode === NetworkMode.None) { throw new Error("Cannot use a load balancer if NetworkMode is None. Use Host or AwsVpc instead."); diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts index 3c253973275af..136af118cef98 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts @@ -3,6 +3,9 @@ import cdk = require('@aws-cdk/cdk'); import { ContainerDefinition, ContainerDefinitionProps } from '../container-definition'; import { cloudformation } from '../ecs.generated'; +/** + * Basic task definition properties + */ export interface BaseTaskDefinitionProps { /** * Namespace for task definition versions @@ -34,10 +37,28 @@ export interface BaseTaskDefinitionProps { volumes?: Volume[]; } +/** + * Base class for Ecs and Fargate task definitions + */ export abstract class BaseTaskDefinition extends cdk.Construct { + /** + * The family name of this task definition + */ public readonly family: string; + + /** + * ARN of this task definition + */ public readonly taskDefinitionArn: string; + + /** + * Task role used by this task definition + */ public readonly taskRole: iam.Role; + + /** + * Network mode used by this task definition + */ public abstract readonly networkMode: NetworkMode; /** @@ -48,8 +69,22 @@ export abstract class BaseTaskDefinition extends cdk.Construct { * container. */ public defaultContainer?: ContainerDefinition; + + /** + * All containers + */ private readonly containers = new Array(); + + /** + * All volumes + */ private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; + + /** + * Execution role for this task definition + * + * Will be created as needed. + */ private executionRole?: iam.Role; constructor(parent: cdk.Construct, name: string, props: BaseTaskDefinitionProps, additionalProps: any) { @@ -87,8 +122,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { } /** - * Add a container to this task - * FIXME pass in actual container instead of container props? + * Create a new container to this task definition */ public addContainer(id: string, props: ContainerDefinitionProps) { const container = new ContainerDefinition(this, id, this, props); @@ -103,6 +137,9 @@ export abstract class BaseTaskDefinition extends cdk.Construct { return container; } + /** + * Add a volume to this task definition + */ private addVolume(volume: Volume) { this.volumes.push(volume); } @@ -148,18 +185,43 @@ export enum NetworkMode { Host = 'host', } +/** + * Compatibilties + */ export enum Compatibilities { + /** + * EC2 capabilities + */ Ec2 = "EC2", + + /** + * Fargate capabilities + */ Fargate = "FARGATE" } -// FIXME separate Volume from InstanceVolume (Host not supported in Fargate) +/** + * Volume definition + */ export interface Volume { + /** + * Path on the host + */ host?: Host; + + /** + * A name for the volume + */ name?: string; // FIXME add dockerVolumeConfiguration } +/** + * A volume host + */ export interface Host { + /** + * Source path on the host + */ sourcePath?: string; } diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index d5820a2169e39..bcbc8065b1044 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -5,6 +5,9 @@ import { cloudformation } from './ecs.generated'; import { LinuxParameters } from './linux-parameters'; import { LogDriver } from './log-drivers/log-driver'; +/** + * Properties of a container definition + */ export interface ContainerDefinitionProps { /** * The image to use for a container. @@ -169,23 +172,53 @@ export interface ContainerDefinitionProps { logging?: LogDriver; } +/** + * A definition for a single container in a Task + */ export class ContainerDefinition extends cdk.Construct { + /** + * Access Linux Parameters + */ public readonly linuxParameters = new LinuxParameters(); + /** + * The configured mount points + */ public readonly mountPoints = new Array(); + /** + * The configured port mappings + */ public readonly portMappings = new Array(); + /** + * The configured volumes + */ public readonly volumesFrom = new Array(); + /** + * The configured ulimits + */ public readonly ulimits = new Array(); + /** + * Whether or not this container is essential + */ public readonly essential: boolean; + /** + * The configured container links + */ private readonly links = new Array(); + /** + * The task definition this container definition is part of + */ private readonly taskDefinition: BaseTaskDefinition; + /** + * Whether this container uses an ECR image + */ private _usesEcrImages: boolean = false; constructor(parent: cdk.Construct, id: string, taskDefinition: BaseTaskDefinition, private readonly props: ContainerDefinitionProps) { @@ -195,6 +228,9 @@ export class ContainerDefinition extends cdk.Construct { props.image.bind(this); } + /** + * Add a link from this container to a different container + */ public addLink(container: ContainerDefinition, alias?: string) { if (alias !== undefined) { this.links.push(`${container.id}:${alias}`); @@ -203,10 +239,16 @@ export class ContainerDefinition extends cdk.Construct { } } + /** + * Add one or more mount points to this container + */ public addMountPoints(...mountPoints: MountPoint[]) { this.mountPoints.push(...mountPoints); } + /** + * Add one or more port mappings to this container + */ public addPortMappings(...portMappings: PortMapping[]) { for (const pm of portMappings) { if (this.taskDefinition.networkMode === NetworkMode.AwsVpc || this.taskDefinition.networkMode === NetworkMode.Host) { @@ -223,10 +265,16 @@ export class ContainerDefinition extends cdk.Construct { this.portMappings.push(...portMappings); } + /** + * Add one or more ulimits to this container + */ public addUlimits(...ulimits: Ulimit[]) { this.ulimits.push(...ulimits); } + /** + * Add one or more volumes to this container + */ public addVolumesFrom(...volumesFrom: VolumeFrom[]) { this.volumesFrom.push(...volumesFrom); } @@ -238,16 +286,19 @@ export class ContainerDefinition extends cdk.Construct { this._usesEcrImages = true; } + /** + * Whether this container uses ECR images + */ public get usesEcrImages() { return this._usesEcrImages; } /** - * Ingress Port is needed to set the security group ingress for the task/service. + * Ingress Port is needed to set the security group ingress for the task/service */ public get ingressPort(): number { if (this.portMappings.length === 0) { - throw new Error(`Container ${this.id} hasn't defined any ports`); + throw new Error(`Container ${this.id} hasn't defined any ports. Call addPortMappings().`); } const defaultPortMapping = this.portMappings[0]; @@ -260,17 +311,21 @@ export class ContainerDefinition extends cdk.Construct { } return defaultPortMapping.containerPort; } + /** * Return the port that the container will be listening on by default */ public get containerPort(): number { if (this.portMappings.length === 0) { - throw new Error(`Container ${this.id} hasn't defined any ports`); + throw new Error(`Container ${this.id} hasn't defined any ports. Call addPortMappings().`); } const defaultPortMapping = this.portMappings[0]; return defaultPortMapping.containerPort; } + /** + * Render this container definition to a CloudFormation object + */ public renderContainerDefinition(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { return { command: this.props.command, @@ -394,16 +449,32 @@ function getHealthCheckCommand(hc: HealthCheck): string[] { } /** - * Container ulimits. Correspond to ulimits options on docker run. + * Container ulimits. + * + * Correspond to ulimits options on docker run. * * NOTE: Does not work for Windows containers. */ export interface Ulimit { + /** + * What resource to enforce a limit on + */ name: UlimitName, + + /** + * Soft limit of the resource + */ softLimit: number, + + /** + * Hard limit of the resource + */ hardLimit: number, } +/** + * Type of resource to set a limit on + */ export enum UlimitName { Core = "core", Cpu = "cpu", @@ -458,8 +529,18 @@ export interface PortMapping { protocol?: Protocol } +/** + * Network protocol + */ export enum Protocol { + /** + * TCP + */ Tcp = "tcp", + + /** + * UDP + */ Udp = "udp", } @@ -485,9 +566,19 @@ function renderMountPoint(mp: MountPoint): cloudformation.TaskDefinitionResource }; } +/** + * A volume from another container + */ export interface VolumeFrom { - sourceContainer: string, - readOnly: boolean, + /** + * Name of the source container + */ + sourceContainer: string, + + /** + * Whether the volume is read only + */ + readOnly: boolean, } function renderVolumeFrom(vf: VolumeFrom): cloudformation.TaskDefinitionResource.VolumeFromProperty { diff --git a/packages/@aws-cdk/aws-ecs/lib/container-image.ts b/packages/@aws-cdk/aws-ecs/lib/container-image.ts index 25ecc9fb5590a..e5131f7a7d42d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-image.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-image.ts @@ -1,16 +1,35 @@ import { ContainerDefinition } from './container-definition'; +/** + * Base class for container images + */ export abstract class ContainerImage { + /** + * Name of the image + */ public abstract readonly imageName: string; + + /** + * Called when the image is used by a ContainerDefinition + */ public abstract bind(containerDefinition: ContainerDefinition): void; } +/** + * Factory for DockerHub images + */ export class DockerHub { + /** + * Reference an image on DockerHub + */ public static image(name: string): ContainerImage { return new DockerHubImage(name); } } +/** + * A DockerHub image + */ class DockerHubImage { constructor(public readonly imageName: string) { } diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index 577169eaceb2c..64c9834c63e47 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -5,6 +5,9 @@ import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; +/** + * Properties to define an ECS cluster + */ export interface EcsClusterProps extends BaseClusterProps { /** * Whether or not the containers can access the instance role @@ -26,12 +29,25 @@ export interface EcsClusterProps extends BaseClusterProps { size?: number; } +/** + * A container cluster that runs on your EC2 instances + */ export class EcsCluster extends BaseCluster implements IEcsCluster { + /** + * Import an existing cluster + */ public static import(parent: cdk.Construct, name: string, props: ImportedEcsClusterProps): IEcsCluster { return new ImportedEcsCluster(parent, name, props); } + /** + * The AutoScalingGroup that the cluster is running on + */ public readonly autoScalingGroup: autoscaling.AutoScalingGroup; + + /** + * SecurityGroup of the EC2 instances + */ public readonly securityGroup: ec2.SecurityGroupRef; constructor(parent: cdk.Construct, name: string, props: EcsClusterProps) { @@ -59,23 +75,6 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { autoScalingGroup.addUserData('sudo service iptables save'); } - // Note: if the ASG doesn't launch or doesn't register itself with - // ECS, *Cluster* stabilization will fail after timing our for an hour - // or so, because the *Service* doesn't have any running instances. - // During this time, you CANNOT DO ANYTHING ELSE WITH YOUR STACK. - // - // Apart from the weird relationship here between Cluster and Service - // (why is Cluster failing and not Service?), the experience is... - // - // NOT GREAT. - // - // Also, there's sort of a bidirectional dependency between Cluster and ASG: - // - // - ASG depends on Cluster to get the ClusterName (which needs to go into - // UserData). - // - Cluster depends on ASG to boot up, so the service is launched, so the - // Cluster can stabilize. - // ECS instances must be able to do these things // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html autoScalingGroup.addToRolePolicy(new iam.PolicyStatement().addActions( @@ -97,8 +96,6 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { this.autoScalingGroup = autoScalingGroup; } - // TODO Add cluster scaling API - /** * Export the EcsCluster */ @@ -147,6 +144,9 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { export class EcsOptimizedAmi implements ec2.IMachineImageSource { private static AmiParameterName = "/aws/service/ecs/optimized-ami/amazon-linux/recommended"; + /** + * Return the correct image + */ public getImage(parent: cdk.Construct): ec2.MachineImage { const ssmProvider = new cdk.SSMParameterProvider(parent, { parameterName: EcsOptimizedAmi.AmiParameterName @@ -159,24 +159,63 @@ export class EcsOptimizedAmi implements ec2.IMachineImageSource { } } +/** + * An ECS cluster + */ export interface IEcsCluster { + /** + * Name of the cluster + */ readonly clusterName: string; + + /** + * VPC that the cluster instances are running in + */ readonly vpc: ec2.VpcNetworkRef; + + /** + * Security group of the cluster instances + */ readonly securityGroup: ec2.SecurityGroupRef; } +/** + * Properties to import an ECS cluster + */ export interface ImportedEcsClusterProps { - readonly clusterName: string; - readonly vpc: ec2.VpcNetworkRefProps; - readonly securityGroup: ec2.SecurityGroupRefProps; + /** + * Name of the cluster + */ + clusterName: string; + + /** + * VPC that the cluster instances are running in + */ + vpc: ec2.VpcNetworkRefProps; + + /** + * Security group of the cluster instances + */ + securityGroup: ec2.SecurityGroupRefProps; } -// /** -// * A EcsCluster that has been imported -// */ +/** + * An EcsCluster that has been imported + */ class ImportedEcsCluster extends cdk.Construct implements IEcsCluster { + /** + * Name of the cluster + */ public readonly clusterName: string; + + /** + * VPC that the cluster instances are running in + */ public readonly vpc: ec2.VpcNetworkRef; + + /** + * Security group of the cluster instances + */ public readonly securityGroup: ec2.SecurityGroupRef; constructor(parent: cdk.Construct, name: string, props: ImportedEcsClusterProps) { diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index f0c6c1a80abe1..e472884657d66 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -8,11 +8,14 @@ import { cloudformation } from '../ecs.generated'; import { IEcsCluster } from './ecs-cluster'; import { EcsTaskDefinition } from './ecs-task-definition'; +/** + * Properties to define an ECS service + */ export interface EcsServiceProps extends BaseServiceProps { /** * Cluster where service will be deployed */ - cluster: IEcsCluster; // should be required? do we assume 'default' exists? + cluster: IEcsCluster; /** * Task Definition used for running tasks in the service @@ -55,10 +58,22 @@ export interface EcsServiceProps extends BaseServiceProps { daemon?: boolean; } +/** + * Start a service on an ECS cluster + */ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { + /** + * Manage allowed network traffic for this construct + */ public readonly connections: ec2.Connections; + + /** + * Name of the cluster + */ public readonly clusterName: string; + protected readonly taskDef: BaseTaskDefinition; + private readonly taskDefinition: EcsTaskDefinition; private readonly constraints: cloudformation.ServiceResource.PlacementConstraintProperty[]; private readonly strategies: cloudformation.ServiceResource.PlacementStrategyProperty[]; @@ -215,6 +230,9 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { } } +/** + * Validate combinations of networking arguments + */ function validateNoNetworkingProps(props: EcsServiceProps) { if (props.vpcPlacement !== undefined || props.securityGroup !== undefined) { throw new Error('vpcPlacement and securityGroup can only be used in AwsVpc networking mode'); diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts index 19659836781d9..aea2031e341c2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts @@ -2,6 +2,9 @@ import cdk = require('@aws-cdk/cdk'); import { BaseTaskDefinition, BaseTaskDefinitionProps, Compatibilities, NetworkMode } from '../base/base-task-definition'; import { cloudformation } from '../ecs.generated'; +/** + * Properties to define an ECS task definition + */ export interface EcsTaskDefinitionProps extends BaseTaskDefinitionProps { /** * The Docker networking mode to use for the containers in the task. @@ -20,8 +23,18 @@ export interface EcsTaskDefinitionProps extends BaseTaskDefinitionProps { placementConstraints?: PlacementConstraint[]; } +/** + * Define Tasks to run on an ECS cluster + */ export class EcsTaskDefinition extends BaseTaskDefinition { + /** + * The networkmode configuration of this task + */ public readonly networkMode: NetworkMode; + + /** + * Placement constraints for task instances + */ private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[]; constructor(parent: cdk.Construct, name: string, props: EcsTaskDefinitionProps = {}) { @@ -49,6 +62,9 @@ export class EcsTaskDefinition extends BaseTaskDefinition { this.placementConstraints.push(pc); } + /** + * Render the placement constraints + */ private renderPlacementConstraint(pc: PlacementConstraint): cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty { return { type: pc.type, @@ -57,12 +73,32 @@ export class EcsTaskDefinition extends BaseTaskDefinition { } } +/** + * A constraint on how instances should be placed + */ export interface PlacementConstraint { - expression?: string; + /** + * The type of constraint + */ type: PlacementConstraintType; + + /** + * Additional information for the constraint + */ + expression?: string; } +/** + * A placement constraint type + */ export enum PlacementConstraintType { + /** + * Place each task on a different instance + */ DistinctInstance = "distinctInstance", - MemberOf = "memberOf" + + /** + * Place tasks only on instances matching the expression in 'expression' + */ + MemberOf = "memberOf" } diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts index a92f990657592..a720237127eea 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts @@ -2,17 +2,28 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; +/** + * Properties to define a Fargate cluster + */ // tslint:disable-next-line:no-empty-interface export interface FargateClusterProps extends BaseClusterProps { } +/** + * Define a cluster to run tasks on managed instances + */ export class FargateCluster extends BaseCluster implements IFargateCluster { + /** + * Import an existing Fargate cluster + */ public static import(parent: cdk.Construct, name: string, props: ImportedFargateClusterProps): IFargateCluster { return new ImportedFargateCluster(parent, name, props); } + constructor(parent: cdk.Construct, name: string, props: FargateClusterProps) { super(parent, name, props); } + /** * Export the FargateCluster */ @@ -24,21 +35,48 @@ export class FargateCluster extends BaseCluster implements IFargateCluster { } } +/** + * A Fargate cluster + */ export interface IFargateCluster { - clusterName: string; - vpc: ec2.VpcNetworkRef; + /** + * Name of the cluster + */ + readonly clusterName: string; + + /** + * VPC where Task ENIs will be placed + */ + readonly vpc: ec2.VpcNetworkRef; } +/** + * Properties to import a Fargate cluster + */ export interface ImportedFargateClusterProps { - readonly clusterName: string; - readonly vpc: ec2.VpcNetworkRefProps; + /** + * Name of the cluster + */ + clusterName: string; + + /** + * VPC where Task ENIs should be placed + */ + vpc: ec2.VpcNetworkRefProps; } -// /** -// * A FargateCluster that has been imported -// */ +/** + * A FargateCluster that has been imported + */ class ImportedFargateCluster extends cdk.Construct implements IFargateCluster { + /** + * Name of the cluster + */ public readonly clusterName: string; + + /** + * VPC where ENIs will be placed + */ public readonly vpc: ec2.VpcNetworkRef; constructor(parent: cdk.Construct, name: string, props: ImportedFargateClusterProps) { diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index b086893b0e1f8..d3ea11ef4a659 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -5,6 +5,9 @@ import { BaseTaskDefinition } from '../base/base-task-definition'; import { IFargateCluster } from './fargate-cluster'; import { FargateTaskDefinition } from './fargate-task-definition'; +/** + * Properties to define a Fargate service + */ export interface FargateServiceProps extends BaseServiceProps { /** * Cluster where service will be deployed @@ -38,8 +41,18 @@ export interface FargateServiceProps extends BaseServiceProps { securityGroup?: ec2.SecurityGroupRef; } +/** + * Start a service on an ECS cluster + */ export class FargateService extends BaseService { + /** + * Manage allowed network traffic for this construct + */ public readonly connections: ec2.Connections; + + /** + * The Task Definition for this service + */ public readonly taskDefinition: FargateTaskDefinition; protected readonly taskDef: BaseTaskDefinition; diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts index 4dd95a84319a2..37d2eba0a4174 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts @@ -1,6 +1,9 @@ import cdk = require('@aws-cdk/cdk'); import { BaseTaskDefinition, BaseTaskDefinitionProps, Compatibilities, NetworkMode } from '../base/base-task-definition'; +/** + * Properties to define a Fargate Task + */ export interface FargateTaskDefinitionProps extends BaseTaskDefinitionProps { /** * The number of cpu units used by the task. @@ -36,7 +39,13 @@ export interface FargateTaskDefinitionProps extends BaseTaskDefinitionProps { memoryMiB?: string; } +/** + * A definition for Tasks on a Fargate cluster + */ export class FargateTaskDefinition extends BaseTaskDefinition { + /** + * The configured network mode + */ public readonly networkMode = NetworkMode.AwsVpc; constructor(parent: cdk.Construct, name: string, props: FargateTaskDefinitionProps = {}) { diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts index bdc7c9a7c0089..e3cdb39ba74f2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -1,37 +1,74 @@ import { cloudformation } from './ecs.generated'; +/** + * Linux parameter setup in a container + */ export class LinuxParameters { + /** + * Whether the init process is enabled + */ public initProcessEnabled?: boolean; + /** + * The shared memory size + */ public sharedMemorySize?: number; + /** + * Capabilities to be added + */ private readonly capAdd: Capability[] = []; + /** + * Capabilities to be dropped + */ private readonly capDrop: Capability[] = []; + /** + * Device mounts + */ private readonly devices: Device[] = []; + /** + * TMPFS mounts + */ private readonly tmpfs: Tmpfs[] = []; /** - * AddCapabilities only works with EC2 launch type + * Add one or more capabilities + * + * Only works with EC2 launch type. */ public addCapabilities(...cap: Capability[]) { this.capAdd.push(...cap); } + /** + * Drop one or more capabilities + * + * Only works with EC2 launch type. + */ public dropCapabilities(...cap: Capability[]) { this.capDrop.push(...cap); } + /** + * Add one or more devices + */ public addDevices(...device: Device[]) { this.devices.push(...device); } + /** + * Add one or more tmpfs mounts + */ public addTmpfs(...tmpfs: Tmpfs[]) { this.tmpfs.push(...tmpfs); } + /** + * Render the Linux parameters to a CloudFormation object + */ public renderLinuxParameters(): cloudformation.TaskDefinitionResource.LinuxParametersProperty { return { initProcessEnabled: this.initProcessEnabled, @@ -46,9 +83,27 @@ export class LinuxParameters { } } +/** + * A host device + */ export interface Device { + /** + * Path in the container + * + * @default Same path as the host + */ containerPath?: string, + + /** + * Path on the host + */ hostPath: string, + + /** + * Permissions + * + * @default Readonly + */ permissions?: DevicePermission[] } @@ -60,9 +115,23 @@ function renderDevice(device: Device): cloudformation.TaskDefinitionResource.Dev }; } +/** + * A tmpfs mount + */ export interface Tmpfs { + /** + * Path in the container to mount + */ containerPath: string, + + /** + * Size of the volume + */ size: number, + + /** + * Mount options + */ mountOptions?: TmpfsMountOption[], } @@ -74,6 +143,9 @@ function renderTmpfs(tmpfs: Tmpfs): cloudformation.TaskDefinitionResource.TmpfsP }; } +/** + * A Linux capability + */ export enum Capability { All = "ALL", AuditControl = "AUDIT_CONTROL", @@ -115,12 +187,29 @@ export enum Capability { WakeAlarm = "WAKE_ALARM" } +/** + * Permissions for device access + */ export enum DevicePermission { + /** + * Read + */ Read = "read", + + /** + * Write + */ Write = "write", + + /** + * Make a node + */ Mknod = "mknod", } +/** + * Options for a tmpfs mount + */ export enum TmpfsMountOption { Defaults = "defaults", Ro = "ro", diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts index 839263d6e927e..bc24316ffb302 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts @@ -5,6 +5,9 @@ import { IEcsCluster } from './ecs/ecs-cluster'; import { EcsService } from './ecs/ecs-service'; import { EcsTaskDefinition } from './ecs/ecs-task-definition'; +/** + * Properties for a LoadBalancedEcsService + */ export interface LoadBalancedEcsServiceProps { /** * The cluster where your Fargate service will be deployed @@ -12,10 +15,7 @@ export interface LoadBalancedEcsServiceProps { cluster: IEcsCluster; /** - * The image to use for a container. - * - * You can use images in the Docker Hub registry or specify other - * repositories (repository-url/image:tag). + * The image to start. */ image: ContainerImage; @@ -56,7 +56,13 @@ export interface LoadBalancedEcsServiceProps { publicLoadBalancer?: boolean; } +/** + * A single task running on an ECS cluster fronted by a load balancer + */ export class LoadBalancedEcsService extends cdk.Construct { + /** + * The load balancer that is fronting the ECS service + */ public readonly loadBalancer: elbv2.ApplicationLoadBalancer; constructor(parent: cdk.Construct, id: string, props: LoadBalancedEcsServiceProps) { @@ -91,5 +97,7 @@ export class LoadBalancedEcsService extends cdk.Construct { port: 80, targets: [service] }); + + new cdk.Output(this, 'LoadBalancerDNS', { value: lb.dnsName }); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts index 23fea36a3729c..274a664ff6ef4 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts @@ -4,8 +4,15 @@ import { DockerHub } from './container-image'; import { FargateCluster } from './fargate/fargate-cluster'; import { LoadBalancedFargateService } from './load-balanced-fargate-service'; +/** + * Properties for a LoadBalancedEcsServiceApplet + */ export interface LoadBalancedFargateServiceAppletProps extends cdk.StackProps { + /** + * The image to start (from DockerHub) + */ image: string; + /** * The number of cpu units used by the task. * Valid values, which determines your range of valid values for the memory parameter: @@ -65,6 +72,9 @@ export interface LoadBalancedFargateServiceAppletProps extends cdk.StackProps { publicTasks?: boolean; } +/** + * An applet for a LoadBalancedFargateService + */ export class LoadBalancedFargateServiceApplet extends cdk.Stack { constructor(parent: cdk.App, id: string, props: LoadBalancedFargateServiceAppletProps) { super(parent, id, props); diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts index 04cae422cc05d..7275de17d7841 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts @@ -5,13 +5,20 @@ import { IFargateCluster } from './fargate/fargate-cluster'; import { FargateService } from './fargate/fargate-service'; import { FargateTaskDefinition } from './fargate/fargate-task-definition'; +/** + * Properties for a LoadBalancedEcsService + */ export interface LoadBalancedFargateServiceProps { /** * The cluster where your Fargate service will be deployed */ cluster: IFargateCluster; + /** + * The image to start + */ image: ContainerImage; + /** * The number of cpu units used by the task. * Valid values, which determines your range of valid values for the memory parameter: @@ -71,6 +78,9 @@ export interface LoadBalancedFargateServiceProps { publicTasks?: boolean; } +/** + * A single task running on an ECS cluster fronted by a load balancer + */ export class LoadBalancedFargateService extends cdk.Construct { public readonly loadBalancer: elbv2.ApplicationLoadBalancer; @@ -110,5 +120,7 @@ export class LoadBalancedFargateService extends cdk.Construct { port: 80, targets: [service] }); + + new cdk.Output(this, 'LoadBalancerDNS', { value: lb.dnsName }); } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 28869a680a345..a0cfc357e6bec 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -54,6 +54,7 @@ "devDependencies": { "@aws-cdk/assert": "^0.14.1", "cdk-build-tools": "^0.14.1", + "cdk-integ-tools": "^0.14.1", "cfn2ts": "^0.14.1", "pkglint": "^0.14.1" }, diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts index 3b2c29c76065f..3af4c35645bf6 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts @@ -1,5 +1,6 @@ import { expect, haveResource } from '@aws-cdk/assert'; import ec2 = require('@aws-cdk/aws-ec2'); +import elb = require('@aws-cdk/aws-elasticloadbalancing'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import ecs = require('../../lib'); @@ -387,5 +388,37 @@ export = { test.done(); } + }, + + 'classic ELB': { + 'can attach to classic ELB'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + const cluster = new ecs.EcsCluster(stack, 'Cluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TD', { networkMode: ecs.NetworkMode.Host }); + const container = taskDefinition.addContainer('web', { + image: ecs.DockerHub.image('test'), + }); + container.addPortMappings({ containerPort: 808 }); + const service = new ecs.EcsService(stack, 'Service', { cluster, taskDefinition }); + + // WHEN + const lb = new elb.LoadBalancer(stack, 'LB', { vpc }); + lb.addTarget(service); + + // THEN + expect(stack).to(haveResource('AWS::ECS::Service', { + LoadBalancers: [ + { + ContainerName: "web", + ContainerPort: 808, + LoadBalancerName: { Ref: "LB8A12904C" } + } + ], + })); + + test.done(); + }, } }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts index afd6ab836f168..19819a1ca3cce 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -1,3 +1,4 @@ +import { expect, haveResource } from '@aws-cdk/assert'; import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import ecs = require('../lib'); @@ -206,4 +207,34 @@ export = { }, }, }, + + 'can add AWS logging to container definition'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef'); + + // WHEN + taskDefinition.addContainer('cont', { + image: ecs.DockerHub.image('test'), + logging: new ecs.AwsLogDriver(stack, 'Logging', { streamPrefix: 'prefix' }) + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + LogConfiguration: { + LogDriver: "awslogs", + Options: { + "awslogs-group": { Ref: "LoggingLogGroupC6B8E20B" }, + "awslogs-stream-prefix": "prefix", + "awslogs-region": { Ref: "AWS::Region" } + } + }, + } + ] + })); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-ecs/test/test.ecs.ts b/packages/@aws-cdk/aws-ecs/test/test.ecs.ts deleted file mode 100644 index 820f6b467f38f..0000000000000 --- a/packages/@aws-cdk/aws-ecs/test/test.ecs.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Test, testCase } from 'nodeunit'; - -export = testCase({ - notTested(test: Test) { - test.ok(true, 'No tests are specified for this package.'); - test.done(); - } -}); diff --git a/packages/@aws-cdk/aws-ecs/test/test.l3s.ts b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts new file mode 100644 index 0000000000000..2ce7bcf1654c3 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts @@ -0,0 +1,43 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../lib'); + +export = { + 'test ECS loadbalanced construct'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + const cluster = new ecs.EcsCluster(stack, 'Cluster', { vpc }); + + // WHEN + new ecs.LoadBalancedEcsService(stack, 'Service', { + cluster, + image: ecs.DockerHub.image('test') + }); + + // THEN - stack containers a load balancer + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer')); + + test.done(); + }, + + 'test Fargateloadbalanced construct'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + const cluster = new ecs.FargateCluster(stack, 'Cluster', { vpc }); + + // WHEN + new ecs.LoadBalancedFargateService(stack, 'Service', { + cluster, + image: ecs.DockerHub.image('test') + }); + + // THEN - stack containers a load balancer + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer')); + + test.done(); + } +}; \ No newline at end of file diff --git a/tools/cdk-integ-tools/lib/integ-helpers.ts b/tools/cdk-integ-tools/lib/integ-helpers.ts index 9a48d4d856e6a..2582b3f5f78cf 100644 --- a/tools/cdk-integ-tools/lib/integ-helpers.ts +++ b/tools/cdk-integ-tools/lib/integ-helpers.ts @@ -5,6 +5,9 @@ import fs = require('fs'); import path = require('path'); import util = require('util'); +const stat = util.promisify(fs.stat); +const readdir = util.promisify(fs.readdir); + export class IntegrationTests { constructor(private readonly directory: string) { } @@ -18,14 +21,33 @@ export class IntegrationTests { } public async discover(): Promise { - const files = await util.promisify(fs.readdir)(this.directory); - const integs = files.filter(fileName => fileName.startsWith('integ.') && fileName.endsWith('.js')); + const files = await this.readTree(); + const integs = files.filter(fileName => path.basename(fileName).startsWith('integ.') && path.basename(fileName).endsWith('.js')); return await this.request(integs); } public async request(files: string[]): Promise { return files.map(fileName => new IntegrationTest(this.directory, fileName)); } + + private async readTree(): Promise { + const ret = new Array(); + + const rootDir = this.directory; + + async function recurse(dir: string) { + const files = await readdir(dir); + for (const file of files) { + const fullPath = path.join(dir, file); + const statf = await stat(fullPath); + if (statf.isFile()) { ret.push(fullPath.substr(rootDir.length + 1)); } + if (statf.isDirectory()) { await recurse(path.join(fullPath)); } + } + } + + await recurse(this.directory); + return ret; + } } export class IntegrationTest { From 9825127053eece62565a371f92292012cf487ffa Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 30 Oct 2018 16:24:37 -0700 Subject: [PATCH 97/97] Unit test for error case on service creation --- .../test/fargate/test.fargate-service.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts index 9eaa639f394bf..30132983d62a7 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts @@ -81,6 +81,24 @@ export = { test.done(); }, + "errors when no container specified on task definition"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.FargateCluster(stack, 'FargateCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + + // THEN + test.throws(() => { + new ecs.FargateService(stack, "FargateService", { + cluster, + taskDefinition, + }); + }); + + test.done(); + }, + "allows specifying assignPublicIP as enabled"(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -96,7 +114,6 @@ export = { cluster, taskDefinition, assignPublicIp: true - }); // THEN