diff --git a/.gitignore b/.gitignore index 7b89f50668180..bd75b435a1ac4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ pack coverage .nyc_output .LAST_BUILD +*.swp +./examples/cdk-examples-typescript/hello-cdk-ecs/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 new file mode 100644 index 0000000000000..b00e3c4b0eb4c --- /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: @aws-cdk/aws-ecs:LoadBalancedFargateServiceApplet +image: 'amazon/amazon-ecs-sample' +cpu: "2048" +memoryMiB: "1024" 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..eb5f700d36513 --- /dev/null +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json @@ -0,0 +1,42 @@ +{ + "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\"}" + } +} 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..7d823061cd580 --- /dev/null +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -0,0 +1,113 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import { InstanceType } from '@aws-cdk/aws-ec2'; +import ecs = require('@aws-cdk/aws-ecs'); +import cdk = require('@aws-cdk/cdk'); + +class BonjourECS extends cdk.Stack { + constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { + super(parent, name, props); + + // For better iteration speed, it might make sense to put this VPC into + // a separate stack and import it here. We then have two stacks to + // deploy, but VPC creation is slow so we'll only have to do that once + // and can iterate quickly on consuming stacks. Not doing that for now. + const vpc = new ec2.VpcNetwork(this, 'MyVpc', { maxAZs: 2 }); + const cluster = new ecs.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", { + cluster, + memoryLimitMiB: 512, + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + }); + + // Output the DNS where you can access your service + new cdk.Output(this, 'LoadBalancerDNS', { value: ecsService.loadBalancer.dnsName }); + } +} + +const app = new cdk.App(); + +new BonjourECS(app, 'Bonjour'); + +app.run(); + +// 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/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 new file mode 100644 index 0000000000000..2d545c1b3cb9a --- /dev/null +++ b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts @@ -0,0 +1,29 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import ecs = require('@aws-cdk/aws-ecs'); +import cdk = require('@aws-cdk/cdk'); + +class BonjourFargate extends cdk.Stack { + constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { + super(parent, name, props); + + // Create VPC and Fargate Cluster + // NOTE: Limit AZs to avoid reaching resource quotas + const vpc = new ec2.VpcNetwork(this, 'MyVpc', { maxAZs: 2 }); + const cluster = new ecs.FargateCluster(this, 'Cluster', { vpc }); + + // Instantiate Fargate Service with just cluster and image + const fargateService = new ecs.LoadBalancedFargateService(this, "FargateService", { + cluster, + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + }); + + // Output the DNS where you can access your service + new cdk.Output(this, 'LoadBalancerDNS', { value: fargateService.loadBalancer.dnsName }); + } +} + +const app = new cdk.App(); + +new BonjourFargate(app, 'Bonjour'); + +app.run(); \ No newline at end of file diff --git a/examples/cdk-examples-typescript/package.json b/examples/cdk-examples-typescript/package.json index dfe93f1e1d10e..94a3a8845454e 100644 --- a/examples/cdk-examples-typescript/package.json +++ b/examples/cdk-examples-typescript/package.json @@ -28,7 +28,9 @@ "@aws-cdk/aws-cognito": "^0.14.1", "@aws-cdk/aws-dynamodb": "^0.14.1", "@aws-cdk/aws-ec2": "^0.14.1", + "@aws-cdk/aws-ecs": "^0.14.1", "@aws-cdk/aws-elasticloadbalancing": "^0.14.1", + "@aws-cdk/aws-elasticloadbalancingv2": "^0.14.1", "@aws-cdk/aws-iam": "^0.14.1", "@aws-cdk/aws-lambda": "^0.14.1", "@aws-cdk/aws-neptune": "^0.14.1", diff --git a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts index f1acfaa91c822..baaaf34d4a58f 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 iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); @@ -44,6 +45,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 { @@ -67,3 +75,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 { + containerDefinition.useEcrImage(); + } +} diff --git a/packages/@aws-cdk/aws-ecr/package.json b/packages/@aws-cdk/aws-ecr/package.json index 82eb02f07efd2..81c0b1b89b1a6 100644 --- a/packages/@aws-cdk/aws-ecr/package.json +++ b/packages/@aws-cdk/aws-ecr/package.json @@ -60,6 +60,7 @@ }, "dependencies": { "@aws-cdk/aws-iam": "^0.14.1", + "@aws-cdk/aws-ecs": "^0.14.1", "@aws-cdk/cdk": "^0.14.1" }, "homepage": "https://github.com/awslabs/aws-cdk" 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 new file mode 100644 index 0000000000000..823ed7ceae715 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts @@ -0,0 +1,63 @@ +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'; + +/** + * Basic cluster properties + */ +export interface BaseClusterProps { + /** + * A name for the cluster. + * + * @default CloudFormation-generated name + */ + clusterName?: string; + + /** + * The VPC where your ECS instances will be running or your ENIs will be deployed + */ + vpc: ec2.VpcNetworkRef; +} + +/** + * Base class for Ecs and Fargate clusters + */ +export abstract class BaseCluster extends cdk.Construct { + /** + * 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) { + super(parent, name); + + const cluster = new cloudformation.ClusterResource(this, 'Resource', {clusterName: props.clusterName}); + + this.vpc = props.vpc; + 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 new file mode 100644 index 0000000000000..4ed25d03085fc --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -0,0 +1,283 @@ +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'; + +/** + * Basic service properties + */ +export interface BaseServiceProps { + /** + * Number of desired copies of running tasks + * + * @default 1 + */ + desiredCount?: number; + + /** + * A name for the service. + * + * @default CloudFormation-generated name + */ + serviceName?: string; + + /** + * 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; + + /** + * Time after startup to ignore unhealthy load balancer checks. + * + * @default ??? FIXME + */ + healthCheckGracePeriodSeconds?: number; + + /** + * 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; +} + +/** + * 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; + protected _securityGroup?: ec2.SecurityGroupRef; + private readonly resource: cloudformation.ServiceResource; + private scalableTaskCount?: ScalableTaskCount; + + constructor(parent: cdk.Construct, name: string, props: BaseServiceProps, additionalProps: any, clusterName: string) { + super(parent, name); + + this.resource = new cloudformation.ServiceResource(this, "Service", { + desiredCount: props.desiredCount || 1, + serviceName: props.serviceName, + loadBalancers: new cdk.Token(() => this.loadBalancers), + deploymentConfiguration: { + maximumPercent: props.maximumPercent || 200, + minimumHealthyPercent: props.minimumHealthyPercent || 50 + }, + /* role: never specified, supplanted by Service Linked Role */ + networkConfiguration: new cdk.Token(() => this.networkConfiguration), + platformVersion: props.platformVersion, + ...additionalProps + }); + this.serviceArn = this.resource.serviceArn; + this.serviceName = this.resource.serviceName; + this.dependencyElements = [this.resource]; + this.clusterName = clusterName; + } + + /** + * 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); + + // 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.taskDef.defaultContainer!.ingressPort; + const portRange = port === 0 ? EPHEMERAL_PORT_RANGE : new ec2.TcpPort(port); + targetGroup.registerConnectable(this, portRange); + + 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!; + } + + /** + * 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 + */ + public metric(metricName: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/ECS', + metricName, + dimensions: { ServiceName: this.serviceName }, + ...props + }); + } + + /** + * 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) { + 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]), + } + }; + } + + /** + * 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."); + } + + this.loadBalancers.push({ + targetGroupArn: targetGroup.targetGroupArn, + containerName: this.taskDef.defaultContainer!.id, + containerPort: this.taskDef.defaultContainer!.containerPort, + }); + + this.resource.addDependency(targetGroup.listenerDependency()); + + 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', + }) + }); + } +} + +/** + * The port range to open up for dynamic port mapping + */ +const EPHEMERAL_PORT_RANGE = new ec2.TcpPortRange(32768, 65535); + +/** + * 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/base/base-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts new file mode 100644 index 0000000000000..136af118cef98 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts @@ -0,0 +1,227 @@ +import iam = require('@aws-cdk/aws-iam'); +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 + * + * @default Automatically generated name + */ + family?: string; + + /** + * 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 + */ + taskRole?: iam.Role; + + /** + * See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide//task_definition_parameters.html#volumes + */ + 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; + + /** + * 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; + + /** + * 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) { + super(parent, name); + + this.family = props.family || this.uniqueId; + + if (props.volumes) { + props.volumes.forEach(v => this.addVolume(v)); + } + + this.executionRole = props.executionRole; + + this.taskRole = props.taskRole || new iam.Role(this, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + 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, + ...additionalProps + }); + + this.taskDefinitionArn = taskDef.taskDefinitionArn; + } + + /** + * Add a policy statement to the Task Role + */ + public addToRolePolicy(statement: iam.PolicyStatement) { + this.taskRole.addToPolicy(statement); + } + + /** + * Create a new container to this task definition + */ + public addContainer(id: string, props: ContainerDefinitionProps) { + const container = new ContainerDefinition(this, id, this, props); + this.containers.push(container); + if (container.usesEcrImages) { + this.generateExecutionRole(); + } + if (this.defaultContainer === undefined && container.essential) { + this.defaultContainer = container; + } + + return container; + } + + /** + * Add a volume to this task definition + */ + private addVolume(volume: Volume) { + this.volumes.push(volume); + } + + /** + * Generate a default execution role that allows pulling from ECR + */ + private generateExecutionRole() { + if (!this.executionRole) { + this.executionRole = new iam.Role(this, 'ExecutionRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + this.executionRole.attachManagedPolicy(new iam.AwsManagedPolicy("service-role/AmazonECSTaskExecutionRolePolicy").policyArn); + } + } +} + +/** + * 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', +} + +/** + * Compatibilties + */ +export enum Compatibilities { + /** + * EC2 capabilities + */ + Ec2 = "EC2", + + /** + * Fargate capabilities + */ + Fargate = "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/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/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts new file mode 100644 index 0000000000000..bcbc8065b1044 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -0,0 +1,589 @@ +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'; +import { LogDriver } from './log-drivers/log-driver'; + +/** + * Properties of a container definition + */ +export interface ContainerDefinitionProps { + /** + * The image to use for a container. + * + * 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[]; + + /** + * 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[]; + + /** + * 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. + * + * You must have at least one essential container in a task. + * + * @default true + */ + 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; + + /** + * The hard limit (in MiB) of memory to present to the container. + * + * If your container attempts to exceed the allocated memory, the container + * is terminated. + * + * At least one of memoryLimitMiB and memoryReservationMiB is required for non-Fargate services. + */ + memoryLimitMiB?: number; + + /** + * The soft limit (in MiB) of memory to reserve for the container. + * + * When system memory is under contention, Docker attempts to keep the + * container memory within the limit. If the container requires more memory, + * it can consume up to the value specified by the Memory property or all of + * the available memory on the container instance—whichever comes first. + * + * At least one of memoryLimitMiB and memoryReservationMiB is required for non-Fargate services. + */ + 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; + + /** + * The working directory in the container to run commands in. + * + * @default / + */ + workingDirectory?: string; + + /** + * Configures a custom log driver for the container. + */ + 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) { + super(parent, id); + this.essential = props.essential !== undefined ? props.essential : true; + this.taskDefinition = taskDefinition; + 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}`); + } else { + this.links.push(`${container.id}`); + } + } + + /** + * 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) { + if (pm.containerPort !== pm.hostPort && pm.hostPort !== undefined) { + 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); + } + + /** + * 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); + } + + /** + * Mark this ContainerDefinition as using an ECR image + */ + public useEcrImage() { + 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 + */ + public get ingressPort(): number { + if (this.portMappings.length === 0) { + throw new Error(`Container ${this.id} hasn't defined any ports. Call addPortMappings().`); + } + 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 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. 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, + 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.essential, + hostname: this.props.hostname, + image: this.props.image.imageName, + memory: this.props.memoryLimitMiB, + memoryReservation: this.props.memoryReservationMiB, + mountPoints: this.mountPoints.map(renderMountPoint), + name: this.id, + portMappings: this.portMappings.map(renderPortMapping), + privileged: this.props.privileged, + readonlyRootFilesystem: this.props.readonlyRootFilesystem, + repositoryCredentials: undefined, // FIXME + ulimits: this.ulimits.map(renderUlimit), + user: this.props.user, + 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'), + extraHosts: this.props.extraHosts && renderKV(this.props.extraHosts, 'hostname', 'ipAddress'), + healthCheck: this.props.healthCheck && renderHealthCheck(this.props.healthCheck), + links: this.links, + linuxParameters: this.linuxParameters.renderLinuxParameters(), + + }; + } +} + +/** + * Container health check configuration + */ +export interface HealthCheck { + /** + * Command to run, as the binary path and arguments. + * + * If you provide a shell command as a single string, you have to quote command-line arguments. + */ + command: 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; +} + +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 { + return { + command: getHealthCheckCommand(hc), + interval: hc.intervalSeconds, + retries: hc.retries, + startPeriod: hc.startPeriod, + timeout: hc.timeout + }; +} + +function getHealthCheckCommand(hc: HealthCheck): string[] { + const 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); +} + +/** + * 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", + 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, + }; +} + +/** + * Map a host port to a container port + */ +export interface PortMapping { + /** + * 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 +} + +/** + * Network protocol + */ +export enum Protocol { + /** + * TCP + */ + Tcp = "tcp", + + /** + * UDP + */ + Udp = "udp", +} + +function renderPortMapping(pm: PortMapping): cloudformation.TaskDefinitionResource.PortMappingProperty { + return { + containerPort: pm.containerPort, + hostPort: pm.hostPort, + protocol: pm.protocol || Protocol.Tcp, + }; +} + +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, + }; +} + +/** + * A volume from another container + */ +export interface VolumeFrom { + /** + * Name of the source container + */ + sourceContainer: string, + + /** + * Whether the volume is read only + */ + readOnly: boolean, +} + +function renderVolumeFrom(vf: VolumeFrom): cloudformation.TaskDefinitionResource.VolumeFromProperty { + return { + sourceContainer: vf.sourceContainer, + readOnly: vf.readOnly, + }; +} 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..e5131f7a7d42d --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/container-image.ts @@ -0,0 +1,40 @@ +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) { + } + + public bind(_containerDefinition: ContainerDefinition): void { + // Nothing + } +} \ 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 new file mode 100644 index 0000000000000..64c9834c63e47 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -0,0 +1,227 @@ +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'; + +/** + * Properties to define an ECS cluster + */ +export interface EcsClusterProps extends BaseClusterProps { + /** + * Whether or not the containers can access the instance role + * + * @default false + */ + containersAccessInstanceRole?: boolean; + + /** + * 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; +} + +/** + * 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) { + super(parent, name, props); + + const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'AutoScalingGroup', { + vpc: props.vpc, + instanceType: props.instanceType || new ec2.InstanceTypePair(ec2.InstanceClass.T2, ec2.InstanceSize.Micro), + machineImage: new EcsOptimizedAmi(), + updateType: autoscaling.UpdateType.ReplacingUpdate, + minSize: 0, + maxSize: props.size || 1, + desiredCapacity: props.size || 1 + }); + + this.securityGroup = autoScalingGroup.connections.securityGroup!; + + // Tie instances to cluster + 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 + autoScalingGroup.addUserData('sudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP'); + autoScalingGroup.addUserData('sudo service iptables save'); + } + + // 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( + "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.autoScalingGroup = autoScalingGroup; + } + + /** + * 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(), + }; + } + + /** + * 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 + }); + } +} + +/** + * Construct a Linux machine image from the latest ECS Optimized AMI published in SSM + */ +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 + }); + + const json = ssmProvider.parameterValue("{\"image_id\": \"\"}"); + const ami = JSON.parse(json).image_id; + + return new ec2.MachineImage(ami, new ec2.LinuxOS()); + } +} + +/** + * 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 { + /** + * 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; +} + +/** + * 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) { + 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); + } +} 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..e472884657d66 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -0,0 +1,287 @@ +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'); +import { BaseService, BaseServiceProps } from '../base/base-service'; +import { BaseTaskDefinition, NetworkMode } from '../base/base-task-definition'; +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; + + /** + * 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; +} + +/** + * 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[]; + 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', + 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 = []; + 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.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 { + 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!.id, + 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); + } +} + +/** + * 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'); + } +} + +/** + * 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 new file mode 100644 index 0000000000000..aea2031e341c2 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts @@ -0,0 +1,104 @@ +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. + * + * @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[]; +} + +/** + * 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 = {}) { + const networkMode = props.networkMode || NetworkMode.Bridge; + + super(parent, name, props, { + 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)); + } + } + + /** + * Constrain where tasks can be placed + */ + private addPlacementConstraint(constraint: PlacementConstraint) { + const pc = this.renderPlacementConstraint(constraint); + this.placementConstraints.push(pc); + } + + /** + * Render the placement constraints + */ + private renderPlacementConstraint(pc: PlacementConstraint): cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty { + return { + type: pc.type, + expression: pc.expression + }; + } +} + +/** + * A constraint on how instances should be placed + */ +export interface PlacementConstraint { + /** + * 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", + + /** + * 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 new file mode 100644 index 0000000000000..a720237127eea --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts @@ -0,0 +1,87 @@ +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 + */ + public export(): ImportedFargateClusterProps { + return { + clusterName: new cdk.Output(this, 'ClusterName', { value: this.clusterName }).makeImportValue().toString(), + vpc: this.vpc.export(), + }; + } +} + +/** + * A Fargate cluster + */ +export interface IFargateCluster { + /** + * 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 { + /** + * Name of the cluster + */ + clusterName: string; + + /** + * VPC where Task ENIs should be placed + */ + vpc: ec2.VpcNetworkRefProps; +} + +/** + * 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) { + 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 new file mode 100644 index 0000000000000..d3ea11ef4a659 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -0,0 +1,76 @@ +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 { 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 + */ + cluster: IFargateCluster; // should be required? do we assume 'default' exists? + + /** + * 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 Private subnet if assignPublicIp, public subnets otherwise + */ + vpcPlacement?: ec2.VpcPlacementStrategy; + + /** + * Existing security group to use for the tasks + * + * @default A new security group is created + */ + 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; + + constructor(parent: cdk.Construct, name: string, props: FargateServiceProps) { + super(parent, name, props, { + 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 }); + + if (!props.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/fargate/fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts new file mode 100644 index 0000000000000..37d2eba0a4174 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts @@ -0,0 +1,59 @@ +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. + * 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; +} + +/** + * 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 = {}) { + super(parent, name, props, { + cpu: props.cpu || '256', + memory: props.memoryMiB || '512', + 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 8cccdf3fb43d7..4031b3a6078fc 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -1,2 +1,27 @@ +export * from './base/base-cluster'; +export * from './base/base-service'; +export * from './base/base-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/aws-log-driver'; +export * from './log-drivers/log-driver'; + // AWS::ECS CloudFormation Resources: +// export * from './ecs.generated'; 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..e3cdb39ba74f2 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -0,0 +1,253 @@ +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[] = []; + + /** + * 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, + sharedMemorySize: this.sharedMemorySize, + capabilities: { + add: this.capAdd, + drop: this.capDrop, + }, + devices: this.devices.map(renderDevice), + tmpfs: this.tmpfs.map(renderTmpfs) + }; + } +} + +/** + * 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[] +} + +function renderDevice(device: Device): cloudformation.TaskDefinitionResource.DeviceProperty { + return { + containerPath: device.containerPath, + hostPath: device.hostPath, + permissions: device.permissions + }; +} + +/** + * A tmpfs mount + */ +export interface Tmpfs { + /** + * Path in the container to mount + */ + containerPath: string, + + /** + * Size of the volume + */ + size: number, + + /** + * Mount options + */ + mountOptions?: TmpfsMountOption[], +} + +function renderTmpfs(tmpfs: Tmpfs): cloudformation.TaskDefinitionResource.TmpfsProperty { + return { + containerPath: tmpfs.containerPath, + size: tmpfs.size, + mountOptions: tmpfs.mountOptions + }; +} + +/** + * A Linux capability + */ +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" +} + +/** + * 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", + 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" +} 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..bc24316ffb302 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts @@ -0,0 +1,103 @@ +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'; + +/** + * Properties for a LoadBalancedEcsService + */ +export interface LoadBalancedEcsServiceProps { + /** + * The cluster where your Fargate service will be deployed + */ + cluster: IEcsCluster; + + /** + * The image to start. + */ + 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; +} + +/** + * 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) { + 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] + }); + + 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 new file mode 100644 index 0000000000000..274a664ff6ef4 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts @@ -0,0 +1,96 @@ +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'; + +/** + * 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: + * 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; +} + +/** + * An applet for a LoadBalancedFargateService + */ +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 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..7275de17d7841 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts @@ -0,0 +1,126 @@ +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); +import { ContainerImage } from './container-image'; +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: + * 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; +} + +/** + * A single task running on an ECS cluster fronted by a load balancer + */ +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] + }); + + new cdk.Output(this, 'LoadBalancerDNS', { value: lb.dnsName }); + } +} \ 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 new file mode 100644 index 0000000000000..8e8caa8f5e8e1 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts @@ -0,0 +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"; + +/** + * 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 renderLogDriver(): 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; +} 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..eb7c1344b5dda --- /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 renderLogDriver(): cloudformation.TaskDefinitionResource.LogConfigurationProperty; +} diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index c1690ff496ea8..a0cfc357e6bec 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -54,10 +54,19 @@ "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" }, "dependencies": { + "@aws-cdk/aws-applicationautoscaling": "^0.14.1", + "@aws-cdk/aws-autoscaling": "^0.14.1", + "@aws-cdk/aws-cloudwatch": "^0.14.1", + "@aws-cdk/aws-ec2": "^0.14.1", + "@aws-cdk/aws-elasticloadbalancing": "^0.14.1", + "@aws-cdk/aws-elasticloadbalancingv2": "^0.14.1", + "@aws-cdk/aws-iam": "^0.14.1", + "@aws-cdk/aws-logs": "^0.14.1", "@aws-cdk/cdk": "^0.14.1" }, "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 new file mode 100644 index 0000000000000..205372b0e36ad --- /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(); +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, }); + +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 new file mode 100644 index 0000000000000..dbd8c8e798c30 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts @@ -0,0 +1,44 @@ + +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(); +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: 256, +}); +container.addPortMappings({ + containerPort: 80, + hostPort: 8080, + 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, }); + +app.run(); \ No newline at end of file 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..ccf2801f83f11 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts @@ -0,0 +1,193 @@ +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'); + +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, 'EcsCluster', { + 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" + } + })); + + 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/ecs/test.ecs-service.ts b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts new file mode 100644 index 0000000000000..3af4c35645bf6 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts @@ -0,0 +1,424 @@ +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'); +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(); + } + }, + + '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/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 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..be30e8b227170 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json @@ -0,0 +1,25 @@ +{ + "context": { + "availability-zones:794715269151:eu-west-1": [ + "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 new file mode 100644 index 0000000000000..b3c6f9401d46a --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts @@ -0,0 +1,45 @@ +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(); +const stack = new cdk.Stack(app, 'aws-ecs-integ'); + +const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); + +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 +}); + +const service = new ecs.FargateService(stack, "Service", { + cluster, + 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', { + port: 80, + targets: [service] +}); + +new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName, }); + +app.run(); 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(); + }, + } +}; 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..30132983d62a7 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts @@ -0,0 +1,131 @@ +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(); + }, + + "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(); + 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 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 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..19819a1ca3cce --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -0,0 +1,240 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +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, + }); + // THEN + test.throws(() => { + container.addPortMappings({ + containerPort: 8080, + hostPort: 8081 + }); + }); + 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 exception 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, + }); + + // THEN + test.throws(() => { + container.addPortMappings({ + containerPort: 8080, + hostPort: 1, + }); + }); + 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) { + // 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: 8080, + hostPort: 8081, + }); + const actual = container.ingressPort; + + // THEN + const expected = 8081; + test.equal(actual, expected); + test.done(); + }, + "Ingress port should be 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(); + }, + }, + }, + + '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/packages/@aws-cdk/cdk/lib/context.ts b/packages/@aws-cdk/cdk/lib/context.ts index 1492113bc1180..49fef72abbd72 100644 --- a/packages/@aws-cdk/cdk/lib/context.ts +++ b/packages/@aws-cdk/cdk/lib/context.ts @@ -167,8 +167,8 @@ export class SSMParameterProvider { /** * Return the SSM parameter string with the indicated key */ - public parameterValue(): any { - return this.provider.getStringValue('dummy'); + public parameterValue(defaultValue = 'dummy'): any { + return this.provider.getStringValue(defaultValue); } } 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 {