Skip to content

Commit 53c3acd

Browse files
alebedev87claude
andcommitted
NE-2421: Support dual-stack IngressController on AWS
Configure the publishing load balancer service for dual-stack AWS clusters based on the Infrastructure CR's ipFamily field (status.platformStatus.aws.ipFamily). For NLB-type load balancers, set ipFamilyPolicy to RequireDualStack and ipFamilies to match the cluster's IP family ordering. For CLB, explicitly set SingleStack/IPv4 since CLB only forwards IPv4 traffic. Without this, DualStackIPv6Primary clusters would default the CLB service to SingleStack/IPv6, causing OVN to refuse IPv4 traffic on the service's NodePort. When the cluster IP family is dual-stack, the AWS DNS provider creates both Alias A and Alias AAAA Route53 records for IngressController wildcard domains. AAAA records are always created regardless of LB type because stale AAAA records cannot be easily cleaned up when the LB type changes from NLB to CLB — the old NLB is deleted before the new CLB is created, so the DNS provider can no longer look up the target hostname to delete the AAAA record. For CLBs the AAAA alias simply won't resolve. When the LB type is changed to CLB on a dual-stack cluster, a warning note is appended to the effectuation message on the cluster operator status indicating that CLBs do not support dual-stack. Co-authored-by: Claude claude-opus-4-6 <noreply@anthropic.com>
1 parent b347d80 commit 53c3acd

5 files changed

Lines changed: 336 additions & 15 deletions

File tree

pkg/dns/aws/dns.go

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
iov1 "github.com/openshift/api/operatoringress/v1"
1212
"github.com/openshift/cluster-ingress-operator/pkg/dns"
1313
logf "github.com/openshift/cluster-ingress-operator/pkg/log"
14+
awsutil "github.com/openshift/cluster-ingress-operator/pkg/util/aws"
1415

1516
"github.com/aws/aws-sdk-go/aws"
1617
"github.com/aws/aws-sdk-go/aws/arn"
@@ -109,6 +110,11 @@ type Config struct {
109110
// Client is a Kubernetes client, which the provider uses to annotate
110111
// DNSRecord CRs.
111112
Client client.Client
113+
114+
// IPFamily is the cluster's IP family configuration from the
115+
// Infrastructure CR. When dual-stack, the provider creates both
116+
// Alias A and Alias AAAA Route53 records.
117+
IPFamily configv1.IPFamilyType
112118
}
113119

