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 062312d6cfb89..b8bb3a6bf3aa2 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 + */ + readonly cluster: ICluster; + /** * Number of desired copies of running tasks * @@ -50,6 +57,11 @@ export interface BaseServiceProps { * @default ??? FIXME */ readonly healthCheckGracePeriodSeconds?: number; + + /** + * Options for enabling AWS Cloud Map service discovery for the service + */ + readonly serviceDiscoveryOptions?: ServiceDiscoveryOptions; } /** @@ -83,8 +95,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 +125,18 @@ 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; + + if (props.serviceDiscoveryOptions) { + this.enableServiceDiscovery(props.serviceDiscoveryOptions); + } + } /** @@ -195,6 +218,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 */ @@ -230,9 +261,138 @@ export abstract class BaseService extends cdk.Construct }) }); } + + /** + * Associate Service Discovery (Cloud Map) service + */ + private addServiceRegistry(registry: ServiceRegistry) { + const sr = this.renderServiceRegistry(registry); + this.serviceRegistries.push(sr); + } + + /** + * Enable CloudMap service discovery for the service + */ + private enableServiceDiscovery(options: ServiceDiscoveryOptions): cloudmap.Service { + const sdNamespace = this.cluster.defaultNamespace; + 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; + } } /** * 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 + */ + readonly 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 + */ + readonly 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 + */ + readonly 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 + */ + readonly 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 + */ + readonly 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. + */ + readonly 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. + */ + readonly containerPort?: number; +} diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 725ab17ea8bd6..4590c2ebb91fd 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 _defaultNamespace?: cloudmap.INamespace; + /** * Whether the cluster has EC2 capacity associated with it */ @@ -69,6 +75,41 @@ export class Cluster extends cdk.Construct implements ICluster { this.clusterName = cluster.clusterName; } + /** + * Add an AWS Cloud Map 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. + */ + public addDefaultCloudMapNamespace(options: NamespaceOptions): cloudmap.INamespace { + if (this._defaultNamespace !== undefined) { + throw new Error("Can only add default namespace once."); + } + + const namespaceType = options.type === undefined || options.type === NamespaceType.PrivateDns + ? cloudmap.NamespaceType.DnsPrivate + : cloudmap.NamespaceType.DnsPublic; + + 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._defaultNamespace = sdNamespace; + + return sdNamespace; + } + + /** + * Getter for namespace added to cluster + */ + public get defaultNamespace(): cloudmap.INamespace | undefined { + return this._defaultNamespace; + } + /** * Add a default-configured AutoScalingGroup running the ECS-optimized AMI to this Cluster * @@ -149,6 +190,7 @@ export class Cluster extends cdk.Construct implements ICluster { vpc: this.vpc.export(), securityGroups: this.connections.securityGroups.map(sg => sg.export()), hasEc2Capacity: this.hasEc2Capacity, + defaultNamespace: this._defaultNamespace && this._defaultNamespace.export(), }; } @@ -252,6 +294,11 @@ export interface ICluster extends cdk.IConstruct { */ readonly hasEc2Capacity: boolean; + /** + * Getter for Cloudmap namespace created in the cluster + */ + readonly defaultNamespace?: cloudmap.INamespace; + /** * Export the Cluster */ @@ -290,6 +337,13 @@ export interface ClusterImportProps { * @default true */ readonly hasEc2Capacity?: boolean; + + /** + * Default namespace properties + * + * @default - No default namespace + */ + readonly defaultNamespace?: cloudmap.NamespaceImportProps; } /** @@ -321,11 +375,17 @@ class ImportedCluster extends cdk.Construct implements ICluster { */ public readonly hasEc2Capacity: boolean; + /** + * Cloudmap namespace created in the cluster + */ + private _defaultNamespace?: cloudmap.INamespace; + constructor(scope: cdk.Construct, id: string, private readonly props: ClusterImportProps) { super(scope, id); this.clusterName = props.clusterName; this.vpc = ec2.VpcNetwork.import(this, "vpc", props.vpc); this.hasEc2Capacity = props.hasEc2Capacity !== false; + this._defaultNamespace = props.defaultNamespace && cloudmap.Namespace.import(this, 'Namespace', props.defaultNamespace); this.clusterArn = props.clusterArn !== undefined ? props.clusterArn : this.node.stack.formatArn({ service: 'ecs', @@ -340,6 +400,10 @@ class ImportedCluster extends cdk.Construct implements ICluster { } } + public get defaultNamespace(): cloudmap.INamespace | undefined { + return this._defaultNamespace; + } + public export() { return this.props; } @@ -379,3 +443,39 @@ export interface AddCapacityOptions extends AddAutoScalingGroupCapacityOptions, */ readonly instanceType: ec2.InstanceType; } + +export interface NamespaceOptions { + /** + * The domain name for the namespace, such as foo.com + */ + readonly name: string; + + /** + * The type of CloudMap Namespace to create in your cluster + * + * @default PrivateDns + */ + readonly type?: 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 + */ + readonly vpc?: ec2.IVpcNetwork; +} + +/** + * The type of CloudMap namespace to create + */ +export enum NamespaceType { + /** + * Create a private DNS namespace + */ + PrivateDns = 'PrivateDns', + + /** + * Create a public DNS namespace + */ + PublicDns = 'PublicDns', +} 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 8b818ddb09443..324ef039cdc49 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -4,7 +4,6 @@ import elb = require('@aws-cdk/aws-elasticloadbalancing'); import cdk = require('@aws-cdk/cdk'); import { BaseService, BaseServiceProps } 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 +11,6 @@ import { isEc2Compatible } from '../util'; * Properties to define an ECS service */ export interface Ec2ServiceProps extends BaseServiceProps { - /** - * Cluster where service will be deployed - */ - readonly cluster: ICluster; - /** * Task Definition used for running tasks in the service */ @@ -70,7 +64,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 +88,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 = []; 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 48c5041f0e0d7..f50ad650baadc 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -2,18 +2,12 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { BaseService, BaseServiceProps } 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 - */ - readonly cluster: ICluster; // should be required? do we assume 'default' exists? - /** * Task Definition used for running tasks in the service */ diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 1b4843b84adb4..beba471b31fdf 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -83,6 +83,7 @@ "@aws-cdk/aws-logs": "^0.27.0", "@aws-cdk/aws-route53": "^0.27.0", "@aws-cdk/aws-secretsmanager": "^0.27.0", + "@aws-cdk/aws-servicediscovery": "^0.27.0", "@aws-cdk/aws-sns": "^0.27.0", "@aws-cdk/cdk": "^0.27.0", "@aws-cdk/cx-api": "^0.27.0" @@ -103,6 +104,7 @@ "@aws-cdk/aws-logs": "^0.27.0", "@aws-cdk/aws-route53": "^0.27.0", "@aws-cdk/aws-secretsmanager": "^0.27.0", + "@aws-cdk/aws-servicediscovery": "^0.27.0", "@aws-cdk/cdk": "^0.27.0" }, "engines": { @@ -115,4 +117,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 82c328b78a8a5..09dc8a742e401 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 @@ -849,7 +849,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 52835ec597619..bef9bc1361830 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 @@ -850,7 +850,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..86313e9efcfb5 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json @@ -0,0 +1,915 @@ +{ + "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": { + "Fn::GetAtt": [ + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionE17A5F5E", + "Arn" + ] + }, + "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..31a4f592d3c97 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.ts @@ -0,0 +1,47 @@ +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.addDefaultCloudMapNamespace({ + 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 +}); + +new ecs.Ec2Service(stack, "FrontendService", { + cluster, + taskDefinition: frontendTD, + serviceDiscoveryOptions: { + 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..4752817a8debf --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json @@ -0,0 +1,879 @@ +{ + "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": { + "Fn::GetAtt": [ + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionE17A5F5E", + "Arn" + ] + }, + "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..bb26b16da810d --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.ts @@ -0,0 +1,45 @@ +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.addDefaultCloudMapNamespace({ + 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 +}); + +new ecs.Ec2Service(stack, "FrontendService", { + cluster, + taskDefinition: frontendTD, + serviceDiscoveryOptions: { + 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..2195bdc80f017 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, NamespaceType, NetworkMode } from '../../lib'; export = { "When creating an ECS Service": { @@ -522,6 +523,408 @@ 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 }); + + // THEN + test.throws(() => { + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + serviceDiscoveryOptions: { + 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 }); + + cluster.addDefaultCloudMapNamespace({ name: 'foo.com' }); + + // THEN + test.throws(() => { + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + serviceDiscoveryOptions: { + 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 }); + + // WHEN + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: NamespaceType.PrivateDns + }); + + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + serviceDiscoveryOptions: { + 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 }); + + // WHEN + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: NamespaceType.PrivateDns + }); + + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + serviceDiscoveryOptions: { + 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 }); + + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + }); + + // THEN + test.throws(() => { + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + serviceDiscoveryOptions: { + 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 }); + + // WHEN + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: NamespaceType.PrivateDns + }); + + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + serviceDiscoveryOptions: { + 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 }); + + // WHEN + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: NamespaceType.PrivateDns + }); + + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + serviceDiscoveryOptions: { + 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..ec4bd3d47bb6d 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,10 +1,11 @@ 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'); -import { ContainerImage } from '../../lib'; +import { ContainerImage, NamespaceType } from '../../lib'; export = { "When creating a Fargate Service": { @@ -237,6 +238,151 @@ 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 }); + + // THEN + test.throws(() => { + new ecs.FargateService(stack, 'Service', { + cluster, + taskDefinition, + serviceDiscoveryOptions: { + 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 }); + + // WHEN + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: NamespaceType.PrivateDns + }); + + new ecs.FargateService(stack, 'Service', { + cluster, + taskDefinition, + serviceDiscoveryOptions: { + 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 }); + + // WHEN + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: NamespaceType.PrivateDns + }); + + new ecs.FargateService(stack, 'Service', { + cluster, + taskDefinition, + serviceDiscoveryOptions: { + 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..3fd7b9aea9563 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,102 @@ export = { test.done(); }, + + "allows adding default 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.addDefaultCloudMapNamespace({ + name: "foo.com" + }); + + // THEN + expect(stack).to(haveResource("AWS::ServiceDiscovery::PrivateDnsNamespace", { + Name: 'foo.com', + Vpc: { + Ref: 'MyVpcF9F0CA6F' + } + })); + + test.done(); + }, + + "allows adding public 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.addDefaultCloudMapNamespace({ + name: "foo.com", + type: ecs.NamespaceType.PublicDns + }); + + // THEN + expect(stack).to(haveResource("AWS::ServiceDiscovery::PublicDnsNamespace", { + Name: 'foo.com', + })); + + test.done(); + }, + + "throws if default service discovery namespace added more than once"(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.addDefaultCloudMapNamespace({ + name: "foo.com" + }); + + // THEN + test.throws(() => { + cluster.addDefaultCloudMapNamespace({ + name: "foo.com" + }); + }, /Can only add default namespace once./); + + test.done(); + }, + + 'export/import of a cluster with a namespace'(test: Test) { + // GIVEN + const stack1 = new cdk.Stack(); + const vpc1 = new ec2.VpcNetwork(stack1, 'Vpc'); + const cluster1 = new ecs.Cluster(stack1, 'Cluster', { vpc: vpc1 }); + cluster1.addDefaultCloudMapNamespace({ + name: 'hello.com', + }); + + const stack2 = new cdk.Stack(); + + // WHEN + const cluster2 = ecs.Cluster.import(stack2, 'Cluster', cluster1.export()); + + // THEN + test.equal(cluster2.defaultNamespace!.type, cloudmap.NamespaceType.DnsPrivate); + test.deepEqual(stack2.node.resolve(cluster2.defaultNamespace!.namespaceId), { + 'Fn::ImportValue': 'Stack:ClusterDefaultServiceDiscoveryNamespaceNamespaceId516C01B9', + }); + + test.done(); + } }; diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/namespace.ts b/packages/@aws-cdk/aws-servicediscovery/lib/namespace.ts index d586a33fed291..f25526e6427ac 100644 --- a/packages/@aws-cdk/aws-servicediscovery/lib/namespace.ts +++ b/packages/@aws-cdk/aws-servicediscovery/lib/namespace.ts @@ -20,6 +20,11 @@ export interface INamespace extends cdk.IConstruct { * Type of Namespace */ readonly type: NamespaceType; + + /** + * Export the namespace properties + */ + export(): NamespaceImportProps; } export interface BaseNamespaceProps { @@ -82,4 +87,48 @@ export abstract class NamespaceBase extends cdk.Construct implements INamespace public abstract readonly namespaceArn: string; public abstract readonly namespaceName: string; public abstract readonly type: NamespaceType; + + public export(): NamespaceImportProps { + return { + namespaceName: new cdk.CfnOutput(this, 'NamespaceName', { value: this.namespaceArn }).makeImportValue().toString(), + namespaceArn: new cdk.CfnOutput(this, 'NamespaceArn', { value: this.namespaceArn }).makeImportValue().toString(), + namespaceId: new cdk.CfnOutput(this, 'NamespaceId', { value: this.namespaceId }).makeImportValue().toString(), + type: this.type, + }; + } +} + +// The class below exists purely so that users can still type Namespace.import(). +// It does not make sense to have HttpNamespace.import({ ..., type: NamespaceType.PublicDns }), +// but at the same time ecs.Cluster wants a type-generic export()/import(). Hence, we put +// it in Namespace. + +/** + * Static Namespace class + */ +export class Namespace { + /** + * Import a namespace + */ + public static import(scope: cdk.Construct, id: string, props: NamespaceImportProps): INamespace { + return new ImportedNamespace(scope, id, props); + } + + private constructor() { + } } + +class ImportedNamespace extends NamespaceBase { + public namespaceId: string; + public namespaceArn: string; + public namespaceName: string; + public type: NamespaceType; + + constructor(scope: cdk.Construct, id: string, props: NamespaceImportProps) { + super(scope, id); + this.namespaceId = props.namespaceId; + this.namespaceArn = props.namespaceArn; + this.namespaceName = props.namespaceName; + this.type = props.type; + } +} \ No newline at end of file diff --git a/packages/decdk/test/__snapshots__/synth.test.js.snap b/packages/decdk/test/__snapshots__/synth.test.js.snap index 833645b3f00d0..ec8cec0b9419f 100644 --- a/packages/decdk/test/__snapshots__/synth.test.js.snap +++ b/packages/decdk/test/__snapshots__/synth.test.js.snap @@ -706,6 +706,7 @@ Object { ], }, }, + "ServiceRegistries": Array [], "TaskDefinition": Object { "Ref": "MyTaskDef01F0D39B", },