diff --git a/api/v1beta1/types.go b/api/v1beta1/types.go index 7a5344ca25b..fd46d95c65b 100644 --- a/api/v1beta1/types.go +++ b/api/v1beta1/types.go @@ -388,6 +388,11 @@ const ( Public = LBType("Public") ) +const ( + // IPv6 is the value for IPv6 address version. + IPv6 = "IPv6" +) + // FrontendIP defines a load balancer frontend IP configuration. type FrontendIP struct { // +kubebuilder:validation:MinLength=1 diff --git a/api/v1beta1/types_class.go b/api/v1beta1/types_class.go index 58b73192059..ceccc46c9dc 100644 --- a/api/v1beta1/types_class.go +++ b/api/v1beta1/types_class.go @@ -535,4 +535,9 @@ type SecurityGroupClass struct { type FrontendIPClass struct { // +optional PrivateIPAddress string `json:"privateIP,omitempty"` + // IPVersion specifies the IP version for this frontend IP. Valid values are "IPv4" and "IPv6". + // Defaults to "IPv4" if not specified. + // +kubebuilder:validation:Enum=IPv4;IPv6 + // +optional + IPVersion string `json:"ipVersion,omitempty"` } diff --git a/azure/scope/cluster.go b/azure/scope/cluster.go index d057c254d1e..4af5aafa69e 100644 --- a/azure/scope/cluster.go +++ b/azure/scope/cluster.go @@ -163,8 +163,8 @@ func (s *ClusterScope) PublicIPSpecs() []azure.ResourceSpecGetter { Name: ip.PublicIP.Name, ResourceGroup: s.ResourceGroup(), ClusterName: s.ClusterName(), - DNSName: "", // Set to default value - IsIPv6: false, // Set to default value + DNSName: "", // Set to default value + IsIPv6: ip.IPVersion == infrav1.IPv6, Location: s.Location(), ExtendedLocation: s.ExtendedLocation(), FailureDomains: s.FailureDomains(), @@ -179,7 +179,7 @@ func (s *ClusterScope) PublicIPSpecs() []azure.ResourceSpecGetter { Name: s.APIServerPublicIP().Name, ResourceGroup: s.ResourceGroup(), DNSName: s.APIServerPublicIP().DNSName, - IsIPv6: false, // Currently azure requires an IPv4 lb rule to enable IPv6 + IsIPv6: s.APIServerLB().FrontendIPs[0].IPVersion == infrav1.IPv6, ClusterName: s.ClusterName(), Location: s.Location(), ExtendedLocation: s.ExtendedLocation(), @@ -199,8 +199,8 @@ func (s *ClusterScope) PublicIPSpecs() []azure.ResourceSpecGetter { Name: ip.PublicIP.Name, ResourceGroup: s.ResourceGroup(), ClusterName: s.ClusterName(), - DNSName: "", // Set to default value - IsIPv6: false, // Set to default value + DNSName: "", // Set to default value + IsIPv6: ip.IPVersion == infrav1.IPv6, Location: s.Location(), ExtendedLocation: s.ExtendedLocation(), FailureDomains: s.FailureDomains(), diff --git a/azure/services/loadbalancers/loadbalancers_test.go b/azure/services/loadbalancers/loadbalancers_test.go index 3a61789a507..ae17db88487 100644 --- a/azure/services/loadbalancers/loadbalancers_test.go +++ b/azure/services/loadbalancers/loadbalancers_test.go @@ -132,6 +132,58 @@ var ( }, } + fakeInternalAPILBSpecIPv6 = LBSpec{ + Name: "my-private-lb", + ResourceGroup: "my-rg", + SubscriptionID: "123", + ClusterName: "my-cluster", + Location: "my-location", + VNetName: "my-vnet", + VNetResourceGroup: "my-rg", + Role: infrav1.APIServerRole, + Type: infrav1.Internal, + SKU: infrav1.SKUStandard, + SubnetName: "my-cp-subnet", + BackendPoolName: "my-private-lb-backendPool", + IdleTimeoutInMinutes: ptr.To[int32](4), + FrontendIPConfigs: []infrav1.FrontendIP{ + { + Name: "my-private-lb-frontEnd-ipv6", + FrontendIPClass: infrav1.FrontendIPClass{ + IPVersion: infrav1.IPv6, + }, + }, + }, + APIServerPort: 6443, + } + + fakePublicAPILBSpecIPv6 = LBSpec{ + Name: "my-publiclb", + ResourceGroup: "my-rg", + SubscriptionID: "123", + ClusterName: "my-cluster", + Location: "my-location", + Role: infrav1.APIServerRole, + Type: infrav1.Public, + SKU: infrav1.SKUStandard, + SubnetName: "my-cp-subnet", + BackendPoolName: "my-publiclb-backendPool", + IdleTimeoutInMinutes: ptr.To[int32](4), + FrontendIPConfigs: []infrav1.FrontendIP{ + { + Name: "my-publiclb-frontEnd-ipv6", + FrontendIPClass: infrav1.FrontendIPClass{ + IPVersion: infrav1.IPv6, + }, + PublicIP: &infrav1.PublicIPSpec{ + Name: "my-publicip-ipv6", + DNSName: "my-cluster.12345.mydomain.com", + }, + }, + }, + APIServerPort: 6443, + } + internalError = &azcore.ResponseError{ RawResponse: &http.Response{ Body: io.NopCloser(strings.NewReader("#: Internal Server Error: StatusCode=500")), diff --git a/azure/services/loadbalancers/spec.go b/azure/services/loadbalancers/spec.go index cbdf9ffe48e..3fdf1cf6934 100644 --- a/azure/services/loadbalancers/spec.go +++ b/azure/services/loadbalancers/spec.go @@ -177,6 +177,11 @@ func getFrontendIPConfigs(lbSpec LBSpec) ([]*armnetwork.FrontendIPConfiguration, }, PrivateIPAddress: ptr.To(ipConfig.PrivateIPAddress), } + if ipConfig.IPVersion == infrav1.IPv6 { + properties.PrivateIPAddressVersion = ptr.To(armnetwork.IPVersionIPv6) + properties.PrivateIPAllocationMethod = ptr.To(armnetwork.IPAllocationMethodDynamic) + properties.PrivateIPAddress = nil + } } else { properties = armnetwork.FrontendIPConfigurationPropertiesFormat{ PublicIPAddress: &armnetwork.PublicIPAddress{ diff --git a/azure/services/loadbalancers/spec_test.go b/azure/services/loadbalancers/spec_test.go index 691e1ce617b..f43cead783c 100644 --- a/azure/services/loadbalancers/spec_test.go +++ b/azure/services/loadbalancers/spec_test.go @@ -178,6 +178,36 @@ func TestParameters(t *testing.T) { }, expectedError: "", }, + { + name: "internal API load balancer with IPv6 frontend", + spec: &fakeInternalAPILBSpecIPv6, + existing: nil, + expect: func(g *WithT, result any) { + g.Expect(result).To(BeAssignableToTypeOf(armnetwork.LoadBalancer{})) + lb := result.(armnetwork.LoadBalancer) + g.Expect(lb.Properties.FrontendIPConfigurations).To(HaveLen(1)) + frontendIP := lb.Properties.FrontendIPConfigurations[0] + g.Expect(*frontendIP.Name).To(Equal("my-private-lb-frontEnd-ipv6")) + g.Expect(*frontendIP.Properties.PrivateIPAllocationMethod).To(Equal(armnetwork.IPAllocationMethodDynamic)) + g.Expect(*frontendIP.Properties.PrivateIPAddressVersion).To(Equal(armnetwork.IPVersionIPv6)) + g.Expect(frontendIP.Properties.PrivateIPAddress).To(BeNil()) + }, + expectedError: "", + }, + { + name: "public API load balancer with IPv6 frontend", + spec: &fakePublicAPILBSpecIPv6, + existing: nil, + expect: func(g *WithT, result any) { + g.Expect(result).To(BeAssignableToTypeOf(armnetwork.LoadBalancer{})) + lb := result.(armnetwork.LoadBalancer) + g.Expect(lb.Properties.FrontendIPConfigurations).To(HaveLen(1)) + frontendIP := lb.Properties.FrontendIPConfigurations[0] + g.Expect(*frontendIP.Name).To(Equal("my-publiclb-frontEnd-ipv6")) + g.Expect(*frontendIP.Properties.PublicIPAddress.ID).To(Equal("/subscriptions/123/resourceGroups/my-rg/providers/Microsoft.Network/publicIPAddresses/my-publicip-ipv6")) + }, + expectedError: "", + }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { diff --git a/docs/book/src/self-managed/api-server-endpoint.md b/docs/book/src/self-managed/api-server-endpoint.md index 0016505c0db..7f7157c4e5b 100644 --- a/docs/book/src/self-managed/api-server-endpoint.md +++ b/docs/book/src/self-managed/api-server-endpoint.md @@ -101,6 +101,50 @@ Note that `dns` is the FQDN associated to your public IP address (look for "DNS When you BYO api server IP, CAPZ does not manage its lifecycle, ie. the IP will not get deleted as part of cluster deletion. +### Frontend IP Version + +By default, frontend IPs are IPv4. You can configure individual frontend IPs to use IPv6 by setting the `ipVersion` field to `"IPv6"`. + +For a public load balancer with an IPv6 frontend: + +```yaml +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: AzureCluster +metadata: + name: my-cluster + namespace: default +spec: + location: eastus + networkSpec: + apiServerLB: + type: Public + frontendIPs: + - name: lb-public-ip-frontend-ipv6 + ipVersion: IPv6 + publicIP: + name: my-public-ipv6 +``` + +For an internal load balancer with an IPv6 frontend: + +```yaml +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: AzureCluster +metadata: + name: my-private-cluster + namespace: default +spec: + location: eastus + networkSpec: + apiServerLB: + type: Internal + frontendIPs: + - name: lb-private-ip-frontend-ipv6 + ipVersion: IPv6 +``` + +Note that IPv6 internal frontend IPs use dynamic allocation and do not support specifying a `privateIP` address. + ### Load Balancer SKU At this time, CAPZ only supports Azure Standard Load Balancers. See [SKU comparison](https://learn.microsoft.com/azure/load-balancer/skus#skus) for more information on Azure Load Balancers SKUs.