Skip to content

Commit

Permalink
fix(ecs-patterns): allow imported load balancers as inputs
Browse files Browse the repository at this point in the history
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 from<LBType>LoadBalancerAttributes 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: #5209
  • Loading branch information
bvtujo authored Feb 12, 2020
1 parent 2ce8fac commit 7f8c90d
Show file tree
Hide file tree
Showing 11 changed files with 452 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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.
*/
Expand All @@ -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
Expand All @@ -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');
Expand All @@ -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 });
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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.
*/
Expand All @@ -268,15 +276,16 @@ 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;

const targetProps = {
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') {
Expand All @@ -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 });
}
}

/**
Expand Down
152 changes: 150 additions & 2 deletions packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -785,7 +785,6 @@ export = {

test.done();
},

'ALBFargate - having *HealthyPercent properties'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
Expand Down Expand Up @@ -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();
},
};
Loading

0 comments on commit 7f8c90d

Please sign in to comment.