Skip to content

feat: Add IPv6 Compatible NLBs for AWS #644

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 61 additions & 24 deletions cloudconfig/aws/ops_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package aws
import (
"errors"
"fmt"
"maps"
"sort"
"strings"

Expand Down Expand Up @@ -92,23 +93,37 @@ func (o OpsGenerator) GenerateVars(state storage.State) (string, error) {
"internal_az_subnet_id_mapping",
"internal_az_subnet_cidr_mapping",
}
cfRequiredOutputs := []string{
"cf_router_lb_name",
"cf_router_lb_internal_security_group",
"cf_ssh_lb_name",
"cf_ssh_lb_internal_security_group",
"cf_tcp_lb_name",
"cf_tcp_lb_internal_security_group",
}
dualstackOutput, ok := terraformOutputs.Map["dualstack"]
if !ok {
return "", fmt.Errorf("missing dualstack terraform output")
}
dualstack := dualstackOutput.(bool)
if dualstack {
requiredOutputs = append(requiredOutputs,
"internal_cidr_ipv6",
"internal_az_subnet_ipv6_cidr_mapping",
)
}

switch state.LB.Type {
case "concourse":
requiredOutputs = append(
requiredOutputs,
"concourse_lb_target_groups",
"concourse_lb_internal_security_group",
)
case "nlb":
fallthrough
case "cf":
requiredOutputs = append(
requiredOutputs,
"cf_router_lb_name",
"cf_router_lb_internal_security_group",
"cf_ssh_lb_name",
"cf_ssh_lb_internal_security_group",
"cf_tcp_lb_name",
"cf_tcp_lb_internal_security_group",
)
requiredOutputs = append(requiredOutputs, cfRequiredOutputs...)
}

for _, output := range requiredOutputs {
Expand All @@ -124,21 +139,34 @@ func (o OpsGenerator) GenerateVars(state storage.State) (string, error) {
if err != nil {
return "", err
}
if dualstack {
internalAZSubnetIPv6CIDRMap := terraformOutputs.GetStringMap("internal_az_subnet_ipv6_cidr_mapping")
ipv6AvailabilityZones, err := generateAZs(3, internalAZSubnetIDMap, internalAZSubnetIPv6CIDRMap)
if err != nil {
return "", err
}
azs = append(azs, ipv6AvailabilityZones...)
}

varsYAML := map[string]interface{}{}
for k, v := range terraformOutputs.Map {
varsYAML[k] = v
}
maps.Copy(varsYAML, terraformOutputs.Map)

for _, az := range azs {
for key, value := range az {
varsYAML[key] = value
}
}

// TODO: Make the ISO Segments handle IPv6
isoSegAZSubnetIDMap := terraformOutputs.GetStringMap("iso_az_subnet_id_mapping")
isoSegAZSubnetCIDRMap := terraformOutputs.GetStringMap("iso_az_subnet_cidr_mapping")
if len(isoSegAZSubnetIDMap) > 0 && len(isoSegAZSubnetCIDRMap) > 0 {
isoSegAzs, err := generateAZs(len(azs), isoSegAZSubnetIDMap, isoSegAZSubnetCIDRMap)
// If not running IPv6, start the index after len(azs) many subnets
// If running IPv6, double we need to offset by another len(azs) to accommodate the IPv6 entries
offset := len(azs)
if dualstack {
offset = len(azs) * 2
}
isoSegAzs, err := generateAZs(offset, isoSegAZSubnetIDMap, isoSegAZSubnetCIDRMap)
if err == nil {
for _, az := range isoSegAzs {
for key, value := range az {
Expand Down Expand Up @@ -219,7 +247,7 @@ func (o OpsGenerator) generateOps(state storage.State) ([]op, error) {
if err != nil {
return []op{}, fmt.Errorf("Retrieve availability zones: %s", err) //nolint:staticcheck
}

// This block doesn't seem to handle generating the OPs for isolation segments?
for i := range azs {
azOp := createOp("replace", "/azs/-", az{
Name: fmt.Sprintf("z%d", i+1),
Expand All @@ -229,8 +257,15 @@ func (o OpsGenerator) generateOps(state storage.State) ([]op, error) {
})
ops = append(ops, azOp)

subnet := generateNetworkSubnet(i)
subnets = append(subnets, subnet)
// IPv4 Subnets don't need offset
ipv4Subnet := generateNetworkSubnet(i, 0)
subnets = append(subnets, ipv4Subnet)

if state.LB.Type == "nlb" {
// IPv6 subnets need to set the same values as IPv4 for
// AZ name (e.g z1, z2, z3) but require an offset value for templating reasons
subnets = append(subnets, generateNetworkSubnet(i, len(azs)))
}
}

ops = append(ops, createOp("replace", "/networks/-", network{
Expand All @@ -246,6 +281,8 @@ func (o OpsGenerator) generateOps(state storage.State) ([]op, error) {
}))

switch state.LB.Type {
case "nlb":
fallthrough
case "cf":
lbSecurityGroups := []map[string]string{
{"name": "cf-router-network-properties", "lb": "((cf_router_lb_name))", "group": "((cf_router_lb_internal_security_group))"},
Expand Down Expand Up @@ -307,21 +344,21 @@ func azify(az int, azName, cidr, subnet string) (map[string]string, error) {
}, nil
}

func generateNetworkSubnet(az int) networkSubnet {
func generateNetworkSubnet(az int, offset int) networkSubnet {
az++
return networkSubnet{
AZ: fmt.Sprintf("z%d", az),
Gateway: fmt.Sprintf("((az%d_gateway))", az),
Range: fmt.Sprintf("((az%d_range))", az),
Gateway: fmt.Sprintf("((az%d_gateway))", az+offset),
Range: fmt.Sprintf("((az%d_range))", az+offset),
Reserved: []string{
fmt.Sprintf("((az%d_reserved_1))", az),
fmt.Sprintf("((az%d_reserved_2))", az),
fmt.Sprintf("((az%d_reserved_1))", az+offset),
fmt.Sprintf("((az%d_reserved_2))", az+offset),
},
Static: []string{
fmt.Sprintf("((az%d_static))", az),
fmt.Sprintf("((az%d_static))", az+offset),
},
CloudProperties: networkSubnetCloudProperties{
Subnet: fmt.Sprintf("((az%d_subnet))", az),
Subnet: fmt.Sprintf("((az%d_subnet))", az+offset),
SecurityGroups: []string{"((internal_security_group))"},
},
}
Expand Down
4 changes: 3 additions & 1 deletion cloudconfig/aws/ops_generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ var _ = Describe("OpsGenerator", func() {
"cf_tcp_lb_internal_security_group": "some-cf-tcp-lb-internal-security-group",
"concourse_lb_target_groups": []string{"some-concourse-lb-target-group", "some-other-concourse-lb-target-group"},
"concourse_lb_internal_security_group": "some-concourse-lb-internal-security-group",
"dualstack": false,
"internal_az_subnet_id_mapping": map[string]interface{}{
"us-east-1c": "some-internal-subnet-ids-3",
"us-east-1a": "some-internal-subnet-ids-1",
Expand Down Expand Up @@ -136,6 +137,7 @@ cf_tcp_lb_internal_security_group: some-cf-tcp-lb-internal-security-group
cf_iso_router_lb_name: some-cf-iso-seg-router-lb-name
concourse_lb_target_groups: [some-concourse-lb-target-group, some-other-concourse-lb-target-group]
concourse_lb_internal_security_group: some-concourse-lb-internal-security-group
dualstack: false
internal_az_subnet_cidr_mapping:
us-east-1a: 10.0.16.0/20
us-east-1b: 10.0.32.0/20
Expand Down Expand Up @@ -191,7 +193,7 @@ iso_az_subnet_id_mapping:
Expect(err).To(MatchError(fmt.Sprintf("missing %s terraform output", outputKey)))
},
Entry("when internal_security_group is missing", "internal_security_group", ""),

Entry("when dualstack is missing", "dualstack", "nlb"),
Entry("when internal_az_subnet_id_mapping is missing", "internal_az_subnet_id_mapping", "cf"),
Entry("when internal_az_subnet_cidr_mapping is missing", "internal_az_subnet_cidr_mapping", "cf"),
Entry("when cf_router_lb_name is missing", "cf_router_lb_name", "cf"),
Expand Down
6 changes: 3 additions & 3 deletions commands/commands_usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ const (
LBUsage = `

Load Balancer options:
--lb-type Load balancer(s) type: "concourse" or "cf"
--lb-cert Path to SSL certificate (supported when type="cf")
--lb-key Path to SSL certificate key (supported when type="cf")
--lb-type Load balancer(s) type: "concourse", "cf", or "nlb"
--lb-cert Path to SSL certificate (supported when type="cf" or "nlb")
--lb-key Path to SSL certificate key (supported when type="cf" or "nlb")
--lb-chain Path to SSL certificate chain (supported when iaas="aws")
--lb-domain Creates a DNS zone and records for the given domain (supported when type="cf")`

Expand Down
6 changes: 3 additions & 3 deletions commands/commands_usage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ var _ = Describe("Commands Usage", func() {
--cloudstack-iso-segment CloudStack Activate iso segment env: $BBL_CLOUDSTACK_ISO_SEGMENT

Load Balancer options:
--lb-type Load balancer(s) type: "concourse" or "cf"
--lb-cert Path to SSL certificate (supported when type="cf")
--lb-key Path to SSL certificate key (supported when type="cf")
--lb-type Load balancer(s) type: "concourse", "cf", or "nlb"
--lb-cert Path to SSL certificate (supported when type="cf" or "nlb")
--lb-key Path to SSL certificate key (supported when type="cf" or "nlb")
--lb-chain Path to SSL certificate chain (supported when iaas="aws")
--lb-domain Creates a DNS zone and records for the given domain (supported when type="cf")`))
})
Expand Down
14 changes: 13 additions & 1 deletion terraform/aws/template_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ type templates struct {
iam string
lbSubnet string
cfLB string
cfNLB string
cfDNS string
concourseLB string
cfCommon string
sslCertificate string
isoSeg string
vpc string
Expand Down Expand Up @@ -43,7 +45,13 @@ func (tg TemplateGenerator) Generate(state storage.State) string {
case "concourse":
template = strings.Join([]string{template, tmpls.lbSubnet, tmpls.concourseLB}, "\n")
case "cf":
template = strings.Join([]string{template, tmpls.lbSubnet, tmpls.cfLB, tmpls.sslCertificate, tmpls.isoSeg}, "\n")
template = strings.Join([]string{template, tmpls.lbSubnet, tmpls.cfLB, tmpls.cfCommon, tmpls.sslCertificate, tmpls.isoSeg}, "\n")

if state.LB.Domain != "" {
template = strings.Join([]string{template, tmpls.cfDNS}, "\n")
}
case "nlb":
template = strings.Join([]string{template, tmpls.lbSubnet, tmpls.cfNLB, tmpls.cfCommon, tmpls.sslCertificate, tmpls.isoSeg}, "\n")

if state.LB.Domain != "" {
template = strings.Join([]string{template, tmpls.cfDNS}, "\n")
Expand All @@ -60,6 +68,8 @@ func (t TemplateGenerator) readTemplates() templates {
"lb_subnet.tf": "",
"cf_lb.tf": "",
"cf_dns.tf": "",
"cf_lb_common.tf": "",
"cf_nlb.tf": "",
"concourse_lb.tf": "",
"ssl_certificate.tf": "",
"iso_segments.tf": "",
Expand Down Expand Up @@ -94,8 +104,10 @@ func (t TemplateGenerator) readTemplates() templates {
base: listings["base.tf"],
iam: listings["iam.tf"],
lbSubnet: listings["lb_subnet.tf"],
cfCommon: listings["cf_lb_common.tf"],
cfLB: listings["cf_lb.tf"],
cfDNS: listings["cf_dns.tf"],
cfNLB: listings["cf_nlb.tf"],
concourseLB: listings["concourse_lb.tf"],
sslCertificate: listings["ssl_certificate.tf"],
isoSeg: listings["iso_segments.tf"],
Expand Down
4 changes: 2 additions & 2 deletions terraform/aws/template_generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ var _ = Describe("TemplateGenerator", func() {

Context("when a CF lb type is provided with no system domain", func() {
BeforeEach(func() {
expectedTemplate = expectTemplate("base", "iam", "vpc", "lb_subnet", "cf_lb", "ssl_certificate", "iso_segments")
expectedTemplate = expectTemplate("base", "iam", "vpc", "lb_subnet", "cf_lb", "cf_lb_common", "ssl_certificate", "iso_segments")
lb = storage.LB{
Type: "cf",
}
Expand All @@ -65,7 +65,7 @@ var _ = Describe("TemplateGenerator", func() {

Context("when a CF lb type is provided with a system domain", func() {
BeforeEach(func() {
expectedTemplate = expectTemplate("base", "iam", "vpc", "lb_subnet", "cf_lb", "ssl_certificate", "iso_segments", "cf_dns")
expectedTemplate = expectTemplate("base", "iam", "vpc", "lb_subnet", "cf_lb", "cf_lb_common", "ssl_certificate", "iso_segments", "cf_dns")
lb = storage.LB{
Type: "cf",
Domain: "some-domain",
Expand Down
6 changes: 3 additions & 3 deletions terraform/aws/templates/cf_dns.tf
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ resource "aws_route53_record" "wildcard_dns" {
type = "CNAME"
ttl = 300

records = ["${aws_elb.cf_router_lb.dns_name}"]
records = var.dualstack ? [aws_lb.cf_router_lb.dns_name] : ["${aws_elb.cf_router_lb.dns_name}"]
}

resource "aws_route53_record" "ssh" {
Expand All @@ -48,7 +48,7 @@ resource "aws_route53_record" "ssh" {
type = "CNAME"
ttl = 300

records = ["${aws_elb.cf_ssh_lb.dns_name}"]
records = var.dualstack ? [aws_lb.cf_ssh_lb.dns_name] : ["${aws_elb.cf_ssh_lb.dns_name}"]
}

resource "aws_route53_record" "bosh" {
Expand All @@ -66,7 +66,7 @@ resource "aws_route53_record" "tcp" {
type = "CNAME"
ttl = 300

records = ["${aws_elb.cf_tcp_lb.dns_name}"]
records = var.dualstack ? [aws_lb.cf_tcp_lb.dns_name] : ["${aws_elb.cf_tcp_lb.dns_name}"]
}

resource "aws_route53_record" "iso" {
Expand Down
Loading
Loading