From de84c4a651085bb917290facb51cfbc6eae56561 Mon Sep 17 00:00:00 2001 From: Penghao He Date: Fri, 4 Oct 2019 10:26:25 -0700 Subject: [PATCH] feat(ecs): add a new API for registering ECS targets (#4212) * Add new API to register ECS targets * Refactor registerContainerTargets * Add more unit tests and integ tests * Rebase and refactor to flat list for new API input * Refactor to eliminate type union * Minor refactoring and name change * Add more comments and README * Minor change to test name --- packages/@aws-cdk/aws-ecs/README.md | 23 + .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 138 +++++ .../fargate/integ.nlb-awsvpc-nw.expected.json | 584 ++++++++++++++++++ .../test/fargate/integ.nlb-awsvpc-nw.ts | 50 ++ .../test/fargate/test.fargate-service.ts | 315 ++++++++++ 5 files changed, 1110 insertions(+) create mode 100644 packages/@aws-cdk/aws-ecs/test/fargate/integ.nlb-awsvpc-nw.expected.json create mode 100644 packages/@aws-cdk/aws-ecs/test/fargate/integ.nlb-awsvpc-nw.ts diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 2f0dd17d7418a..ffd919e5b3693 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -277,6 +277,29 @@ const target = listener.addTargets('ECS', { }); ``` +Alternatively, you can also create all load balancer targets to be registered in this service, add them to target groups, and attach target groups to listeners accordingly. + +```ts +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); + +const service = new ecs.FargateService(this, 'Service', { /* ... */ }); + +const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { vpc, internetFacing: true }); +const listener = lb.addListener('Listener', { port: 80 }); +service.registerLoadBalancerTargets( + { + containerTarget: { + containerName: 'web', + containerPort: 80, + }, + targetGroupId: 'ECS', + listener: ecs.ListenerConfig.applicationListener(listener, { + protocol: elbv2.ApplicationProtocol.HTTPS + }), + }, +); +``` + ### Include a classic load balancer `Services` can also be directly attached to a classic load balancer as targets: 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 0efe699da805c..d66a806f8a71e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -8,6 +8,7 @@ import cloudmap = require('@aws-cdk/aws-servicediscovery'); import { Construct, Duration, IResolvable, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; import { LoadBalancerTargetOptions, NetworkMode, TaskDefinition } from '../base/task-definition'; import { ICluster } from '../cluster'; +import { Protocol } from '../container-definition'; import { CfnService } from '../ecs.generated'; import { ScalableTaskCount } from './scalable-task-count'; @@ -23,6 +24,37 @@ export interface IService extends IResource { readonly serviceArn: string; } +export interface EcsTarget { + /** + * The name of the container. + */ + readonly containerName: string; + + /** + * The port number of the container. Only applicable when using application/network load balancers. + * + * @default - Container port of the first added port mapping. + */ + readonly containerPort?: number; + + /** + * The protocol used for the port mapping. Only applicable when using application load balancers. + * + * @default Protocol.TCP + */ + readonly protocol?: Protocol; + + /** + * ID for a target group to be created. + */ + readonly newTargetGroupId: string; + + /** + * Listener and properties for adding target group to the listener. + */ + readonly listener: ListenerConfig; +} + /** * Interface for ECS load balancer target. */ @@ -116,6 +148,83 @@ export interface BaseServiceProps extends BaseServiceOptions { readonly launchType: LaunchType; } +/** + * Base class for configuring listener when registering targets. + */ +export abstract class ListenerConfig { + /** + * Create a config for adding target group to ALB listener. + */ + public static applicationListener(listener: elbv2.ApplicationListener, props?: elbv2.AddApplicationTargetsProps): ListenerConfig { + return new ApplicationListenerConfig(listener, props); + } + + /** + * Create a config for adding target group to NLB listener. + */ + public static networkListener(listener: elbv2.NetworkListener, props?: elbv2.AddNetworkTargetsProps): ListenerConfig { + return new NetworkListenerConfig(listener, props); + } + + /** + * Create and attach a target group to listener. + */ + public abstract addTargets(id: string, target: LoadBalancerTargetOptions, service: BaseService): void; +} + +/** + * Class for configuring application load balancer listener when registering targets. + */ +class ApplicationListenerConfig extends ListenerConfig { + constructor(private readonly listener: elbv2.ApplicationListener, private readonly props?: elbv2.AddApplicationTargetsProps) { + super(); + } + + /** + * Create and attach a target group to listener. + */ + public addTargets(id: string, target: LoadBalancerTargetOptions, service: BaseService) { + const props = this.props || {}; + const protocol = props.protocol; + const port = props.port !== undefined ? props.port : (protocol === undefined ? 80 : + (protocol === elbv2.ApplicationProtocol.HTTPS ? 443 : 80)); + this.listener.addTargets(id, { + ... props, + targets: [ + service.loadBalancerTarget({ + ...target + }) + ], + port + }); + } +} + +/** + * Class for configuring network load balancer listener when registering targets. + */ +class NetworkListenerConfig extends ListenerConfig { + constructor(private readonly listener: elbv2.NetworkListener, private readonly props?: elbv2.AddNetworkTargetsProps) { + super(); + } + + /** + * Create and attach a target group to listener. + */ + public addTargets(id: string, target: LoadBalancerTargetOptions, service: BaseService) { + const port = this.props !== undefined ? this.props.port : 80; + this.listener.addTargets(id, { + ... this.props, + targets: [ + service.loadBalancerTarget({ + ...target + }) + ], + port + }); + } +} + /** * The base class for Ec2Service and FargateService services. */ @@ -283,6 +392,35 @@ export abstract class BaseService extends Resource }; } + /** + * Use this function to create all load balancer targets to be registered in this service, add them to + * target groups, and attach target groups to listeners accordingly. + * + * @example + * + * service.registerLoadBalancerTargets( + * { + * containerTarget: { + * containerName: 'web', + * containerPort: 80, + * }, + * targetGroupId: 'ECS', + * listener: ecs.ListenerConfig.applicationListener(listener, { + * protocol: elbv2.ApplicationProtocol.HTTPS + * }), + * }, + * ) + */ + public registerLoadBalancerTargets(...targets: EcsTarget[]) { + for (const target of targets) { + target.listener.addTargets(target.newTargetGroupId, { + containerName: target.containerName, + containerPort: target.containerPort, + protocol: target.protocol + }, this); + } + } + /** * This method is called to attach this service to a Network Load Balancer. * diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.nlb-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.nlb-awsvpc-nw.expected.json new file mode 100644 index 0000000000000..e1dc69831cc06 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.nlb-awsvpc-nw.expected.json @@ -0,0 +1,584 @@ +{ + "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/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/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/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/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/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/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/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/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/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/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/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/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "FargateCluster7CCD5F93": { + "Type": "AWS::ECS::Cluster" + }, + "TaskDefTaskRole1EDB4A67": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDef54694570": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "amazon/amazon-ecs-sample", + "Name": "web", + "PortMappings": [ + { + "ContainerPort": 80, + "Protocol": "tcp" + } + ] + } + ], + "Cpu": "512", + "Family": "awsecsintegTaskDef6FDFB69A", + "Memory": "1024", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + } + } + }, + "ServiceD69D759B": { + "Type": "AWS::ECS::Service", + "Properties": { + "TaskDefinition": { + "Ref": "TaskDef54694570" + }, + "Cluster": { + "Ref": "FargateCluster7CCD5F93" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "DesiredCount": 1, + "EnableECSManagedTags": false, + "HealthCheckGracePeriodSeconds": 60, + "LaunchType": "FARGATE", + "LoadBalancers": [ + { + "ContainerName": "web", + "ContainerPort": 80, + "TargetGroupArn": { + "Ref": "LBPublicListenerECSGroupD6A32205" + } + } + ], + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "ServiceSecurityGroupC96ED6A7", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + } + } + }, + "DependsOn": [ + "LBPublicListenerECSGroupD6A32205", + "LBPublicListener6E1F3D94" + ] + }, + "ServiceSecurityGroupC96ED6A7": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ/Service/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "ServiceTaskCountTarget23E25614": { + "Type": "AWS::ApplicationAutoScaling::ScalableTarget", + "Properties": { + "MaxCapacity": 10, + "MinCapacity": 1, + "ResourceId": { + "Fn::Join": [ + "", + [ + "service/", + { + "Ref": "FargateCluster7CCD5F93" + }, + "/", + { + "Fn::GetAtt": [ + "ServiceD69D759B", + "Name" + ] + } + ] + ] + }, + "RoleARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService" + ] + ] + }, + "ScalableDimension": "ecs:service:DesiredCount", + "ServiceNamespace": "ecs" + } + }, + "ServiceTaskCountTargetReasonableCpu4174EFCE": { + "Type": "AWS::ApplicationAutoScaling::ScalingPolicy", + "Properties": { + "PolicyName": "awsecsintegServiceTaskCountTargetReasonableCpuDB6AEA73", + "PolicyType": "TargetTrackingScaling", + "ScalingTargetId": { + "Ref": "ServiceTaskCountTarget23E25614" + }, + "TargetTrackingScalingPolicyConfiguration": { + "PredefinedMetricSpecification": { + "PredefinedMetricType": "ECSServiceAverageCPUUtilization" + }, + "TargetValue": 10 + } + } + }, + "LB8A12904C": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Properties": { + "Scheme": "internet-facing", + "Subnets": [ + { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + ], + "Type": "network" + }, + "DependsOn": [ + "VpcPublicSubnet1DefaultRoute3DA9E72A", + "VpcPublicSubnet2DefaultRoute97F91067" + ] + }, + "LBPublicListener6E1F3D94": { + "Type": "AWS::ElasticLoadBalancingV2::Listener", + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "LBPublicListenerECSGroupD6A32205" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Ref": "LB8A12904C" + }, + "Port": 80, + "Protocol": "TCP" + } + }, + "LBPublicListenerECSGroupD6A32205": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "Port": 80, + "Protocol": "TCP", + "TargetType": "ip", + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + } + }, + "Outputs": { + "LoadBalancerDNS": { + "Value": { + "Fn::GetAtt": [ + "LB8A12904C", + "DNSName" + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.nlb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.nlb-awsvpc-nw.ts new file mode 100644 index 0000000000000..90f54a5c4c8c3 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.nlb-awsvpc-nw.ts @@ -0,0 +1,50 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/core'); +import ecs = require('../../lib'); + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-ecs-integ'); + +const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 2 }); + +const cluster = new ecs.Cluster(stack, 'FargateCluster', { vpc }); + +const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { + memoryLimitMiB: 1024, + cpu: 512 +}); + +const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), +}); + +container.addPortMappings({ + containerPort: 80, + protocol: ecs.Protocol.TCP +}); + +const service = new ecs.FargateService(stack, "Service", { + cluster, + taskDefinition, +}); + +const scaling = service.autoScaleTaskCount({ maxCapacity: 10 }); +// Quite low to try and force it to scale +scaling.scaleOnCpuUtilization('ReasonableCpu', { targetUtilizationPercent: 10 }); + +const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc, internetFacing: true }); +const listener = lb.addListener('PublicListener', { port: 80}); + +service.registerLoadBalancerTargets( + { + containerName: 'web', + containerPort: 80, + listener: ecs.ListenerConfig.networkListener(listener), + newTargetGroupId: 'ECS', + } +); + +new cdk.CfnOutput(stack, 'LoadBalancerDNS', { value: lb.loadBalancerDnsName, }); + +app.synth(); 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 2a82bf0a2ae3c..96d72e1f568d0 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 @@ -666,6 +666,321 @@ export = { test.done(); } + }, + + 'allows load balancing to any container and port of service': { + 'with application load balancers': { + 'with default target group port and protocol'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + }); + container.addPortMappings({ containerPort: 8000 }); + + const service = new ecs.FargateService(stack, 'Service', { + cluster, + taskDefinition + }); + + // WHEN + const lb = new elbv2.ApplicationLoadBalancer(stack, "lb", { vpc }); + const listener = lb.addListener("listener", { port: 80 }); + + service.registerLoadBalancerTargets( + { + containerName: 'MainContainer', + containerPort: 8000, + listener: ecs.ListenerConfig.applicationListener(listener), + newTargetGroupId: 'target1', + } + ); + + // THEN + expect(stack).to(haveResource('AWS::ECS::Service', { + LoadBalancers: [ + { + ContainerName: "MainContainer", + ContainerPort: 8000, + TargetGroupArn: { + Ref: "lblistenertarget1Group1A1A5C9E" + } + } + ], + })); + + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Port: 80, + Protocol: "HTTP", + })); + + test.done(); + }, + + 'with default target group port and HTTP protocol'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + }); + container.addPortMappings({ containerPort: 8000 }); + + const service = new ecs.FargateService(stack, 'Service', { + cluster, + taskDefinition + }); + + // WHEN + const lb = new elbv2.ApplicationLoadBalancer(stack, "lb", { vpc }); + const listener = lb.addListener("listener", { port: 80 }); + + service.registerLoadBalancerTargets( + { + containerName: 'MainContainer', + containerPort: 8000, + listener: ecs.ListenerConfig.applicationListener(listener, { + protocol: elbv2.ApplicationProtocol.HTTP + }), + newTargetGroupId: 'target1', + } + ); + + // THEN + expect(stack).to(haveResource('AWS::ECS::Service', { + LoadBalancers: [ + { + ContainerName: "MainContainer", + ContainerPort: 8000, + TargetGroupArn: { + Ref: "lblistenertarget1Group1A1A5C9E" + } + } + ], + })); + + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Port: 80, + Protocol: "HTTP", + })); + + test.done(); + }, + + 'with default target group port and HTTPS protocol'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + }); + container.addPortMappings({ containerPort: 8000 }); + + const service = new ecs.FargateService(stack, 'Service', { + cluster, + taskDefinition + }); + + // WHEN + const lb = new elbv2.ApplicationLoadBalancer(stack, "lb", { vpc }); + const listener = lb.addListener("listener", { port: 80 }); + + service.registerLoadBalancerTargets( + { + containerName: 'MainContainer', + containerPort: 8000, + listener: ecs.ListenerConfig.applicationListener(listener, { + protocol: elbv2.ApplicationProtocol.HTTPS + }), + newTargetGroupId: 'target1', + } + ); + + // THEN + expect(stack).to(haveResource('AWS::ECS::Service', { + LoadBalancers: [ + { + ContainerName: "MainContainer", + ContainerPort: 8000, + TargetGroupArn: { + Ref: "lblistenertarget1Group1A1A5C9E" + } + } + ], + })); + + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Port: 443, + Protocol: "HTTPS", + })); + + test.done(); + }, + + 'with any target group port and protocol'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + }); + container.addPortMappings({ containerPort: 8000 }); + + const service = new ecs.FargateService(stack, 'Service', { + cluster, + taskDefinition + }); + + // WHEN + const lb = new elbv2.ApplicationLoadBalancer(stack, "lb", { vpc }); + const listener = lb.addListener("listener", { port: 80 }); + + service.registerLoadBalancerTargets( + { + containerName: 'MainContainer', + containerPort: 8000, + listener: ecs.ListenerConfig.applicationListener(listener, { + port: 83, + protocol: elbv2.ApplicationProtocol.HTTP + }), + newTargetGroupId: 'target1' + } + ); + + // THEN + expect(stack).to(haveResource('AWS::ECS::Service', { + LoadBalancers: [ + { + ContainerName: "MainContainer", + ContainerPort: 8000, + TargetGroupArn: { + Ref: "lblistenertarget1Group1A1A5C9E" + } + } + ], + })); + + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Port: 83, + Protocol: "HTTP", + })); + + test.done(); + }, + }, + + 'with network load balancers': { + 'with default target group port'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + }); + container.addPortMappings({ containerPort: 8000 }); + + const service = new ecs.FargateService(stack, 'Service', { + cluster, + taskDefinition + }); + + // WHEN + const lb = new elbv2.NetworkLoadBalancer(stack, "lb", { vpc }); + const listener = lb.addListener("listener", { port: 80 }); + + service.registerLoadBalancerTargets( + { + containerName: 'MainContainer', + containerPort: 8000, + listener: ecs.ListenerConfig.networkListener(listener), + newTargetGroupId: 'target1', + } + ); + + // THEN + expect(stack).to(haveResource('AWS::ECS::Service', { + LoadBalancers: [ + { + ContainerName: "MainContainer", + ContainerPort: 8000, + TargetGroupArn: { + Ref: "lblistenertarget1Group1A1A5C9E" + } + } + ], + })); + + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Port: 80, + Protocol: "TCP", + })); + + test.done(); + }, + + 'with any target group port'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + }); + container.addPortMappings({ containerPort: 8000 }); + + const service = new ecs.FargateService(stack, 'Service', { + cluster, + taskDefinition + }); + + // WHEN + const lb = new elbv2.NetworkLoadBalancer(stack, "lb", { vpc }); + const listener = lb.addListener("listener", { port: 80 }); + + service.registerLoadBalancerTargets( + { + containerName: 'MainContainer', + containerPort: 8000, + listener: ecs.ListenerConfig.networkListener(listener, { + port: 81 + }), + newTargetGroupId: 'target1' + } + ); + + // THEN + expect(stack).to(haveResource('AWS::ECS::Service', { + LoadBalancers: [ + { + ContainerName: "MainContainer", + ContainerPort: 8000, + TargetGroupArn: { + Ref: "lblistenertarget1Group1A1A5C9E" + } + } + ], + })); + + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + Port: 81, + Protocol: "TCP", + })); + + test.done(); + }, + } } },