Skip to content

Commit f903785

Browse files
committed
hack: create the nlb and route53 records for default router
1 parent 538d015 commit f903785

File tree

2 files changed

+258
-0
lines changed

2 files changed

+258
-0
lines changed

pkg/asset/manifests/ingress.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ func (ing *Ingress) generateDefaultIngressController(config *types.InstallConfig
163163
case aws.Name:
164164
// FIXME: CCM cannot create an IPv6-enabled load balancer
165165
// Thus, we set publish to NodePort and manage the Load Balancer in the installer.
166+
// Remove after CCM support dualstack NLB.
166167
if config.IsDualStackInfra() {
167168
obj = getDefaultIngressController()
168169
obj.Spec.EndpointPublishingStrategy = &operatorv1.EndpointPublishingStrategy{

pkg/infrastructure/aws/clusterapi/aws.go

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)