Skip to content

Commit ee4fab5

Browse files
committed
feat(route53-targets): enhance LoadBalancerTarget to support IPv4-only and dual-stack NLBs with appropriate DNS naming
1 parent 7147e75 commit ee4fab5

File tree

3 files changed

+177
-1
lines changed

3 files changed

+177
-1
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env node
2+
import * as ec2 from 'aws-cdk-lib/aws-ec2';
3+
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
4+
import * as route53 from 'aws-cdk-lib/aws-route53';
5+
import * as cdk from 'aws-cdk-lib';
6+
import * as targets from 'aws-cdk-lib/aws-route53-targets';
7+
import { IntegTest } from '@aws-cdk/integ-tests-alpha';
8+
9+
const app = new cdk.App();
10+
const stack = new cdk.Stack(app, 'aws-cdk-nlb-integ');
11+
12+
const vpc = new ec2.Vpc(stack, 'VPC', {
13+
maxAzs: 2,
14+
restrictDefaultSecurityGroup: false,
15+
});
16+
17+
// IPv4-only NLB (default)
18+
const ipv4Nlb = new elbv2.NetworkLoadBalancer(stack, 'IPv4NLB', {
19+
vpc,
20+
internetFacing: true,
21+
// ipAddressType defaults to IPv4
22+
});
23+
24+
// Dual-stack NLB
25+
const dualStackNlb = new elbv2.NetworkLoadBalancer(stack, 'DualStackNLB', {
26+
vpc,
27+
internetFacing: true,
28+
ipAddressType: elbv2.IpAddressType.DUAL_STACK,
29+
});
30+
31+
const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' });
32+
33+
// IPv4-only NLB alias record (should NOT have dualstack prefix)
34+
new route53.ARecord(zone, 'IPv4NLBAlias', {
35+
zone,
36+
recordName: 'ipv4-nlb',
37+
target: route53.RecordTarget.fromAlias(new targets.LoadBalancerTarget(ipv4Nlb)),
38+
});
39+
40+
// Dual-stack NLB alias record (should have dualstack prefix)
41+
new route53.ARecord(zone, 'DualStackNLBAlias', {
42+
zone,
43+
recordName: 'dualstack-nlb',
44+
target: route53.RecordTarget.fromAlias(new targets.LoadBalancerTarget(dualStackNlb)),
45+
});
46+
47+
// IPv4-only NLB with health check
48+
new route53.ARecord(stack, 'IPv4NLBAliasWithHealthCheck', {
49+
zone,
50+
recordName: 'ipv4-nlb-health',
51+
target: route53.RecordTarget.fromAlias(
52+
new targets.LoadBalancerTarget(ipv4Nlb, {
53+
evaluateTargetHealth: true,
54+
}),
55+
),
56+
});
57+
58+
new IntegTest(app, 'nlb-alias-target', {
59+
testCases: [stack],
60+
});
61+
62+
app.synth();

packages/aws-cdk-lib/aws-route53-targets/lib/load-balancer-target.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,32 @@ export class LoadBalancerTarget implements route53.IAliasRecordTarget {
1111
public bind(_record: route53.IRecordSet, _zone?: route53.IHostedZone): route53.AliasRecordTargetConfig {
1212
return {
1313
hostedZoneId: this.loadBalancer.loadBalancerCanonicalHostedZoneId,
14-
dnsName: `dualstack.${this.loadBalancer.loadBalancerDnsName}`,
14+
dnsName: this.getDnsName(),
1515
evaluateTargetHealth: this.props?.evaluateTargetHealth,
1616
};
1717
}
18+
19+
private getDnsName(): string {
20+
// Check if this is a Network Load Balancer with IPv4-only addressing
21+
if (this.isNetworkLoadBalancer() && this.isIpv4Only()) {
22+
// IPv4-only NLB - no dualstack prefix needed
23+
return this.loadBalancer.loadBalancerDnsName;
24+
}
25+
26+
// For ALBs and dual-stack NLBs, use dualstack prefix for backward compatibility
27+
return `dualstack.${this.loadBalancer.loadBalancerDnsName}`;
28+
}
29+
30+
private isNetworkLoadBalancer(): boolean {
31+
// Check if this has the NLB-specific properties/methods
32+
// NLBs have addListener method that returns NetworkListener, ALBs return ApplicationListener
33+
// We'll use a duck-typing approach by checking the constructor name
34+
return this.loadBalancer.constructor.name === 'NetworkLoadBalancer' ||
35+
this.loadBalancer.constructor.name === 'LookedUpNetworkLoadBalancer';
36+
}
37+
38+
private isIpv4Only(): boolean {
39+
const ipAddressType = (this.loadBalancer as any).ipAddressType;
40+
return ipAddressType === elbv2.IpAddressType.IPV4 || ipAddressType === undefined;
41+
}
1842
}

