diff --git a/design/aws-ecs-service-discovery-integration.md b/design/aws-ecs-service-discovery-integration.md new file mode 100644 index 0000000000000..63ce8414af939 --- /dev/null +++ b/design/aws-ecs-service-discovery-integration.md @@ -0,0 +1,216 @@ +# AWS ECS - Support for AWS Cloud Map (Service Discovery) Integration + +To address issue [#1554](https://github.com/awslabs/aws-cdk/issues/1554), the +ECS construct library should provide a way to set +[`serviceRegistries`](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_CreateService.html#ECS-CreateService-request-serviceRegistries) +on their ECS service at the L1 level. + + +## General approach +Rather than having the customer instantiate Cloud Map constructs directly and +thus be forced to learn the API of a separate construct library, we should +allow the customer to pass in the domain name and other configuration minimally +needed to instantiate a Cloud Map namespace within an ECS cluster and create a +Cloud Map service for each ECS service. + +The current proposal is to add a method on the ECS Cluster construct, +`addNamespace` that would take a set of properties that include the namespace +type (Public DNS, Private DNS or Http) and namespace name (domain name). + +While it is possible to use more than one namespace for services in an ECS +cluster, realistically, we would expect ECS customers to only have one +namespace per given cluster. A Cloud Map service within that namespace would +then be created for each ECS service using service discovery in the cluster, +and would be discoverable by service name within that namespace, e.g. +frontend.mydomain.com, backend.mydomain.com, etc. + +ECS will automatically register service discovery instances that are accessible +by IP address (IPv4 only) on the createService API call. + +// FIXME public namespace needs to be imported? + +## Code changes + +The following are the new methods/interfaces that would be required for the proposed approach: + +#### Cluster#addNamespace(options: NamespaceOptions) + +This will allow adding a Cloud Map namespace, which will be accessible as a +member on the cluster. In the case of a Private DNS Namespace, a Route53 hosted +zone will be created for the customer. + +```ts +export interface NamespaceOptions { + /** + * The domain name for the namespace, such as foo.com + */ + name: string; + + /** + * The type of CloudMap Namespace to create in your cluster + * + * @default PrivateDns + */ + type?: cloudmap.NamespaceType; + + /** + * The Amazon VPC that you want to associate the namespace with. Required for Private DNS namespaces + * + * @default VPC of the cluster for Private DNS Namespace, otherwise none + */ + vpc?: ec2.IVpcNetwork; +} +``` + +#### service#enableServiceDiscovery(options: ServiceDiscoveryOptions) + +This method would create a Cloud Map service, whose arn would then be passed as the serviceRegistry when the ECS service is created. + +Other fields in the service registry are optionally needed depending on the +network mode of the task definition used by the ECS service. + +- If the task definition uses the bridge or host network mode, a containerName + and containerPort combination are needed. These will be taken from the +defaultContainer on the task definition. + +- If the task definition uses the awsvpc network mode and a type SRV DNS record + is used, you must specify either a containerName and containerPort +combination. These will be taken from the defaultContainer on the task definition. +NOTE: In this case, the API also allows you to simply pass in "port" at the +mutual exclusion of the `containerName` and `containerPort` combination, but +for simplicity we are only including `containerName` and `containerPort` and +not `port`. + +NOTE: warn about creating service with public namespace? + +If the customer wishes to have maximum configurability for their service, we will also add + +```ts +export interface ServiceDiscoveryOptions { + /** + * Name of the cloudmap service to attach to the ECS Service + * + * @default CloudFormation-generated name + */ + name?: string, + + /** + * The DNS type of the record that you want AWS Cloud Map to create. Supported record types + * include A or SRV. + + * @default: A + */ + dnsRecordType?: cloudmap.DnsRecordType.A | cloudmap.DnsRecordType.SRV, + + /** + * The amount of time, in seconds, that you want DNS resolvers to cache the settings for this + * record. + * + * @default 60 + */ + dnsTtlSec?: number; + + /** + * The number of 30-second intervals that you want Cloud Map to wait after receiving an + * UpdateInstanceCustomHealthStatus request before it changes the health status of a service instance. + * NOTE: This is used for a Custom HealthCheckCustomConfig + */ + failureThreshold?: number, +} +``` + +A full example would look like the following: + +``` +const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); + +// Cloud Map Namespace +const namespace = new servicediscovery.PrivateDnsNamespace(stack, 'MyNamespace', { + name: 'mydomain.com', + vpc, +}); + +// Cloud Map Service + +const cloudMapService = namespace.createService('MyCloudMapService', { + dnsRecordType: servicediscovery.DnsRecordType.A, + dnsTtlSec: 300, + customHealthCheck: { + failureThreshold = 1 + } +}); + +// ECS Cluster +const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + +cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro') +}); + +cluster.addNamespace({ name: "foo.com" }) + +const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + +const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + memoryLimitMiB: 256, +}); + +container.addPortMappings({ + containerPort: 80, + hostPort: 8080, + protocol: ecs.Protocol.Tcp +}); + +const ecsService = new ecs.Ec2Service(stack, "MyECSService", { + cluster, + taskDefinition, +}); + +ecsService.enableServiceDiscovery( + dnsRecordType: servicediscovery.DnsRecordType.A, + dnsTtlSec: 300, + customHealthCheck: { + failureThreshold = 1 + } +) + +``` +#### Service Discovery Considerations +##### See: (https://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-discovery.html) + +The following should be considered when using service discovery: + +Service discovery is supported for tasks using the Fargate launch type if they are using platform version v1.1.0 or later. For more information, see AWS Fargate Platform Versions. + +The Create Service workflow in the Amazon ECS console only supports registering services into private DNS namespaces. When a AWS Cloud Map private DNS namespace is created, a Route 53 private hosted zone will be created automatically. + +Amazon ECS does not support registering services into public DNS namespaces. + +The DNS records created for a service discovery service always register with the private IP address for the task, rather than the public IP address, even when public namespaces are used. + +Service discovery requires that tasks specify either the awsvpc, bridge, or host network mode (none is not supported). + +If the task definition your service task specifies uses the awsvpc network mode, you can create any combination of A or SRV records for each service task. If you use SRV records, a port is required. + +If the task definition that your service task specifies uses the bridge or host network mode, an SRV record is the only supported DNS record type. Create an SRV record for each service task. The SRV record must specify a container name and container port combination from the task definition. + +DNS records for a service discovery service can be queried within your VPC. They use the following format: .. For more information, see Step 3: Verify Service Discovery. + +When doing a DNS query on the service name, A records return a set of IP addresses that correspond to your tasks. SRV records return a set of IP addresses and ports per task. + +You can configure service discovery for an ECS service that is behind a load balancer, but service discovery traffic is always routed to the task and not the load balancer. + +Service discovery does not support the use of Classic Load Balancers. + +It is recommended to use container-level health checks managed by Amazon ECS for your service discovery service. + +HealthCheckCustomConfig—Amazon ECS manages health checks on your behalf. Amazon ECS uses information from container and health checks, as well as your task state, to update the health with AWS Cloud Map. This is specified using the --health-check-custom-config parameter when creating your service discovery service. For more information, see HealthCheckCustomConfig in the AWS Cloud Map API Reference. + +If you are using the Amazon ECS console, the workflow creates one service discovery service per ECS service. It maps all of the task IP addresses as A records, or task IP addresses and port as SRV records. + +Service discovery can only be configured when first creating a service. Updating existing services to configure service discovery for the first time or change the current configuration is not supported. + +The AWS Cloud Map resources created when service discovery is used must be cleaned up manually. For more information, see Step 4: Clean Up in the Tutorial: Creating a Service Using Service Discovery topic. + + diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 0ce286a03d0e6..aabff09a9c55b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -3,8 +3,10 @@ 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 cloudmap = require('@aws-cdk/aws-servicediscovery'); import cdk = require('@aws-cdk/cdk'); import { NetworkMode, TaskDefinition } from '../base/task-definition'; +import { ICluster } from '../cluster'; import { CfnService } from '../ecs.generated'; import { ScalableTaskCount } from './scalable-task-count'; @@ -12,6 +14,11 @@ import { ScalableTaskCount } from './scalable-task-count'; * Basic service properties */ export interface BaseServiceProps { + /** + * Cluster where service will be deployed + */ + cluster: ICluster; + /** * Number of desired copies of running tasks * @@ -83,8 +90,12 @@ export abstract class BaseService extends cdk.Construct */ public readonly taskDefinition: TaskDefinition; + protected cloudmapService: cloudmap.Service; + protected cluster: ICluster; protected loadBalancers = new Array(); protected networkConfiguration?: CfnService.NetworkConfigurationProperty; + protected serviceRegistries = new Array(); + private readonly resource: CfnService; private scalableTaskCount?: ScalableTaskCount; @@ -109,11 +120,13 @@ export abstract class BaseService extends cdk.Construct healthCheckGracePeriodSeconds: props.healthCheckGracePeriodSeconds, /* role: never specified, supplanted by Service Linked Role */ networkConfiguration: new cdk.Token(() => this.networkConfiguration), + serviceRegistries: new cdk.Token(() => this.serviceRegistries), ...additionalProps }); this.serviceArn = this.resource.serviceArn; this.serviceName = this.resource.serviceName; this.clusterName = clusterName; + this.cluster = props.cluster; } /** @@ -161,6 +174,14 @@ export abstract class BaseService extends cdk.Construct }); } + /** + * Associate Service Discovery (Cloud Map) service + */ + public addServiceRegistry(registry: ServiceRegistry) { + const sr = this.renderServiceRegistry(registry); + this.serviceRegistries.push(sr); + } + /** * Return the given named metric for this Service */ @@ -195,6 +216,14 @@ export abstract class BaseService extends cdk.Construct }; } + private renderServiceRegistry(registry: ServiceRegistry): CfnService.ServiceRegistryProperty { + return { + registryArn: registry.arn, + containerName: registry.containerName, + containerPort: registry.containerPort, + }; + } + /** * Shared logic for attaching to an ELBv2 */ @@ -236,3 +265,64 @@ export abstract class BaseService extends cdk.Construct * The port range to open up for dynamic port mapping */ const EPHEMERAL_PORT_RANGE = new ec2.TcpPortRange(32768, 65535); + +/** + * Options for enabling service discovery on an ECS service + */ +export interface ServiceDiscoveryOptions { + /** + * Name of the cloudmap service to attach to the ECS Service + * + * @default CloudFormation-generated name + */ + name?: string, + + /** + * The DNS type of the record that you want AWS Cloud Map to create. Supported record types include A or SRV. + * + * @default: A + */ + dnsRecordType?: cloudmap.DnsRecordType.A | cloudmap.DnsRecordType.SRV, + + /** + * The amount of time, in seconds, that you want DNS resolvers to cache the settings for this record. + * + * @default 60 + */ + dnsTtlSec?: number; + + /** + * The number of 30-second intervals that you want Cloud Map to wait after receiving an + * UpdateInstanceCustomHealthStatus request before it changes the health status of a service instance. + * NOTE: This is used for HealthCheckCustomConfig + */ + failureThreshold?: number, +} + +/** + * Service Registry for ECS service + */ +export interface ServiceRegistry { + /** + * Arn of the Cloud Map Service that will register a Cloud Map Instance for your ECS Service + */ + arn: string; + + /** + * The container name value, already specified in the task definition, to be used for your service discovery service. + * If the task definition that your service task specifies uses the bridge or host network mode, + * you must specify a containerName and containerPort combination from the task definition. + * If the task definition that your service task specifies uses the awsvpc network mode and a type SRV DNS record is + * used, you must specify either a containerName and containerPort combination or a port value, but not both. + */ + containerName?: string; + + /** + * The container port value, already specified in the task definition, to be used for your service discovery service. + * If the task definition that your service task specifies uses the bridge or host network mode, + * you must specify a containerName and containerPort combination from the task definition. + * If the task definition that your service task specifies uses the awsvpc network mode and a type SRV DNS record is + * used, you must specify either a containerName and containerPort combination or a port value, but not both. + */ + containerPort?: number; +} diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 591c0da164f96..49c65f8ba75b2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -2,6 +2,7 @@ import autoscaling = require('@aws-cdk/aws-autoscaling'); import cloudwatch = require ('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); import iam = require('@aws-cdk/aws-iam'); +import cloudmap = require('@aws-cdk/aws-servicediscovery'); import cdk = require('@aws-cdk/cdk'); import { InstanceDrainHook } from './drain-hook/instance-drain-hook'; import { CfnCluster } from './ecs.generated'; @@ -54,6 +55,11 @@ export class Cluster extends cdk.Construct implements ICluster { */ public readonly clusterName: string; + /** + * The service discovery namespace created in this cluster + */ + private _serviceDiscoveryNamespace?: cloudmap.INamespace; + /** * Whether the cluster has EC2 capacity associated with it */ @@ -69,6 +75,43 @@ export class Cluster extends cdk.Construct implements ICluster { this.clusterName = cluster.clusterName; } + /** + * Add an AWS Cloud Map Private DNS namespace for this cluster. + * NOTE: HttpNamespaces are not supported, as ECS always requires a DNSConfig when registering an instance to a Cloud + * Map service. + * + * FIXME Support adding public dns namespace as import? + */ + public addNamespace(options: NamespaceOptions): cloudmap.INamespace { + if (options.type === cloudmap.NamespaceType.Http) { + throw new Error("Http Namespaces are not supported in ECS."); + } + + const namespaceType = options.type !== undefined + ? options.type + : cloudmap.NamespaceType.DnsPrivate; + + const sdNamespace = namespaceType === cloudmap.NamespaceType.DnsPrivate ? + new cloudmap.PrivateDnsNamespace(this, 'DefaultServiceDiscoveryNamespace', { + name: options.name, + vpc: this.vpc + }) : + new cloudmap.PublicDnsNamespace(this, 'DefaultServiceDiscoveryNamespace', { + name: options.name, + }); + + this._serviceDiscoveryNamespace = sdNamespace; + + return sdNamespace; + } + + /** + * Getter for namespace added to cluster + */ + public serviceDiscoveryNamespace(): cloudmap.INamespace | undefined { + return this._serviceDiscoveryNamespace; + } + /** * Add a default-configured AutoScalingGroup running the ECS-optimized AMI to this Cluster * @@ -252,6 +295,11 @@ export interface ICluster extends cdk.IConstruct { */ readonly hasEc2Capacity: boolean; + /** + * Getter for Cloudmap namespace created in the cluster + */ + serviceDiscoveryNamespace(): cloudmap.INamespace | undefined; + /** * Export the Cluster */ @@ -321,6 +369,11 @@ class ImportedCluster extends cdk.Construct implements ICluster { */ public readonly hasEc2Capacity: boolean; + /** + * Cloudmap namespace created in the cluster + */ + private _serviceDiscoveryNamespace: cloudmap.INamespace; + constructor(scope: cdk.Construct, id: string, private readonly props: ClusterImportProps) { super(scope, id); this.clusterName = props.clusterName; @@ -340,6 +393,10 @@ class ImportedCluster extends cdk.Construct implements ICluster { } } + public serviceDiscoveryNamespace(): cloudmap.INamespace | undefined { + return this._serviceDiscoveryNamespace; + } + public export() { return this.props; } @@ -379,3 +436,24 @@ export interface AddCapacityOptions extends AddAutoScalingGroupCapacityOptions, */ instanceType: ec2.InstanceType; } + +export interface NamespaceOptions { + /** + * The domain name for the namespace, such as foo.com + */ + name: string; + + /** + * The type of CloudMap Namespace to create in your cluster + * + * @default PrivateDns + */ + type?: cloudmap.NamespaceType; + + /** + * The Amazon VPC that you want to associate the namespace with. Required for Private DNS namespaces + * + * @default VPC of the cluster for Private DNS Namespace, otherwise none + */ + vpc?: ec2.IVpcNetwork; +} diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts index 1a20739cc4362..c5d834a78dff0 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -1,10 +1,10 @@ import cloudwatch = require ('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); import elb = require('@aws-cdk/aws-elasticloadbalancing'); +import cloudmap = require('@aws-cdk/aws-servicediscovery'); import cdk = require('@aws-cdk/cdk'); -import { BaseService, BaseServiceProps } from '../base/base-service'; +import { BaseService, BaseServiceProps, ServiceDiscoveryOptions } from '../base/base-service'; import { NetworkMode, TaskDefinition } from '../base/task-definition'; -import { ICluster } from '../cluster'; import { CfnService } from '../ecs.generated'; import { isEc2Compatible } from '../util'; @@ -12,11 +12,6 @@ import { isEc2Compatible } from '../util'; * Properties to define an ECS service */ export interface Ec2ServiceProps extends BaseServiceProps { - /** - * Cluster where service will be deployed - */ - cluster: ICluster; - /** * Task Definition used for running tasks in the service */ @@ -70,7 +65,6 @@ export class Ec2Service extends BaseService implements elb.ILoadBalancerTarget { private readonly constraints: CfnService.PlacementConstraintProperty[]; private readonly strategies: CfnService.PlacementStrategyProperty[]; private readonly daemon: boolean; - private readonly cluster: ICluster; constructor(scope: cdk.Construct, id: string, props: Ec2ServiceProps) { if (props.daemon && props.desiredCount !== undefined) { @@ -95,7 +89,6 @@ export class Ec2Service extends BaseService implements elb.ILoadBalancerTarget { schedulingStrategy: props.daemon ? 'DAEMON' : 'REPLICA', }, props.cluster.clusterName, props.taskDefinition); - this.cluster = props.cluster; this.clusterName = props.cluster.clusterName; this.constraints = []; this.strategies = []; @@ -198,6 +191,66 @@ export class Ec2Service extends BaseService implements elb.ILoadBalancerTarget { }); } + /** + * Enable CloudMap service discovery for the service + */ + public enableServiceDiscovery(options: ServiceDiscoveryOptions): cloudmap.Service { + const sdNamespace = this.cluster.serviceDiscoveryNamespace(); + if (sdNamespace === undefined) { + throw new Error("Cannot enable service discovery if a Cloudmap Namespace has not been created in the cluster."); + } + + // Determine DNS type based on network mode + const networkMode = this.taskDefinition.networkMode; + if (networkMode === NetworkMode.None) { + throw new Error("Cannot use a service discovery if NetworkMode is None. Use Bridge, Host or AwsVpc instead."); + } + + // Bridge or host network mode requires SRV records + let dnsRecordType = options.dnsRecordType; + + if (networkMode === NetworkMode.Bridge || networkMode === NetworkMode.Host) { + if (dnsRecordType === undefined) { + dnsRecordType = cloudmap.DnsRecordType.SRV; + } + if (dnsRecordType !== cloudmap.DnsRecordType.SRV) { + throw new Error("SRV records must be used when network mode is Bridge or Host."); + } + } + + // Default DNS record type for AwsVpc network mode is A Records + if (networkMode === NetworkMode.AwsVpc) { + if (dnsRecordType === undefined) { + dnsRecordType = cloudmap.DnsRecordType.A; + } + } + + // If the task definition that your service task specifies uses the AWSVPC network mode and a type SRV DNS record is + // used, you must specify a containerName and containerPort combination + const containerName = dnsRecordType === cloudmap.DnsRecordType.SRV ? this.taskDefinition.defaultContainer!.node.id : undefined; + const containerPort = dnsRecordType === cloudmap.DnsRecordType.SRV ? this.taskDefinition.defaultContainer!.containerPort : undefined; + + const cloudmapService = new cloudmap.Service(this, 'CloudmapService', { + namespace: sdNamespace, + name: options.name, + dnsRecordType: dnsRecordType!, + customHealthCheck: { failureThreshold: options.failureThreshold || 1 } + }); + + const serviceArn = cloudmapService.serviceArn; + + // add Cloudmap service to the ECS Service's serviceRegistry + this.addServiceRegistry({ + arn: serviceArn, + containerName, + containerPort + }); + + this.cloudmapService = cloudmapService; + + return cloudmapService; + } + /** * Return the given named metric for this Service */ diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index 5a146d1fca2b1..8e879855ad635 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -1,19 +1,14 @@ import ec2 = require('@aws-cdk/aws-ec2'); +import cloudmap = require('@aws-cdk/aws-servicediscovery'); import cdk = require('@aws-cdk/cdk'); -import { BaseService, BaseServiceProps } from '../base/base-service'; +import { BaseService, BaseServiceProps, ServiceDiscoveryOptions } from '../base/base-service'; import { TaskDefinition } from '../base/task-definition'; -import { ICluster } from '../cluster'; import { isFargateCompatible } from '../util'; /** * Properties to define a Fargate service */ export interface FargateServiceProps extends BaseServiceProps { - /** - * Cluster where service will be deployed - */ - cluster: ICluster; // should be required? do we assume 'default' exists? - /** * Task Definition used for running tasks in the service */ @@ -76,6 +71,39 @@ export class FargateService extends BaseService { throw new Error('A TaskDefinition must have at least one essential container'); } } + + public enableServiceDiscovery(options: ServiceDiscoveryOptions): cloudmap.Service { + const sdNamespace = this.cluster.serviceDiscoveryNamespace(); + if (sdNamespace === undefined) { + throw new Error("Cannot enable service discovery if a Cloudmap Namespace has not been created in the cluster."); + } + + const dnsRecordType = options.dnsRecordType === undefined + ? cloudmap.DnsRecordType.A : options.dnsRecordType; + + // If the task definition that your service task specifies uses the awsvpc network mode and a type SRV DNS record + // is used, you must specify a containerName and containerPort combination + const containerName = dnsRecordType === cloudmap.DnsRecordType.SRV ? this.taskDefinition.defaultContainer!.node.id : undefined; + const containerPort = dnsRecordType === cloudmap.DnsRecordType.SRV ? this.taskDefinition.defaultContainer!.containerPort : undefined; + + const cloudmapService = new cloudmap.Service(this, 'CloudmapService', { + namespace: sdNamespace, + name: options.name, + dnsRecordType: dnsRecordType!, + customHealthCheck: { failureThreshold: options.failureThreshold || 1 } + }); + + const serviceArn = cloudmapService.serviceArn; + + // add Cloudmap service to the ECS Service's serviceRegistry + this.addServiceRegistry({ + arn: serviceArn, + containerName, + containerPort + }); + + return cloudmapService; + } } /** diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index ab62843aec19c..92f1db2a3eed4 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -79,6 +79,7 @@ "@aws-cdk/aws-logs": "^0.26.0", "@aws-cdk/aws-route53": "^0.26.0", "@aws-cdk/aws-secretsmanager": "^0.26.0", + "@aws-cdk/aws-servicediscovery": "^0.26.0", "@aws-cdk/aws-sns": "^0.26.0", "@aws-cdk/cdk": "^0.26.0", "@aws-cdk/cx-api": "^0.26.0" @@ -99,6 +100,7 @@ "@aws-cdk/aws-logs": "^0.26.0", "@aws-cdk/aws-route53": "^0.26.0", "@aws-cdk/aws-secretsmanager": "^0.26.0", + "@aws-cdk/aws-servicediscovery": "^0.26.0", "@aws-cdk/cdk": "^0.26.0" }, "engines": { @@ -111,4 +113,4 @@ "construct-ctor:@aws-cdk/aws-ecs.LoadBalancedFargateServiceApplet..params[0]" ] } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json index 4836aa89c2bca..c91151191acf1 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json @@ -846,7 +846,8 @@ }, "PlacementConstraints": [], "PlacementStrategies": [], - "SchedulingStrategy": "REPLICA" + "SchedulingStrategy": "REPLICA", + "ServiceRegistries": [] }, "DependsOn": [ "LBPublicListenerECSGroupD6A32205", diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json index 28fc6b69b6471..16bda9dc2da71 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json @@ -847,7 +847,8 @@ ], "PlacementConstraints": [], "PlacementStrategies": [], - "SchedulingStrategy": "REPLICA" + "SchedulingStrategy": "REPLICA", + "ServiceRegistries": [] }, "DependsOn": [ "LBPublicListenerECSGroupD6A32205", diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json new file mode 100644 index 0000000000000..14bc6b5a51957 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json @@ -0,0 +1,912 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "EcsCluster97242B84": { + "Type": "AWS::ECS::Cluster" + }, + "EcsClusterDefaultAutoScalingGroupInstanceSecurityGroup912E1231": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ-ecs/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [], + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/EcsCluster/DefaultAutoScalingGroup" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ec2.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "EcsClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy04DC6C80": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecs:CreateCluster", + "ecs:DeregisterContainerInstance", + "ecs:DiscoverPollEndpoint", + "ecs:Poll", + "ecs:RegisterContainerInstance", + "ecs:StartTelemetrySession", + "ecs:Submit*", + "ecr:GetAuthorizationToken", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EcsClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy04DC6C80", + "Roles": [ + { + "Ref": "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863" + } + ] + } + }, + "EcsClusterDefaultAutoScalingGroupInstanceProfile2CE606B3": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863" + } + ] + } + }, + "EcsClusterDefaultAutoScalingGroupLaunchConfigB7E376C1": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": "ami-1234", + "InstanceType": "t2.micro", + "IamInstanceProfile": { + "Ref": "EcsClusterDefaultAutoScalingGroupInstanceProfile2CE606B3" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "EcsClusterDefaultAutoScalingGroupInstanceSecurityGroup912E1231", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\necho ECS_CLUSTER=", + { + "Ref": "EcsCluster97242B84" + }, + " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save\necho ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config" + ] + ] + } + } + }, + "DependsOn": [ + "EcsClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy04DC6C80", + "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863" + ] + }, + "EcsClusterDefaultAutoScalingGroupASGC1A785DB": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": "1", + "MinSize": "1", + "DesiredCapacity": "1", + "LaunchConfigurationName": { + "Ref": "EcsClusterDefaultAutoScalingGroupLaunchConfigB7E376C1" + }, + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "aws-ecs-integ-ecs/EcsCluster/DefaultAutoScalingGroup" + } + ], + "VPCZoneIdentifier": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + }, + "UpdatePolicy": { + "AutoScalingReplacingUpdate": { + "WillReplace": true + }, + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true + } + } + }, + "EcsClusterDefaultAutoScalingGroupDrainECSHookTopicC705BD25": { + "Type": "AWS::SNS::Topic" + }, + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole94543EDA": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "lambda.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicyA45BF396": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "autoscaling:CompleteLifecycleAction", + "ec2:DescribeInstances", + "ec2:DescribeInstanceAttribute", + "ec2:DescribeInstanceStatus", + "ec2:DescribeHosts" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:ListContainerInstances", + "ecs:SubmitContainerStateChange", + "ecs:SubmitTaskStateChange", + "ecs:DescribeContainerInstances", + "ecs:UpdateContainerInstancesState", + "ecs:ListTasks", + "ecs:DescribeTasks" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicyA45BF396", + "Roles": [ + { + "Ref": "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole94543EDA" + } + ] + } + }, + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionE17A5F5E": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "import boto3, json, os, time\n\necs = boto3.client('ecs')\nautoscaling = boto3.client('autoscaling')\n\n\ndef lambda_handler(event, context):\n print(json.dumps(event))\n cluster = os.environ['CLUSTER']\n snsTopicArn = event['Records'][0]['Sns']['TopicArn']\n lifecycle_event = json.loads(event['Records'][0]['Sns']['Message'])\n instance_id = lifecycle_event.get('EC2InstanceId')\n if not instance_id:\n print('Got event without EC2InstanceId: %s', json.dumps(event))\n return\n\n instance_arn = container_instance_arn(cluster, instance_id)\n print('Instance %s has container instance ARN %s' % (lifecycle_event['EC2InstanceId'], instance_arn))\n\n if not instance_arn:\n return\n\n while has_tasks(cluster, instance_arn):\n time.sleep(10)\n\n try:\n print('Terminating instance %s' % instance_id)\n autoscaling.complete_lifecycle_action(\n LifecycleActionResult='CONTINUE',\n **pick(lifecycle_event, 'LifecycleHookName', 'LifecycleActionToken', 'AutoScalingGroupName'))\n except Exception as e:\n # Lifecycle action may have already completed.\n print(str(e))\n\n\ndef container_instance_arn(cluster, instance_id):\n \"\"\"Turn an instance ID into a container instance ARN.\"\"\"\n arns = ecs.list_container_instances(cluster=cluster, filter='ec2InstanceId==' + instance_id)['containerInstanceArns']\n if not arns:\n return None\n return arns[0]\n\n\ndef has_tasks(cluster, instance_arn):\n \"\"\"Return True if the instance is running tasks for the given cluster.\"\"\"\n instances = ecs.describe_container_instances(cluster=cluster, containerInstances=[instance_arn])['containerInstances']\n if not instances:\n return False\n instance = instances[0]\n\n if instance['status'] == 'ACTIVE':\n # Start draining, then try again later\n set_container_instance_to_draining(cluster, instance_arn)\n return True\n\n tasks = instance['runningTasksCount'] + instance['pendingTasksCount']\n print('Instance %s has %s tasks' % (instance_arn, tasks))\n\n return tasks > 0\n\n\ndef set_container_instance_to_draining(cluster, instance_arn):\n ecs.update_container_instances_state(\n cluster=cluster,\n containerInstances=[instance_arn], status='DRAINING')\n\n\ndef pick(dct, *keys):\n \"\"\"Pick a subset of a dict.\"\"\"\n return {k: v for k, v in dct.items() if k in keys}\n" + }, + "Handler": "index.lambda_handler", + "Role": { + "Fn::GetAtt": [ + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole94543EDA", + "Arn" + ] + }, + "Runtime": "python3.6", + "Environment": { + "Variables": { + "CLUSTER": { + "Ref": "EcsCluster97242B84" + } + } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/EcsCluster/DefaultAutoScalingGroup" + } + ], + "Timeout": 310 + }, + "DependsOn": [ + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicyA45BF396", + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole94543EDA" + ] + }, + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionTopicSubscriptionDA5F8A10": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "lambda", + "TopicArn": { + "Ref": "EcsClusterDefaultAutoScalingGroupDrainECSHookTopicC705BD25" + }, + "Endpoint": { + "Fn::GetAtt": [ + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionE17A5F5E", + "Arn" + ] + } + } + }, + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionTopicE6B1EBA6": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionE17A5F5E" + }, + "Principal": "sns.amazonaws.com", + "SourceArn": { + "Ref": "EcsClusterDefaultAutoScalingGroupDrainECSHookTopicC705BD25" + } + } + }, + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "autoscaling.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicy75002F88": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "EcsClusterDefaultAutoScalingGroupDrainECSHookTopicC705BD25" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicy75002F88", + "Roles": [ + { + "Ref": "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B" + } + ] + } + }, + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookFFA63029": { + "Type": "AWS::AutoScaling::LifecycleHook", + "Properties": { + "AutoScalingGroupName": { + "Ref": "EcsClusterDefaultAutoScalingGroupASGC1A785DB" + }, + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING", + "DefaultResult": "CONTINUE", + "HeartbeatTimeout": 300, + "NotificationTargetARN": { + "Ref": "EcsClusterDefaultAutoScalingGroupDrainECSHookTopicC705BD25" + }, + "RoleARN": { + "Fn::GetAtt": [ + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B", + "Arn" + ] + } + }, + "DependsOn": [ + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicy75002F88", + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B" + ] + }, + "EcsClusterDefaultServiceDiscoveryNamespaceB0971B2F": { + "Type": "AWS::ServiceDiscovery::PrivateDnsNamespace", + "Properties": { + "Name": "scorekeep.com", + "Vpc": { + "Ref": "Vpc8378EB38" + } + } + }, + "TaskDefTaskRole1EDB4A67": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ecs-tasks.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDef54694570": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "amazon/amazon-ecs-sample", + "Links": [], + "LinuxParameters": { + "Capabilities": { + "Add": [], + "Drop": [] + }, + "Devices": [], + "Tmpfs": [] + }, + "Memory": 256, + "MountPoints": [], + "Name": "frontend", + "PortMappings": [ + { + "ContainerPort": 80, + "HostPort": 80, + "Protocol": "tcp" + } + ], + "Ulimits": [], + "VolumesFrom": [] + } + ], + "Family": "awsecsintegecsTaskDef8DD0C801", + "NetworkMode": "awsvpc", + "PlacementConstraints": [], + "RequiresCompatibilities": [ + "EC2" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + }, + "Volumes": [] + } + }, + "FrontendServiceBC94BA93": { + "Type": "AWS::ECS::Service", + "Properties": { + "TaskDefinition": { + "Ref": "TaskDef54694570" + }, + "Cluster": { + "Ref": "EcsCluster97242B84" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "DesiredCount": 1, + "LaunchType": "EC2", + "LoadBalancers": [], + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "FrontendServiceSecurityGroup85470DEC", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + } + }, + "PlacementConstraints": [], + "PlacementStrategies": [], + "SchedulingStrategy": "REPLICA", + "ServiceRegistries": [ + { + "RegistryArn": { + "Fn::GetAtt": [ + "FrontendServiceCloudmapService6FE76C06", + "Arn" + ] + } + } + ] + } + }, + "FrontendServiceSecurityGroup85470DEC": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ-ecs/FrontendService/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "FrontendServiceCloudmapService6FE76C06": { + "Type": "AWS::ServiceDiscovery::Service", + "Properties": { + "DnsConfig": { + "DnsRecords": [ + { + "TTL": "60", + "Type": "A" + } + ], + "NamespaceId": { + "Fn::GetAtt": [ + "EcsClusterDefaultServiceDiscoveryNamespaceB0971B2F", + "Id" + ] + }, + "RoutingPolicy": "MULTIVALUE" + }, + "HealthCheckCustomConfig": { + "FailureThreshold": 1 + }, + "Name": "frontend", + "NamespaceId": { + "Fn::GetAtt": [ + "EcsClusterDefaultServiceDiscoveryNamespaceB0971B2F", + "Id" + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.ts new file mode 100644 index 0000000000000..d9c01c8cadcc8 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.ts @@ -0,0 +1,48 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +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-ecs'); + +const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); + +const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + +cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro') +}); + +// Add Private DNS Namespace +const domainName = "scorekeep.com"; +cluster.addNamespace({ + name: domainName, +}); + +// Create frontend service +const frontendTD = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { + networkMode: NetworkMode.AwsVpc +}); + +const frontend = frontendTD.addContainer('frontend', { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + memoryLimitMiB: 256, +}); + +frontend.addPortMappings({ + containerPort: 80, + hostPort: 80, + protocol: ecs.Protocol.Tcp +}); + +const frontendService = new ecs.Ec2Service(stack, "FrontendService", { + cluster, + taskDefinition: frontendTD, +}); + +frontendService.enableServiceDiscovery({ + name: "frontend", +}); + +app.run(); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json new file mode 100644 index 0000000000000..30246c0ab306e --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json @@ -0,0 +1,876 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "EcsCluster97242B84": { + "Type": "AWS::ECS::Cluster" + }, + "EcsClusterDefaultAutoScalingGroupInstanceSecurityGroup912E1231": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ-ecs/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [], + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/EcsCluster/DefaultAutoScalingGroup" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ec2.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "EcsClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy04DC6C80": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecs:CreateCluster", + "ecs:DeregisterContainerInstance", + "ecs:DiscoverPollEndpoint", + "ecs:Poll", + "ecs:RegisterContainerInstance", + "ecs:StartTelemetrySession", + "ecs:Submit*", + "ecr:GetAuthorizationToken", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EcsClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy04DC6C80", + "Roles": [ + { + "Ref": "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863" + } + ] + } + }, + "EcsClusterDefaultAutoScalingGroupInstanceProfile2CE606B3": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863" + } + ] + } + }, + "EcsClusterDefaultAutoScalingGroupLaunchConfigB7E376C1": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": "ami-1234", + "InstanceType": "t2.micro", + "IamInstanceProfile": { + "Ref": "EcsClusterDefaultAutoScalingGroupInstanceProfile2CE606B3" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "EcsClusterDefaultAutoScalingGroupInstanceSecurityGroup912E1231", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\necho ECS_CLUSTER=", + { + "Ref": "EcsCluster97242B84" + }, + " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save\necho ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config" + ] + ] + } + } + }, + "DependsOn": [ + "EcsClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy04DC6C80", + "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863" + ] + }, + "EcsClusterDefaultAutoScalingGroupASGC1A785DB": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": "1", + "MinSize": "1", + "DesiredCapacity": "1", + "LaunchConfigurationName": { + "Ref": "EcsClusterDefaultAutoScalingGroupLaunchConfigB7E376C1" + }, + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "aws-ecs-integ-ecs/EcsCluster/DefaultAutoScalingGroup" + } + ], + "VPCZoneIdentifier": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + }, + "UpdatePolicy": { + "AutoScalingReplacingUpdate": { + "WillReplace": true + }, + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true + } + } + }, + "EcsClusterDefaultAutoScalingGroupDrainECSHookTopicC705BD25": { + "Type": "AWS::SNS::Topic" + }, + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole94543EDA": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "lambda.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicyA45BF396": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "autoscaling:CompleteLifecycleAction", + "ec2:DescribeInstances", + "ec2:DescribeInstanceAttribute", + "ec2:DescribeInstanceStatus", + "ec2:DescribeHosts" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:ListContainerInstances", + "ecs:SubmitContainerStateChange", + "ecs:SubmitTaskStateChange", + "ecs:DescribeContainerInstances", + "ecs:UpdateContainerInstancesState", + "ecs:ListTasks", + "ecs:DescribeTasks" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicyA45BF396", + "Roles": [ + { + "Ref": "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole94543EDA" + } + ] + } + }, + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionE17A5F5E": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "import boto3, json, os, time\n\necs = boto3.client('ecs')\nautoscaling = boto3.client('autoscaling')\n\n\ndef lambda_handler(event, context):\n print(json.dumps(event))\n cluster = os.environ['CLUSTER']\n snsTopicArn = event['Records'][0]['Sns']['TopicArn']\n lifecycle_event = json.loads(event['Records'][0]['Sns']['Message'])\n instance_id = lifecycle_event.get('EC2InstanceId')\n if not instance_id:\n print('Got event without EC2InstanceId: %s', json.dumps(event))\n return\n\n instance_arn = container_instance_arn(cluster, instance_id)\n print('Instance %s has container instance ARN %s' % (lifecycle_event['EC2InstanceId'], instance_arn))\n\n if not instance_arn:\n return\n\n while has_tasks(cluster, instance_arn):\n time.sleep(10)\n\n try:\n print('Terminating instance %s' % instance_id)\n autoscaling.complete_lifecycle_action(\n LifecycleActionResult='CONTINUE',\n **pick(lifecycle_event, 'LifecycleHookName', 'LifecycleActionToken', 'AutoScalingGroupName'))\n except Exception as e:\n # Lifecycle action may have already completed.\n print(str(e))\n\n\ndef container_instance_arn(cluster, instance_id):\n \"\"\"Turn an instance ID into a container instance ARN.\"\"\"\n arns = ecs.list_container_instances(cluster=cluster, filter='ec2InstanceId==' + instance_id)['containerInstanceArns']\n if not arns:\n return None\n return arns[0]\n\n\ndef has_tasks(cluster, instance_arn):\n \"\"\"Return True if the instance is running tasks for the given cluster.\"\"\"\n instances = ecs.describe_container_instances(cluster=cluster, containerInstances=[instance_arn])['containerInstances']\n if not instances:\n return False\n instance = instances[0]\n\n if instance['status'] == 'ACTIVE':\n # Start draining, then try again later\n set_container_instance_to_draining(cluster, instance_arn)\n return True\n\n tasks = instance['runningTasksCount'] + instance['pendingTasksCount']\n print('Instance %s has %s tasks' % (instance_arn, tasks))\n\n return tasks > 0\n\n\ndef set_container_instance_to_draining(cluster, instance_arn):\n ecs.update_container_instances_state(\n cluster=cluster,\n containerInstances=[instance_arn], status='DRAINING')\n\n\ndef pick(dct, *keys):\n \"\"\"Pick a subset of a dict.\"\"\"\n return {k: v for k, v in dct.items() if k in keys}\n" + }, + "Handler": "index.lambda_handler", + "Role": { + "Fn::GetAtt": [ + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole94543EDA", + "Arn" + ] + }, + "Runtime": "python3.6", + "Environment": { + "Variables": { + "CLUSTER": { + "Ref": "EcsCluster97242B84" + } + } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/EcsCluster/DefaultAutoScalingGroup" + } + ], + "Timeout": 310 + }, + "DependsOn": [ + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicyA45BF396", + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole94543EDA" + ] + }, + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionTopicSubscriptionDA5F8A10": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "lambda", + "TopicArn": { + "Ref": "EcsClusterDefaultAutoScalingGroupDrainECSHookTopicC705BD25" + }, + "Endpoint": { + "Fn::GetAtt": [ + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionE17A5F5E", + "Arn" + ] + } + } + }, + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionTopicE6B1EBA6": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionE17A5F5E" + }, + "Principal": "sns.amazonaws.com", + "SourceArn": { + "Ref": "EcsClusterDefaultAutoScalingGroupDrainECSHookTopicC705BD25" + } + } + }, + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "autoscaling.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicy75002F88": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "EcsClusterDefaultAutoScalingGroupDrainECSHookTopicC705BD25" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicy75002F88", + "Roles": [ + { + "Ref": "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B" + } + ] + } + }, + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookFFA63029": { + "Type": "AWS::AutoScaling::LifecycleHook", + "Properties": { + "AutoScalingGroupName": { + "Ref": "EcsClusterDefaultAutoScalingGroupASGC1A785DB" + }, + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING", + "DefaultResult": "CONTINUE", + "HeartbeatTimeout": 300, + "NotificationTargetARN": { + "Ref": "EcsClusterDefaultAutoScalingGroupDrainECSHookTopicC705BD25" + }, + "RoleARN": { + "Fn::GetAtt": [ + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B", + "Arn" + ] + } + }, + "DependsOn": [ + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicy75002F88", + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B" + ] + }, + "EcsClusterDefaultServiceDiscoveryNamespaceB0971B2F": { + "Type": "AWS::ServiceDiscovery::PrivateDnsNamespace", + "Properties": { + "Name": "scorekeep.com", + "Vpc": { + "Ref": "Vpc8378EB38" + } + } + }, + "frontendTDTaskRole638562A0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ecs-tasks.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "frontendTDB289C8FA": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "amazon/amazon-ecs-sample", + "Links": [], + "LinuxParameters": { + "Capabilities": { + "Add": [], + "Drop": [] + }, + "Devices": [], + "Tmpfs": [] + }, + "Memory": 256, + "MountPoints": [], + "Name": "frontend", + "PortMappings": [ + { + "ContainerPort": 80, + "HostPort": 80, + "Protocol": "tcp" + } + ], + "Ulimits": [], + "VolumesFrom": [] + } + ], + "Family": "awsecsintegecsfrontendTD16AB905D", + "NetworkMode": "bridge", + "PlacementConstraints": [], + "RequiresCompatibilities": [ + "EC2" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "frontendTDTaskRole638562A0", + "Arn" + ] + }, + "Volumes": [] + } + }, + "FrontendServiceBC94BA93": { + "Type": "AWS::ECS::Service", + "Properties": { + "TaskDefinition": { + "Ref": "frontendTDB289C8FA" + }, + "Cluster": { + "Ref": "EcsCluster97242B84" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "DesiredCount": 1, + "LaunchType": "EC2", + "LoadBalancers": [], + "PlacementConstraints": [], + "PlacementStrategies": [], + "SchedulingStrategy": "REPLICA", + "ServiceRegistries": [ + { + "ContainerName": "frontend", + "ContainerPort": 80, + "RegistryArn": { + "Fn::GetAtt": [ + "FrontendServiceCloudmapService6FE76C06", + "Arn" + ] + } + } + ] + } + }, + "FrontendServiceCloudmapService6FE76C06": { + "Type": "AWS::ServiceDiscovery::Service", + "Properties": { + "DnsConfig": { + "DnsRecords": [ + { + "TTL": "60", + "Type": "SRV" + } + ], + "NamespaceId": { + "Fn::GetAtt": [ + "EcsClusterDefaultServiceDiscoveryNamespaceB0971B2F", + "Id" + ] + }, + "RoutingPolicy": "MULTIVALUE" + }, + "HealthCheckCustomConfig": { + "FailureThreshold": 1 + }, + "Name": "frontend", + "NamespaceId": { + "Fn::GetAtt": [ + "EcsClusterDefaultServiceDiscoveryNamespaceB0971B2F", + "Id" + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.ts new file mode 100644 index 0000000000000..8866f5c59ed54 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.ts @@ -0,0 +1,46 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +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.Cluster(stack, 'EcsCluster', { vpc }); + +cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro') +}); + +// Add Private DNS Namespace +const domainName = "scorekeep.com"; +cluster.addNamespace({ + name: domainName, +}); + +// Create frontend service +// default network mode is bridge +const frontendTD = new ecs.Ec2TaskDefinition(stack, 'frontendTD'); + +const frontend = frontendTD.addContainer('frontend', { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + memoryLimitMiB: 256, +}); + +frontend.addPortMappings({ + containerPort: 80, + hostPort: 80, + protocol: ecs.Protocol.Tcp +}); + +const frontendService = new ecs.Ec2Service(stack, "FrontendService", { + cluster, + taskDefinition: frontendTD, +}); + +frontendService.enableServiceDiscovery({ + name: "frontend", +}); + +app.run(); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts index 45eb699e9785e..d119a1e6de695 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts @@ -1,10 +1,11 @@ import { expect, haveResource } from '@aws-cdk/assert'; import ec2 = require('@aws-cdk/aws-ec2'); import elb = require('@aws-cdk/aws-elasticloadbalancing'); +import cloudmap = require('@aws-cdk/aws-servicediscovery'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import ecs = require('../../lib'); -import { BinPackResource, BuiltInAttributes, NetworkMode } from '../../lib'; +import { BinPackResource, BuiltInAttributes, ContainerImage, NetworkMode } from '../../lib'; export = { "When creating an ECS Service": { @@ -522,6 +523,391 @@ export = { ], })); + test.done(); + }, + }, + + 'When enabling service discovery': { + 'throws if namespace has not been added to cluster'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + // default network mode is bridge + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512 + }); + container.addPortMappings({ containerPort: 8000 }); + + const service = new ecs.Ec2Service(stack, 'Service', { cluster, taskDefinition }); + + // THEN + test.throws(() => { + service.enableServiceDiscovery({ + name: 'myApp', + }); + }, /Cannot enable service discovery if a Cloudmap Namespace has not been created in the cluster./); + + test.done(); + }, + + 'throws if network mode is none'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { + networkMode: NetworkMode.None + }); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512 + }); + container.addPortMappings({ containerPort: 8000 }); + const service = new ecs.Ec2Service(stack, 'Service', { cluster, taskDefinition }); + + cluster.addNamespace({ name: 'foo.com' }); + + // THEN + test.throws(() => { + service.enableServiceDiscovery({ + name: 'myApp', + }); + }, /Cannot use a service discovery if NetworkMode is None. Use Bridge, Host or AwsVpc instead./); + + test.done(); + }, + + 'creates AWS Cloud Map service for Private DNS namespace with bridge network mode'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + // default network mode is bridge + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512 + }); + container.addPortMappings({ containerPort: 8000 }); + + const service = new ecs.Ec2Service(stack, 'Service', { cluster, taskDefinition }); + + // WHEN + cluster.addNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DnsPrivate + }); + + service.enableServiceDiscovery({ + name: 'myApp', + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + ServiceRegistries: [ + { + ContainerName: "MainContainer", + ContainerPort: 8000, + RegistryArn: { + "Fn::GetAtt": [ + "ServiceCloudmapService046058A4", + "Arn" + ] + } + } + ] + })); + + expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + DnsConfig: { + DnsRecords: [ + { + TTL: "60", + Type: "SRV" + } + ], + NamespaceId: { + 'Fn::GetAtt': [ + 'EcsClusterDefaultServiceDiscoveryNamespaceB0971B2F', + 'Id' + ] + }, + RoutingPolicy: 'MULTIVALUE' + }, + HealthCheckCustomConfig: { + FailureThreshold: 1 + }, + Name: "myApp", + NamespaceId: { + 'Fn::GetAtt': [ + 'EcsClusterDefaultServiceDiscoveryNamespaceB0971B2F', + 'Id' + ] + } + })); + + test.done(); + }, + + 'creates AWS Cloud Map service for Private DNS namespace with host network mode'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { + networkMode: NetworkMode.Host + }); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512 + }); + container.addPortMappings({ containerPort: 8000 }); + const service = new ecs.Ec2Service(stack, 'Service', { cluster, taskDefinition }); + + // WHEN + cluster.addNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DnsPrivate + }); + + service.enableServiceDiscovery({ + name: 'myApp', + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + ServiceRegistries: [ + { + ContainerName: "MainContainer", + ContainerPort: 8000, + RegistryArn: { + "Fn::GetAtt": [ + "ServiceCloudmapService046058A4", + "Arn" + ] + } + } + ] + })); + + expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + DnsConfig: { + DnsRecords: [ + { + TTL: "60", + Type: "SRV" + } + ], + NamespaceId: { + 'Fn::GetAtt': [ + 'EcsClusterDefaultServiceDiscoveryNamespaceB0971B2F', + 'Id' + ] + }, + RoutingPolicy: 'MULTIVALUE' + }, + HealthCheckCustomConfig: { + FailureThreshold: 1 + }, + Name: "myApp", + NamespaceId: { + 'Fn::GetAtt': [ + 'EcsClusterDefaultServiceDiscoveryNamespaceB0971B2F', + 'Id' + ] + } + })); + + test.done(); + }, + + 'throws if wrong DNS record type specified with bridge network mode'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + // default network mode is bridge + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512 + }); + container.addPortMappings({ containerPort: 8000 }); + + const service = new ecs.Ec2Service(stack, 'Service', { cluster, taskDefinition }); + + cluster.addNamespace({ + name: 'foo.com', + }); + + // THEN + test.throws(() => { + service.enableServiceDiscovery({ + name: 'myApp', + dnsRecordType: cloudmap.DnsRecordType.A + }); + }, /SRV records must be used when network mode is Bridge or Host./); + + test.done(); + }, + + 'creates AWS Cloud Map service for Private DNS namespace with AwsVpc network mode'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { + networkMode: NetworkMode.AwsVpc + }); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512 + }); + container.addPortMappings({ containerPort: 8000 }); + + const service = new ecs.Ec2Service(stack, 'Service', { cluster, taskDefinition }); + + // WHEN + cluster.addNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DnsPrivate + }); + + service.enableServiceDiscovery({ + name: 'myApp', + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + ServiceRegistries: [ + { + RegistryArn: { + "Fn::GetAtt": [ + "ServiceCloudmapService046058A4", + "Arn" + ] + } + } + ] + })); + + expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + DnsConfig: { + DnsRecords: [ + { + TTL: "60", + Type: "A" + } + ], + NamespaceId: { + 'Fn::GetAtt': [ + 'EcsClusterDefaultServiceDiscoveryNamespaceB0971B2F', + 'Id' + ] + }, + RoutingPolicy: 'MULTIVALUE' + }, + HealthCheckCustomConfig: { + FailureThreshold: 1 + }, + Name: "myApp", + NamespaceId: { + 'Fn::GetAtt': [ + 'EcsClusterDefaultServiceDiscoveryNamespaceB0971B2F', + 'Id' + ] + } + })); + + test.done(); + }, + + 'creates AWS Cloud Map service for Private DNS namespace with AwsVpc network mode with SRV records'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { + networkMode: NetworkMode.AwsVpc + }); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512 + }); + container.addPortMappings({ containerPort: 8000 }); + const service = new ecs.Ec2Service(stack, 'Service', { cluster, taskDefinition }); + + // WHEN + cluster.addNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DnsPrivate + }); + + service.enableServiceDiscovery({ + name: 'myApp', + dnsRecordType: cloudmap.DnsRecordType.SRV + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + ServiceRegistries: [ + { + ContainerName: "MainContainer", + ContainerPort: 8000, + RegistryArn: { + "Fn::GetAtt": [ + "ServiceCloudmapService046058A4", + "Arn" + ] + } + } + ] + })); + + expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + DnsConfig: { + DnsRecords: [ + { + TTL: "60", + Type: "SRV" + } + ], + NamespaceId: { + 'Fn::GetAtt': [ + 'EcsClusterDefaultServiceDiscoveryNamespaceB0971B2F', + 'Id' + ] + }, + RoutingPolicy: 'MULTIVALUE' + }, + HealthCheckCustomConfig: { + FailureThreshold: 1 + }, + Name: "myApp", + NamespaceId: { + 'Fn::GetAtt': [ + 'EcsClusterDefaultServiceDiscoveryNamespaceB0971B2F', + 'Id' + ] + } + })); + test.done(); }, } diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json index 1ec79c6f8beff..3ca6180e8e608 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json @@ -965,7 +965,8 @@ } ] } - } + }, + "ServiceRegistries": [] }, "DependsOn": [ "FargateServiceLBPublicListenerECSGroupBE57E081", diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.l3.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.l3.expected.json index 0e5ffef65c260..67092cc624428 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.l3.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.l3.expected.json @@ -642,7 +642,8 @@ } ] } - } + }, + "ServiceRegistries": [] }, "DependsOn": [ "L3LBPublicListenerECSGroup648EEA11", @@ -698,4 +699,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.expected.json index 714a7bdb5e2a1..21beb1d2c0d63 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.expected.json @@ -461,7 +461,8 @@ } ] } - } + }, + "ServiceRegistries": [] }, "DependsOn": [ "LBPublicListenerFargateGroup5EE2FBAF", @@ -681,4 +682,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts index 705a893c0b763..da121d63c93a0 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts @@ -1,6 +1,7 @@ import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import ec2 = require('@aws-cdk/aws-ec2'); import elbv2 = require("@aws-cdk/aws-elasticloadbalancingv2"); +import cloudmap = require('@aws-cdk/aws-servicediscovery'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import ecs = require('../../lib'); @@ -237,6 +238,143 @@ export = { test.done(); } + }, + + 'When enabling service discovery': { + 'throws if namespace has not been added to cluster'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512 + }); + container.addPortMappings({ containerPort: 8000 }); + + const service = new ecs.FargateService(stack, 'Service', { cluster, taskDefinition }); + + // THEN + test.throws(() => { + service.enableServiceDiscovery({ + name: 'myApp', + }); + }, /Cannot enable service discovery if a Cloudmap Namespace has not been created in the cluster./); + + test.done(); + }, + + 'creates cloud map service for Private DNS namespace'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + }); + container.addPortMappings({ containerPort: 8000 }); + const service = new ecs.FargateService(stack, 'Service', { cluster, taskDefinition }); + + // WHEN + cluster.addNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DnsPrivate + }); + + service.enableServiceDiscovery({ + name: 'myApp', + }); + + // THEN + expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + DnsConfig: { + DnsRecords: [ + { + TTL: "60", + Type: "A" + } + ], + NamespaceId: { + "Fn::GetAtt": [ + "EcsClusterDefaultServiceDiscoveryNamespaceB0971B2F", + "Id" + ] + }, + RoutingPolicy: "MULTIVALUE" + }, + HealthCheckCustomConfig: { + FailureThreshold: 1 + }, + Name: "myApp", + NamespaceId: { + 'Fn::GetAtt': [ + "EcsClusterDefaultServiceDiscoveryNamespaceB0971B2F", + "Id" + ] + } + })); + + test.done(); + }, + + 'creates AWS Cloud Map service for Private DNS namespace with SRV records'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512 + }); + container.addPortMappings({ containerPort: 8000 }); + const service = new ecs.FargateService(stack, 'Service', { cluster, taskDefinition }); + + // WHEN + cluster.addNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DnsPrivate + }); + + service.enableServiceDiscovery({ + name: 'myApp', + dnsRecordType: cloudmap.DnsRecordType.SRV + }); + // THEN + expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + DnsConfig: { + DnsRecords: [ + { + TTL: "60", + Type: "SRV" + } + ], + NamespaceId: { + 'Fn::GetAtt': [ + 'EcsClusterDefaultServiceDiscoveryNamespaceB0971B2F', + 'Id' + ] + }, + RoutingPolicy: 'MULTIVALUE' + }, + HealthCheckCustomConfig: { + FailureThreshold: 1 + }, + Name: "myApp", + NamespaceId: { + 'Fn::GetAtt': [ + 'EcsClusterDefaultServiceDiscoveryNamespaceB0971B2F', + 'Id' + ] + } + })); + + test.done(); + }, } }; diff --git a/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts index 48183f6051ba6..55cf628243fc2 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts @@ -1,6 +1,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import ec2 = require('@aws-cdk/aws-ec2'); import { InstanceType } from '@aws-cdk/aws-ec2'; +import cloudmap = require('@aws-cdk/aws-servicediscovery'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import ecs = require('../lib'); @@ -218,4 +219,51 @@ export = { test.done(); }, + + "allows adding service discovery namespace"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro'), + }); + + // WHEN + cluster.addNamespace({ + name: "foo.com" + }); + + // THEN + expect(stack).to(haveResource("AWS::ServiceDiscovery::PrivateDnsNamespace", { + Name: 'foo.com', + Vpc: { + Ref: 'MyVpcF9F0CA6F' + } + })); + + test.done(); + }, + + "throws if http namespace is added"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro'), + }); + + // THEN + test.throws(() => { + cluster.addNamespace({ + name: "foo.com", + type: cloudmap.NamespaceType.Http + }); + }, /Http Namespaces are not supported in ECS./); + + test.done(); + }, };