Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Container Load Balancer - Network Security Group (NSG) #8054

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions internal/testutil/fixture/azure_loadbalancer.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ func (f *AzureLoadBalancerFixture) IPv4Addresses() []string {
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add some details about the change in the PR description?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated the PR description

}

func (f *AzureLoadBalancerFixture) IPv4PrefixAddress() []string {
return []string{
"10.0.0.1/32",
}
}

func (f *AzureLoadBalancerFixture) IPv6PrefixAddress() []string {
return []string{
"2001:db8:ac10:fe01::1/128",
}
}

func (f *AzureLoadBalancerFixture) IPv6Addresses() []string {
return []string{
"2001:db8:ac10:fe01::",
Expand Down
27 changes: 27 additions & 0 deletions internal/testutil/fixture/azure_managedcluster.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package fixture

import (
armcontainerservice "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v6"
"k8s.io/utils/ptr"
)

func (f *AzureFixture) ManagedCluster() *AzureManagedClusterFixture {
return &AzureManagedClusterFixture{
mc: &armcontainerservice.ManagedCluster{
Name: ptr.To("mangaedcluster"),
Properties: &armcontainerservice.ManagedClusterProperties{
NetworkProfile: &armcontainerservice.NetworkProfile{
PodCidrs: []*string{ptr.To("192.1.0.0/16")},
},
},
},
}
}

type AzureManagedClusterFixture struct {
mc *armcontainerservice.ManagedCluster
}

func (f *AzureManagedClusterFixture) Build() *armcontainerservice.ManagedCluster {
return f.mc
}
36 changes: 36 additions & 0 deletions internal/testutil/fixture/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,42 @@ func (f *KubernetesServiceFixture) WithIngressIPs(ips []string) *KubernetesServi
return f
}

func (f *KubernetesServiceFixture) WithOnlyTCPPorts() *KubernetesServiceFixture {
f.svc.Spec.Ports = []v1.ServicePort{
{
Name: "http",
Protocol: v1.ProtocolTCP,
Port: 80,
NodePort: 50080,
},
{
Name: "dns-tcp",
Protocol: v1.ProtocolTCP,
Port: 53,
NodePort: 50053,
},
{
Name: "https",
Protocol: v1.ProtocolTCP,
Port: 443,
NodePort: 50443,
},
}
return f
}

func (f *KubernetesServiceFixture) WithOnlyUDPPorts() *KubernetesServiceFixture {
f.svc.Spec.Ports = []v1.ServicePort{
{
Name: "dns-udp",
Protocol: v1.ProtocolUDP,
Port: 53,
NodePort: 50053,
},
}
return f
}

func (f *KubernetesServiceFixture) Build() v1.Service {
return f.svc
}
5 changes: 2 additions & 3 deletions pkg/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,9 +381,8 @@ const (
LoadBalancerBackendPoolConfigurationTypeNodeIPConfiguration = "nodeIPConfiguration"
// LoadBalancerBackendPoolConfigurationTypeNodeIP is the lb backend pool config type node ip
LoadBalancerBackendPoolConfigurationTypeNodeIP = "nodeIP"
// LoadBalancerBackendPoolConfigurationTypePODIP is the lb backend pool config type pod ip
// TODO (nilo19): support pod IP in the future
LoadBalancerBackendPoolConfigurationTypePODIP = "podIP"
// LoadBalancerBackendPoolConfigurationTypePodIP is the lb backend pool config type pod ip
LoadBalancerBackendPoolConfigurationTypePodIP = "podIP"
)

// error messages
Expand Down
4 changes: 2 additions & 2 deletions pkg/provider/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,13 +294,13 @@ func (az *Cloud) InitializeCloudFromConfig(ctx context.Context, config *config.C

if config.LoadBalancerBackendPoolConfigurationType == "" ||
// TODO(nilo19): support pod IP mode in the future
strings.EqualFold(config.LoadBalancerBackendPoolConfigurationType, consts.LoadBalancerBackendPoolConfigurationTypePODIP) {
strings.EqualFold(config.LoadBalancerBackendPoolConfigurationType, consts.LoadBalancerBackendPoolConfigurationTypePodIP) {
config.LoadBalancerBackendPoolConfigurationType = consts.LoadBalancerBackendPoolConfigurationTypeNodeIPConfiguration
} else {
supportedLoadBalancerBackendPoolConfigurationTypes := utilsets.NewString(
strings.ToLower(consts.LoadBalancerBackendPoolConfigurationTypeNodeIPConfiguration),
strings.ToLower(consts.LoadBalancerBackendPoolConfigurationTypeNodeIP),
strings.ToLower(consts.LoadBalancerBackendPoolConfigurationTypePODIP))
strings.ToLower(consts.LoadBalancerBackendPoolConfigurationTypePodIP))
if !supportedLoadBalancerBackendPoolConfigurationTypes.Has(strings.ToLower(config.LoadBalancerBackendPoolConfigurationType)) {
return fmt.Errorf("loadBalancerBackendPoolConfigurationType %s is not supported, supported values are %v", config.LoadBalancerBackendPoolConfigurationType, supportedLoadBalancerBackendPoolConfigurationTypes.UnsortedList())
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/provider/azure_fakes.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"sigs.k8s.io/cloud-provider-azure/pkg/azclient/diskclient/mock_diskclient"
"sigs.k8s.io/cloud-provider-azure/pkg/azclient/interfaceclient/mock_interfaceclient"
"sigs.k8s.io/cloud-provider-azure/pkg/azclient/loadbalancerclient/mock_loadbalancerclient"
"sigs.k8s.io/cloud-provider-azure/pkg/azclient/managedclusterclient/mock_managedclusterclient"
"sigs.k8s.io/cloud-provider-azure/pkg/azclient/mock_azclient"
"sigs.k8s.io/cloud-provider-azure/pkg/azclient/privateendpointclient/mock_privateendpointclient"
"sigs.k8s.io/cloud-provider-azure/pkg/azclient/privatelinkserviceclient/mock_privatelinkserviceclient"
Expand Down Expand Up @@ -134,6 +135,8 @@ func GetTestCloud(ctrl *gomock.Controller) (az *Cloud) {
clientFactory.EXPECT().GetVirtualMachineScaleSetClient().Return(virtualMachineScaleSetsClient).AnyTimes()
virtualMachineScaleSetVMsClient := mock_virtualmachinescalesetvmclient.NewMockInterface(ctrl)
clientFactory.EXPECT().GetVirtualMachineScaleSetVMClient().Return(virtualMachineScaleSetVMsClient).AnyTimes()
managedClusterClient := mock_managedclusterclient.NewMockInterface(ctrl)
clientFactory.EXPECT().GetManagedClusterClient().Return(managedClusterClient).AnyTimes()

virtualMachinesClient := mock_virtualmachineclient.NewMockInterface(ctrl)
clientFactory.EXPECT().GetVirtualMachineClient().Return(virtualMachinesClient).AnyTimes()
Expand Down Expand Up @@ -185,3 +188,10 @@ func GetTestCloudWithExtendedLocation(ctrl *gomock.Controller) (az *Cloud) {
az.Config.ExtendedLocationType = "EdgeZone"
return az
}

// GetTestCloudWithContainerLoadBalancer returns a fake azure cloud for unit tests in Azure supporting container load balancer.
func GetTestCloudWithContainerLoadBalancer(ctrl *gomock.Controller) (az *Cloud) {
az = GetTestCloud(ctrl)
az.LoadBalancerBackendPoolConfigurationType = consts.LoadBalancerBackendPoolConfigurationTypePodIP
return az
}
90 changes: 63 additions & 27 deletions pkg/provider/azure_loadbalancer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3063,48 +3063,84 @@ func (az *Cloud) reconcileSecurityGroup(
}

var (
disableFloatingIP = consts.IsK8sServiceDisableLoadBalancerFloatingIP(service)
lbIPAddresses, _ = iputil.ParseAddresses(lbIPs)
lbIPv4Addresses, lbIPv6Addresses = iputil.GroupAddressesByFamily(lbIPAddresses)
additionalIPv4Addresses, additionalIPv6Addresses = iputil.GroupAddressesByFamily(additionalIPs)
backendIPv4Addresses, backendIPv6Addresses []netip.Addr
dstIPv4Addresses, dstIPv6Addresses []netip.Addr
dstIpv4AddressPrefix, dstIpv6AddressPrefix []netip.Prefix
)
{
// Get backend node IPs
lb, lbFound, err := az.getAzureLoadBalancer(ctx, lbName, azcache.CacheReadTypeDefault)
{

if az.IsLBBackendPoolTypePodIP() {
if !az.RetrievedClusterPodCidr {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, this would block the security boundary of the Kubernetes cluster. The cluster identity should not have the permission to manage itself.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's use new cloud config options and ask the provisioning service (e.g. AKS or capz) to setup the CIDRs on changing.

mcClient := az.NetworkClientFactory.GetManagedClusterClient()
managedCluster, err := mcClient.Get(ctx, az.ResourceGroup, clusterName)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to get managed cluster: %w", err)
}

if managedCluster.Properties == nil || managedCluster.Properties.NetworkProfile == nil ||
managedCluster.Properties.NetworkProfile.PodCidrs == nil {
klog.Errorf("Failed to get PodCidrs for cluster %q", clusterName)
return nil, fmt.Errorf("failed to get PodCidrs for cluster %q", clusterName)
}
if wantLb && !lbFound {
logger.Error(err, "Failed to get load balancer")
return nil, fmt.Errorf("unable to get lb %s", lbName)
podCidrs := managedCluster.Properties.NetworkProfile.PodCidrs
if len(podCidrs) == 0 {
klog.Errorf("Failed to get PodCidrs for cluster %q", clusterName)
return nil, fmt.Errorf("failed to get PodCidrs for cluster %q", clusterName)
}
for _, podCidr := range podCidrs {
prefix, parseErr := netip.ParsePrefix(*podCidr)
if parseErr != nil {
klog.Errorf("Failed to parse PodCidr %q: %v", *podCidr, parseErr)
return nil, fmt.Errorf("failed to parse PodCidr %q: %w", *podCidr, parseErr)
}
if prefix.Addr().Is4() {
dstIpv4AddressPrefix = append(dstIpv4AddressPrefix, prefix)
az.PodCidrIPv4 = prefix
} else {
dstIpv6AddressPrefix = append(dstIpv6AddressPrefix, prefix)
az.PodCidrIPv6 = prefix
}
}
az.RetrievedClusterPodCidr = true

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can podcidrs change during cluster operations (eg. adding new node pool)? If yes, then do we need to handle this differently to account for potential new cidrs?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I understand, the pod subnet (hence the cidr) is set during cluster's deployment and cannot be updated during its lifetime. Please confirm @kartickmsft

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that can happen when completely new node pool is added: https://learn.microsoft.com/en-us/azure/aks/create-node-pools#add-a-node-pool-with-a-unique-subnet

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

POD subnet can be newly configured for a new nodepool for Azure CNI Dynamic IP allocation and Enhanced subnet option (https://learn.microsoft.com/en-us/azure/aks/configure-azure-cni-dynamic-ip-allocation#adding-node-pool).
So, yeah, we need to handle it differently. As per my understanding, cloud-provider doesn't get any notification about nodepool addition/deletion. If this understanding is correct, maybe reading PODCIDRs each time maybe the only option.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it may change. We'd need to figure out a way to get notified on such changes

Copy link
Member

@feiskyer feiskyer Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this understanding is correct, maybe reading PODCIDRs each time maybe the only option.

This may be not enough as there may be down times when new IPs are picked while they are blocked by NSG.

} else {
dstIpv4AddressPrefix = []netip.Prefix{az.PodCidrIPv4}
dstIpv6AddressPrefix = []netip.Prefix{az.PodCidrIPv6}
}
} else {
var (
disableFloatingIP = consts.IsK8sServiceDisableLoadBalancerFloatingIP(service)
lbIPAddresses, _ = iputil.ParseAddresses(lbIPs)
lbIPv4Addresses, lbIPv6Addresses = iputil.GroupAddressesByFamily(lbIPAddresses)
additionalIPv4Addresses, additionalIPv6Addresses = iputil.GroupAddressesByFamily(additionalIPs)
backendIPv4Addresses, backendIPv6Addresses []netip.Addr
)
// Get backend node IPs
lb, lbFound, err := az.getAzureLoadBalancer(ctx, lbName, azcache.CacheReadTypeDefault)
if err != nil {
return nil, err
}
if wantLb && !lbFound {
logger.Error(err, "Failed to get load balancer")
return nil, fmt.Errorf("unable to get lb %s", lbName)
}
var backendIPv4List, backendIPv6List []string
if lbFound {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For POD IP based backendpool, we are not populating the POD IPs in the NSG. Rather, the POD subnet. So, we could skip GetBackendPrivateIPs for POD IP based Backend pool.

Copy link
Author

@georgeedward2000 georgeedward2000 Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of the initial flow - node ip based backendpool

It is part of the block that is on the else branch of :
if az.IsLBBackendPoolTypePodIP() {
} else {
// HERE
}

backendIPv4List, backendIPv6List = az.LoadBalancerBackendPool.GetBackendPrivateIPs(ctx, clusterName, service, lb)
}
backendIPv4Addresses, _ = iputil.ParseAddresses(backendIPv4List)
backendIPv6Addresses, _ = iputil.ParseAddresses(backendIPv6List)
}

var (
dstIPv4Addresses = additionalIPv4Addresses
dstIPv6Addresses = additionalIPv6Addresses
)

if disableFloatingIP {
// use the backend node IPs
dstIPv4Addresses = append(dstIPv4Addresses, backendIPv4Addresses...)
dstIPv6Addresses = append(dstIPv6Addresses, backendIPv6Addresses...)
} else {
// use the LoadBalancer IPs
dstIPv4Addresses = append(dstIPv4Addresses, lbIPv4Addresses...)
dstIPv6Addresses = append(dstIPv6Addresses, lbIPv6Addresses...)
}
if disableFloatingIP {
// use the backend node IPs
dstIPv4Addresses = append(dstIPv4Addresses, backendIPv4Addresses...)
dstIPv6Addresses = append(dstIPv6Addresses, backendIPv6Addresses...)
} else {
// use the LoadBalancer IPs
dstIPv4Addresses = append(dstIPv4Addresses, lbIPv4Addresses...)
dstIPv6Addresses = append(dstIPv6Addresses, lbIPv6Addresses...)
}

{
retainPortRanges, err := az.listSharedIPPortMapping(ctx, service, append(dstIPv4Addresses, dstIPv6Addresses...))
if err != nil {
logger.Error(err, "Failed to list retain port ranges")
Expand All @@ -3118,7 +3154,7 @@ func (az *Cloud) reconcileSecurityGroup(
}

if wantLb {
err := accessControl.PatchSecurityGroup(dstIPv4Addresses, dstIPv6Addresses)
err := accessControl.PatchSecurityGroup(dstIPv4Addresses, dstIPv6Addresses, dstIpv4AddressPrefix, dstIpv6AddressPrefix)
if err != nil {
logger.Error(err, "Failed to patch security group")
return nil, err
Expand Down
Loading