114120
// ServiceEndpoint stores the configuration of a custom url to
@@ -619,22 +625,34 @@ func (m *Provider) updateRecord(domain, zoneID, target, targetHostedZoneID, acti
619625
},
620626
}
621627
} else {
622-
input.ChangeBatch = &route53.ChangeBatch{
623-
Changes: []*route53.Change{
624-
{
625-
Action: aws.String(action),
626-
ResourceRecordSet: &route53.ResourceRecordSet{
627-
Name: aws.String(domain),
628-
Type: aws.String(route53.RRTypeA),
629-
AliasTarget: &route53.AliasTarget{
630-
HostedZoneId: aws.String(targetHostedZoneID),
631-
DNSName: aws.String(target),
632-
EvaluateTargetHealth: aws.Bool(false),
633-
},
634-
},
628+
aliasTarget := &route53.AliasTarget{
629+
HostedZoneId: aws.String(targetHostedZoneID),
630+
DNSName: aws.String(target),
631+
EvaluateTargetHealth: aws.Bool(false),
632+
}
633+
changes := []*route53.Change{
634+
{
635+
Action: aws.String(action),
636+
ResourceRecordSet: &route53.ResourceRecordSet{
637+
Name: aws.String(domain),
638+
Type: aws.String(route53.RRTypeA),
639+
AliasTarget: aliasTarget,
635640
},
636641
},
637642
}
643+
if awsutil.IsDualStack(m.config.IPFamily) {
644+
changes = append(changes, &route53.Change{
645+
Action: aws.String(action),
646+
ResourceRecordSet: &route53.ResourceRecordSet{
647+
Name: aws.String(domain),
648+
Type: aws.String(route53.RRTypeAaaa),
649+
AliasTarget: aliasTarget,
650+
},
651+
})
652+
}
653+
input.ChangeBatch = &route53.ChangeBatch{
654+
Changes: changes,
655+
}
638656
}
639657
resp, err := m.route53.ChangeResourceRecordSets(&input)
640658
if err != nil {

pkg/operator/controller/dns/controller.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -594,8 +594,9 @@ func (r *reconciler) createDNSProvider(dnsConfig *configv1.DNS, platformStatus *
594594
switch platformStatus.Type {
595595
case configv1.AWSPlatformType:
596596
cfg := awsdns.Config{
597-
Region: platformStatus.AWS.Region,
598-
Client: r.client,
597+
Region: platformStatus.AWS.Region,
598+
IPFamily: platformStatus.AWS.IPFamily,
599+
Client: r.client,
599600
}
600601

601602
sharedCredsFile, err := awsutil.SharedCredentialsFileFromSecret(creds)

pkg/operator/controller/ingress/load_balancer_service.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"github.com/openshift/cluster-ingress-operator/pkg/manifests"
1717
"github.com/openshift/cluster-ingress-operator/pkg/operator/controller"
18+
awsutil "github.com/openshift/cluster-ingress-operator/pkg/util/aws"
1819
corev1 "k8s.io/api/core/v1"
1920

2021
configv1 "github.com/openshift/api/config/v1"
@@ -496,6 +497,32 @@ func desiredLoadBalancerService(ci *operatorv1.IngressController, deploymentRef
496497
}
497498
}
498499

500+
// Set ipFamilies and ipFamilyPolicy for dual-stack clusters.
501+
if platform.AWS != nil && awsutil.IsDualStack(platform.AWS.IPFamily) {
502+
if isAWSNLB(lbStatus) {
503+
// NLB supports dual-stack natively.
504+
ipFamilyPolicy := corev1.IPFamilyPolicyRequireDualStack
505+
service.Spec.IPFamilyPolicy = &ipFamilyPolicy
506+
if platform.AWS.IPFamily == configv1.DualStackIPv4Primary {
507+
service.Spec.IPFamilies = []corev1.IPFamily{corev1.IPv4Protocol, corev1.IPv6Protocol}
508+
} else {
509+
service.Spec.IPFamilies = []corev1.IPFamily{corev1.IPv6Protocol, corev1.IPv4Protocol}
510+
}
511+
} else {
512+
// CLB does not support dual-stack and only forwards
513+
// IPv4 traffic. On DualStackIPv4Primary clusters,
514+
// the service defaults to SingleStack/IPv4, which
515+
// is correct. However on DualStackIPv6Primary, the
516+
// default would be SingleStack/IPv6, causing OVN to
517+
// refuse IPv4 traffic on the service's NodePort.
518+
// Explicitly set SingleStack/IPv4 to ensure CLB
519+
// traffic is always accepted.
520+
ipFamilyPolicy := corev1.IPFamilyPolicySingleStack
521+
service.Spec.IPFamilyPolicy = &ipFamilyPolicy
522+
service.Spec.IPFamilies = []corev1.IPFamily{corev1.IPv4Protocol}
523+
}
524+
}
525+
499526
// Set the load balancer for AWS to be as aggressive as Azure (2 fail @ 5s interval, 2 healthy)
500527
service.Annotations[awsLBHealthCheckTimeoutAnnotation] = awsLBHealthCheckTimeoutDefault
501528
service.Annotations[awsLBHealthCheckUnhealthyThresholdAnnotation] = awsLBHealthCheckUnhealthyThresholdDefault
@@ -715,6 +742,27 @@ func loadBalancerServiceChanged(current, expected *corev1.Service) (bool, *corev
715742
}
716743
}
717744

745+
// Reconcile ipFamilies and ipFamilyPolicy when the desired service
746+
// specifies them (i.e. the cluster is dual-stack).
747+
if len(expected.Spec.IPFamilies) != 0 {
748+
if !reflect.DeepEqual(current.Spec.IPFamilies, expected.Spec.IPFamilies) {
749+
if !changed {
750+
changed = true
751+
updated = current.DeepCopy()
752+
}
753+
updated.Spec.IPFamilies = expected.Spec.IPFamilies
754+
}
755+
}
756+
if expected.Spec.IPFamilyPolicy != nil {
757+
if current.Spec.IPFamilyPolicy == nil || *current.Spec.IPFamilyPolicy != *expected.Spec.IPFamilyPolicy {
758+
if !changed {
759+
changed = true
760+
updated = current.DeepCopy()
761+
}
762+
updated.Spec.IPFamilyPolicy = expected.Spec.IPFamilyPolicy
763+
}
764+
}
765+
718766
return changed, updated
719767
}
720768

@@ -858,6 +906,10 @@ func loadBalancerServiceIsProgressing(ic *operatorv1.IngressController, service
858906
if wantLBType != haveLBType {
859907
patchSpec := fmt.Sprintf(`{"spec":{"endpointPublishingStrategy":{"type":"LoadBalancerService","loadBalancer":{"providerParameters":{"type":"AWS","aws":{"type":"%s"}}}}}}`, haveLBType)
860908
err := createEffectuationMessage("loadBalancer.providerParameters.aws.type", string(haveLBType), string(wantLBType), patchSpec, ic, service)
909+
// Add a note to the effectuation message about CLB not supporting dual-stack.
910+
if wantLBType == operatorv1.AWSClassicLoadBalancer && platform.AWS != nil && awsutil.IsDualStack(platform.AWS.IPFamily) {
911+
err = fmt.Errorf("%w Classic Load Balancers do not support dual-stack. The IngressController %q will use IPv4-only despite that the cluster is configured as %q. Use an NLB type to support dual-stack networking.", err, ic.Name, platform.AWS.IPFamily)
912+
}
861913
errs = append(errs, err)
862914
}
863915

@@ -1386,3 +1438,13 @@ func getAllowedSourceRanges(eps *operatorv1.EndpointPublishingStrategy) []operat
13861438

13871439
return nil
13881440
}
1441+
1442+
// isAWSNLB returns true if the IngressController's load balancer status
1443+
// indicates an AWS Network Load Balancer.
1444+
func isAWSNLB(lbStatus *operatorv1.LoadBalancerStrategy) bool {
1445+
return lbStatus != nil &&
1446+
lbStatus.ProviderParameters != nil &&
1447+
lbStatus.ProviderParameters.Type == operatorv1.AWSLoadBalancerProvider &&
1448+
lbStatus.ProviderParameters.AWS != nil &&
1449+
lbStatus.ProviderParameters.AWS.Type == operatorv1.AWSNetworkLoadBalancer
1450+
}

0 commit comments

Comments
 (0)