Skip to content

Commit e1388f7

Browse files
committed
Create multiple control plane loadbalancers concurrently
1 parent 63b8bcf commit e1388f7

File tree

2 files changed

+80
-39
lines changed

2 files changed

+80
-39
lines changed

pkg/cloud/services/elb/loadbalancer.go

Lines changed: 76 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -67,60 +67,94 @@ const additionalTargetGroupPrefix = "additional-listener-"
6767
// cantAttachSGToNLBRegions is a set of regions that do not support Security Groups in NLBs.
6868
var cantAttachSGToNLBRegions = sets.New("us-iso-east-1", "us-iso-west-1", "us-isob-east-1")
6969

70+
type lbReconciler func() error
71+
7072
// ReconcileLoadbalancers reconciles the load balancers for the given cluster.
7173
func (s *Service) ReconcileLoadbalancers() error {
7274
s.scope.Debug("Reconciling load balancers")
7375

7476
var errs []error
77+
var lbReconcilers []lbReconciler
78+
79+
// The following splits load balancer reconciliation into 2 phases:
80+
// 1. Get or create the load balancer
81+
// 2. Reconcile the load balancer
82+
// We ensure that we only wait for the load balancer to become available in
83+
// the reconcile phase. This is useful when creating multiple load
84+
// balancers, as they can take several minutes to become available.
7585

7686
for _, lbSpec := range s.scope.ControlPlaneLoadBalancers() {
7787
if lbSpec == nil {
7888
continue
7989
}
8090
switch lbSpec.LoadBalancerType {
8191
case infrav1.LoadBalancerTypeClassic:
82-
errs = append(errs, s.reconcileClassicLoadBalancer())
92+
reconciler, err := s.getOrCreateClassicLoadBalancer()
93+
if err != nil {
94+
errs = append(errs, err)
95+
} else {
96+
lbReconcilers = append(lbReconcilers, reconciler)
97+
}
8398
case infrav1.LoadBalancerTypeNLB, infrav1.LoadBalancerTypeALB, infrav1.LoadBalancerTypeELB:
84-
errs = append(errs, s.reconcileV2LB(lbSpec))
99+
reconciler, err := s.getOrCreateV2LB(lbSpec)
100+
if err != nil {
101+
errs = append(errs, err)
102+
} else {
103+
lbReconcilers = append(lbReconcilers, reconciler)
104+
}
85105
default:
86106
errs = append(errs, fmt.Errorf("unknown or unsupported load balancer type on primary load balancer: %s", lbSpec.LoadBalancerType))
87107
}
88108
}
89109

110+
// Reconcile all load balancers
111+
for _, reconciler := range lbReconcilers {
112+
if err := reconciler(); err != nil {
113+
errs = append(errs, err)
114+
}
115+
}
116+
90117
return kerrors.NewAggregate(errs)
91118
}
92119

