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();