Skip to content
Open
37 changes: 37 additions & 0 deletions api/v1beta1/azurecluster_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,43 @@ func (*AzureClusterWebhook) ValidateUpdate(_ context.Context, oldRaw, newObj run
allErrs = append(allErrs, err)
}

// Validate availability zones are immutable for load balancers
if c.Spec.NetworkSpec.APIServerLB != nil && old.Spec.NetworkSpec.APIServerLB != nil {
if !webhookutils.EnsureStringSlicesAreEquivalent(
c.Spec.NetworkSpec.APIServerLB.AvailabilityZones,
old.Spec.NetworkSpec.APIServerLB.AvailabilityZones) {
allErrs = append(allErrs,
field.Invalid(
field.NewPath("spec", "networkSpec", "apiServerLB", "availabilityZones"),
c.Spec.NetworkSpec.APIServerLB.AvailabilityZones,
"field is immutable"))
}
}

if c.Spec.NetworkSpec.NodeOutboundLB != nil && old.Spec.NetworkSpec.NodeOutboundLB != nil {
if !webhookutils.EnsureStringSlicesAreEquivalent(
c.Spec.NetworkSpec.NodeOutboundLB.AvailabilityZones,
old.Spec.NetworkSpec.NodeOutboundLB.AvailabilityZones) {
allErrs = append(allErrs,
field.Invalid(
field.NewPath("spec", "networkSpec", "nodeOutboundLB", "availabilityZones"),
c.Spec.NetworkSpec.NodeOutboundLB.AvailabilityZones,
"field is immutable"))
}
}

if c.Spec.NetworkSpec.ControlPlaneOutboundLB != nil && old.Spec.NetworkSpec.ControlPlaneOutboundLB != nil {
if !webhookutils.EnsureStringSlicesAreEquivalent(
c.Spec.NetworkSpec.ControlPlaneOutboundLB.AvailabilityZones,
old.Spec.NetworkSpec.ControlPlaneOutboundLB.AvailabilityZones) {
allErrs = append(allErrs,
field.Invalid(
field.NewPath("spec", "networkSpec", "controlPlaneOutboundLB", "availabilityZones"),
c.Spec.NetworkSpec.ControlPlaneOutboundLB.AvailabilityZones,
"field is immutable"))
}
}

allErrs = append(allErrs, c.validateSubnetUpdate(old)...)

if len(allErrs) == 0 {
Expand Down
8 changes: 8 additions & 0 deletions api/v1beta1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,14 @@ type LoadBalancerSpec struct {
// BackendPool describes the backend pool of the load balancer.
// +optional
BackendPool BackendPool `json:"backendPool,omitempty"`
// AvailabilityZones is a list of availability zones for the load balancer.
// When specified for an internal load balancer, the frontend IP configuration
// will be zone-redundant across the specified zones.
// For public load balancers, this should be set on the associated public IP addresses instead.
// +optional
// +listType=set
// +kubebuilder:validation:MaxItems=3
AvailabilityZones []string `json:"availabilityZones,omitempty"`

LoadBalancerClassSpec `json:",inline"`
}
Expand Down
5 changes: 5 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions azure/scope/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ func (s *ClusterScope) LBSpecs() []azure.ResourceSpecGetter {
IdleTimeoutInMinutes: s.APIServerLB().IdleTimeoutInMinutes,
AdditionalTags: s.AdditionalTags(),
AdditionalPorts: s.AdditionalAPIServerLBPorts(),
AvailabilityZones: s.APIServerLB().AvailabilityZones,
}

if s.APIServerLB().FrontendIPs != nil {
Expand Down Expand Up @@ -301,6 +302,7 @@ func (s *ClusterScope) LBSpecs() []azure.ResourceSpecGetter {
IdleTimeoutInMinutes: s.APIServerLB().IdleTimeoutInMinutes,
AdditionalTags: s.AdditionalTags(),
AdditionalPorts: s.AdditionalAPIServerLBPorts(),
AvailabilityZones: s.APIServerLB().AvailabilityZones,
}

privateIPFound := false
Expand Down Expand Up @@ -348,6 +350,7 @@ func (s *ClusterScope) LBSpecs() []azure.ResourceSpecGetter {
IdleTimeoutInMinutes: s.NodeOutboundLB().IdleTimeoutInMinutes,
Role: infrav1.NodeOutboundRole,
AdditionalTags: s.AdditionalTags(),
AvailabilityZones: s.NodeOutboundLB().AvailabilityZones,
})
}

Expand All @@ -369,6 +372,7 @@ func (s *ClusterScope) LBSpecs() []azure.ResourceSpecGetter {
IdleTimeoutInMinutes: s.ControlPlaneOutboundLB().IdleTimeoutInMinutes,
Role: infrav1.ControlPlaneOutboundRole,
AdditionalTags: s.AdditionalTags(),
AvailabilityZones: s.ControlPlaneOutboundLB().AvailabilityZones,
})
}

Expand Down
24 changes: 24 additions & 0 deletions azure/services/loadbalancers/loadbalancers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,30 @@ var (
APIServerPort: 6443,
}

fakeInternalAPILBSpecWithZones = LBSpec{
Name: "my-private-lb",
ResourceGroup: "my-rg",
SubscriptionID: "123",
ClusterName: "my-cluster",
Location: "my-location",
Role: infrav1.APIServerRole,
Type: infrav1.Internal,
SKU: infrav1.SKUStandard,
SubnetName: "my-cp-subnet",
BackendPoolName: "my-private-lb-backendPool",
IdleTimeoutInMinutes: ptr.To[int32](4),
AvailabilityZones: []string{"1", "2", "3"},
FrontendIPConfigs: []infrav1.FrontendIP{
{
Name: "my-private-lb-frontEnd",
FrontendIPClass: infrav1.FrontendIPClass{
PrivateIPAddress: "10.0.0.10",
},
},
},
APIServerPort: 6443,
}