93-
// reconcileV2LB creates a load balancer. It also takes care of generating unique names across
94-
// namespaces by appending the namespace to the name.
95-
func (s *Service) reconcileV2LB(lbSpec *infrav1.AWSLoadBalancerSpec) error {
120+
// getOrCreateV2LB gets an existing load balancer, or creates a new one if it does not exist.
121+
// It also takes care of generating unique names across namespaces by appending the namespace to the name.
122+
// It returns a function that reconciles the load balancer.
123+
func (s *Service) getOrCreateV2LB(lbSpec *infrav1.AWSLoadBalancerSpec) (lbReconciler, error) {
96124
name, err := LBName(s.scope, lbSpec)
97125
if err != nil {
98-
return errors.Wrap(err, "failed to get control plane load balancer name")
126+
return nil, errors.Wrap(err, "failed to get control plane load balancer name")
99127
}
100128

101129
// Get default api server spec.
102130
desiredLB, err := s.getAPIServerLBSpec(name, lbSpec)
103131
if err != nil {
104-
return err
132+
return nil, err
105133
}
106134
lb, err := s.describeLB(name, lbSpec)
107135
switch {
108136
case IsNotFound(err) && s.scope.ControlPlaneEndpoint().IsValid():
109137
// if elb is not found and owner cluster ControlPlaneEndpoint is already populated, then we should not recreate the elb.
110-
return errors.Wrapf(err, "no loadbalancer exists for the AWSCluster %s, the cluster has become unrecoverable and should be deleted manually", s.scope.InfraClusterName())
138+
return nil, errors.Wrapf(err, "no loadbalancer exists for the AWSCluster %s, the cluster has become unrecoverable and should be deleted manually", s.scope.InfraClusterName())
111139
case IsNotFound(err):
112140
lb, err = s.createLB(desiredLB, lbSpec)
113141
if err != nil {
114142
s.scope.Error(err, "failed to create LB")
115-
return err
143+
return nil, err
116144
}
117145

118146
s.scope.Debug("Created new network load balancer for apiserver", "api-server-lb-name", lb.Name)
119147
case err != nil:
120148
// Failed to describe the classic ELB
121-
return err
149+
return nil, err
122150
}
123151

152+
return func() error {
153+
return s.reconcileV2LB(lb, desiredLB, lbSpec)
154+
}, nil
155+
}
156+
157+
func (s *Service) reconcileV2LB(lb *infrav1.LoadBalancer, desiredLB *infrav1.LoadBalancer, lbSpec *infrav1.AWSLoadBalancerSpec) error {
124158
wReq := &elbv2.DescribeLoadBalancersInput{
125159
LoadBalancerArns: aws.StringSlice([]string{lb.ARN}),
126160
}
@@ -507,37 +541,46 @@ func (s *Service) describeLB(name string, lbSpec *infrav1.AWSLoadBalancerSpec) (
507541
return fromSDKTypeToLB(out.LoadBalancers[0], outAtt.Attributes, tags), nil
508542
}
509543

510-
func (s *Service) reconcileClassicLoadBalancer() error {
544+
// getOrCreateClassicLoadBalancer gets an existing classic load balancer, or creates a new one if it does not exist.
545+
// It also takes care of generating unique names across namespaces by appending the namespace to the name.
546+
// It returns a function that reconciles the load balancer.
547+
func (s *Service) getOrCreateClassicLoadBalancer() (lbReconciler, error) {
511548
// Generate a default control plane load balancer name. The load balancer name cannot be
512549
// generated by the defaulting webhook, because it is derived from the cluster name, and that
513550
// name is undefined at defaulting time when generateName is used.
514551
name, err := ELBName(s.scope)
515552
if err != nil {
516-
return errors.Wrap(err, "failed to get control plane load balancer name")
553+
return nil, errors.Wrap(err, "failed to get control plane load balancer name")
517554
}
518555

519556
// Get default api server spec.
520557
spec, err := s.getAPIServerClassicELBSpec(name)
521558
if err != nil {
522-
return err
559+
return nil, err
523560
}
524561

525562
apiELB, err := s.describeClassicELB(spec.Name)
526563
switch {
527564
case IsNotFound(err) && s.scope.ControlPlaneEndpoint().IsValid():
528565
// if elb is not found and owner cluster ControlPlaneEndpoint is already populated, then we should not recreate the elb.
529-
return errors.Wrapf(err, "no loadbalancer exists for the AWSCluster %s, the cluster has become unrecoverable and should be deleted manually", s.scope.InfraClusterName())
566+
return nil, errors.Wrapf(err, "no loadbalancer exists for the AWSCluster %s, the cluster has become unrecoverable and should be deleted manually", s.scope.InfraClusterName())
530567
case IsNotFound(err):
531568
apiELB, err = s.createClassicELB(spec)
532569
if err != nil {
533-
return err
570+
return nil, err
534571
}
535572
s.scope.Debug("Created new classic load balancer for apiserver", "api-server-elb-name", apiELB.Name)
536573
case err != nil:
537574
// Failed to describe the classic ELB
538-
return err
575+
return nil, err
539576
}
540577

578+
return func() error {
579+
return s.reconcileClassicLoadBalancer(apiELB, spec)
580+
}, nil
581+
}
582+
583+
func (s *Service) reconcileClassicLoadBalancer(apiELB *infrav1.LoadBalancer, spec *infrav1.LoadBalancer) error {
541584
if apiELB.IsManaged(s.scope.Name()) {
542585
if !cmp.Equal(spec.ClassicElbAttributes, apiELB.ClassicElbAttributes) {
543586
err := s.configureAttributes(apiELB.Name, spec.ClassicElbAttributes)
@@ -546,6 +589,9 @@ func (s *Service) reconcileClassicLoadBalancer() error {
546589
}
547590
}
548591

592+
// BUG: note that describeClassicELB doesn't set HealthCheck in its output,
593+
// so we're configuring the health check on every reconcile whether it's
594+
// needed or not.
549595
if !cmp.Equal(spec.HealthCheck, apiELB.HealthCheck) {
550596
s.scope.Debug("Reconciling health check for apiserver load balancer", "health-check", spec.HealthCheck)
551597
err := s.configureHealthCheck(apiELB.Name, spec.HealthCheck)
@@ -597,7 +643,7 @@ func (s *Service) reconcileClassicLoadBalancer() error {
597643
}
598644

599645
func (s *Service) configureHealthCheck(name string, healthCheck *infrav1.ClassicELBHealthCheck) error {
600-
if _, err := s.ELBClient.ConfigureHealthCheck(&elb.ConfigureHealthCheckInput{
646+
healthCheckInput := &elb.ConfigureHealthCheckInput{
601647
LoadBalancerName: aws.String(name),
602648
HealthCheck: &elb.HealthCheck{
603649
Target: aws.String(healthCheck.Target),
@@ -606,7 +652,14 @@ func (s *Service) configureHealthCheck(name string, healthCheck *infrav1.Classic
606652
HealthyThreshold: aws.Int64(healthCheck.HealthyThreshold),
607653
UnhealthyThreshold: aws.Int64(healthCheck.UnhealthyThreshold),
608654
},
609-
}); err != nil {
655+
}
656+
657+
if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) {
658+
if _, err := s.ELBClient.ConfigureHealthCheck(healthCheckInput); err != nil {
659+
return false, err
660+
}
661+
return true, nil
662+
}, awserrors.LoadBalancerNotFound); err != nil {
610663
return errors.Wrapf(err, "failed to configure health check for classic load balancer: %s", name)
611664
}
612665
return nil
@@ -1193,30 +1246,15 @@ func (s *Service) createClassicELB(spec *infrav1.LoadBalancer) (*infrav1.LoadBal
11931246
return nil, errors.Wrapf(err, "failed to create classic load balancer: %v", spec)
11941247
}
11951248

1196-
if spec.HealthCheck != nil {
1197-
if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) {
1198-
if _, err := s.ELBClient.ConfigureHealthCheck(&elb.ConfigureHealthCheckInput{
1199-
LoadBalancerName: aws.String(spec.Name),
1200-
HealthCheck: &elb.HealthCheck{
1201-
Target: aws.String(spec.HealthCheck.Target),
1202-
Interval: aws.Int64(int64(spec.HealthCheck.Interval.Seconds())),
1203-
Timeout: aws.Int64(int64(spec.HealthCheck.Timeout.Seconds())),
1204-
HealthyThreshold: aws.Int64(spec.HealthCheck.HealthyThreshold),
1205-
UnhealthyThreshold: aws.Int64(spec.HealthCheck.UnhealthyThreshold),
1206-
},
1207-
}); err != nil {
1208-
return false, err
1209-
}
1210-
return true, nil
1211-
}, awserrors.LoadBalancerNotFound); err != nil {
1212-
return nil, errors.Wrapf(err, "failed to configure health check for classic load balancer: %v", spec)
1213-
}
1214-
}
1215-
12161249
s.scope.Info("Created classic load balancer", "dns-name", *out.DNSName)
12171250

12181251
res := spec.DeepCopy()
12191252
res.DNSName = *out.DNSName
1253+
1254+
// We haven't configured any health check yet. Don't report it here so it
1255+
// will be set later during reconciliation.
1256+
res.HealthCheck = nil
1257+
12201258
return res, nil
12211259
}
12221260

pkg/cloud/services/elb/loadbalancer_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2546,7 +2546,10 @@ func TestReconcileV2LB(t *testing.T) {
25462546
scope: clusterScope,
25472547
ELBV2Client: elbV2APIMocks,
25482548
}
2549-
err = s.reconcileV2LB(clusterScope.ControlPlaneLoadBalancer())
2549+
reconciler, err := s.getOrCreateV2LB(clusterScope.ControlPlaneLoadBalancer())
2550+
if err == nil {
2551+
err = reconciler()
2552+
}
25502553
lb := s.scope.Network().APIServerELB
25512554

25522555
tc.check(t, &lb, err)

0 commit comments

Comments
 (0)