Skip to content

Commit e8142e9

Browse files
authored
fix(elbv2): unable to add multiple certificates to NLB (#19289)
This PR does a couple of things to update the NetworkListener to be on par with ApplicationListener. 1. Add a NetworkListenerCertificate construct that allows you to associate multiple certificates with a listener. 2. Add a `addCertificates` method to `NetworkListener` similar to the same method on the `ApplicationListener`. This is needed because even though the `certificates` property on a `Listener`is an array, it expects only one certificate. To add more than one you have to create an `AWS::ElasticLoadBalancingV2::ListenerCertificate`. This functionality was added to `ApplicationListner` via #13490. fixes #8918, #15328 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 88a7839 commit e8142e9

File tree

3 files changed

+139
-2
lines changed

3 files changed

+139
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Construct } from 'constructs';
2+
import { CfnListenerCertificate } from '../elasticloadbalancingv2.generated';
3+
import { IListenerCertificate } from '../shared/listener-certificate';
4+
import { INetworkListener } from './network-listener';
5+
6+
// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
7+
// eslint-disable-next-line no-duplicate-imports, import/order
8+
import { Construct as CoreConstruct } from '@aws-cdk/core';
9+
10+
/**
11+
* Properties for adding a set of certificates to a listener
12+
*/
13+
export interface NetworkListenerCertificateProps {
14+
/**
15+
* The listener to attach the rule to
16+
*/
17+
readonly listener: INetworkListener;
18+
19+
/**
20+
* Certificates to attach
21+
*
22+
* Duplicates are not allowed.
23+
*/
24+
readonly certificates: IListenerCertificate[];
25+
}
26+
27+
/**
28+
* Add certificates to a listener
29+
*/
30+
export class NetworkListenerCertificate extends CoreConstruct {
31+
constructor(scope: Construct, id: string, props: NetworkListenerCertificateProps) {
32+
super(scope, id);
33+
34+
const certificates = [
35+
...(props.certificates || []).map(c => ({ certificateArn: c.certificateArn })),
36+
];
37+
38+
new CfnListenerCertificate(this, 'Resource', {
39+
listenerArn: props.listener.listenerArn,
40+
certificates,
41+
});
42+
}
43+
}

packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts

+35-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
2-
import { Duration, IResource, Resource } from '@aws-cdk/core';
2+
import { Duration, IResource, Resource, Lazy } from '@aws-cdk/core';
33
import { Construct } from 'constructs';
44
import { BaseListener, BaseListenerLookupOptions } from '../shared/base-listener';
55
import { HealthCheck } from '../shared/base-target-group';
66
import { AlpnPolicy, Protocol, SslPolicy } from '../shared/enums';
77
import { IListenerCertificate } from '../shared/listener-certificate';
88
import { validateNetworkProtocol } from '../shared/util';
99
import { NetworkListenerAction } from './network-listener-action';
10+
import { NetworkListenerCertificate } from './network-listener-certificate';
1011
import { INetworkLoadBalancer } from './network-load-balancer';
1112
import { INetworkLoadBalancerTarget, INetworkTargetGroup, NetworkTargetGroup } from './network-target-group';
1213

@@ -160,6 +161,11 @@ export class NetworkListener extends BaseListener implements INetworkListener {
160161
*/
161162
public readonly loadBalancer: INetworkLoadBalancer;
162163

164+
/**
165+
* ARNs of certificates added to this listener
166+
*/
167+
private readonly certificateArns: string[];
168+
163169
/**
164170
* the protocol of the listener
165171
*/
@@ -188,13 +194,17 @@ export class NetworkListener extends BaseListener implements INetworkListener {
188194
protocol: proto,
189195
port: props.port,
190196
sslPolicy: props.sslPolicy,
191-
certificates: props.certificates,
197+
certificates: Lazy.any({ produce: () => this.certificateArns.map(certificateArn => ({ certificateArn })) }, { omitEmptyArray: true }),
192198
alpnPolicy: props.alpnPolicy ? [props.alpnPolicy] : undefined,
193199
});
194200

201+
this.certificateArns = [];
195202
this.loadBalancer = props.loadBalancer;
196203
this.protocol = proto;
197204