fakeNodeOutboundLBSpec = LBSpec{
Name: "my-cluster",
ResourceGroup: "my-rg",
Expand Down
12 changes: 12 additions & 0 deletions azure/services/loadbalancers/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type LBSpec struct {
IdleTimeoutInMinutes *int32
AdditionalTags map[string]string
AdditionalPorts []infrav1.LoadBalancerPort
AvailabilityZones []string
}

// ResourceName returns the name of the load balancer.
Expand Down Expand Up @@ -167,6 +168,16 @@ func (s *LBSpec) Parameters(_ context.Context, existing interface{}) (parameters
func getFrontendIPConfigs(lbSpec LBSpec) ([]*armnetwork.FrontendIPConfiguration, []*armnetwork.SubResource) {
frontendIPConfigurations := make([]*armnetwork.FrontendIPConfiguration, 0)
frontendIDs := make([]*armnetwork.SubResource, 0)

// Convert availability zones to []*string for Azure SDK
var zones []*string
if len(lbSpec.AvailabilityZones) > 0 {
zones = make([]*string, len(lbSpec.AvailabilityZones))
for i, zone := range lbSpec.AvailabilityZones {
zones[i] = ptr.To(zone)
}
}

for _, ipConfig := range lbSpec.FrontendIPConfigs {
var properties armnetwork.FrontendIPConfigurationPropertiesFormat
if lbSpec.Type == infrav1.Internal {
Expand All @@ -187,6 +198,7 @@ func getFrontendIPConfigs(lbSpec LBSpec) ([]*armnetwork.FrontendIPConfiguration,
frontendIPConfigurations = append(frontendIPConfigurations, &armnetwork.FrontendIPConfiguration{
Properties: &properties,
Name: ptr.To(ipConfig.Name),
Zones: zones,
})
frontendIDs = append(frontendIDs, &armnetwork.SubResource{
ID: ptr.To(azure.FrontendIPConfigID(lbSpec.SubscriptionID, lbSpec.ResourceGroup, lbSpec.Name, ipConfig.Name)),
Expand Down
16 changes: 16 additions & 0 deletions azure/services/loadbalancers/spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,22 @@ func TestParameters(t *testing.T) {
},
expectedError: "",
},
{
name: "internal load balancer with availability zones",
spec: &fakeInternalAPILBSpecWithZones,
existing: nil,
expect: func(g *WithT, result interface{}) {
g.Expect(result).To(BeAssignableToTypeOf(armnetwork.LoadBalancer{}))
lb := result.(armnetwork.LoadBalancer)
// Verify zones are set on frontend IP configuration
g.Expect(lb.Properties.FrontendIPConfigurations).To(HaveLen(1))
g.Expect(lb.Properties.FrontendIPConfigurations[0].Zones).To(HaveLen(3))
g.Expect(*lb.Properties.FrontendIPConfigurations[0].Zones[0]).To(Equal("1"))
g.Expect(*lb.Properties.FrontendIPConfigurations[0].Zones[1]).To(Equal("2"))
g.Expect(*lb.Properties.FrontendIPConfigurations[0].Zones[2]).To(Equal("3"))
},
expectedError: "",
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,17 @@ spec:
description: APIServerLB is the configuration for the control-plane
load balancer.
properties:
availabilityZones:
description: |-
AvailabilityZones is a list of availability zones for the load balancer.
When specified for an internal load balancer, the frontend IP configuration
will be zone-redundant across the specified zones.
For public load balancers, this should be set on the associated public IP addresses instead.
items:
type: string
maxItems: 3
type: array
x-kubernetes-list-type: set
backendPool:
description: BackendPool describes the backend pool of the
load balancer.
Expand Down Expand Up @@ -772,6 +783,17 @@ spec:
ControlPlaneOutboundLB is the configuration for the control-plane outbound load balancer.
This is different from APIServerLB, and is used only in private clusters (optionally) for enabling outbound traffic.
properties:
availabilityZones:
description: |-
AvailabilityZones is a list of availability zones for the load balancer.
When specified for an internal load balancer, the frontend IP configuration
will be zone-redundant across the specified zones.
For public load balancers, this should be set on the associated public IP addresses instead.
items:
type: string
maxItems: 3
type: array
x-kubernetes-list-type: set
backendPool:
description: BackendPool describes the backend pool of the
load balancer.
Expand Down Expand Up @@ -854,6 +876,17 @@ spec:
description: NodeOutboundLB is the configuration for the node
outbound load balancer.
properties:
availabilityZones:
description: |-
AvailabilityZones is a list of availability zones for the load balancer.
When specified for an internal load balancer, the frontend IP configuration
will be zone-redundant across the specified zones.
For public load balancers, this should be set on the associated public IP addresses instead.
items:
type: string
maxItems: 3
type: array
x-kubernetes-list-type: set
backendPool:
description: BackendPool describes the backend pool of the
load balancer.
Expand Down
1 change: 1 addition & 0 deletions docs/book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
- [Externally managed Azure infrastructure](./self-managed/externally-managed-azure-infrastructure.md)
- [Failure Domains](./self-managed/failure-domains.md)
- [Flatcar](./self-managed/flatcar.md)
- [Load Balancer Zone Redundancy](./self-managed/load-balancer-zone-redundancy.md)
- [GPU-enabled Clusters](./self-managed/gpu.md)
- [IPv6](./self-managed/ipv6.md)
- [Machine Pools (VMSS)](./self-managed/machinepools.md)
Expand Down
Loading
Loading