@@ -10,6 +10,7 @@ import (
1010 "github.com/aws/aws-sdk-go-v2/aws"
1111 configv2 "github.com/aws/aws-sdk-go-v2/config"
1212 "github.com/aws/aws-sdk-go-v2/service/ec2"
13+ ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
1314 elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
1415 elbv2types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types"
1516 "github.com/aws/aws-sdk-go-v2/service/s3"
@@ -231,6 +232,15 @@ func (*Provider) InfraReady(ctx context.Context, in clusterapi.InfraReadyInput)
231232 }
232233 logrus .Debugln ("Created private API record in private zone" )
233234
235+ // FIXME: Provision the dualstack NLB and DNS record sets for default router.
236+ // Remove after CCM support dualstack NLB.
237+ if isDualStack {
238+ if err := provisionDefaultRouterResources (ctx , in , awsCluster , phzID ); err != nil {
239+ return fmt .Errorf ("failed to create resources for default router in dualstack mode: %w" , err )
240+ }
241+ logrus .Debugln ("Created resources for default router in dualstack mode" )
242+ }
243+
234244 return nil
235245}
236246
@@ -522,3 +532,250 @@ func removeS3Bucket(ctx context.Context, region string, bucketName string, endpo
522532 logrus .Debugf ("bucket %q emptied" , bucketName )
523533 return nil
524534}
535+
536+ // provisionDefaultRouterResources is a HACK around AWS CCM limitation that does not support dual stack NLB.
537+ // After the infra is ready, we provision the LB and create the necessary record sets for the default router.
538+ // NOTE: When the cluster operators are progressing, we need to wait till worker nodes are ready to register all nodes
539+ // to the target group of this ingress NLB.
540+ func provisionDefaultRouterResources (ctx context.Context , in clusterapi.InfraReadyInput , infraCluster * capa.AWSCluster , privateZoneID string ) error {
541+ ic := in .InstallConfig .Config
542+ region := ic .Platform .AWS .Region
543+
544+ // Tags specifications
545+ clusterTagKey := aws .String (fmt .Sprintf ("kubernetes.io/cluster/%s" , in .InfraID ))
546+ clusterTagValue := aws .String ("owned" )
547+
548+ // Create clients to call AWS API
549+ // FIXME: Let's ignore the custom endpoints for now
550+ cfg , err := configv2 .LoadDefaultConfig (ctx , configv2 .WithRegion (region ))
551+ if err != nil {
552+ return fmt .Errorf ("failed to load AWS config: %w" , err )
553+ }
554+ ec2Client := ec2 .NewFromConfig (cfg )
555+ elbv2Client := elbv2 .NewFromConfig (cfg )
556+
557+ // CCM: Create security groups
558+ sgInput := & ec2.CreateSecurityGroupInput {
559+ VpcId : aws .String (infraCluster .Spec .NetworkSpec .VPC .ID ),
560+ GroupName : aws .String (fmt .Sprintf ("k8s-elb-ingress-%s" , in .InfraID )),
561+ Description : aws .String ("Security group for Kubernetes ELB (openshift-ingress/router-default)" ),
562+ TagSpecifications : []ec2types.TagSpecification {
563+ {
564+ ResourceType : ec2types .ResourceTypeSecurityGroup ,
565+ Tags : []ec2types.Tag {{Key : clusterTagKey , Value : clusterTagValue }},
566+ },
567+ },
568+ }
569+
570+ sgOut , err := ec2Client .CreateSecurityGroup (ctx , sgInput )
571+ if err != nil {
572+ return fmt .Errorf ("failed to create security group for ingress elb: %w" , err )
573+ }
574+ securityGroupID := sgOut .GroupId
575+ logrus .Infof ("CCM: created security group for ingress LB: %s" , * securityGroupID )
576+
577+ // CCM: Authorize access to port 80 and 443 to anywhere IPv4 and IPv6.
578+ _ , err = ec2Client .AuthorizeSecurityGroupIngress (ctx , & ec2.AuthorizeSecurityGroupIngressInput {
579+ GroupId : securityGroupID ,
580+ IpPermissions : []ec2types.IpPermission {
581+ {FromPort : aws .Int32 (80 ), ToPort : aws .Int32 (80 ), IpProtocol : aws .String ("tcp" ), IpRanges : []ec2types.IpRange {{CidrIp : aws .String (capiutils .AnyIPv4CidrBlock .String ())}}},
582+ {FromPort : aws .Int32 (80 ), ToPort : aws .Int32 (80 ), IpProtocol : aws .String ("tcp" ), Ipv6Ranges : []ec2types.Ipv6Range {{CidrIpv6 : aws .String (capiutils .AnyIPv6CidrBlock .String ())}}},
583+ {FromPort : aws .Int32 (443 ), ToPort : aws .Int32 (443 ), IpProtocol : aws .String ("tcp" ), IpRanges : []ec2types.IpRange {{CidrIp : aws .String (capiutils .AnyIPv4CidrBlock .String ())}}},
584+ {FromPort : aws .Int32 (443 ), ToPort : aws .Int32 (443 ), IpProtocol : aws .String ("tcp" ), Ipv6Ranges : []ec2types.Ipv6Range {{CidrIpv6 : aws .String (capiutils .AnyIPv6CidrBlock .String ())}}},
585+ },
586+ })
587+ if err != nil {
588+ return fmt .Errorf ("failed to authorize security group ingress rules for ingress elb: %w" , err )
589+ }
590+ logrus .Info ("CCM: added ingress rules for 80/443 TCP for ingress LB security group" )
591+
592+ // CCM: Create the dual stack NLB for ingress
593+ // FIXME: Let's ignore the IPv4 IP pool.
594+ subnetIDs := infraCluster .Spec .NetworkSpec .Subnets .FilterPublic ().IDs ()
595+ scheme := elbv2types .LoadBalancerSchemeEnumInternetFacing
596+ if ! in .InstallConfig .Config .PublicIngress () {
597+ subnetIDs = infraCluster .Spec .NetworkSpec .Subnets .FilterPrivate ().IDs ()
598+ scheme = elbv2types .LoadBalancerSchemeEnumInternal
599+ }
600+
601+ lbInput := & elbv2.CreateLoadBalancerInput {
602+ Name : aws .String (fmt .Sprintf ("%s-ingress-lb" , in .InfraID )),
603+ Type : elbv2types .LoadBalancerTypeEnumNetwork ,
604+ Subnets : subnetIDs ,
605+ Scheme : scheme ,
606+ IpAddressType : elbv2types .IpAddressTypeDualstack ,
607+ Tags : []elbv2types.Tag {
608+ {Key : clusterTagKey , Value : clusterTagValue },
609+ },
610+ SecurityGroups : []string {aws .ToString (securityGroupID )},
611+ }
612+
613+ lbOut , err := elbv2Client .CreateLoadBalancer (ctx , lbInput )
614+ if err != nil {
615+ return fmt .Errorf ("failed to create ingress dualstack nlb: %w" , err )
616+ }
617+ lbSpec := lbOut .LoadBalancers [0 ]
618+ logrus .Infof ("CCM: created dualstack ingress LB: %s. Waiting to be ready..." , * lbSpec .LoadBalancerArn )
619+
620+ // Wait till the load balancer becomes ready (5 minutes)
621+ waitDuration := 5 * time .Minute
622+ lbDescInput := & elbv2.DescribeLoadBalancersInput {
623+ LoadBalancerArns : []string {aws .ToString (lbSpec .LoadBalancerArn )},
624+ }
625+ lbWaiter := elbv2 .NewLoadBalancerAvailableWaiter (elbv2Client )
626+ if err := lbWaiter .Wait (ctx , lbDescInput , waitDuration ); err != nil {
627+ return fmt .Errorf ("failed to wait for ingress lb to become ready: %w" , err )
628+ }
629+ logrus .Info ("CCM: dualstack ingress LB is ready" )
630+
631+ // Track target groups by port to create the corresponding
632+ // listeners on the ingress nlb.
633+ targetGroupArns := map [int32 ]string {}
634+
635+ // CCM: Create target group
636+ openPorts := []int32 {443 , 80 }
637+ for _ , port := range openPorts {
638+ name := "ingress-secure"
639+ if port == 80 {
640+ name = fmt .Sprint ("ingress-insecure" )
641+ }
642+ targetGroupInput := & elbv2.CreateTargetGroupInput {
643+ Name : aws .String (name ),
644+ Port : aws .Int32 (port ),
645+ Protocol : elbv2types .ProtocolEnumTcp ,
646+ VpcId : aws .String (infraCluster .Spec .NetworkSpec .VPC .ID ),
647+ // Communication between lb and target group is over IPv4
648+ // as worker nodes currently cannot set primary IPv6 (i.e handled by MAPI).
649+ // Thus, they cannot be registered with the IPv6 target group.
650+ // This is inconsistent with API LBs (over IPv6).
651+ IpAddressType : elbv2types .TargetGroupIpAddressTypeEnumIpv4 ,
652+ Tags : []elbv2types.Tag {
653+ {Key : clusterTagKey , Value : clusterTagValue },
654+ },
655+ }
656+
657+ tgOut , err := elbv2Client .CreateTargetGroup (ctx , targetGroupInput )
658+ if err != nil {
659+ return fmt .Errorf ("failed to create target groups for ingress elb: %w" , err )
660+ }
661+ logrus .Infof ("CCM: created target group on port %d for ingress LB" , port )
662+
663+ // TODO: This hack requires manual registration of worker nodes
664+ // to the below target groups.
665+ //
666+ // FIXME: What happens to worker node scaling or recreating?
667+ // Do new nodes get registered automatically?
668+ //
669+ // HOWTO:
670+ // We need to get the port exposed on nodes for the ingress default controller.
671+ //
672+ // oc -n openshift-ingress get svc router-nodeport-default -o=wide
673+ //
674+ //
675+ // Note down the ports open on nodes, for example:
676+ // 80:30278/TCP
677+ // 443:31745/TCP
678+ // 1936:31350/TCP (health check)
679+ // Then, use these ports to configure the target port and health check port.
680+
681+ targetGroupARn := tgOut .TargetGroups [0 ].TargetGroupArn
682+ targetGroupArns [port ] = aws .ToString (targetGroupARn )
683+
684+ // Set target group attribute
685+ attrModifyInput := & elbv2.ModifyTargetGroupAttributesInput {
686+ TargetGroupArn : targetGroupARn ,
687+ Attributes : []elbv2types.TargetGroupAttribute {
688+ {
689+ Key : aws .String ("preserve_client_ip.enabled" ),
690+ Value : aws .String ("false" ),
691+ },
692+ {
693+ Key : aws .String ("proxy_protocol_v2.enabled" ),
694+ Value : aws .String ("true" ),
695+ },
696+ {
697+ Key : aws .String ("target_health_state.unhealthy.connection_termination.enabled" ),
698+ Value : aws .String ("false" ),
699+ },
700+ {
701+ Key : aws .String ("target_health_state.unhealthy.draining_interval_seconds" ),
702+ Value : aws .String ("300" ),
703+ },
704+ },
705+ }
706+ if _ , err := elbv2Client .ModifyTargetGroupAttributes (ctx , attrModifyInput ); err != nil {
707+ return fmt .Errorf ("failed to modify target group attributes for ingress elb: %w" , err )
708+ }
709+ logrus .Infof ("CCM: modified attributes of target group on port %d for ingress LB" , port )
710+ }
711+
712+ // CCM: Create listeners for ingress nlb.
713+ for _ , port := range openPorts {
714+ listenerInput := & elbv2.CreateListenerInput {
715+ DefaultActions : []elbv2types.Action {
716+ {
717+ TargetGroupArn : aws .String (targetGroupArns [port ]),
718+ Type : elbv2types .ActionTypeEnumForward ,
719+ },
720+ },
721+ LoadBalancerArn : lbSpec .LoadBalancerArn ,
722+ Port : aws .Int32 (port ),
723+ Protocol : elbv2types .ProtocolEnumTcp ,
724+ Tags : []elbv2types.Tag {
725+ {Key : clusterTagKey , Value : clusterTagValue },
726+ },
727+ }
728+
729+ if _ , err := elbv2Client .CreateListener (ctx , listenerInput ); err != nil {
730+ return fmt .Errorf ("failed to create listeners for ingress elb: %w" , err )
731+ }
732+ logrus .Infof ("CCM: created listener on port %d for ingress LB" , port )
733+ }
734+
735+ // CIO: Create the wildcards DNS records for the ingress LB
736+ // in private zone and public zone (if public ingress)
737+ awsSession , err := in .InstallConfig .AWS .Session (ctx )
738+ if err != nil {
739+ return fmt .Errorf ("failed to get aws session: %w" , err )
740+ }
741+ client := awsconfig .NewClient (awsSession )
742+
743+ appsName := fmt .Sprintf ("*.apps.%s." , in .InstallConfig .Config .ClusterDomain ())
744+
745+ // Private zone
746+ if err := client .CreateOrUpdateRecord (ctx , & awsconfig.CreateRecordInput {
747+ Name : appsName ,
748+ Region : infraCluster .Spec .Region ,
749+ DNSTarget : aws .ToString (lbSpec .DNSName ),
750+ ZoneID : privateZoneID ,
751+ AliasZoneID : aws .ToString (lbSpec .CanonicalHostedZoneId ),
752+ HostedZoneRole : in .InstallConfig .Config .AWS .HostedZoneRole ,
753+ EnableAAAA : true ,
754+ }); err != nil {
755+ return fmt .Errorf ("failed to create records for ingress in private zone: %w" , err )
756+ }
757+ logrus .Info ("Created ingress record in private zone" )
758+
759+ // CIO: Public zone if cluster is public
760+ if ic .PublicIngress () {
761+ zone , err := client .GetBaseDomain (in .InstallConfig .Config .BaseDomain )
762+ if err != nil {
763+ return err
764+ }
765+
766+ if err := client .CreateOrUpdateRecord (ctx , & awsconfig.CreateRecordInput {
767+ Name : appsName ,
768+ Region : infraCluster .Spec .Region ,
769+ DNSTarget : aws .ToString (lbSpec .DNSName ),
770+ ZoneID : aws .ToString (zone .Id ),
771+ AliasZoneID : aws .ToString (lbSpec .CanonicalHostedZoneId ),
772+ HostedZoneRole : "" , // we dont want to assume role here
773+ EnableAAAA : true ,
774+ }); err != nil {
775+ return fmt .Errorf ("failed to create records for ingress in public zone: %w" , err )
776+ }
777+ logrus .Info ("Created ingress record in public zone" )
778+ }
779+
780+ return nil
781+ }
0 commit comments