205+
if (certs.length > 0) {
206+
this.addCertificates('DefaultCertificates', certs);
207+
}
198208
if (props.defaultAction && props.defaultTargetGroups) {
199209
throw new Error('Specify at most one of \'defaultAction\' and \'defaultTargetGroups\'');
200210
}
@@ -208,6 +218,29 @@ export class NetworkListener extends BaseListener implements INetworkListener {
208218
}
209219
}
210220

221+
/**
222+
* Add one or more certificates to this listener.
223+
*
224+
* After the first certificate, this creates NetworkListenerCertificates
225+
* resources since cloudformation requires the certificates array on the
226+
* listener resource to have a length of 1.
227+
*/
228+
public addCertificates(id: string, certificates: IListenerCertificate[]): void {
229+
const additionalCerts = [...certificates];
230+
if (this.certificateArns.length === 0 && additionalCerts.length > 0) {
231+
const first = additionalCerts.splice(0, 1)[0];
232+
this.certificateArns.push(first.certificateArn);
233+
}
234+
// Only one certificate can be specified per resource, even though
235+
// `certificates` is of type Array
236+
for (let i = 0; i < additionalCerts.length; i++) {
237+
new NetworkListenerCertificate(this, `${id}${i + 1}`, {
238+
listener: this,
239+
certificates: [additionalCerts[i]],
240+
});
241+
}
242+
}
243+
211244
/**
212245
* Load balance incoming requests to the given target groups.
213246
*

packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts

+61
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,62 @@ describe('tests', () => {
416416
})).toThrow(/Protocol must be TLS when certificates have been specified/);
417417
});
418418

419+
test('Can pass multiple certificates to network listener constructor', () => {
420+
// GIVEN
421+
const stack = new cdk.Stack();
422+
const vpc = new ec2.Vpc(stack, 'Stack');
423+
const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc });
424+
425+
// WHEN
426+
lb.addListener('Listener', {
427+
port: 443,
428+
certificates: [
429+
importedCertificate(stack, 'cert1'),
430+
importedCertificate(stack, 'cert2'),
431+
],
432+
defaultTargetGroups: [new elbv2.NetworkTargetGroup(stack, 'Group', { vpc, port: 80 })],
433+
});
434+
435+
// THEN
436+
Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', {
437+
Protocol: 'TLS',
438+
});
439+
440+
Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerCertificate', {
441+
Certificates: [{ CertificateArn: 'cert2' }],
442+
});
443+
});
444+
445+
test('Can add multiple certificates to network listener after construction', () => {
446+
// GIVEN
447+
const stack = new cdk.Stack();
448+
const vpc = new ec2.Vpc(stack, 'Stack');
449+
const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc });
450+
451+
// WHEN
452+
const listener = lb.addListener('Listener', {
453+
port: 443,
454+
certificates: [
455+
importedCertificate(stack, 'cert1'),
456+
],
457+
defaultTargetGroups: [new elbv2.NetworkTargetGroup(stack, 'Group', { vpc, port: 80 })],
458+
});
459+
460+
listener.addCertificates('extra', [
461+
importedCertificate(stack, 'cert2'),
462+
]);
463+
464+
465+
// THEN
466+
Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', {
467+
Protocol: 'TLS',
468+
});
469+
470+
Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::ListenerCertificate', {
471+
Certificates: [{ CertificateArn: 'cert2' }],
472+
});
473+
});
474+
419475
test('not allowed to specify defaultTargetGroups and defaultAction together', () => {
420476
// GIVEN
421477
const stack = new cdk.Stack();
@@ -462,3 +518,8 @@ class ResourceWithLBDependency extends cdk.CfnResource {
462518
this.node.addDependency(targetGroup.loadBalancerAttached);
463519
}
464520
}
521+
522+
function importedCertificate(stack: cdk.Stack,
523+
certificateArn = 'arn:aws:certificatemanager:123456789012:testregion:certificate/fd0b8392-3c0e-4704-81b6-8edf8612c852') {
524+
return acm.Certificate.fromCertificateArn(stack, certificateArn, certificateArn);
525+
}

0 commit comments

Comments
 (0)