Skip to content

Commit b3a6721

Browse files
authored
Merge pull request #5569 from mdbooth/issue-5568
✨Create multiple control plane loadbalancers concurrently
2 parents 4b2f770 + bd8471e commit b3a6721

File tree

2 files changed

+243
-72
lines changed

2 files changed

+243
-72
lines changed

pkg/cloud/services/elb/loadbalancer.go

Lines changed: 89 additions & 42 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
}
@@ -442,12 +476,21 @@ func (s *Service) createLB(spec *infrav1.LoadBalancer, lbSpec *infrav1.AWSLoadBa
442476

443477
// Target Groups and listeners will be reconciled separately
444478

445-
s.scope.Info("Created network load balancer", "dns-name", *out.LoadBalancers[0].DNSName)
479+
if out.LoadBalancers[0].DNSName == nil {
480+
return nil, fmt.Errorf("CreateLoadBalancer did not return a DNS name for %s", spec.Name)
481+
}
482+
dnsName := *out.LoadBalancers[0].DNSName
483+
if out.LoadBalancers[0].LoadBalancerArn == nil {
484+
return nil, fmt.Errorf("CreateLoadBalancer did not return an ARN for %s", spec.Name)
485+
}
486+
arn := *out.LoadBalancers[0].LoadBalancerArn
487+
488+
s.scope.Info("Created network load balancer", "dns-name", dnsName)
446489

447490
res := spec.DeepCopy()
448-
s.scope.Debug("applying load balancer DNS to result", "dns", *out.LoadBalancers[0].DNSName)
449-
res.DNSName = *out.LoadBalancers[0].DNSName
450-
res.ARN = *out.LoadBalancers[0].LoadBalancerArn
491+
s.scope.Debug("applying load balancer DNS to result", "dns", dnsName)
492+
res.DNSName = dnsName
493+
res.ARN = arn
451494
return res, nil
452495
}
453496

@@ -507,37 +550,46 @@ func (s *Service) describeLB(name string, lbSpec *infrav1.AWSLoadBalancerSpec) (
507550
return fromSDKTypeToLB(out.LoadBalancers[0], outAtt.Attributes, tags), nil
508551
}
509552

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

519565
// Get default api server spec.
520566
spec, err := s.getAPIServerClassicELBSpec(name)
521567
if err != nil {
522-
return err
568+
return nil, err
523569
}
524570

525571
apiELB, err := s.describeClassicELB(spec.Name)
526572
switch {
527573
case IsNotFound(err) && s.scope.ControlPlaneEndpoint().IsValid():
528574
// 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())
575+
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())
530576
case IsNotFound(err):
531577
apiELB, err = s.createClassicELB(spec)
532578
if err != nil {
533-
return err
579+
return nil, err
534580
}
535581
s.scope.Debug("Created new classic load balancer for apiserver", "api-server-elb-name", apiELB.Name)
536582
case err != nil:
537583
// Failed to describe the classic ELB
538-
return err
584+
return nil, err
539585
}
540586

587+
return func() error {
588+
return s.reconcileClassicLoadBalancer(apiELB, spec)
589+
}, nil
590+
}
591+
592+
func (s *Service) reconcileClassicLoadBalancer(apiELB *infrav1.LoadBalancer, spec *infrav1.LoadBalancer) error {
541593
if apiELB.IsManaged(s.scope.Name()) {
542594
if !cmp.Equal(spec.ClassicElbAttributes, apiELB.ClassicElbAttributes) {
543595
err := s.configureAttributes(apiELB.Name, spec.ClassicElbAttributes)
@@ -546,6 +598,9 @@ func (s *Service) reconcileClassicLoadBalancer() error {
546598
}
547599
}
548600

601+
// BUG: note that describeClassicELB doesn't set HealthCheck in its output,
602+
// so we're configuring the health check on every reconcile whether it's
603+
// needed or not.
549604
if !cmp.Equal(spec.HealthCheck, apiELB.HealthCheck) {
550605
s.scope.Debug("Reconciling health check for apiserver load balancer", "health-check", spec.HealthCheck)
551606
err := s.configureHealthCheck(apiELB.Name, spec.HealthCheck)
@@ -597,7 +652,7 @@ func (s *Service) reconcileClassicLoadBalancer() error {
597652
}
598653

599654
func (s *Service) configureHealthCheck(name string, healthCheck *infrav1.ClassicELBHealthCheck) error {
600-
if _, err := s.ELBClient.ConfigureHealthCheck(&elb.ConfigureHealthCheckInput{
655+
healthCheckInput := &elb.ConfigureHealthCheckInput{
601656
LoadBalancerName: aws.String(name),
602657
HealthCheck: &elb.HealthCheck{
603658
Target: aws.String(healthCheck.Target),
@@ -606,7 +661,14 @@ func (s *Service) configureHealthCheck(name string, healthCheck *infrav1.Classic
606661
HealthyThreshold: aws.Int64(healthCheck.HealthyThreshold),
607662
UnhealthyThreshold: aws.Int64(healthCheck.UnhealthyThreshold),
608663
},
609-
}); err != nil {
664+
}
665+
666+
if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) {
667+
if _, err := s.ELBClient.ConfigureHealthCheck(healthCheckInput); err != nil {
668+
return false, err
669+
}
670+
return true, nil
671+
}, awserrors.LoadBalancerNotFound); err != nil {
610672
return errors.Wrapf(err, "failed to configure health check for classic load balancer: %s", name)
611673
}
612674
return nil
@@ -1193,30 +1255,15 @@ func (s *Service) createClassicELB(spec *infrav1.LoadBalancer) (*infrav1.LoadBal
11931255
return nil, errors.Wrapf(err, "failed to create classic load balancer: %v", spec)
11941256
}
11951257

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-
12161258
s.scope.Info("Created classic load balancer", "dns-name", *out.DNSName)
12171259

12181260
res := spec.DeepCopy()
12191261
res.DNSName = *out.DNSName
1262+
1263+
// We haven't configured any health check yet. Don't report it here so it
1264+
// will be set later during reconciliation.
1265+
res.HealthCheck = nil
1266+
12201267
return res, nil
12211268
}
12221269

0 commit comments

Comments
 (0)