packages/aws-cdk-lib/aws-route53-targets/test/load-balancer-target.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,93 @@ test('use ALB as record target with health check', () => {
6565
},
6666
});
6767
});
68+
69+
test('use IPv4-only NLB as record target', () => {
70+
// GIVEN
71+
const stack = new Stack();
72+
const vpc = new ec2.Vpc(stack, 'VPC', {
73+
maxAzs: 2,
74+
});
75+
const lb = new elbv2.NetworkLoadBalancer(stack, 'NLB', {
76+
vpc,
77+
internetFacing: true,
78+
ipAddressType: elbv2.IpAddressType.IPV4,
79+
});
80+
81+
const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' });
82+
83+
// WHEN
84+
new route53.ARecord(zone, 'Alias', {
85+
zone,
86+
recordName: '_foo',
87+
target: route53.RecordTarget.fromAlias(new targets.LoadBalancerTarget(lb)),
88+
});
89+
90+
// THEN - IPv4-only NLB should NOT use dualstack prefix
91+
Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', {
92+
AliasTarget: {
93+
DNSName: { 'Fn::GetAtt': ['NLB55158F82', 'DNSName'] },
94+
HostedZoneId: { 'Fn::GetAtt': ['NLB55158F82', 'CanonicalHostedZoneID'] },
95+
},
96+
});
97+
});
98+
99+
test('use dual-stack NLB as record target', () => {
100+
// GIVEN
101+
const stack = new Stack();
102+
const vpc = new ec2.Vpc(stack, 'VPC', {
103+
maxAzs: 2,
104+
});
105+
const lb = new elbv2.NetworkLoadBalancer(stack, 'NLB', {
106+
vpc,
107+
internetFacing: true,
108+
ipAddressType: elbv2.IpAddressType.DUAL_STACK,
109+
});
110+
111+
const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' });
112+
113+
// WHEN
114+
new route53.ARecord(zone, 'Alias', {
115+
zone,
116+
recordName: '_foo',
117+
target: route53.RecordTarget.fromAlias(new targets.LoadBalancerTarget(lb)),
118+
});
119+
120+
// THEN - Dual-stack NLB should use dualstack prefix
121+
Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', {
122+
AliasTarget: {
123+
DNSName: { 'Fn::Join': ['', ['dualstack.', { 'Fn::GetAtt': ['NLB55158F82', 'DNSName'] }]] },
124+
HostedZoneId: { 'Fn::GetAtt': ['NLB55158F82', 'CanonicalHostedZoneID'] },
125+
},
126+
});
127+
});
128+
129+
test('use default NLB as record target (should be IPv4-only)', () => {
130+
// GIVEN
131+
const stack = new Stack();
132+
const vpc = new ec2.Vpc(stack, 'VPC', {
133+
maxAzs: 2,
134+
});
135+
const lb = new elbv2.NetworkLoadBalancer(stack, 'NLB', {
136+
vpc,
137+
internetFacing: true,
138+
// ipAddressType not specified - defaults to IPv4
139+
});
140+
141+
const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' });
142+
143+
// WHEN
144+
new route53.ARecord(zone, 'Alias', {
145+
zone,
146+
recordName: '_foo',
147+
target: route53.RecordTarget.fromAlias(new targets.LoadBalancerTarget(lb)),
148+
});
149+
150+
// THEN - Default NLB should NOT use dualstack prefix
151+
Template.fromStack(stack).hasResourceProperties('AWS::Route53::RecordSet', {
152+
AliasTarget: {
153+
DNSName: { 'Fn::GetAtt': ['NLB55158F82', 'DNSName'] },
154+
HostedZoneId: { 'Fn::GetAtt': ['NLB55158F82', 'CanonicalHostedZoneID'] },
155+
},
156+
});
157+
});

0 commit comments

Comments
 (0)