From 7f8c90decd58d2016611e6f94fc506156e047b59 Mon Sep 17 00:00:00 2001 From: Austin Ely Date: Wed, 12 Feb 2020 10:56:31 -0800 Subject: [PATCH] fix(ecs-patterns): allow imported load balancers as inputs This PR adds functionality in aws-ecs-patterns to allow imported Network and Application load balancers to be used when initializing the LoadBalancedEC2Service and LoadBalancedFargateService constructs. By necessity, this means: 1) editing the Network and Application load balancer constructs to add an optional IVpc property to be used when calling the fromLoadBalancerAttributes static methods, and 2) changing the error which is thrown when calling addTargets on imported load balancers. Unit tests are added in aws-ecs-patterns to test the new import functionality and in aws-elasticloadbalancingv2 to ensure that addTargets behaves properly when a Vpc is or is not specified for imported load balancers. Resolves: https://github.com/aws/aws-cdk/issues/5209 --- .../application-load-balanced-service-base.ts | 34 +++- .../network-load-balanced-service-base.ts | 29 +++- .../aws-ecs-patterns/test/ec2/test.l3s.ts | 152 ++++++++++++++++- .../test.load-balanced-fargate-service.ts | 157 +++++++++++++++++- .../lib/alb/application-listener.ts | 2 +- .../lib/alb/application-load-balancer.ts | 17 +- .../lib/nlb/network-listener.ts | 2 +- .../lib/nlb/network-load-balancer.ts | 10 +- .../lib/shared/base-load-balancer.ts | 7 +- .../test/alb/test.load-balancer.ts | 43 +++++ .../test/nlb/test.load-balancer.ts | 30 ++++ 11 files changed, 452 insertions(+), 31 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts index 4d305ad2940c2..68365b43be51e 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts @@ -1,7 +1,8 @@ import { DnsValidatedCertificate, ICertificate } from '@aws-cdk/aws-certificatemanager'; import { IVpc } from '@aws-cdk/aws-ec2'; import { AwsLogDriver, BaseService, CloudMapOptions, Cluster, ContainerImage, ICluster, LogDriver, PropagatedTagSource, Secret } from '@aws-cdk/aws-ecs'; -import { ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol, ApplicationTargetGroup, ListenerCertificate } from '@aws-cdk/aws-elasticloadbalancingv2'; +import { ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol, ApplicationTargetGroup, + IApplicationLoadBalancer, ListenerCertificate} from '@aws-cdk/aws-elasticloadbalancingv2'; import { IRole } from '@aws-cdk/aws-iam'; import { ARecord, IHostedZone, RecordTarget } from '@aws-cdk/aws-route53'; import { LoadBalancerTarget } from '@aws-cdk/aws-route53-targets'; @@ -119,12 +120,14 @@ export interface ApplicationLoadBalancedServiceBaseProps { /** * The application load balancer that will serve traffic to the service. + * The VPC attribute of a load balancer must be specified for it to be used + * to create a new service with this pattern. * * [disable-awslint:ref-via-interface] * * @default - a new load balancer will be created. */ - readonly loadBalancer?: ApplicationLoadBalancer; + readonly loadBalancer?: IApplicationLoadBalancer; /** * Listener port of the application load balancer that will serve traffic to the service. @@ -252,7 +255,12 @@ export abstract class ApplicationLoadBalancedServiceBase extends cdk.Construct { /** * The Application Load Balancer for the service. */ - public readonly loadBalancer: ApplicationLoadBalancer; + public get loadBalancer(): ApplicationLoadBalancer { + if (!this._applicationLoadBalancer) { + throw new Error('.loadBalancer can only be accessed if the class was constructed with an owned, not imported, load balancer'); + } + return this._applicationLoadBalancer; + } /** * The listener for the service. @@ -274,6 +282,8 @@ export abstract class ApplicationLoadBalancedServiceBase extends cdk.Construct { */ public readonly cluster: ICluster; + private readonly _applicationLoadBalancer?: ApplicationLoadBalancer; + /** * Constructs a new instance of the ApplicationLoadBalancedServiceBase class. */ @@ -297,18 +307,20 @@ export abstract class ApplicationLoadBalancedServiceBase extends cdk.Construct { internetFacing }; - this.loadBalancer = props.loadBalancer !== undefined ? props.loadBalancer : new ApplicationLoadBalancer(this, 'LB', lbProps); + const loadBalancer = props.loadBalancer !== undefined ? props.loadBalancer + : new ApplicationLoadBalancer(this, 'LB', lbProps); if (props.certificate !== undefined && props.protocol !== undefined && props.protocol !== ApplicationProtocol.HTTPS) { throw new Error('The HTTPS protocol must be used when a certificate is given'); } - const protocol = props.protocol !== undefined ? props.protocol : (props.certificate ? ApplicationProtocol.HTTPS : ApplicationProtocol.HTTP); + const protocol = props.protocol !== undefined ? props.protocol : + (props.certificate ? ApplicationProtocol.HTTPS : ApplicationProtocol.HTTP); const targetProps = { port: 80 }; - this.listener = this.loadBalancer.addListener('PublicListener', { + this.listener = loadBalancer.addListener('PublicListener', { protocol, port: props.listenerPort, open: true @@ -333,7 +345,7 @@ export abstract class ApplicationLoadBalancedServiceBase extends cdk.Construct { this.listener.addCertificates('Arns', [ListenerCertificate.fromCertificateManager(this.certificate)]); } - let domainName = this.loadBalancer.loadBalancerDnsName; + let domainName = loadBalancer.loadBalancerDnsName; if (typeof props.domainName !== 'undefined') { if (typeof props.domainZone === 'undefined') { throw new Error('A Route53 hosted domain zone name is required to configure the specified domain name'); @@ -342,13 +354,17 @@ export abstract class ApplicationLoadBalancedServiceBase extends cdk.Construct { const record = new ARecord(this, "DNS", { zone: props.domainZone, recordName: props.domainName, - target: RecordTarget.fromAlias(new LoadBalancerTarget(this.loadBalancer)), + target: RecordTarget.fromAlias(new LoadBalancerTarget(loadBalancer)), }); domainName = record.domainName; } - new cdk.CfnOutput(this, 'LoadBalancerDNS', { value: this.loadBalancer.loadBalancerDnsName }); + if (loadBalancer instanceof ApplicationLoadBalancer) { + this._applicationLoadBalancer = loadBalancer; + } + + new cdk.CfnOutput(this, 'LoadBalancerDNS', { value: loadBalancer.loadBalancerDnsName }); new cdk.CfnOutput(this, 'ServiceURL', { value: protocol.toLowerCase() + '://' + domainName }); } diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts index b6dd6492edbef..6e5e7a07edc61 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts @@ -1,6 +1,6 @@ import { IVpc } from '@aws-cdk/aws-ec2'; import { AwsLogDriver, BaseService, CloudMapOptions, Cluster, ContainerImage, ICluster, LogDriver, PropagatedTagSource, Secret } from '@aws-cdk/aws-ecs'; -import { NetworkListener, NetworkLoadBalancer, NetworkTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2'; +import { INetworkLoadBalancer, NetworkListener, NetworkLoadBalancer, NetworkTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2'; import { IRole } from '@aws-cdk/aws-iam'; import { ARecord, IHostedZone, RecordTarget } from '@aws-cdk/aws-route53'; import { LoadBalancerTarget } from '@aws-cdk/aws-route53-targets'; @@ -97,12 +97,14 @@ export interface NetworkLoadBalancedServiceBaseProps { /** * The network load balancer that will serve traffic to the service. + * If the load balancer has been imported, the vpc attribute must be specified + * in the call to fromNetworkLoadBalancerAttributes(). * * [disable-awslint:ref-via-interface] * * @default - a new load balancer will be created. */ - readonly loadBalancer?: NetworkLoadBalancer; + readonly loadBalancer?: INetworkLoadBalancer; /** * Listener port of the network load balancer that will serve traffic to the service. @@ -228,7 +230,12 @@ export abstract class NetworkLoadBalancedServiceBase extends cdk.Construct { /** * The Network Load Balancer for the service. */ - public readonly loadBalancer: NetworkLoadBalancer; + public get loadBalancer(): NetworkLoadBalancer { + if (!this._networkLoadBalancer) { + throw new Error(".loadBalancer can only be accessed if the class was constructed with an owned, not imported, load balancer"); + } + return this._networkLoadBalancer; + } /** * The listener for the service. @@ -245,6 +252,7 @@ export abstract class NetworkLoadBalancedServiceBase extends cdk.Construct { */ public readonly cluster: ICluster; + private readonly _networkLoadBalancer?: NetworkLoadBalancer; /** * Constructs a new instance of the NetworkLoadBalancedServiceBase class. */ @@ -268,7 +276,8 @@ export abstract class NetworkLoadBalancedServiceBase extends cdk.Construct { internetFacing }; - this.loadBalancer = props.loadBalancer !== undefined ? props.loadBalancer : new NetworkLoadBalancer(this, 'LB', lbProps); + const loadBalancer = props.loadBalancer !== undefined ? props.loadBalancer : + new NetworkLoadBalancer(this, 'LB', lbProps); const listenerPort = props.listenerPort !== undefined ? props.listenerPort : 80; @@ -276,7 +285,7 @@ export abstract class NetworkLoadBalancedServiceBase extends cdk.Construct { port: 80 }; - this.listener = this.loadBalancer.addListener('PublicListener', { port: listenerPort }); + this.listener = loadBalancer.addListener('PublicListener', { port: listenerPort }); this.targetGroup = this.listener.addTargets('ECS', targetProps); if (typeof props.domainName !== 'undefined') { @@ -287,11 +296,17 @@ export abstract class NetworkLoadBalancedServiceBase extends cdk.Construct { new ARecord(this, "DNS", { zone: props.domainZone, recordName: props.domainName, - target: RecordTarget.fromAlias(new LoadBalancerTarget(this.loadBalancer)), + target: RecordTarget.fromAlias(new LoadBalancerTarget(loadBalancer)), }); } - new cdk.CfnOutput(this, 'LoadBalancerDNS', { value: this.loadBalancer.loadBalancerDnsName }); + if (loadBalancer instanceof NetworkLoadBalancer) { + this._networkLoadBalancer = loadBalancer; + } + + if (props.loadBalancer === undefined) { + new cdk.CfnOutput(this, 'LoadBalancerDNS', { value: this.loadBalancer.loadBalancerDnsName }); + } } /** diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts index 825c2cd6129f9..53b5600fc7234 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts @@ -2,7 +2,7 @@ import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import { Certificate } from '@aws-cdk/aws-certificatemanager'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; -import { ApplicationProtocol } from '@aws-cdk/aws-elasticloadbalancingv2'; +import { ApplicationLoadBalancer, ApplicationProtocol, NetworkLoadBalancer } from '@aws-cdk/aws-elasticloadbalancingv2'; import { PublicHostedZone } from '@aws-cdk/aws-route53'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; @@ -785,7 +785,6 @@ export = { test.done(); }, - 'ALBFargate - having *HealthyPercent properties'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -901,4 +900,153 @@ export = { test.done(); }, + + 'NetworkLoadbalancedEC2Service accepts previously created load balancer'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc'); + const cluster = new ecs.Cluster(stack, "Cluster", {vpc, clusterName: "MyCluster" }); + cluster.addCapacity("Capacity", {instanceType: new ec2.InstanceType('t2.micro')}); + const nlb = new NetworkLoadBalancer(stack, 'NLB', { vpc }); + const taskDef = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + const container = taskDef.addContainer('Container', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 1024, + }); + container.addPortMappings({ containerPort: 80 }); + + // WHEN + new ecsPatterns.NetworkLoadBalancedEc2Service(stack, 'Service', { + cluster, + loadBalancer: nlb, + taskDefinition: taskDef, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + LaunchType: 'EC2' + })); + expect(stack).to(haveResourceLike('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Type: 'network', + })); + test.done(); + }, + + 'NetworkLoadBalancedEC2Service accepts imported load balancer'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const nlbArn = "arn:aws:elasticloadbalancing::000000000000::dummyloadbalancer"; + const vpc = new ec2.Vpc(stack, "Vpc"); + const cluster = new ecs.Cluster(stack, "Cluster", {vpc, clusterName: "MyCluster" }); + cluster.addCapacity("Capacity", {instanceType: new ec2.InstanceType('t2.micro')}); + const nlb = NetworkLoadBalancer.fromNetworkLoadBalancerAttributes(stack, "NLB", { + loadBalancerArn: nlbArn, + vpc, + }); + const taskDef = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + const container = taskDef.addContainer('Container', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 1024, + }); + container.addPortMappings({ + containerPort: 80, + }); + + // WHEN + new ecsPatterns.NetworkLoadBalancedEc2Service(stack, "Service", { + cluster, + loadBalancer: nlb, + desiredCount: 1, + taskDefinition: taskDef + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + LaunchType: 'EC2', + LoadBalancers: [{ContainerName: 'Container', ContainerPort: 80}] + })); + expect(stack).to(haveResourceLike('AWS::ElasticLoadBalancingV2::TargetGroup')); + expect(stack).to(haveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + LoadBalancerArn: nlb.loadBalancerArn, + Port: 80, + })); + test.done(); + }, + + 'ApplicationLoadBalancedEC2Service accepts previously created load balancer'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc'); + const cluster = new ecs.Cluster(stack, "Cluster", {vpc, clusterName: "MyCluster" }); + cluster.addCapacity("Capacity", {instanceType: new ec2.InstanceType('t2.micro')}); + const sg = new ec2.SecurityGroup(stack, 'SG', { vpc }); + const alb = new ApplicationLoadBalancer(stack, 'NLB', { + vpc, + securityGroup: sg, + }); + const taskDef = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + const container = taskDef.addContainer('Container', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 1024, + }); + container.addPortMappings({ containerPort: 80 }); + + // WHEN + new ecsPatterns.ApplicationLoadBalancedEc2Service(stack, 'Service', { + cluster, + loadBalancer: alb, + taskDefinition: taskDef, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + LaunchType: 'EC2' + })); + expect(stack).to(haveResourceLike('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Type: 'application' + })); + test.done(); + }, + + 'ApplicationLoadBalancedEC2Service accepts imported load balancer'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const albArn = "arn:aws:elasticloadbalancing::000000000000::dummyloadbalancer"; + const vpc = new ec2.Vpc(stack, "Vpc"); + const cluster = new ecs.Cluster(stack, "Cluster", {vpc, clusterName: "MyCluster" }); + cluster.addCapacity("Capacity", {instanceType: new ec2.InstanceType('t2.micro')}); + const sg = new ec2.SecurityGroup(stack, "SG", { vpc, }); + const alb = ApplicationLoadBalancer.fromApplicationLoadBalancerAttributes(stack, 'ALB', { + loadBalancerArn: albArn, + vpc, + securityGroupId: sg.securityGroupId, + loadBalancerDnsName: "MyName" + }); + const taskDef = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + const container = taskDef.addContainer('Container', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 1024, + }); + container.addPortMappings({ + containerPort: 80, + }); + // WHEN + new ecsPatterns.ApplicationLoadBalancedEc2Service(stack, "Service", { + cluster, + loadBalancer: alb, + taskDefinition: taskDef, + }); + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + LaunchType: 'EC2', + LoadBalancers: [{ContainerName: 'Container', ContainerPort: 80}] + })); + expect(stack).to(haveResourceLike('AWS::ElasticLoadBalancingV2::TargetGroup')); + expect(stack).to(haveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + LoadBalancerArn: alb.loadBalancerArn, + Port: 80, + })); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts index cd15dbcd773e8..72353f74ec427 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts @@ -1,7 +1,7 @@ import { expect, haveResource, haveResourceLike, SynthUtils } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; -import { ApplicationProtocol } from '@aws-cdk/aws-elasticloadbalancingv2'; +import { ApplicationLoadBalancer, ApplicationProtocol, NetworkLoadBalancer } from '@aws-cdk/aws-elasticloadbalancingv2'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; @@ -470,4 +470,157 @@ export = { test.done(); }, -}; + 'passing in existing network load balancer to NLB Fargate Service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const nlb = new NetworkLoadBalancer(stack, 'NLB', { vpc }); + + // WHEN + new ecsPatterns.NetworkLoadBalancedFargateService(stack, "Service", { + vpc, + loadBalancer: nlb, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + LaunchType: "FARGATE", + })); + expect(stack).to(haveResourceLike('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Type: 'network' + })); + test.done(); + }, + + 'passing in imported network load balancer and resources to NLB Fargate service'(test: Test) { + // GIVEN + const stack1 = new cdk.Stack(); + const vpc1 = new ec2.Vpc(stack1, 'VPC'); + const cluster1 = new ecs.Cluster(stack1, 'Cluster', { vpc: vpc1 }); + const nlbArn = "arn:aws:elasticloadbalancing::000000000000::dummyloadbalancer"; + const stack2 = new cdk.Stack(stack1, 'Stack2'); + const cluster2 = ecs.Cluster.fromClusterAttributes(stack2, 'ImportedCluster', { + vpc: vpc1, + securityGroups: cluster1.connections.securityGroups, + clusterName: 'cluster-name' + }); + + // WHEN + const nlb2 = NetworkLoadBalancer.fromNetworkLoadBalancerAttributes(stack2, "ImportedNLB", { + loadBalancerArn: nlbArn, + vpc: vpc1, + }); + const taskDef = new ecs.FargateTaskDefinition(stack2, 'TaskDef', { + cpu: 1024, + memoryLimitMiB: 1024, + }); + const container = taskDef.addContainer('myContainer', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 1024 + }); + container.addPortMappings({ + containerPort: 80, + }); + + new ecsPatterns.NetworkLoadBalancedFargateService(stack2, 'FargateNLBService', { + cluster: cluster2, + loadBalancer: nlb2, + desiredCount: 1, + taskDefinition: taskDef, + }); + + // THEN + expect(stack2).to(haveResourceLike('AWS::ECS::Service', { + LaunchType: "FARGATE", + LoadBalancers: [{ContainerName: 'myContainer', ContainerPort: 80}] + })); + expect(stack2).to(haveResourceLike('AWS::ElasticLoadBalancingV2::TargetGroup')); + expect(stack2).to(haveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + LoadBalancerArn: nlb2.loadBalancerArn, + Port: 80, + })); + + test.done(); + }, + + 'passing in previously created application load balancer to ALB Fargate Service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, "Vpc"); + const cluster = new ecs.Cluster(stack, "Cluster", { vpc, clusterName: "MyCluster" }); + const sg = new ec2.SecurityGroup(stack, "SecurityGroup", { vpc }); + cluster.connections.addSecurityGroup(sg); + const alb = new ApplicationLoadBalancer(stack, "ALB", { vpc, securityGroup: sg }); + + // WHEN + new ecsPatterns.ApplicationLoadBalancedFargateService(stack, "Service", { + cluster, + loadBalancer: alb, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + } + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + LaunchType: 'FARGATE', + })); + expect(stack).to(haveResourceLike('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Type: 'application' + })); + test.done(); + }, + + 'passing in imported application load balancer and resources to ALB Fargate Service'(test: Test) { + // GIVEN + const stack1 = new cdk.Stack(); + const albArn = "arn:aws:elasticloadbalancing::000000000000::dummyloadbalancer"; + const vpc = new ec2.Vpc(stack1, "Vpc"); + const cluster = new ecs.Cluster(stack1, "Cluster", { vpc, clusterName: "MyClusterName", }); + const sg = new ec2.SecurityGroup(stack1, "SecurityGroup", { vpc }); + cluster.connections.addSecurityGroup(sg); + const alb = ApplicationLoadBalancer.fromApplicationLoadBalancerAttributes(stack1, "ALB", { + loadBalancerArn: albArn, + vpc, + securityGroupId: sg.securityGroupId, + loadBalancerDnsName: "MyDnsName" + }); + + // WHEN + const taskDef = new ecs.FargateTaskDefinition(stack1, 'TaskDef', { + cpu: 1024, + memoryLimitMiB: 1024, + }); + const container = taskDef.addContainer('Container', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 1024, + }); + container.addPortMappings({ + containerPort: 80, + }); + + new ecsPatterns.ApplicationLoadBalancedFargateService(stack1, 'FargateALBService', { + cluster, + loadBalancer: alb, + desiredCount: 1, + taskDefinition: taskDef, + }); + + // THEN + expect(stack1).to(haveResourceLike('AWS::ECS::Service', { + LaunchType: "FARGATE", + LoadBalancers: [{ContainerName: 'Container', ContainerPort: 80}] + })); + expect(stack1).to(haveResourceLike('AWS::ElasticLoadBalancingV2::TargetGroup')); + expect(stack1).to(haveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + LoadBalancerArn: alb.loadBalancerArn, + Port: 80, + })); + + test.done(); + }, + +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index 52ad15466034a..f72d35d3ae28d 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -234,7 +234,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis public addTargets(id: string, props: AddApplicationTargetsProps): ApplicationTargetGroup { if (!this.loadBalancer.vpc) { // tslint:disable-next-line:max-line-length - throw new Error('Can only call addTargets() when using a constructed Load Balancer; construct a new TargetGroup and use addTargetGroup'); + throw new Error('Can only call addTargets() when using a constructed Load Balancer or an imported Load Balancer with specified vpc; construct a new TargetGroup and use addTargetGroup'); } const group = new ApplicationTargetGroup(this, id + 'Group', { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts index 0886486a89458..f62ad0ba89615 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts @@ -452,7 +452,9 @@ export interface IApplicationLoadBalancer extends ILoadBalancerV2, ec2.IConnecta readonly loadBalancerArn: string; /** - * The VPC this load balancer has been created in (if available) + * The VPC this load balancer has been created in (if available). + * If this interface is the result of an import call to fromApplicationLoadBalancerAttributes, + * the vpc attribute will be undefined unless specified in the optional properties of that method. */ readonly vpc?: ec2.IVpc; @@ -498,6 +500,15 @@ export interface ApplicationLoadBalancerAttributes { * @default true */ readonly securityGroupAllowsAllOutbound?: boolean; + + /** + * The VPC this load balancer has been created in, if available + * + * @default - If the Load Balancer was imported and a VPC was not specified, + * the VPC is not available. + */ + readonly vpc?: ec2.IVpc; + } /** @@ -517,13 +528,13 @@ class ImportedApplicationLoadBalancer extends Resource implements IApplicationLo /** * VPC of the load balancer * - * Always undefined. + * Undefined if optional vpc is not specified. */ public readonly vpc?: ec2.IVpc; constructor(scope: Construct, id: string, private readonly props: ApplicationLoadBalancerAttributes) { super(scope, id); - + this.vpc = props.vpc; this.loadBalancerArn = props.loadBalancerArn; this.connections = new ec2.Connections({ securityGroups: [ec2.SecurityGroup.fromSecurityGroupId(this, 'SecurityGroup', props.securityGroupId, { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts index 2aa71dd644749..8f4168ef7ccc5 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts @@ -137,7 +137,7 @@ export class NetworkListener extends BaseListener implements INetworkListener { public addTargets(id: string, props: AddNetworkTargetsProps): NetworkTargetGroup { if (!this.loadBalancer.vpc) { // tslint:disable-next-line:max-line-length - throw new Error('Can only call addTargets() when using a constructed Load Balancer; construct a new TargetGroup and use addTargetGroup'); + throw new Error('Can only call addTargets() when using a constructed Load Balancer or imported Load Balancer with specified VPC; construct a new TargetGroup and use addTargetGroup'); } const group = new NetworkTargetGroup(this, id + 'Group', { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts index 64f8782ee8d98..15f626989b36a 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts @@ -38,6 +38,14 @@ export interface NetworkLoadBalancerAttributes { * @default - When not provided, LB cannot be used as Route53 Alias target. */ readonly loadBalancerDnsName?: string; + + /** + * The VPC to associate with the load balancer. + * + * @default - When not provided, listeners cannot be created on imported load + * balancers. + */ + readonly vpc?: ec2.IVpc; } /** @@ -49,7 +57,7 @@ export class NetworkLoadBalancer extends BaseLoadBalancer implements INetworkLoa public static fromNetworkLoadBalancerAttributes(scope: Construct, id: string, attrs: NetworkLoadBalancerAttributes): INetworkLoadBalancer { class Import extends Resource implements INetworkLoadBalancer { public readonly loadBalancerArn = attrs.loadBalancerArn; - public readonly vpc?: ec2.IVpc = undefined; + public readonly vpc?: ec2.IVpc = attrs.vpc; public addListener(lid: string, props: BaseNetworkListenerProps): NetworkListener { return new NetworkListener(this, lid, { loadBalancer: this, diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts index 75082a6581f3f..c9ebcff76e616 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts @@ -113,12 +113,9 @@ export abstract class BaseLoadBalancer extends Resource { public readonly loadBalancerSecurityGroups: string[]; /** - * The VPC this load balancer has been created in, if available - * - * If the Load Balancer was imported, the VPC is not available. + * The VPC this load balancer has been created in. */ - public readonly vpc?: ec2.IVpc; - + public readonly vpc: ec2.IVpc; /** * Attributes set on this load balancer */ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts index 0d698c346deaa..fb139a0d3fa55 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts @@ -268,4 +268,47 @@ export = { })); test.done(); }, + + 'imported load balancer with no vpc throws error when calling addTargets'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc'); + const albArn = 'myArn'; + const sg = new ec2.SecurityGroup(stack, "sg", { + vpc, + securityGroupName: 'mySg', + }); + const alb = elbv2.ApplicationLoadBalancer.fromApplicationLoadBalancerAttributes(stack, 'ALB', { + loadBalancerArn: albArn, + securityGroupId: sg.securityGroupId, + }); + + // WHEN + const listener = alb.addListener('Listener', { port: 80 }); + test.throws(() => listener.addTargets('Targets', {port: 8080})); + + test.done(); + }, + + 'imported load balancer with vpc does not throw error when calling addTargets'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc'); + const albArn = 'MyArn'; + const sg = new ec2.SecurityGroup(stack, 'sg', { + vpc, + securityGroupName: 'mySg', + }); + const alb = elbv2.ApplicationLoadBalancer.fromApplicationLoadBalancerAttributes(stack, 'ALB', { + loadBalancerArn: albArn, + securityGroupId: sg.securityGroupId, + vpc, + }); + + // WHEN + const listener = alb.addListener('Listener', { port: 80 }); + test.doesNotThrow(() => listener.addTargets('Targets', {port: 8080})); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts index 79d3808157f4b..6b53784f4f074 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts @@ -195,6 +195,36 @@ export = { test.done(); }, + 'imported network load balancer with no vpc specified throws error when calling addTargets'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const nlbArn = "arn:aws:elasticloadbalancing::000000000000::dummyloadbalancer"; + const nlb = elbv2.NetworkLoadBalancer.fromNetworkLoadBalancerAttributes(stack, 'NLB', { + loadBalancerArn: nlbArn, + }); + // WHEN + const listener = nlb.addListener('Listener', {port: 80}); + test.throws(() => listener.addTargets('targetgroup', {port: 8080})); + + test.done(); + }, + + 'imported network load balancer with vpc does not throw error when calling addTargets'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc'); + const nlbArn = "arn:aws:elasticloadbalancing::000000000000::dummyloadbalancer"; + const nlb = elbv2.NetworkLoadBalancer.fromNetworkLoadBalancerAttributes(stack, 'NLB', { + loadBalancerArn: nlbArn, + vpc, + }); + // WHEN + const listener = nlb.addListener('Listener', {port: 80}); + test.doesNotThrow(() => listener.addTargets('targetgroup', {port: 8080})); + + test.done(); + }, + 'Trivial construction: internal with Isolated subnets only'(test: Test) { // GIVEN const stack = new cdk.Stack();