Repository: kubernetes-sigs/cloud-provider-azure
Pull request #5441 (2024-02-18 13:05:27)

[release-1.29] chore: Add case-insensitive string set utility
This is an automated cherry-pick of #5368

/assign nilo19

```release-note
NONE
```


Patch:
diff --git a/pkg/provider/azure.go b/pkg/provider/azure.go
index 28fc17a9e..97b0caec7 100644
--- a/pkg/provider/azure.go
+++ b/pkg/provider/azure.go
@@ -31,10 +31,10 @@ import (
 	"github.com/Azure/go-autorest/autorest"
 	"github.com/Azure/go-autorest/autorest/adal"
 	"github.com/Azure/go-autorest/autorest/azure"
+
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/apimachinery/pkg/util/wait"
 	"k8s.io/client-go/informers"
 	clientset "k8s.io/client-go/kubernetes"
@@ -48,7 +48,6 @@ import (
 	cloudnodeutil "k8s.io/cloud-provider/node/helpers"
 	nodeutil "k8s.io/component-helpers/node/util"
 	"k8s.io/klog/v2"
-	"sigs.k8s.io/yaml"
 
 	"sigs.k8s.io/cloud-provider-azure/pkg/azclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azclient/configloader"
@@ -76,10 +75,14 @@ import (
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/vmssclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/vmssvmclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/zoneclient"
+
+	"sigs.k8s.io/yaml"
+
 	azcache "sigs.k8s.io/cloud-provider-azure/pkg/cache"
 	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
 	ratelimitconfig "sigs.k8s.io/cloud-provider-azure/pkg/provider/config"
 	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
+	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 	"sigs.k8s.io/cloud-provider-azure/pkg/util/taints"
 )
 
@@ -331,11 +334,11 @@ type MultipleStandardLoadBalancerConfigurationSpec struct {
 // MultipleStandardLoadBalancerConfigurationStatus stores the properties regarding multiple standard load balancers.
 type MultipleStandardLoadBalancerConfigurationStatus struct {
 	// ActiveServices stores the services that are supposed to use the load balancer.
-	ActiveServices sets.Set[string] `json:"activeServices" yaml:"activeServices"`
+	ActiveServices *utilsets.IgnoreCaseSet `json:"activeServices" yaml:"activeServices"`
 
 	// ActiveNodes stores the nodes that are supposed to be in the load balancer.
 	// It will be used in EnsureHostsInPool to make sure the given ones are in the backend pool.
-	ActiveNodes sets.Set[string] `json:"activeNodes" yaml:"activeNodes"`
+	ActiveNodes *utilsets.IgnoreCaseSet `json:"activeNodes" yaml:"activeNodes"`
 }
 
 // HasExtendedLocation returns true if extendedlocation prop are specified.
@@ -392,17 +395,17 @@ type Cloud struct {
 	// Lock for access to node caches, includes nodeZones, nodeResourceGroups, and unmanagedNodes.
 	nodeCachesLock sync.RWMutex
 	// nodeNames holds current nodes for tracking added nodes in VM caches.
-	nodeNames sets.Set[string]
+	nodeNames *utilsets.IgnoreCaseSet
 	// nodeZones is a mapping from Zone to a sets.Set[string] of Node's names in the Zone
 	// it is updated by the nodeInformer
-	nodeZones map[string]sets.Set[string]
+	nodeZones map[string]*utilsets.IgnoreCaseSet
 	// nodeResourceGroups holds nodes external resource groups
 	nodeResourceGroups map[string]string
 	// unmanagedNodes holds a list of nodes not managed by Azure cloud provider.
-	unmanagedNodes sets.Set[string]
+	unmanagedNodes *utilsets.IgnoreCaseSet
 	// excludeLoadBalancerNodes holds a list of nodes that should be excluded from LoadBalancer.
-	excludeLoadBalancerNodes   sets.Set[string]
-	nodePrivateIPs             map[string]sets.Set[string]
+	excludeLoadBalancerNodes   *utilsets.IgnoreCaseSet
+	nodePrivateIPs             map[string]*utilsets.IgnoreCaseSet
 	nodePrivateIPToNodeNameMap map[string]string
 	// nodeInformerSynced is for determining if the informer has synced.
 	nodeInformerSynced cache.InformerSynced
@@ -455,13 +458,13 @@ type Cloud struct {
 // NewCloud returns a Cloud with initialized clients
 func NewCloud(ctx context.Context, config *Config, callFromCCM bool) (cloudprovider.Interface, error) {
 	az := &Cloud{
-		nodeNames:                  sets.New[string](),
-		nodeZones:                  map[string]sets.Set[string]{},
+		nodeNames:                  utilsets.NewString(),
+		nodeZones:                  map[string]*utilsets.IgnoreCaseSet{},
 		nodeResourceGroups:         map[string]string{},
-		unmanagedNodes:             sets.New[string](),
+		unmanagedNodes:             utilsets.NewString(),
 		routeCIDRs:                 map[string]string{},
-		excludeLoadBalancerNodes:   sets.New[string](),
-		nodePrivateIPs:             map[string]sets.Set[string]{},
+		excludeLoadBalancerNodes:   utilsets.NewString(),
+		nodePrivateIPs:             map[string]*utilsets.IgnoreCaseSet{},
 		nodePrivateIPToNodeNameMap: map[string]string{},
 	}
 
@@ -565,7 +568,7 @@ func (az *Cloud) InitializeCloudFromConfig(ctx context.Context, config *Config,
 		// The default cloud config type is cloudConfigTypeMerge.
 		config.CloudConfigType = configloader.CloudConfigTypeMerge
 	} else {
-		supportedCloudConfigTypes := sets.New(
+		supportedCloudConfigTypes := utilsets.NewString(
 			string(configloader.CloudConfigTypeMerge),
 			string(configloader.CloudConfigTypeFile),
 			string(configloader.CloudConfigTypeSecret))
@@ -579,7 +582,7 @@ func (az *Cloud) InitializeCloudFromConfig(ctx context.Context, config *Config,
 		strings.EqualFold(config.LoadBalancerBackendPoolConfigurationType, consts.LoadBalancerBackendPoolConfigurationTypePODIP) {
 		config.LoadBalancerBackendPoolConfigurationType = consts.LoadBalancerBackendPoolConfigurationTypeNodeIPConfiguration
 	} else {
-		supportedLoadBalancerBackendPoolConfigurationTypes := sets.New(
+		supportedLoadBalancerBackendPoolConfigurationTypes := utilsets.NewString(
 			strings.ToLower(consts.LoadBalancerBackendPoolConfigurationTypeNodeIPConfiguration),
 			strings.ToLower(consts.LoadBalancerBackendPoolConfigurationTypeNodeIP),
 			strings.ToLower(consts.LoadBalancerBackendPoolConfigurationTypePODIP))
@@ -591,7 +594,7 @@ func (az *Cloud) InitializeCloudFromConfig(ctx context.Context, config *Config,
 	if config.ClusterServiceLoadBalancerHealthProbeMode == "" {
 		config.ClusterServiceLoadBalancerHealthProbeMode = consts.ClusterServiceLoadBalancerHealthProbeModeServiceNodePort
 	} else {
-		supportedClusterServiceLoadBalancerHealthProbeModes := sets.New(
+		supportedClusterServiceLoadBalancerHealthProbeModes := utilsets.NewString(
 			strings.ToLower(consts.ClusterServiceLoadBalancerHealthProbeModeServiceNodePort),
 			strings.ToLower(consts.ClusterServiceLoadBalancerHealthProbeModeShared),
 		)
@@ -782,8 +785,8 @@ func (az *Cloud) checkEnableMultipleStandardLoadBalancers() error {
 		return fmt.Errorf("multiple standard load balancers cannot be used with backend pool type %s", consts.LoadBalancerBackendPoolConfigurationTypeNodeIPConfiguration)
 	}
 
-	names := sets.New[string]()
-	primaryVMSets := sets.New[string]()
+	names := utilsets.NewString()
+	primaryVMSets := utilsets.NewString()
 	for _, multiSLBConfig := range az.MultipleStandardLoadBalancerConfigurations {
 		if names.Has(multiSLBConfig.Name) {
 			return fmt.Errorf("duplicated multiple standard load balancer configuration name %s", multiSLBConfig.Name)
@@ -1257,15 +1260,12 @@ func (az *Cloud) updateNodeCaches(prevNode, newNode *v1.Node) {
 
 	if newNode != nil {
 		// Add to nodeNames cache.
-		az.nodeNames.Insert(newNode.ObjectMeta.Name)
+		az.nodeNames = utilsets.SafeInsert(az.nodeNames, newNode.ObjectMeta.Name)
 
 		// Add to nodeZones cache.
 		newZone, ok := newNode.ObjectMeta.Labels[v1.LabelTopologyZone]
 		if ok && az.isAvailabilityZone(newZone) {
-			if az.nodeZones[newZone] == nil {
-				az.nodeZones[newZone] = sets.New[string]()
-			}
-			az.nodeZones[newZone].Insert(newNode.ObjectMeta.Name)
+			az.nodeZones[newZone] = utilsets.SafeInsert(az.nodeZones[newZone], newNode.ObjectMeta.Name)
 		}
 
 		// Add to nodeResourceGroups cache.
@@ -1301,15 +1301,12 @@ func (az *Cloud) updateNodeCaches(prevNode, newNode *v1.Node) {
 
 		// Add to nodePrivateIPs cache
 		for _, address := range getNodePrivateIPAddresses(newNode) {
-			if az.nodePrivateIPs[strings.ToLower(newNode.Name)] == nil {
-				az.nodePrivateIPs[strings.ToLower(newNode.Name)] = sets.New[string]()
-			}
 			if az.nodePrivateIPToNodeNameMap == nil {
 				az.nodePrivateIPToNodeNameMap = make(map[string]string)
 			}
 
 			klog.V(6).Infof("adding IP address %s of the node %s", address, newNode.Name)
-			az.nodePrivateIPs[strings.ToLower(newNode.Name)].Insert(address)
+			az.nodePrivateIPs[strings.ToLower(newNode.Name)] = utilsets.SafeInsert(az.nodePrivateIPs[strings.ToLower(newNode.Name)], address)
 			az.nodePrivateIPToNodeNameMap[address] = newNode.Name
 		}
 	}
@@ -1345,7 +1342,7 @@ func (az *Cloud) updateNodeTaint(node *v1.Node) {
 }
 
 // GetActiveZones returns all the zones in which k8s nodes are currently running.
-func (az *Cloud) GetActiveZones() (sets.Set[string], error) {
+func (az *Cloud) GetActiveZones() (*utilsets.IgnoreCaseSet, error) {
 	if az.nodeInformerSynced == nil {
 		return nil, fmt.Errorf("azure cloud provider doesn't have informers set")
 	}
@@ -1356,9 +1353,9 @@ func (az *Cloud) GetActiveZones() (sets.Set[string], error) {
 		return nil, fmt.Errorf("node informer is not synced when trying to GetActiveZones")
 	}
 
-	zones := sets.New[string]()
+	zones := utilsets.NewString()
 	for zone, nodes := range az.nodeZones {
-		if len(nodes) > 0 {
+		if nodes.Len() > 0 {
 			zones.Insert(zone)
 		}
 	}
@@ -1393,7 +1390,7 @@ func (az *Cloud) GetNodeResourceGroup(nodeName string) (string, error) {
 }
 
 // GetNodeNames returns a set of all node names in the k8s cluster.
-func (az *Cloud) GetNodeNames() (sets.Set[string], error) {
+func (az *Cloud) GetNodeNames() (*utilsets.IgnoreCaseSet, error) {
 	// Kubelet won't set az.nodeInformerSynced, return nil.
 	if az.nodeInformerSynced == nil {
 		return nil, nil
@@ -1405,14 +1402,14 @@ func (az *Cloud) GetNodeNames() (sets.Set[string], error) {
 		return nil, fmt.Errorf("node informer is not synced when trying to GetNodeNames")
 	}
 
-	return sets.New(az.nodeNames.UnsortedList()...), nil
+	return utilsets.NewString(az.nodeNames.UnsortedList()...), nil
 }
 
 // GetResourceGroups returns a set of resource groups that all nodes are running on.
-func (az *Cloud) GetResourceGroups() (sets.Set[string], error) {
+func (az *Cloud) GetResourceGroups() (*utilsets.IgnoreCaseSet, error) {
 	// Kubelet won't set az.nodeInformerSynced, always return configured resourceGroup.
 	if az.nodeInformerSynced == nil {
-		return sets.New(az.ResourceGroup), nil
+		return utilsets.NewString(az.ResourceGroup), nil
 	}
 
 	az.nodeCachesLock.RLock()
@@ -1421,7 +1418,7 @@ func (az *Cloud) GetResourceGroups() (sets.Set[string], error) {
 		return nil, fmt.Errorf("node informer is not synced when trying to GetResourceGroups")
 	}
 
-	resourceGroups := sets.New(az.ResourceGroup)
+	resourceGroups := utilsets.NewString(az.ResourceGroup)
 	for _, rg := range az.nodeResourceGroups {
 		resourceGroups.Insert(rg)
 	}
@@ -1430,7 +1427,7 @@ func (az *Cloud) GetResourceGroups() (sets.Set[string], error) {
 }
 
 // GetUnmanagedNodes returns a list of nodes not managed by Azure cloud provider (e.g. on-prem nodes).
-func (az *Cloud) GetUnmanagedNodes() (sets.Set[string], error) {
+func (az *Cloud) GetUnmanagedNodes() (*utilsets.IgnoreCaseSet, error) {
 	// Kubelet won't set az.nodeInformerSynced, always return nil.
 	if az.nodeInformerSynced == nil {
 		return nil, nil
@@ -1442,7 +1439,7 @@ func (az *Cloud) GetUnmanagedNodes() (sets.Set[string], error) {
 		return nil, fmt.Errorf("node informer is not synced when trying to GetUnmanagedNodes")
 	}
 
-	return sets.New(az.unmanagedNodes.UnsortedList()...), nil
+	return utilsets.NewString(az.unmanagedNodes.UnsortedList()...), nil
 }
 
 // ShouldNodeExcludedFromLoadBalancer returns true if node is unmanaged, in external resource group or labeled with "node.kubernetes.io/exclude-from-external-load-balancers".
@@ -1466,7 +1463,7 @@ func (az *Cloud) ShouldNodeExcludedFromLoadBalancer(nodeName string) (bool, erro
 	return az.excludeLoadBalancerNodes.Has(nodeName), nil
 }
 
-func (az *Cloud) getActiveNodesByLoadBalancerName(lbName string) sets.Set[string] {
+func (az *Cloud) getActiveNodesByLoadBalancerName(lbName string) *utilsets.IgnoreCaseSet {
 	az.multipleStandardLoadBalancersActiveNodesLock.Lock()
 	defer az.multipleStandardLoadBalancersActiveNodesLock.Unlock()
 
@@ -1476,7 +1473,7 @@ func (az *Cloud) getActiveNodesByLoadBalancerName(lbName string) sets.Set[string
 		}
 	}
 
-	return sets.New[string]()
+	return utilsets.NewString()
 }
 
 func isNodeReady(node *v1.Node) bool {
diff --git a/pkg/provider/azure_fakes.go b/pkg/provider/azure_fakes.go
index 451609d3e..60d6ab70f 100644
--- a/pkg/provider/azure_fakes.go
+++ b/pkg/provider/azure_fakes.go
@@ -25,7 +25,6 @@ import (
 
 	"go.uber.org/mock/gomock"
 
-	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/client-go/tools/record"
 
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/diskclient/mockdiskclient"
@@ -42,6 +41,7 @@ import (
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/vmssclient/mockvmssclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/vmssvmclient/mockvmssvmclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
+	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 )
 
 // NewTestScaleSet creates a fake ScaleSet for unit test
@@ -96,12 +96,12 @@ func GetTestCloud(ctrl *gomock.Controller) (az *Cloud) {
 			VMType:                                   consts.VMTypeStandard,
 			LoadBalancerBackendPoolConfigurationType: consts.LoadBalancerBackendPoolConfigurationTypeNodeIPConfiguration,
 		},
-		nodeZones:                map[string]sets.Set[string]{},
+		nodeZones:                map[string]*utilsets.IgnoreCaseSet{},
 		nodeInformerSynced:       func() bool { return true },
 		nodeResourceGroups:       map[string]string{},
-		unmanagedNodes:           sets.New[string](),
-		excludeLoadBalancerNodes: sets.New[string](),
-		nodePrivateIPs:           map[string]sets.Set[string]{},
+		unmanagedNodes:           utilsets.NewString(),
+		excludeLoadBalancerNodes: utilsets.NewString(),
+		nodePrivateIPs:           map[string]*utilsets.IgnoreCaseSet{},
 		routeCIDRs:               map[string]string{},
 		eventRecorder:            &record.FakeRecorder{},
 		lockMap:                  newLockMap(),
diff --git a/pkg/provider/azure_loadbalancer.go b/pkg/provider/azure_loadbalancer.go
index 1649ee008..c4a4d2c50 100644
--- a/pkg/provider/azure_loadbalancer.go
+++ b/pkg/provider/azure_loadbalancer.go
@@ -36,7 +36,6 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	utilerrors "k8s.io/apimachinery/pkg/util/errors"
-	"k8s.io/apimachinery/pkg/util/sets"
 	cloudprovider "k8s.io/cloud-provider"
 	servicehelpers "k8s.io/cloud-provider/service/helpers"
 	"k8s.io/klog/v2"
@@ -50,6 +49,7 @@ import (
 	"sigs.k8s.io/cloud-provider-azure/pkg/provider/loadbalancer"
 	"sigs.k8s.io/cloud-provider-azure/pkg/provider/loadbalancer/iputil"
 	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
+	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 )
 
 var _ cloudprovider.LoadBalancer = (*Cloud)(nil)
@@ -596,12 +596,10 @@ func (az *Cloud) safeDeleteLoadBalancer(lb network.LoadBalancer, clusterName, vm
 			trimSuffixIgnoreCase(pointer.StringDeref(lb.Name, ""), consts.InternalLoadBalancerNameSuffix),
 			az.MultipleStandardLoadBalancerConfigurations[i].Name,
 		) {
-			if az.MultipleStandardLoadBalancerConfigurations[i].ActiveNodes != nil {
-				for nodeName := range az.MultipleStandardLoadBalancerConfigurations[i].ActiveNodes {
-					az.nodesWithCorrectLoadBalancerByPrimaryVMSet.Delete(strings.ToLower(nodeName))
-				}
+			for _, nodeName := range az.MultipleStandardLoadBalancerConfigurations[i].ActiveNodes.UnsortedList() {
+				az.nodesWithCorrectLoadBalancerByPrimaryVMSet.Delete(nodeName)
 			}
-			az.MultipleStandardLoadBalancerConfigurations[i].ActiveNodes = sets.New[string]()
+			az.MultipleStandardLoadBalancerConfigurations[i].ActiveNodes = utilsets.NewString()
 			break
 		}
 	}
@@ -1599,10 +1597,7 @@ func (az *Cloud) reconcileMultipleStandardLoadBalancerConfigurations(
 					for i := range az.MultipleStandardLoadBalancerConfigurations {
 						if strings.EqualFold(trimSuffixIgnoreCase(lbName, consts.InternalLoadBalancerNameSuffix), az.MultipleStandardLoadBalancerConfigurations[i].Name) {
 							az.multipleStandardLoadBalancersActiveServicesLock.Lock()
-							if az.MultipleStandardLoadBalancerConfigurations[i].ActiveServices == nil {
-								az.MultipleStandardLoadBalancerConfigurations[i].ActiveServices = sets.New[string]()
-							}
-							az.MultipleStandardLoadBalancerConfigurations[i].ActiveServices.Insert(strings.ToLower(svcName))
+							az.MultipleStandardLoadBalancerConfigurations[i].ActiveServices = utilsets.SafeInsert(az.MultipleStandardLoadBalancerConfigurations[i].ActiveServices, svcName)
 							az.multipleStandardLoadBalancersActiveServicesLock.Unlock()
 							klog.V(2).Infof("reconcileMultipleStandardLoadBalancerConfigurations: service(%s) is active on lb(%s)", svcName, lbName)
 						}
@@ -1918,9 +1913,9 @@ func (az *Cloud) removeNodeFromLBConfig(nodeNameToLBConfigIDXMap map[string]int,
 // removeDeletedNodesFromLoadBalancerConfigurations removes the deleted nodes
 // that do not exist in nodes list from the load balancer configurations
 func (az *Cloud) removeDeletedNodesFromLoadBalancerConfigurations(nodes []*v1.Node) map[string]int {
-	nodeNamesSet := sets.New[string]()
+	nodeNamesSet := utilsets.NewString()
 	for _, node := range nodes {
-		nodeNamesSet.Insert(strings.ToLower(node.Name))
+		nodeNamesSet.Insert(node.Name)
 	}
 
 	az.multipleStandardLoadBalancersActiveNodesLock.Lock()
@@ -1930,12 +1925,12 @@ func (az *Cloud) removeDeletedNodesFromLoadBalancerConfigurations(nodes []*v1.No
 	nodeNameToLBConfigIDXMap := make(map[string]int)
 	for i, multiSLBConfig := range az.MultipleStandardLoadBalancerConfigurations {
 		if multiSLBConfig.ActiveNodes != nil {
-			for nodeName := range multiSLBConfig.ActiveNodes {
+			for _, nodeName := range multiSLBConfig.ActiveNodes.UnsortedList() {
 				if nodeNamesSet.Has(nodeName) {
 					nodeNameToLBConfigIDXMap[nodeName] = i
 				} else {
 					klog.V(4).Infof("reconcileMultipleStandardLoadBalancerBackendNodes: node(%s) is gone, remove it from lb(%s)", nodeName, multiSLBConfig.Name)
-					az.MultipleStandardLoadBalancerConfigurations[i].ActiveNodes, _ = safeRemoveKeyFromStringsSet(az.MultipleStandardLoadBalancerConfigurations[i].ActiveNodes, strings.ToLower(nodeName))
+					az.MultipleStandardLoadBalancerConfigurations[i].ActiveNodes.Delete(nodeName)
 				}
 			}
 		}
@@ -1971,14 +1966,14 @@ func (az *Cloud) accommodateNodesByPrimaryVMSet(
 					continue
 				}
 
-				az.nodesWithCorrectLoadBalancerByPrimaryVMSet.Store(strings.ToLower(node.Name), sets.Empty{})
+				az.nodesWithCorrectLoadBalancerByPrimaryVMSet.Store(strings.ToLower(node.Name), struct{}{})
 				if !multiSLBConfig.ActiveNodes.Has(node.Name) {
 					klog.V(4).Infof("accommodateNodesByPrimaryVMSet: node(%s) should be on lb(%s) because of primary vmSet (%s)", node.Name, multiSLBConfig.Name, vmSetName)
 
 					az.removeNodeFromLBConfig(nodeNameToLBConfigIDXMap, node.Name)
 
 					az.multipleStandardLoadBalancersActiveNodesLock.Lock()
-					az.MultipleStandardLoadBalancerConfigurations[i].ActiveNodes = safeAddKeyToStringsSet(az.MultipleStandardLoadBalancerConfigurations[i].ActiveNodes, strings.ToLower(node.Name))
+					az.MultipleStandardLoadBalancerConfigurations[i].ActiveNodes = utilsets.SafeInsert(az.MultipleStandardLoadBalancerConfigurations[i].ActiveNodes, node.Name)
 					az.multipleStandardLoadBalancersActiveNodesLock.Unlock()
 				}
 				break
@@ -2077,7 +2072,7 @@ func (az *Cloud) accommodateNodesByNodeSelector(
 
 		klog.V(4).Infof("accommodateNodesByNodeSelector: node(%s) should be on lb(%s) it is the eligible LB with fewest number of nodes", node.Name, az.MultipleStandardLoadBalancerConfigurations[minNodesIDX].Name)
 		az.multipleStandardLoadBalancersActiveNodesLock.Lock()
-		az.MultipleStandardLoadBalancerConfigurations[minNodesIDX].ActiveNodes = safeAddKeyToStringsSet(az.MultipleStandardLoadBalancerConfigurations[minNodesIDX].ActiveNodes, strings.ToLower(node.Name))
+		az.MultipleStandardLoadBalancerConfigurations[minNodesIDX].ActiveNodes = utilsets.SafeInsert(az.MultipleStandardLoadBalancerConfigurations[minNodesIDX].ActiveNodes, node.Name)
 		az.multipleStandardLoadBalancersActiveNodesLock.Unlock()
 	}
 
@@ -2139,16 +2134,13 @@ func (az *Cloud) reconcileMultipleStandardLoadBalancerConfigurationStatus(wantLb
 	for i := range az.MultipleStandardLoadBalancerConfigurations {
 		if strings.EqualFold(lbName, az.MultipleStandardLoadBalancerConfigurations[i].Name) {
 			az.multipleStandardLoadBalancersActiveServicesLock.Lock()
-			if az.MultipleStandardLoadBalancerConfigurations[i].ActiveServices == nil {
-				az.MultipleStandardLoadBalancerConfigurations[i].ActiveServices = sets.New[string]()
-			}
 
 			if wantLb {
 				klog.V(4).Infof("reconcileMultipleStandardLoadBalancerConfigurationStatus: service(%s) is active on lb(%s)", svcName, lbName)
-				az.MultipleStandardLoadBalancerConfigurations[i].ActiveServices.Insert(strings.ToLower(svcName))
+				az.MultipleStandardLoadBalancerConfigurations[i].ActiveServices = utilsets.SafeInsert(az.MultipleStandardLoadBalancerConfigurations[i].ActiveServices, svcName)
 			} else {
 				klog.V(4).Infof("reconcileMultipleStandardLoadBalancerConfigurationStatus: service(%s) is not active on lb(%s) any more", svcName, lbName)
-				az.MultipleStandardLoadBalancerConfigurations[i].ActiveServices, _ = safeRemoveKeyFromStringsSet(az.MultipleStandardLoadBalancerConfigurations[i].ActiveServices, strings.ToLower(svcName))
+				az.MultipleStandardLoadBalancerConfigurations[i].ActiveServices.Delete(svcName)
 			}
 			az.multipleStandardLoadBalancersActiveServicesLock.Unlock()
 			break
@@ -3298,7 +3290,7 @@ func (az *Cloud) safeDeletePublicIP(service *v1.Service, pipResourceGroup string
 
 			// Check whether there are still load balancer rules referring to it.
 			if len(referencedLBRules) > 0 {
-				referencedLBRuleIDs := sets.New[string]()
+				referencedLBRuleIDs := utilsets.NewString()
 				for _, refer := range referencedLBRules {
 					referencedLBRuleIDs.Insert(pointer.StringDeref(refer.ID, ""))
 				}
@@ -3864,9 +3856,9 @@ func (az *Cloud) getEligibleLoadBalancersForService(service *v1.Service) ([]stri
 	// If there is no annotation given, it selects all LBs.
 	lbsFromAnnotation := consts.GetLoadBalancerConfigurationsNames(service)
 	if len(lbsFromAnnotation) > 0 {
-		lbNamesSet := sets.New[string](lbsFromAnnotation...)
+		lbNamesSet := utilsets.NewString(lbsFromAnnotation...)
 		for _, multiSLBConfig := range az.MultipleStandardLoadBalancerConfigurations {
-			if lbNamesSet.Has(strings.ToLower(multiSLBConfig.Name)) {
+			if lbNamesSet.Has(multiSLBConfig.Name) {
 				logger.V(4).Info("selects the load balancer by annotation",
 					"load balancer configuration name", multiSLBConfig.Name)
 				eligibleLBs = append(eligibleLBs, multiSLBConfig)
@@ -4002,10 +3994,7 @@ func (az *Cloud) isLoadBalancerInUseByService(service *v1.Service, lbConfig Mult
 	defer az.multipleStandardLoadBalancersActiveServicesLock.Unlock()
 
 	serviceName := getServiceName(service)
-	if lbConfig.ActiveServices != nil {
-		return lbConfig.ActiveServices.Has(serviceName)
-	}
-	return false
+	return lbConfig.ActiveServices.Has(serviceName)
 }
 
 // There are two cases when a service owns the frontend IP config:
diff --git a/pkg/provider/azure_loadbalancer_backendpool.go b/pkg/provider/azure_loadbalancer_backendpool.go
index 67f213c32..b33ee2e71 100644
--- a/pkg/provider/azure_loadbalancer_backendpool.go
+++ b/pkg/provider/azure_loadbalancer_backendpool.go
@@ -26,7 +26,6 @@ import (
 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2022-07-01/network"
 
 	v1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/util/sets"
 	cloudprovider "k8s.io/cloud-provider"
 	"k8s.io/klog/v2"
 	utilnet "k8s.io/utils/net"
@@ -35,6 +34,7 @@ import (
 	"sigs.k8s.io/cloud-provider-azure/pkg/cache"
 	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
 	"sigs.k8s.io/cloud-provider-azure/pkg/metrics"
+	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 )
 
 type BackendPool interface {
@@ -304,8 +304,8 @@ func getBackendIPConfigurationsToBeDeleted(
 		return []network.InterfaceIPConfiguration{}
 	}
 
-	bipConfigNotFoundIDSet := sets.New[string]()
-	bipConfigExcludeIDSet := sets.New[string]()
+	bipConfigNotFoundIDSet := utilsets.NewString()
+	bipConfigExcludeIDSet := utilsets.NewString()
 	for _, ipConfig := range bipConfigNotFound {
 		bipConfigNotFoundIDSet.Insert(pointer.StringDeref(ipConfig.ID, ""))
 	}
@@ -344,7 +344,7 @@ func (bc *backendPoolTypeNodeIPConfig) GetBackendPrivateIPs(clusterName string,
 		return nil, nil
 	}
 
-	backendPrivateIPv4s, backendPrivateIPv6s := sets.New[string](), sets.New[string]()
+	backendPrivateIPv4s, backendPrivateIPv6s := utilsets.NewString(), utilsets.NewString()
 	for _, bp := range *lb.BackendAddressPools {
 		found, _ := isLBBackendPoolsExisting(lbBackendPoolNames, bp.Name)
 		if found {
@@ -399,7 +399,7 @@ func (bi *backendPoolTypeNodeIP) EnsureHostsInPool(service *v1.Service, nodes []
 	var (
 		changed               bool
 		numOfAdd, numOfDelete int
-		activeNodes           sets.Set[string]
+		activeNodes           *utilsets.IgnoreCaseSet
 		err                   error
 	)
 	if bi.useMultipleStandardLoadBalancers() {
@@ -420,9 +420,6 @@ func (bi *backendPoolTypeNodeIP) EnsureHostsInPool(service *v1.Service, nodes []
 				return err
 			}
 		}
-		if activeNodes == nil {
-			activeNodes = sets.New[string]()
-		}
 	}
 
 	lbBackendPoolName := bi.getBackendPoolNameForService(service, clusterName, isIPv6)
@@ -437,7 +434,7 @@ func (bi *backendPoolTypeNodeIP) EnsureHostsInPool(service *v1.Service, nodes []
 			backendPool.LoadBalancerBackendAddresses = &lbBackendPoolAddresses
 		}
 
-		existingIPs := sets.New[string]()
+		existingIPs := utilsets.NewString()
 		for _, loadBalancerBackendAddress := range *backendPool.LoadBalancerBackendAddresses {
 			if loadBalancerBackendAddress.LoadBalancerBackendAddressPropertiesFormat != nil &&
 				loadBalancerBackendAddress.IPAddress != nil {
@@ -447,7 +444,7 @@ func (bi *backendPoolTypeNodeIP) EnsureHostsInPool(service *v1.Service, nodes []
 		}
 
 		var nodeIPsToBeAdded []string
-		nodePrivateIPsSet := sets.New[string]()
+		nodePrivateIPsSet := utilsets.NewString()
 		for _, node := range nodes {
 			if isControlPlaneNode(node) {
 				klog.V(4).Infof("bi.EnsureHostsInPool: skipping control plane node %s", node.Name)
@@ -458,7 +455,7 @@ func (bi *backendPoolTypeNodeIP) EnsureHostsInPool(service *v1.Service, nodes []
 			nodePrivateIPsSet.Insert(privateIP)
 
 			if bi.useMultipleStandardLoadBalancers() {
-				if activeNodes == nil || !activeNodes.Has(node.Name) {
+				if !activeNodes.Has(node.Name) {
 					klog.V(4).Infof("bi.EnsureHostsInPool: node %s should not be in load balancer %q", node.Name, lbName)
 					continue
 				}
@@ -519,7 +516,7 @@ func (bi *backendPoolTypeNodeIP) CleanupVMSetFromBackendPoolByCondition(slb *net
 		found, isIPv6 := isLBBackendPoolsExisting(lbBackendPoolNames, bp.Name)
 		if found {
 			klog.V(2).Infof("bi.CleanupVMSetFromBackendPoolByCondition: checking the backend pool %s from standard load balancer %s", pointer.StringDeref(bp.Name, ""), pointer.StringDeref(slb.Name, ""))
-			vmIPsToBeDeleted := sets.New[string]()
+			vmIPsToBeDeleted := utilsets.NewString()
 			for _, node := range nodes {
 				vmSetName, err := bi.VMSet.GetNodeVMSetName(node)
 				if err != nil {
@@ -662,8 +659,8 @@ func (bi *backendPoolTypeNodeIP) ReconcileBackendPools(clusterName string, servi
 		for _, i := range bpIdxes {
 			bp := newBackendPools[i]
 			var nodeIPAddressesToBeDeleted []string
-			for nodeName := range bi.excludeLoadBalancerNodes {
-				for ip := range bi.nodePrivateIPs[strings.ToLower(nodeName)] {
+			for _, nodeName := range bi.excludeLoadBalancerNodes.UnsortedList() {
+				for _, ip := range bi.nodePrivateIPs[strings.ToLower(nodeName)].UnsortedList() {
 					klog.V(2).Infof("bi.ReconcileBackendPools for service (%s): found unwanted node private IP %s, decouple it from the LB %s", serviceName, ip, lbName)
 					nodeIPAddressesToBeDeleted = append(nodeIPAddressesToBeDeleted, ip)
 				}
@@ -744,7 +741,7 @@ func (bi *backendPoolTypeNodeIP) GetBackendPrivateIPs(clusterName string, servic
 		return nil, nil
 	}
 
-	backendPrivateIPv4s, backendPrivateIPv6s := sets.New[string](), sets.New[string]()
+	backendPrivateIPv4s, backendPrivateIPv6s := utilsets.NewString(), utilsets.NewString()
 	for _, bp := range *lb.BackendAddressPools {
 		found, _ := isLBBackendPoolsExisting(lbBackendPoolNames, bp.Name)
 		if found {
@@ -839,7 +836,7 @@ func removeNodeIPAddressesFromBackendPool(
 	removeAll, useMultipleStandardLoadBalancers bool,
 ) bool {
 	changed := false
-	nodeIPsSet := sets.New(nodeIPAddresses...)
+	nodeIPsSet := utilsets.NewString(nodeIPAddresses...)
 
 	if backendPool.BackendAddressPoolPropertiesFormat == nil ||
 		backendPool.LoadBalancerBackendAddresses == nil {
diff --git a/pkg/provider/azure_loadbalancer_repo.go b/pkg/provider/azure_loadbalancer_repo.go
index c34b4738d..cfee45118 100644
--- a/pkg/provider/azure_loadbalancer_repo.go
+++ b/pkg/provider/azure_loadbalancer_repo.go
@@ -26,8 +26,8 @@ import (
 	"time"
 
 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2022-07-01/network"
+
 	v1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/apimachinery/pkg/util/wait"
 	"k8s.io/klog/v2"
 	"k8s.io/utils/pointer"
@@ -35,6 +35,7 @@ import (
 	azcache "sigs.k8s.io/cloud-provider-azure/pkg/cache"
 	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
 	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
+	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 )
 
 // DeleteLB invokes az.LoadBalancerClient.Delete with exponential backoff retry
@@ -87,7 +88,7 @@ func (az *Cloud) ListManagedLBs(service *v1.Service, nodes []*v1.Node, clusterNa
 		return nil, nil
 	}
 
-	managedLBNames := sets.New[string](strings.ToLower(clusterName))
+	managedLBNames := utilsets.NewString(clusterName)
 	managedLBs := make([]network.LoadBalancer, 0)
 	if strings.EqualFold(az.LoadBalancerSku, consts.LoadBalancerSkuBasic) {
 		// return early if wantLb=false
@@ -110,13 +111,13 @@ func (az *Cloud) ListManagedLBs(service *v1.Service, nodes []*v1.Node, clusterNa
 		}
 
 		for agentPoolVMSetName := range agentPoolVMSetNamesMap {
-			managedLBNames.Insert(strings.ToLower(az.mapVMSetNameToLoadBalancerName(agentPoolVMSetName, clusterName)))
+			managedLBNames.Insert(az.mapVMSetNameToLoadBalancerName(agentPoolVMSetName, clusterName))
 		}
 	}
 
 	if az.useMultipleStandardLoadBalancers() {
 		for _, multiSLBConfig := range az.MultipleStandardLoadBalancerConfigurations {
-			managedLBNames.Insert(strings.ToLower(multiSLBConfig.Name), fmt.Sprintf("%s%s", strings.ToLower(multiSLBConfig.Name), consts.InternalLoadBalancerNameSuffix))
+			managedLBNames.Insert(multiSLBConfig.Name, fmt.Sprintf("%s%s", multiSLBConfig.Name, consts.InternalLoadBalancerNameSuffix))
 		}
 	}
 
diff --git a/pkg/provider/azure_local_services.go b/pkg/provider/azure_local_services.go
index 25ff0e816..63640d7b4 100644
--- a/pkg/provider/azure_local_services.go
+++ b/pkg/provider/azure_local_services.go
@@ -38,6 +38,7 @@ import (
 
 	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
 	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
+	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 )
 
 // batchProcessor collects operations in a certain interval and then processes them in batches.
@@ -329,11 +330,11 @@ func (az *Cloud) setUpEndpointSlicesInformer(informerFactory informers.SharedInf
 				}
 				for _, previousNodeName := range previousNodeNames {
 					nodeIPsSet := az.nodePrivateIPs[strings.ToLower(previousNodeName)]
-					previousIPs = append(previousIPs, setToStrings(nodeIPsSet)...)
+					previousIPs = append(previousIPs, nodeIPsSet.UnsortedList()...)
 				}
 				for _, currentNodeName := range currentNodeNames {
 					nodeIPsSet := az.nodePrivateIPs[strings.ToLower(currentNodeName)]
-					currentIPs = append(currentIPs, setToStrings(nodeIPsSet)...)
+					currentIPs = append(currentIPs, nodeIPsSet.UnsortedList()...)
 				}
 
 				if az.backendPoolUpdater != nil {
@@ -462,7 +463,7 @@ func newServiceInfo(ipFamily, lbName string) *serviceInfo {
 }
 
 // getLocalServiceEndpointsNodeNames gets the node names that host all endpoints of the local service.
-func (az *Cloud) getLocalServiceEndpointsNodeNames(service *v1.Service) (sets.Set[string], error) {
+func (az *Cloud) getLocalServiceEndpointsNodeNames(service *v1.Service) (*utilsets.IgnoreCaseSet, error) {
 	var (
 		ep           *discovery_v1.EndpointSlice
 		foundInCache bool
@@ -505,7 +506,7 @@ func (az *Cloud) getLocalServiceEndpointsNodeNames(service *v1.Service) (sets.Se
 		nodeNames = append(nodeNames, pointer.StringDeref(endpoint.NodeName, ""))
 	}
 
-	return sets.New[string](nodeNames...), nil
+	return utilsets.NewString(nodeNames...), nil
 }
 
 // cleanupLocalServiceBackendPool cleans up the backend pool of
@@ -553,11 +554,9 @@ func (az *Cloud) checkAndApplyLocalServiceBackendPoolUpdates(lb network.LoadBala
 		return err
 	}
 	var expectedIPs []string
-	for nodeName := range endpointsNodeNames {
+	for _, nodeName := range endpointsNodeNames.UnsortedList() {
 		ips := az.nodePrivateIPs[strings.ToLower(nodeName)]
-		for ip := range ips {
-			expectedIPs = append(expectedIPs, ip)
-		}
+		expectedIPs = append(expectedIPs, ips.UnsortedList()...)
 	}
 	currentIPsInBackendPools := make(map[string][]string)
 	for _, bp := range *lb.BackendAddressPools {
diff --git a/pkg/provider/azure_utils.go b/pkg/provider/azure_utils.go
index 89b74d42d..541c74b18 100644
--- a/pkg/provider/azure_utils.go
+++ b/pkg/provider/azure_utils.go
@@ -29,7 +29,6 @@ import (
 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2022-07-01/network"
 
 	v1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/klog/v2"
 	utilnet "k8s.io/utils/net"
 	"k8s.io/utils/pointer"
@@ -505,36 +504,6 @@ func IntInSlice(i int, list []int) bool {
 	return false
 }
 
-func safeAddKeyToStringsSet(set sets.Set[string], key string) sets.Set[string] {
-	if set != nil {
-		set.Insert(key)
-	} else {
-		set = sets.New[string](key)
-	}
-
-	return set
-}
-
-func safeRemoveKeyFromStringsSet(set sets.Set[string], key string) (sets.Set[string], bool) {
-	var has bool
-	if set != nil {
-		if set.Has(key) {
-			has = true
-		}
-		set.Delete(key)
-	}
-
-	return set, has
-}
-
-func setToStrings(set sets.Set[string]) []string {
-	var res []string
-	for key := range set {
-		res = append(res, key)
-	}
-	return res
-}
-
 func isLocalService(service *v1.Service) bool {
 	return service.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyLocal
 }
diff --git a/pkg/provider/azure_vmss_cache.go b/pkg/provider/azure_vmss_cache.go
index 8cc176333..11949faf9 100644
--- a/pkg/provider/azure_vmss_cache.go
+++ b/pkg/provider/azure_vmss_cache.go
@@ -25,12 +25,12 @@ import (
 
 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute"
 
-	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/klog/v2"
 	"k8s.io/utils/pointer"
 
 	azcache "sigs.k8s.io/cloud-provider-azure/pkg/cache"
 	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
+	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 )
 
 type VMSSVirtualMachineEntry struct {
@@ -48,11 +48,11 @@ type VMSSEntry struct {
 }
 
 type NonVmssUniformNodesEntry struct {
-	VMSSFlexVMNodeNames   sets.Set[string]
-	VMSSFlexVMProviderIDs sets.Set[string]
-	AvSetVMNodeNames      sets.Set[string]
-	AvSetVMProviderIDs    sets.Set[string]
-	ClusterNodeNames      sets.Set[string]
+	VMSSFlexVMNodeNames   *utilsets.IgnoreCaseSet
+	VMSSFlexVMProviderIDs *utilsets.IgnoreCaseSet
+	AvSetVMNodeNames      *utilsets.IgnoreCaseSet
+	AvSetVMProviderIDs    *utilsets.IgnoreCaseSet
+	ClusterNodeNames      *utilsets.IgnoreCaseSet
 }
 
 type VMManagementType string
@@ -324,10 +324,10 @@ func (ss *ScaleSet) updateCache(nodeName, resourceGroupName, vmssName, instanceI
 
 func (ss *ScaleSet) newNonVmssUniformNodesCache() (azcache.Resource, error) {
 	getter := func(key string) (interface{}, error) {
-		vmssFlexVMNodeNames := sets.New[string]()
-		vmssFlexVMProviderIDs := sets.New[string]()
-		avSetVMNodeNames := sets.New[string]()
-		avSetVMProviderIDs := sets.New[string]()
+		vmssFlexVMNodeNames := utilsets.NewString()
+		vmssFlexVMProviderIDs := utilsets.NewString()
+		avSetVMNodeNames := utilsets.NewString()
+		avSetVMProviderIDs := utilsets.NewString()
 		resourceGroups, err := ss.GetResourceGroups()
 		if err != nil {
 			return nil, err
diff --git a/pkg/util/sets/string.go b/pkg/util/sets/string.go
new file mode 100644
index 000000000..2562fcf6e
--- /dev/null
+++ b/pkg/util/sets/string.go
@@ -0,0 +1,100 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package sets
+
+import (
+	"strings"
+
+	"k8s.io/apimachinery/pkg/util/sets"
+)
+
+// IgnoreCaseSet is a set of strings that is case-insensitive.
+type IgnoreCaseSet struct {
+	set sets.Set[string]
+}
+
+// NewString creates a new IgnoreCaseSet with the given items.
+func NewString(items ...string) *IgnoreCaseSet {
+	var lowerItems []string
+	for _, item := range items {
+		lowerItems = append(lowerItems, strings.ToLower(item))
+	}
+	set := sets.New[string](lowerItems...)
+	return &IgnoreCaseSet{set: set}
+}
+
+// Insert adds the given items to the set. It only works if the set is initialized.
+func (s *IgnoreCaseSet) Insert(items ...string) {
+	var lowerItems []string
+	for _, item := range items {
+		lowerItems = append(lowerItems, strings.ToLower(item))
+	}
+	for _, item := range lowerItems {
+		s.set.Insert(item)
+	}
+}
+
+// SafeInsert creates a new IgnoreCaseSet with the given items if the set is not initialized.
+// This is the recommended way to insert elements into the set.
+func SafeInsert(s *IgnoreCaseSet, items ...string) *IgnoreCaseSet {
+	if s.Initialized() {
+		s.Insert(items...)
+		return s
+	}
+	return NewString(items...)
+}
+
+// Delete removes the given item from the set.
+// It will be a no-op if the set is not initialized or the item is not in the set.
+func (s *IgnoreCaseSet) Delete(item string) bool {
+	var has bool
+	item = strings.ToLower(item)
+	if s.Initialized() && s.Has(item) {
+		s.set.Delete(item)
+		has = true
+	}
+	return has
+}
+
+// Has returns true if the given item is in the set, and the set is initialized.
+func (s *IgnoreCaseSet) Has(item string) bool {
+	if !s.Initialized() {
+		return false
+	}
+	return s.set.Has(strings.ToLower(item))
+}
+
+// Initialized returns true if the set is initialized.
+func (s *IgnoreCaseSet) Initialized() bool {
+	return s != nil && s.set != nil
+}
+
+// UnsortedList returns the items in the set in an arbitrary order.
+func (s *IgnoreCaseSet) UnsortedList() []string {
+	if !s.Initialized() {
+		return []string{}
+	}
+	return s.set.UnsortedList()
+}
+
+// Len returns the number of items in the set.
+func (s *IgnoreCaseSet) Len() int {
+	if !s.Initialized() {
+		return 0
+	}
+	return s.set.Len()
+}


Test patch:
diff --git a/pkg/provider/azure_instances_test.go b/pkg/provider/azure_instances_test.go
index 888761556..1dfccb250 100644
--- a/pkg/provider/azure_instances_test.go
+++ b/pkg/provider/azure_instances_test.go
@@ -24,7 +24,6 @@ import (
 	"strings"
 	"testing"
 
-	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/apimachinery/pkg/util/wait"
 
 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute"
@@ -46,6 +45,7 @@ import (
 	azcache "sigs.k8s.io/cloud-provider-azure/pkg/cache"
 	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
 	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
+	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 )
 
 // setTestVirtualMachines sets test virtual machine with powerstate.
@@ -883,7 +883,7 @@ func TestInstanceMetadata(t *testing.T) {
 
 	t.Run("instance not exists", func(t *testing.T) {
 		cloud := GetTestCloud(ctrl)
-		cloud.unmanagedNodes = sets.New("node0")
+		cloud.unmanagedNodes = utilsets.NewString("node0")
 
 		meta, err := cloud.InstanceMetadata(context.Background(), &v1.Node{
 			ObjectMeta: metav1.ObjectMeta{
@@ -992,7 +992,7 @@ func TestCloud_InstanceExists(t *testing.T) {
 	t.Run("should return true when instance is not managed by azure", func(t *testing.T) {
 		ctx := context.Background()
 		cloud := GetTestCloud(ctrl)
-		cloud.unmanagedNodes = sets.New("foo")
+		cloud.unmanagedNodes = utilsets.NewString("foo")
 		node := &v1.Node{
 			ObjectMeta: metav1.ObjectMeta{
 				Name: "foo",
diff --git a/pkg/provider/azure_loadbalancer_backendpool_test.go b/pkg/provider/azure_loadbalancer_backendpool_test.go
index 98ebab754..cfc3808ed 100644
--- a/pkg/provider/azure_loadbalancer_backendpool_test.go
+++ b/pkg/provider/azure_loadbalancer_backendpool_test.go
@@ -24,11 +24,11 @@ import (
 
 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2022-07-01/network"
 	"github.com/stretchr/testify/assert"
+
 	"go.uber.org/mock/gomock"
 
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/client-go/kubernetes/fake"
 	cloudprovider "k8s.io/cloud-provider"
 	"k8s.io/utils/pointer"
@@ -36,6 +36,7 @@ import (
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/loadbalancerclient/mockloadbalancerclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
 	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
+	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 )
 
 func TestEnsureHostsInPoolNodeIP(t *testing.T) {
@@ -221,7 +222,7 @@ func TestEnsureHostsInPoolNodeIP(t *testing.T) {
 				{
 					Name: "kubernetes",
 					MultipleStandardLoadBalancerConfigurationStatus: MultipleStandardLoadBalancerConfigurationStatus{
-						ActiveNodes: sets.New[string]("vmss-2"),
+						ActiveNodes: utilsets.NewString("vmss-2"),
 					},
 				},
 			},
@@ -420,9 +421,9 @@ func TestEnsureHostsInPoolNodeIP(t *testing.T) {
 				az.endpointSlicesCache.Store("another/eps", epsInAnotherNamespace)
 			}
 			az.KubeClient = kubeClient
-			az.nodePrivateIPs = map[string]sets.Set[string]{
-				"vmss-0": sets.New[string]("1.2.3.4"),
-				"vmss-1": sets.New[string]("5.6.7.8"),
+			az.nodePrivateIPs = map[string]*utilsets.IgnoreCaseSet{
+				"vmss-0": utilsets.NewString("1.2.3.4"),
+				"vmss-1": utilsets.NewString("5.6.7.8"),
 			}
 
 			service := getTestServiceDualStack("svc-1", v1.ProtocolTCP, nil, 80)
@@ -647,7 +648,7 @@ func TestReconcileBackendPoolsNodeIPConfig(t *testing.T) {
 	az.VMSet = mockVMSet
 	az.LoadBalancerClient = mockLBClient
 	az.nodeInformerSynced = func() bool { return true }
-	az.excludeLoadBalancerNodes = sets.New("k8s-agentpool1-00000000")
+	az.excludeLoadBalancerNodes = utilsets.NewString("k8s-agentpool1-00000000")
 
 	bc := newBackendPoolTypeNodeIPConfig(az)
 	svc := getTestService("test", v1.ProtocolTCP, nil, false, 80)
@@ -688,7 +689,7 @@ func TestReconcileBackendPoolsNodeIPConfigRemoveIPConfig(t *testing.T) {
 	az := GetTestCloud(ctrl)
 	az.VMSet = mockVMSet
 	az.nodeInformerSynced = func() bool { return true }
-	az.excludeLoadBalancerNodes = sets.New("k8s-agentpool1-00000000")
+	az.excludeLoadBalancerNodes = utilsets.NewString("k8s-agentpool1-00000000")
 
 	bc := newBackendPoolTypeNodeIPConfig(az)
 	svc := getTestService("test", v1.ProtocolTCP, nil, false, 80)
@@ -806,8 +807,8 @@ func TestReconcileBackendPoolsNodeIP(t *testing.T) {
 	az := GetTestCloud(ctrl)
 	az.LoadBalancerBackendPoolConfigurationType = consts.LoadBalancerBackendPoolConfigurationTypeNodeIP
 	az.KubeClient = fake.NewSimpleClientset(nodes[0], nodes[1])
-	az.excludeLoadBalancerNodes = sets.New("vmss-0")
-	az.nodePrivateIPs["vmss-0"] = sets.New("10.0.0.1")
+	az.excludeLoadBalancerNodes = utilsets.NewString("vmss-0")
+	az.nodePrivateIPs["vmss-0"] = utilsets.NewString("10.0.0.1")
 
 	lbClient := mockloadbalancerclient.NewMockInterface(ctrl)
 	lbClient.EXPECT().CreateOrUpdateBackendPools(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), bp, gomock.Any()).Return(nil)
@@ -1026,8 +1027,8 @@ func TestGetBackendPrivateIPsNodeIPConfig(t *testing.T) {
 	mockVMSet.EXPECT().GetNodeNameByIPConfigurationID("ipconfig2").Return("node2", "", nil)
 
 	az := GetTestCloud(ctrl)
-	az.nodePrivateIPs = map[string]sets.Set[string]{
-		"node1": sets.New("1.2.3.4", "fe80::1"),
+	az.nodePrivateIPs = map[string]*utilsets.IgnoreCaseSet{
+		"node1": utilsets.NewString("1.2.3.4", "fe80::1"),
 	}
 	az.VMSet = mockVMSet
 	bc := newBackendPoolTypeNodeIPConfig(az)
diff --git a/pkg/provider/azure_loadbalancer_test.go b/pkg/provider/azure_loadbalancer_test.go
index eb2addcc0..b55f4b098 100644
--- a/pkg/provider/azure_loadbalancer_test.go
+++ b/pkg/provider/azure_loadbalancer_test.go
@@ -31,6 +31,7 @@ import (
 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute"
 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2022-07-01/network"
 	"github.com/stretchr/testify/assert"
+
 	"go.uber.org/mock/gomock"
 
 	"golang.org/x/text/cases"
@@ -51,6 +52,7 @@ import (
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/zoneclient/mockzoneclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
 	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
+	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 )
 
 // LBInUseRawError is the LoadBalancerInUseByVirtualMachineScaleSet raw error
@@ -1652,7 +1654,7 @@ func TestGetServiceLoadBalancerMultiSLB(t *testing.T) {
 				{
 					Name: "lb2",
 					MultipleStandardLoadBalancerConfigurationStatus: MultipleStandardLoadBalancerConfigurationStatus{
-						ActiveServices: sets.Set[string]{"default/test1": sets.Empty{}},
+						ActiveServices: utilsets.NewString("default/test1"),
 					},
 				},
 			},
@@ -1733,7 +1735,7 @@ func TestGetServiceLoadBalancerMultiSLB(t *testing.T) {
 				{
 					Name: "lb2",
 					MultipleStandardLoadBalancerConfigurationStatus: MultipleStandardLoadBalancerConfigurationStatus{
-						ActiveServices: sets.Set[string]{"default/test1": sets.Empty{}},
+						ActiveServices: utilsets.NewString("default/test1"),
 					},
 				},
 			},
@@ -6750,9 +6752,9 @@ func TestSafeDeleteLoadBalancer(t *testing.T) {
 		expectedDeleteCall            bool
 		expectedDecoupleErr           error
 		multiSLBConfigs               []MultipleStandardLoadBalancerConfiguration
-		nodesWithCorrectVMSet         sets.Set[string]
+		nodesWithCorrectVMSet         *utilsets.IgnoreCaseSet
 		expectedMultiSLBConfigs       []MultipleStandardLoadBalancerConfiguration
-		expectedNodesWithCorrectVMSet sets.Set[string]
+		expectedNodesWithCorrectVMSet *utilsets.IgnoreCaseSet
 		expectedErr                   *retry.Error
 	}{
 		{
@@ -6776,32 +6778,32 @@ func TestSafeDeleteLoadBalancer(t *testing.T) {
 				{
 					Name: "test",
 					MultipleStandardLoadBalancerConfigurationStatus: MultipleStandardLoadBalancerConfigurationStatus{
-						ActiveNodes: sets.New[string]("node1", "node2"),
+						ActiveNodes: utilsets.NewString("node1", "node2"),
 					},
 				},
 				{
 					Name: "test2",
 					MultipleStandardLoadBalancerConfigurationStatus: MultipleStandardLoadBalancerConfigurationStatus{
-						ActiveNodes: sets.New[string]("node3"),
+						ActiveNodes: utilsets.NewString("node3"),
 					},
 				},
 			},
-			nodesWithCorrectVMSet: sets.New[string]("node1", "node2", "node3"),
+			nodesWithCorrectVMSet: utilsets.NewString("node1", "node2", "node3"),
 			expectedMultiSLBConfigs: []MultipleStandardLoadBalancerConfiguration{
 				{
 					Name: "test",
 					MultipleStandardLoadBalancerConfigurationStatus: MultipleStandardLoadBalancerConfigurationStatus{
-						ActiveNodes: sets.New[string](),
+						ActiveNodes: utilsets.NewString(),
 					},
 				},
 				{
 					Name: "test2",
 					MultipleStandardLoadBalancerConfigurationStatus: MultipleStandardLoadBalancerConfigurationStatus{
-						ActiveNodes: sets.New[string]("node3"),
+						ActiveNodes: utilsets.NewString("node3"),
 					},
 				},
 			},
-			expectedNodesWithCorrectVMSet: sets.New[string]("node3"),
+			expectedNodesWithCorrectVMSet: utilsets.NewString("node3"),
 		},
 	}
 
@@ -6823,7 +6825,7 @@ func TestSafeDeleteLoadBalancer(t *testing.T) {
 			cloud.LoadBalancerClient = mockLBClient
 			if len(tc.multiSLBConfigs) > 0 {
 				cloud.MultipleStandardLoadBalancerConfigurations = tc.multiSLBConfigs
-				for nodeName := range tc.nodesWithCorrectVMSet {
+				for _, nodeName := range tc.nodesWithCorrectVMSet.UnsortedList() {
 					cloud.nodesWithCorrectLoadBalancerByPrimaryVMSet.Store(nodeName, sets.Empty{})
 				}
 			}
@@ -6838,7 +6840,7 @@ func TestSafeDeleteLoadBalancer(t *testing.T) {
 			assert.Equal(t, tc.expectedErr, err)
 			if len(tc.multiSLBConfigs) > 0 {
 				assert.Equal(t, tc.expectedMultiSLBConfigs, cloud.MultipleStandardLoadBalancerConfigurations)
-				actualNodesWithCorrectLoadBalancerByPrimaryVMSet := sets.New[string]()
+				actualNodesWithCorrectLoadBalancerByPrimaryVMSet := utilsets.NewString()
 				cloud.nodesWithCorrectLoadBalancerByPrimaryVMSet.Range(func(key, value interface{}) bool {
 					actualNodesWithCorrectLoadBalancerByPrimaryVMSet.Insert(key.(string))
 					return true
@@ -7118,7 +7120,7 @@ func TestGetEligibleLoadBalancers(t *testing.T) {
 						},
 					},
 					MultipleStandardLoadBalancerConfigurationStatus: MultipleStandardLoadBalancerConfigurationStatus{
-						ActiveServices: sets.New[string]("default/test"),
+						ActiveServices: utilsets.NewString("default/test"),
 					},
 				},
 			},
@@ -7183,7 +7185,7 @@ func TestGetEligibleLoadBalancers(t *testing.T) {
 						AllowServicePlacement: pointer.Bool(false),
 					},
 					MultipleStandardLoadBalancerConfigurationStatus: MultipleStandardLoadBalancerConfigurationStatus{
-						ActiveServices: sets.New[string]("default/test"),
+						ActiveServices: utilsets.NewString("default/test"),
 					},
 				},
 				{
@@ -7503,15 +7505,15 @@ func TestReconcileMultipleStandardLoadBalancerConfigurations(t *testing.T) {
 		useMultipleLB          bool
 		noPrimaryConfig        bool
 		nodes                  []*v1.Node
-		expectedActiveServices map[string]sets.Set[string]
+		expectedActiveServices map[string]*utilsets.IgnoreCaseSet
 		expectedErr            error
 	}{
 		{
 			description:   "should set active services correctly",
 			useMultipleLB: true,
-			expectedActiveServices: map[string]sets.Set[string]{
-				"kubernetes": sets.New[string]("default/lbsvconkubernetes"),
-				"lb1":        sets.New[string]("ns1/lbsvconlb1"),
+			expectedActiveServices: map[string]*utilsets.IgnoreCaseSet{
+				"kubernetes": utilsets.NewString("default/lbsvconkubernetes"),
+				"lb1":        utilsets.NewString("ns1/lbsvconlb1"),
 			},
 		},
 		{
@@ -7608,13 +7610,9 @@ func TestReconcileMultipleStandardLoadBalancerConfigurations(t *testing.T) {
 			err := az.reconcileMultipleStandardLoadBalancerConfigurations(&existingLBs, &svc, "kubernetes", &existingLBs, tc.nodes)
 			assert.Equal(t, err, tc.expectedErr)
 
-			activeServices := make(map[string]sets.Set[string])
+			activeServices := make(map[string]*utilsets.IgnoreCaseSet)
 			for _, multiSLBConfig := range az.MultipleStandardLoadBalancerConfigurations {
-				svcNames := sets.New[string]()
-				for activeService := range multiSLBConfig.ActiveServices {
-					svcNames.Insert(activeService)
-				}
-				activeServices[multiSLBConfig.Name] = svcNames
+				activeServices[multiSLBConfig.Name] = multiSLBConfig.ActiveServices
 			}
 			for lbConfigName, svcNames := range tc.expectedActiveServices {
 				assert.Equal(t, svcNames, activeServices[lbConfigName])
@@ -8080,7 +8078,7 @@ func TestReconcileMultipleStandardLoadBalancerNodes(t *testing.T) {
 		existingNodes        []*v1.Node
 		existingLBs          []network.LoadBalancer
 		expectedPutLBTimes   int
-		expectedLBToNodesMap map[string]sets.Set[string]
+		expectedLBToNodesMap map[string]*utilsets.IgnoreCaseSet
 	}{
 		{
 			description: "should remove unwanted nodes and arrange existing nodes with primary vmSet as expected",
@@ -8091,7 +8089,7 @@ func TestReconcileMultipleStandardLoadBalancerNodes(t *testing.T) {
 						PrimaryVMSet: "vmss-1",
 					},
 					MultipleStandardLoadBalancerConfigurationStatus: MultipleStandardLoadBalancerConfigurationStatus{
-						ActiveNodes: sets.Set[string]{"node1": sets.Empty{}, "node2": sets.Empty{}},
+						ActiveNodes: utilsets.NewString("node1", "node2"),
 					},
 				},
 				{
@@ -8100,7 +8098,7 @@ func TestReconcileMultipleStandardLoadBalancerNodes(t *testing.T) {
 						PrimaryVMSet: "vmss-2",
 					},
 					MultipleStandardLoadBalancerConfigurationStatus: MultipleStandardLoadBalancerConfigurationStatus{
-						ActiveNodes: sets.Set[string]{"node3": sets.Empty{}, "node4": sets.Empty{}},
+						ActiveNodes: utilsets.NewString("node3", "node4"),
 					},
 				},
 			},
@@ -8164,9 +8162,9 @@ func TestReconcileMultipleStandardLoadBalancerNodes(t *testing.T) {
 				},
 			},
 			expectedPutLBTimes: 1,
-			expectedLBToNodesMap: map[string]sets.Set[string]{
-				"lb1": {"node1": sets.Empty{}},
-				"lb2": {"node2": sets.Empty{}, "node3": sets.Empty{}},
+			expectedLBToNodesMap: map[string]*utilsets.IgnoreCaseSet{
+				"lb1": utilsets.NewString("node1"),
+				"lb2": utilsets.NewString("node2", "node3"),
 			},
 		},
 		{
@@ -8182,7 +8180,7 @@ func TestReconcileMultipleStandardLoadBalancerNodes(t *testing.T) {
 						},
 					},
 					MultipleStandardLoadBalancerConfigurationStatus: MultipleStandardLoadBalancerConfigurationStatus{
-						ActiveNodes: sets.Set[string]{"node1": sets.Empty{}, "node2": sets.Empty{}},
+						ActiveNodes: utilsets.NewString("node1", "node2"),
 					},
 				},
 				{
@@ -8194,7 +8192,7 @@ func TestReconcileMultipleStandardLoadBalancerNodes(t *testing.T) {
 						},
 					},
 					MultipleStandardLoadBalancerConfigurationStatus: MultipleStandardLoadBalancerConfigurationStatus{
-						ActiveNodes: sets.Set[string]{"node3": sets.Empty{}, "node4": sets.Empty{}},
+						ActiveNodes: utilsets.NewString("node3", "node4"),
 					},
 				},
 				{
@@ -8268,11 +8266,11 @@ func TestReconcileMultipleStandardLoadBalancerNodes(t *testing.T) {
 					},
 				},
 			},
-			expectedLBToNodesMap: map[string]sets.Set[string]{
-				"lb1": {"node1": sets.Empty{}},
-				"lb2": {"node3": sets.Empty{}},
-				"lb3": {"node5": sets.Empty{}},
-				"lb4": {"node2": sets.Empty{}, "node6": sets.Empty{}},
+			expectedLBToNodesMap: map[string]*utilsets.IgnoreCaseSet{
+				"lb1": utilsets.NewString("node1"),
+				"lb2": utilsets.NewString("node3"),
+				"lb3": utilsets.NewString("node5"),
+				"lb4": utilsets.NewString("node2", "node6"),
 			},
 		},
 		{
@@ -8287,15 +8285,15 @@ func TestReconcileMultipleStandardLoadBalancerNodes(t *testing.T) {
 						},
 					},
 					MultipleStandardLoadBalancerConfigurationStatus: MultipleStandardLoadBalancerConfigurationStatus{
-						ActiveNodes: sets.Set[string]{"node1": sets.Empty{}, "node2": sets.Empty{}},
+						ActiveNodes: utilsets.NewString("node1", "node2"),
 					},
 				},
 			},
 			existingNodes: []*v1.Node{
 				getTestNodeWithMetadata("node1", "vmss-2", map[string]string{"k2": "v2"}, "10.1.0.1"),
 			},
-			expectedLBToNodesMap: map[string]sets.Set[string]{
-				"lb1": {},
+			expectedLBToNodesMap: map[string]*utilsets.IgnoreCaseSet{
+				"lb1": utilsets.NewString(),
 			},
 		},
 		{
@@ -8359,11 +8357,11 @@ func TestReconcileMultipleStandardLoadBalancerNodes(t *testing.T) {
 				getTestNodeWithMetadata("node5", "vmss-3", map[string]string{"k2": "v2"}, "10.1.0.5"),
 				getTestNodeWithMetadata("node6", "vmss-3", map[string]string{"k3": "v3"}, "10.1.0.6"),
 			},
-			expectedLBToNodesMap: map[string]sets.Set[string]{
+			expectedLBToNodesMap: map[string]*utilsets.IgnoreCaseSet{
 				"lb1": nil,
-				"lb2": {"node3": sets.Empty{}, "node5": sets.Empty{}},
+				"lb2": utilsets.NewString("node3", "node5"),
 				"lb3": nil,
-				"lb4": {"node1": sets.Empty{}, "node2": sets.Empty{}, "node6": sets.Empty{}},
+				"lb4": utilsets.NewString("node1", "node2", "node6"),
 			},
 		},
 		{
@@ -8417,9 +8415,9 @@ func TestReconcileMultipleStandardLoadBalancerNodes(t *testing.T) {
 				getTestNodeWithMetadata("node5", "vmss-3", map[string]string{"k2": "v2"}, "10.1.0.5"),
 				getTestNodeWithMetadata("node6", "vmss-3", map[string]string{"k3": "v3"}, "10.1.0.6"),
 			},
-			expectedLBToNodesMap: map[string]sets.Set[string]{
+			expectedLBToNodesMap: map[string]*utilsets.IgnoreCaseSet{
 				"lb1": nil,
-				"lb2": {"node3": sets.Empty{}, "node5": sets.Empty{}},
+				"lb2": utilsets.NewString("node3", "node5"),
 				"lb3": nil,
 				"lb4": nil,
 			},
@@ -8435,7 +8433,7 @@ func TestReconcileMultipleStandardLoadBalancerNodes(t *testing.T) {
 			svc := getTestService("test", v1.ProtocolTCP, nil, false)
 			_ = az.reconcileMultipleStandardLoadBalancerBackendNodes(tc.lbName, &tc.existingLBs, &svc, tc.existingNodes)
 
-			expectedLBToNodesMap := make(map[string]sets.Set[string])
+			expectedLBToNodesMap := make(map[string]*utilsets.IgnoreCaseSet)
 			for _, multiSLBConfig := range az.MultipleStandardLoadBalancerConfigurations {
 				expectedLBToNodesMap[multiSLBConfig.Name] = multiSLBConfig.ActiveNodes
 			}
diff --git a/pkg/provider/azure_local_services_test.go b/pkg/provider/azure_local_services_test.go
index fdd44cbe1..34cfd8e8e 100644
--- a/pkg/provider/azure_local_services_test.go
+++ b/pkg/provider/azure_local_services_test.go
@@ -32,7 +32,6 @@ import (
 	v1 "k8s.io/api/core/v1"
 	discovery_v1 "k8s.io/api/discovery/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/client-go/informers"
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/kubernetes/fake"
@@ -41,6 +40,7 @@ import (
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/loadbalancerclient/mockloadbalancerclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
 	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
+	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 )
 
 func TestLoadBalancerBackendPoolUpdater(t *testing.T) {
@@ -488,9 +488,9 @@ func TestEndpointSlicesInformer(t *testing.T) {
 				},
 			}
 			cloud.localServiceNameToServiceInfoMap.Store("test/svc1", newServiceInfo(consts.IPVersionIPv4String, "lb1"))
-			cloud.nodePrivateIPs = map[string]sets.Set[string]{
-				"node1": sets.New[string]("10.0.0.1"),
-				"node2": sets.New[string]("10.0.0.2"),
+			cloud.nodePrivateIPs = map[string]*utilsets.IgnoreCaseSet{
+				"node1": utilsets.NewString("10.0.0.1"),
+				"node2": utilsets.NewString("10.0.0.2"),
 			}
 
 			existingBackendPool := getTestBackendAddressPoolWithIPs("lb1", "test-svc1", []string{"10.0.0.1"})
@@ -574,9 +574,9 @@ func TestCheckAndApplyLocalServiceBackendPoolUpdates(t *testing.T) {
 				},
 			}
 			cloud.localServiceNameToServiceInfoMap.Store("default/svc1", newServiceInfo(consts.IPVersionIPv4String, "lb1"))
-			cloud.nodePrivateIPs = map[string]sets.Set[string]{
-				"node1": sets.New[string]("10.0.0.1", "fd00::1"),
-				"node2": sets.New[string]("10.0.0.2", "fd00::2"),
+			cloud.nodePrivateIPs = map[string]*utilsets.IgnoreCaseSet{
+				"node1": utilsets.NewString("10.0.0.1", "fd00::1"),
+				"node2": utilsets.NewString("10.0.0.2", "fd00::2"),
 			}
 
 			existingBackendPool := getTestBackendAddressPoolWithIPs("lb1", "default-svc1", []string{"10.0.0.1"})
diff --git a/pkg/provider/azure_routes_test.go b/pkg/provider/azure_routes_test.go
index ddc2f1e91..de3fb7682 100644
--- a/pkg/provider/azure_routes_test.go
+++ b/pkg/provider/azure_routes_test.go
@@ -29,12 +29,12 @@ import (
 	"go.uber.org/mock/gomock"
 
 	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/sets"
 	cloudprovider "k8s.io/cloud-provider"
 	"k8s.io/utils/pointer"
 
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/routetableclient/mockroutetableclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
+	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 )
 
 func TestDeleteRoute(t *testing.T) {
@@ -49,7 +49,7 @@ func TestDeleteRoute(t *testing.T) {
 			RouteTableName:          "bar",
 			Location:                "location",
 		},
-		unmanagedNodes:     sets.New[string](),
+		unmanagedNodes:     utilsets.NewString(),
 		nodeInformerSynced: func() bool { return true },
 	}
 	cache, _ := cloud.newRouteTableCache()
@@ -121,7 +121,7 @@ func TestDeleteRouteDualStack(t *testing.T) {
 			RouteTableName:          "bar",
 			Location:                "location",
 		},
-		unmanagedNodes:       sets.New[string](),
+		unmanagedNodes:       utilsets.NewString(),
 		nodeInformerSynced:   func() bool { return true },
 		ipv6DualStackEnabled: true,
 	}
@@ -194,7 +194,7 @@ func TestCreateRoute(t *testing.T) {
 			RouteTableName:          "bar",
 			Location:                "location",
 		},
-		unmanagedNodes:     sets.New[string](),
+		unmanagedNodes:     utilsets.NewString(),
 		nodeInformerSynced: func() bool { return true },
 	}
 	cache, _ := cloud.newRouteTableCache()
@@ -353,7 +353,7 @@ func TestCreateRoute(t *testing.T) {
 			cloud.unmanagedNodes.Insert(test.unmanagedNodeName)
 			cloud.routeCIDRs = test.routeCIDRs
 		} else {
-			cloud.unmanagedNodes = sets.New[string]()
+			cloud.unmanagedNodes = utilsets.NewString()
 			cloud.routeCIDRs = nil
 		}
 		if test.nodeInformerNotSynced {
@@ -601,7 +601,7 @@ func TestListRoutes(t *testing.T) {
 			RouteTableName:          "bar",
 			Location:                "location",
 		},
-		unmanagedNodes:     sets.New[string](),
+		unmanagedNodes:     utilsets.NewString(),
 		nodeInformerSynced: func() bool { return true },
 	}
 	cache, _ := cloud.newRouteTableCache()
@@ -713,7 +713,7 @@ func TestListRoutes(t *testing.T) {
 			cloud.unmanagedNodes.Insert(test.unmanagedNodeName)
 			cloud.routeCIDRs = test.routeCIDRs
 		} else {
-			cloud.unmanagedNodes = sets.New[string]()
+			cloud.unmanagedNodes = utilsets.NewString()
 			cloud.routeCIDRs = nil
 		}
 
@@ -741,7 +741,7 @@ func TestCleanupOutdatedRoutes(t *testing.T) {
 	for _, testCase := range []struct {
 		description                          string
 		existingRoutes, expectedRoutes       []network.Route
-		existingNodeNames                    sets.Set[string]
+		existingNodeNames                    *utilsets.IgnoreCaseSet
 		expectedChanged, enableIPV6DualStack bool
 	}{
 		{
@@ -753,7 +753,7 @@ func TestCleanupOutdatedRoutes(t *testing.T) {
 			expectedRoutes: []network.Route{
 				{Name: pointer.String("aks-node1-vmss000000____xxx")},
 			},
-			existingNodeNames:   sets.New("aks-node1-vmss000000"),
+			existingNodeNames:   utilsets.NewString("aks-node1-vmss000000"),
 			enableIPV6DualStack: true,
 			expectedChanged:     true,
 		},
@@ -766,7 +766,7 @@ func TestCleanupOutdatedRoutes(t *testing.T) {
 			expectedRoutes: []network.Route{
 				{Name: pointer.String("aks-node1-vmss000000")},
 			},
-			existingNodeNames: sets.New("aks-node1-vmss000000"),
+			existingNodeNames: utilsets.NewString("aks-node1-vmss000000"),
 			expectedChanged:   true,
 		},
 		{
@@ -779,7 +779,7 @@ func TestCleanupOutdatedRoutes(t *testing.T) {
 				{Name: pointer.String("aks-node1-vmss000000____xxx")},
 				{Name: pointer.String("aks-node1-vmss000000")},
 			},
-			existingNodeNames:   sets.New("aks-node1-vmss000001"),
+			existingNodeNames:   utilsets.NewString("aks-node1-vmss000001"),
 			enableIPV6DualStack: true,
 		},
 		{
@@ -792,7 +792,7 @@ func TestCleanupOutdatedRoutes(t *testing.T) {
 				{Name: pointer.String("aks-node1-vmss000000____xxx")},
 				{Name: pointer.String("aks-node1-vmss000000")},
 			},
-			existingNodeNames: sets.New("aks-node1-vmss000001"),
+			existingNodeNames: utilsets.NewString("aks-node1-vmss000001"),
 		},
 	} {
 		t.Run(testCase.description, func(t *testing.T) {
diff --git a/pkg/provider/azure_standard_test.go b/pkg/provider/azure_standard_test.go
index 9369480ad..703ca0aca 100644
--- a/pkg/provider/azure_standard_test.go
+++ b/pkg/provider/azure_standard_test.go
@@ -31,7 +31,6 @@ import (
 	v1 "k8s.io/api/core/v1"
 	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/sets"
 	cloudprovider "k8s.io/cloud-provider"
 	"k8s.io/utils/pointer"
 
@@ -41,6 +40,7 @@ import (
 	azcache "sigs.k8s.io/cloud-provider-azure/pkg/cache"
 	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
 	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
+	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 )
 
 const (
@@ -1572,7 +1572,7 @@ func TestStandardEnsureHostsInPool(t *testing.T) {
 		t.Run(test.name, func(t *testing.T) {
 			cloud.Config.LoadBalancerSku = consts.LoadBalancerSkuStandard
 			cloud.Config.ExcludeMasterFromStandardLB = pointer.Bool(true)
-			cloud.excludeLoadBalancerNodes = sets.New(test.excludeLBNodes...)
+			cloud.excludeLoadBalancerNodes = utilsets.NewString(test.excludeLBNodes...)
 
 			testVM := buildDefaultTestVirtualMachine(availabilitySetID, []string{test.nicID})
 			testNIC := buildDefaultTestInterface(false, []string{backendAddressPoolID})
diff --git a/pkg/provider/azure_test.go b/pkg/provider/azure_test.go
index 8b7ccfd40..e26e56043 100644
--- a/pkg/provider/azure_test.go
+++ b/pkg/provider/azure_test.go
@@ -30,12 +30,12 @@ import (
 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute"
 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2022-07-01/network"
 	"github.com/stretchr/testify/assert"
+
 	"go.uber.org/mock/gomock"
 
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/client-go/informers"
 	"k8s.io/client-go/kubernetes/fake"
 	cloudprovider "k8s.io/cloud-provider"
@@ -44,9 +44,8 @@ import (
 	"k8s.io/utils/pointer"
 
 	"sigs.k8s.io/cloud-provider-azure/pkg/azclient"
-	"sigs.k8s.io/cloud-provider-azure/pkg/azclient/mock_azclient"
-
 	"sigs.k8s.io/cloud-provider-azure/pkg/azclient/configloader"
+	"sigs.k8s.io/cloud-provider-azure/pkg/azclient/mock_azclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/interfaceclient/mockinterfaceclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/loadbalancerclient/mockloadbalancerclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/privatelinkserviceclient/mockprivatelinkserviceclient"
@@ -58,6 +57,7 @@ import (
 	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
 	providerconfig "sigs.k8s.io/cloud-provider-azure/pkg/provider/config"
 	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
+	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 	"sigs.k8s.io/cloud-provider-azure/pkg/util/taints"
 )
 
@@ -2281,11 +2281,11 @@ func getCloudFromConfig(t *testing.T, config string) *Cloud {
 	assert.NoError(t, err)
 
 	az := &Cloud{
-		nodeNames:                sets.New[string](),
-		nodeZones:                map[string]sets.Set[string]{},
+		nodeNames:                utilsets.NewString(),
+		nodeZones:                map[string]*utilsets.IgnoreCaseSet{},
 		nodeResourceGroups:       map[string]string{},
-		unmanagedNodes:           sets.New[string](),
-		excludeLoadBalancerNodes: sets.New[string](),
+		unmanagedNodes:           utilsets.NewString(),
+		excludeLoadBalancerNodes: utilsets.NewString(),
 		routeCIDRs:               map[string]string{},
 		ZoneClient:               mockzoneclient.NewMockInterface(ctrl),
 		ComputeClientFactory:     mock_azclient.NewMockClientFactory(ctrl),
@@ -2400,7 +2400,7 @@ func TestGetResourceGroups(t *testing.T) {
 	tests := []struct {
 		name               string
 		nodeResourceGroups map[string]string
-		expected           sets.Set[string]
+		expected           *utilsets.IgnoreCaseSet
 		informerSynced     bool
 		expectError        bool
 	}{
@@ -2408,13 +2408,13 @@ func TestGetResourceGroups(t *testing.T) {
 			name:               "cloud provider configured RG should be returned by default",
 			nodeResourceGroups: map[string]string{},
 			informerSynced:     true,
-			expected:           sets.New("rg"),
+			expected:           utilsets.NewString("rg"),
 		},
 		{
 			name:               "cloud provider configured RG and node RGs should be returned",
 			nodeResourceGroups: map[string]string{"node1": "rg1", "node2": "rg2"},
 			informerSynced:     true,
-			expected:           sets.New("rg", "rg1", "rg2"),
+			expected:           utilsets.NewString("rg", "rg1", "rg2"),
 		},
 		{
 			name:               "error should be returned if informer hasn't synced yet",
@@ -2514,12 +2514,12 @@ func TestUpdateNodeCaches(t *testing.T) {
 	az := GetTestCloud(ctrl)
 	// delete node appearing in unmanagedNodes and excludeLoadBalancerNodes
 	zone := fmt.Sprintf("%s-0", az.Location)
-	nodesInZone := sets.New("prevNode")
-	az.nodeZones = map[string]sets.Set[string]{zone: nodesInZone}
+	nodesInZone := utilsets.NewString("prevNode")
+	az.nodeZones = map[string]*utilsets.IgnoreCaseSet{zone: nodesInZone}
 	az.nodeResourceGroups = map[string]string{"prevNode": "rg"}
-	az.unmanagedNodes = sets.New("prevNode")
-	az.excludeLoadBalancerNodes = sets.New("prevNode")
-	az.nodeNames = sets.New("prevNode")
+	az.unmanagedNodes = utilsets.NewString("prevNode")
+	az.excludeLoadBalancerNodes = utilsets.NewString("prevNode")
+	az.nodeNames = utilsets.NewString("prevNode")
 
 	prevNode := v1.Node{
 		ObjectMeta: metav1.ObjectMeta{
@@ -2533,11 +2533,11 @@ func TestUpdateNodeCaches(t *testing.T) {
 	}
 
 	az.updateNodeCaches(&prevNode, nil)
-	assert.Equal(t, 0, len(az.nodeZones[zone]))
+	assert.Equal(t, 0, az.nodeZones[zone].Len())
 	assert.Equal(t, 0, len(az.nodeResourceGroups))
-	assert.Equal(t, 0, len(az.unmanagedNodes))
-	assert.Equal(t, 1, len(az.excludeLoadBalancerNodes))
-	assert.Equal(t, 0, len(az.nodeNames))
+	assert.Equal(t, 0, az.unmanagedNodes.Len())
+	assert.Equal(t, 1, az.excludeLoadBalancerNodes.Len())
+	assert.Equal(t, 0, az.nodeNames.Len())
 
 	// add new (unmanaged and to-be-excluded) node
 	newNode := v1.Node{
@@ -2553,11 +2553,11 @@ func TestUpdateNodeCaches(t *testing.T) {
 	}
 
 	az.updateNodeCaches(nil, &newNode)
-	assert.Equal(t, 1, len(az.nodeZones[zone]))
+	assert.Equal(t, 1, az.nodeZones[zone].Len())
 	assert.Equal(t, 1, len(az.nodeResourceGroups))
-	assert.Equal(t, 1, len(az.unmanagedNodes))
-	assert.Equal(t, 2, len(az.excludeLoadBalancerNodes))
-	assert.Equal(t, 1, len(az.nodeNames))
+	assert.Equal(t, 1, az.unmanagedNodes.Len())
+	assert.Equal(t, 2, az.excludeLoadBalancerNodes.Len())
+	assert.Equal(t, 1, az.nodeNames.Len())
 }
 
 func TestUpdateNodeTaint(t *testing.T) {
@@ -2684,14 +2684,14 @@ func TestUpdateNodeCacheExcludeLoadBalancer(t *testing.T) {
 	az := GetTestCloud(ctrl)
 
 	zone := fmt.Sprintf("%s-0", az.Location)
-	nodesInZone := sets.New("aNode")
-	az.nodeZones = map[string]sets.Set[string]{zone: nodesInZone}
+	nodesInZone := utilsets.NewString("aNode")
+	az.nodeZones = map[string]*utilsets.IgnoreCaseSet{zone: nodesInZone}
 	az.nodeResourceGroups = map[string]string{"aNode": "rg"}
 
 	// a non-ready node should be included
-	az.unmanagedNodes = sets.New[string]()
-	az.excludeLoadBalancerNodes = sets.New[string]()
-	az.nodeNames = sets.New[string]()
+	az.unmanagedNodes = utilsets.NewString()
+	az.excludeLoadBalancerNodes = utilsets.NewString()
+	az.nodeNames = utilsets.NewString()
 	nonReadyNode := v1.Node{
 		ObjectMeta: metav1.ObjectMeta{
 			Labels: map[string]string{
@@ -2709,7 +2709,7 @@ func TestUpdateNodeCacheExcludeLoadBalancer(t *testing.T) {
 		},
 	}
 	az.updateNodeCaches(nil, &nonReadyNode)
-	assert.Equal(t, 0, len(az.excludeLoadBalancerNodes))
+	assert.Equal(t, 0, az.excludeLoadBalancerNodes.Len())
 
 	// node becomes ready => no impact on az.excludeLoadBalancerNodes
 	readyNode := v1.Node{
@@ -2729,7 +2729,7 @@ func TestUpdateNodeCacheExcludeLoadBalancer(t *testing.T) {
 		},
 	}
 	az.updateNodeCaches(&nonReadyNode, &readyNode)
-	assert.Equal(t, 0, len(az.excludeLoadBalancerNodes))
+	assert.Equal(t, 0, az.excludeLoadBalancerNodes.Len())
 
 	// new non-ready node with taint is added to the cluster: don't exclude it
 	nonReadyTaintedNode := v1.Node{
@@ -2756,7 +2756,7 @@ func TestUpdateNodeCacheExcludeLoadBalancer(t *testing.T) {
 		},
 	}
 	az.updateNodeCaches(nil, &nonReadyTaintedNode)
-	assert.Equal(t, 0, len(az.excludeLoadBalancerNodes))
+	assert.Equal(t, 0, az.excludeLoadBalancerNodes.Len())
 }
 
 func TestGetActiveZones(t *testing.T) {
@@ -2778,10 +2778,10 @@ func TestGetActiveZones(t *testing.T) {
 
 	az.nodeInformerSynced = func() bool { return true }
 	zone := fmt.Sprintf("%s-0", az.Location)
-	nodesInZone := sets.New("node1")
-	az.nodeZones = map[string]sets.Set[string]{zone: nodesInZone}
+	nodesInZone := utilsets.NewString("node1")
+	az.nodeZones = map[string]*utilsets.IgnoreCaseSet{zone: nodesInZone}
 
-	expectedZones := sets.New(zone)
+	expectedZones := utilsets.NewString(zone)
 	zones, err = az.GetActiveZones()
 	assert.Equal(t, expectedZones, zones)
 	assert.NoError(t, err)
diff --git a/pkg/provider/azure_vmss_test.go b/pkg/provider/azure_vmss_test.go
index 439308383..f92981eb2 100644
--- a/pkg/provider/azure_vmss_test.go
+++ b/pkg/provider/azure_vmss_test.go
@@ -31,7 +31,6 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	utilerrors "k8s.io/apimachinery/pkg/util/errors"
-	"k8s.io/apimachinery/pkg/util/sets"
 	cloudprovider "k8s.io/cloud-provider"
 	"k8s.io/utils/pointer"
 	"k8s.io/utils/ptr"
@@ -45,6 +44,7 @@ import (
 	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
 	"sigs.k8s.io/cloud-provider-azure/pkg/provider/virtualmachine"
 	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
+	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 )
 
 const (
@@ -1458,7 +1458,7 @@ func TestGetAgentPoolScaleSets(t *testing.T) {
 	for _, test := range testCases {
 		ss, err := NewTestScaleSet(ctrl)
 		assert.NoError(t, err, "unexpected error when creating test VMSS")
-		ss.excludeLoadBalancerNodes = sets.New(test.excludeLBNodes...)
+		ss.excludeLoadBalancerNodes = utilsets.NewString(test.excludeLBNodes...)
 
 		expectedVMSS := buildTestVMSS(testVMSSName, "vmss-vm-")
 		mockVMSSClient := ss.VirtualMachineScaleSetsClient.(*mockvmssclient.MockInterface)
diff --git a/pkg/provider/azure_wrap_test.go b/pkg/provider/azure_wrap_test.go
index e8971ae37..3ce33f7cf 100644
--- a/pkg/provider/azure_wrap_test.go
+++ b/pkg/provider/azure_wrap_test.go
@@ -24,10 +24,9 @@ import (
 	"github.com/stretchr/testify/assert"
 	"go.uber.org/mock/gomock"
 
-	"k8s.io/apimachinery/pkg/util/sets"
-
 	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
 	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
+	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 )
 
 func TestExtractNotFound(t *testing.T) {
@@ -63,32 +62,32 @@ func TestIsNodeUnmanaged(t *testing.T) {
 
 	tests := []struct {
 		name           string
-		unmanagedNodes sets.Set[string]
+		unmanagedNodes *utilsets.IgnoreCaseSet
 		node           string
 		expected       bool
 		expectErr      bool
 	}{
 		{
 			name:           "unmanaged node should return true",
-			unmanagedNodes: sets.New("node1", "node2"),
+			unmanagedNodes: utilsets.NewString("node1", "node2"),
 			node:           "node1",
 			expected:       true,
 		},
 		{
 			name:           "managed node should return false",
-			unmanagedNodes: sets.New("node1", "node2"),
+			unmanagedNodes: utilsets.NewString("node1", "node2"),
 			node:           "node3",
 			expected:       false,
 		},
 		{
 			name:           "empty unmanagedNodes should return true",
-			unmanagedNodes: sets.New[string](),
+			unmanagedNodes: utilsets.NewString(),
 			node:           "node3",
 			expected:       false,
 		},
 		{
 			name:           "no synced informer should report error",
-			unmanagedNodes: sets.New[string](),
+			unmanagedNodes: utilsets.NewString(),
 			node:           "node1",
 			expectErr:      true,
 		},
diff --git a/pkg/provider/azure_zones_test.go b/pkg/provider/azure_zones_test.go
index bc42f5315..57b3c0bbd 100644
--- a/pkg/provider/azure_zones_test.go
+++ b/pkg/provider/azure_zones_test.go
@@ -24,18 +24,18 @@ import (
 	"testing"
 	"time"
 
-	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/zoneclient/mockzoneclient"
-
 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute"
 	"github.com/stretchr/testify/assert"
+
 	"go.uber.org/mock/gomock"
 
-	"k8s.io/apimachinery/pkg/util/sets"
 	cloudprovider "k8s.io/cloud-provider"
 	"k8s.io/utils/pointer"
 
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/vmclient/mockvmclient"
+	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/zoneclient/mockzoneclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
+	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 )
 
 const (
@@ -226,7 +226,7 @@ func TestGetZoneByProviderID(t *testing.T) {
 
 func TestAvailabilitySetGetZoneByNodeName(t *testing.T) {
 	az := &Cloud{
-		unmanagedNodes: sets.Set[string]{"vm-0": sets.Empty{}},
+		unmanagedNodes: utilsets.NewString("vm-0"),
 		nodeInformerSynced: func() bool {
 			return true
 		},
@@ -236,7 +236,7 @@ func TestAvailabilitySetGetZoneByNodeName(t *testing.T) {
 	assert.Equal(t, cloudprovider.Zone{}, zone)
 
 	az = &Cloud{
-		unmanagedNodes: sets.Set[string]{"vm-0": sets.Empty{}},
+		unmanagedNodes: utilsets.NewString("vm-0"),
 		nodeInformerSynced: func() bool {
 			return false
 		},
diff --git a/pkg/util/sets/string_test.go b/pkg/util/sets/string_test.go
new file mode 100644
index 000000000..b0286b7d3
--- /dev/null
+++ b/pkg/util/sets/string_test.go
@@ -0,0 +1,369 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package sets
+
+import (
+	"testing"
+
+	"k8s.io/apimachinery/pkg/util/sets"
+)
+
+func TestNewString(t *testing.T) {
+	tests := []struct {
+		name          string
+		items         []string
+		expectedItems sets.Set[string]
+	}{
+		{
+			name:  "empty",
+			items: nil,
+		},
+		{
+			name:          "non-empty",
+			items:         []string{"Foo", "Bar"},
+			expectedItems: sets.New[string]("foo", "bar"),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			s := NewString(tt.items...)
+			if !s.set.Equal(tt.expectedItems) {
+				t.Errorf("NewString() = %v, want %v", s.set, tt.expectedItems)
+			}
+		})
+	}
+}
+
+func TestInsert(t *testing.T) {
+	tests := []struct {
+		name          string
+		set           *IgnoreCaseSet
+		items         []string
+		expectedItems sets.Set[string]
+	}{
+		{
+			name:          "empty set",
+			set:           NewString(),
+			items:         []string{"foo"},
+			expectedItems: sets.New[string]("foo"),
+		},
+		{
+			name:          "non-empty set",
+			set:           NewString("foo"),
+			items:         []string{"bar"},
+			expectedItems: sets.New[string]("foo", "bar"),
+		},
+		{
+			name:          "non-empty set with existing items",
+			set:           NewString("foo"),
+			items:         []string{"foo", "bar"},
+			expectedItems: sets.New[string]("foo", "bar"),
+		},
+		{
+			name:          "non-empty set with different case",
+			set:           NewString("foo"),
+			items:         []string{"FOO"},
+			expectedItems: sets.New[string]("foo"),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tt.set.Insert(tt.items...)
+			if !tt.set.set.Equal(tt.expectedItems) {
+				t.Errorf("Insert() = %v, want %v", tt.set.set, tt.expectedItems)
+			}
+		})
+	}
+}
+
+func TestSafeInsert(t *testing.T) {
+	tests := []struct {
+		name          string
+		set           *IgnoreCaseSet
+		items         []string
+		expectedItems sets.Set[string]
+	}{
+		{
+			name:          "nil set",
+			set:           nil,
+			items:         []string{"Foo"},
+			expectedItems: sets.New[string]("foo"),
+		},
+		{
+			name:          "empty set",
+			set:           NewString(),
+			items:         []string{"foo"},
+			expectedItems: sets.New[string]("foo"),
+		},
+		{
+			name:          "non-empty set",
+			set:           NewString("foo"),
+			items:         []string{"bar"},
+			expectedItems: sets.New[string]("foo", "bar"),
+		},
+		{
+			name:          "non-empty set with existing items",
+			set:           NewString("foo"),
+			items:         []string{"foo", "bar"},
+			expectedItems: sets.New[string]("foo", "bar"),
+		},
+		{
+			name:          "non-empty set with different case",
+			set:           NewString("foo"),
+			items:         []string{"FOO"},
+			expectedItems: sets.New[string]("foo"),
+		},
+		{
+			name:          "empty set with nil inner set",
+			set:           &IgnoreCaseSet{},
+			items:         []string{"foo"},
+			expectedItems: sets.New[string]("foo"),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			s := SafeInsert(tt.set, tt.items...)
+			if !s.set.Equal(tt.expectedItems) {
+				t.Errorf("SafeInsert() = %v, want %v", s.set, tt.expectedItems)
+			}
+		})
+	}
+}
+
+func TestDelete(t *testing.T) {
+	tests := []struct {
+		name          string
+		set           *IgnoreCaseSet
+		item          string
+		expectedItems sets.Set[string]
+		want          bool
+	}{
+		{
+			name:          "nil set",
+			set:           nil,
+			item:          "foo",
+			expectedItems: nil,
+			want:          false,
+		},
+		{
+			name:          "empty set",
+			set:           NewString(),
+			item:          "foo",
+			expectedItems: nil,
+			want:          false,
+		},
+		{
+			name:          "non-empty set",
+			set:           NewString("foo"),
+			item:          "foo",
+			expectedItems: nil,
+			want:          true,
+		},
+		{
+			name:          "non-empty set with different case",
+			set:           NewString("foo"),
+			item:          "FOO",
+			expectedItems: nil,
+			want:          true,
+		},
+		{
+			name:          "non-empty set with different item",
+			set:           NewString("foo"),
+			item:          "bar",
+			expectedItems: sets.New[string]("foo"),
+			want:          false,
+		},
+		{
+			name:          "empty set with nil inner set",
+			set:           &IgnoreCaseSet{},
+			item:          "foo",
+			expectedItems: nil,
+			want:          false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := tt.set.Delete(tt.item); got != tt.want {
+				t.Errorf("Delete() = %v, want %v", got, tt.want)
+			}
+			if tt.expectedItems != nil && !tt.set.set.Equal(tt.expectedItems) {
+				t.Errorf("Delete() = %v, want %v", tt.set.set, tt.expectedItems)
+			}
+		})
+	}
+}
+
+func TestIsInitialized(t *testing.T) {
+	tests := []struct {
+		name string
+		set  *IgnoreCaseSet
+		want bool
+	}{
+		{
+			name: "nil set",
+			set:  nil,
+			want: false,
+		},
+		{
+			name: "empty set",
+			set:  NewString(),
+			want: true,
+		},
+		{
+			name: "non-empty set",
+			set:  NewString("foo"),
+			want: true,
+		},
+		{
+			name: "empty set with nil inner set",
+			set:  &IgnoreCaseSet{},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := tt.set.Initialized(); got != tt.want {
+				t.Errorf("Initialized() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestHas(t *testing.T) {
+	tests := []struct {
+		name string
+		set  *IgnoreCaseSet
+		item string
+		want bool
+	}{
+		{
+			name: "nil set",
+			set:  nil,
+			item: "foo",
+			want: false,
+		},
+		{
+			name: "empty set",
+			set:  NewString(),
+			item: "foo",
+			want: false,
+		},
+		{
+			name: "non-empty set",
+			set:  NewString("foo"),
+			item: "foo",
+			want: true,
+		},
+		{
+			name: "non-empty set with different case",
+			set:  NewString("foo"),
+			item: "FOO",
+			want: true,
+		},
+		{
+			name: "non-empty set with different item",
+			set:  NewString("foo"),
+			item: "bar",
+			want: false,
+		},
+		{
+			name: "empty set with nil inner set",
+			set:  &IgnoreCaseSet{},
+			item: "foo",
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := tt.set.Has(tt.item); got != tt.want {
+				t.Errorf("Has() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestUnsortedList(t *testing.T) {
+	tests := []struct {
+		name string
+		set  *IgnoreCaseSet
+		want []string
+	}{
+		{
+			name: "nil set",
+			set:  nil,
+			want: nil,
+		},
+		{
+			name: "empty set",
+			set:  NewString(),
+			want: []string{},
+		},
+		{
+			name: "non-empty set",
+			set:  NewString("foo", "bar"),
+			want: []string{"bar", "foo"},
+		},
+		{
+			name: "empty set with nil inner set",
+			set:  &IgnoreCaseSet{},
+			want: nil,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := tt.set.UnsortedList(); !sets.New[string](got...).Equal(sets.New[string](tt.want...)) {
+				t.Errorf("UnsortedList() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestLen(t *testing.T) {
+	tests := []struct {
+		name string
+		set  *IgnoreCaseSet
+		want int
+	}{
+		{
+			name: "nil set",
+			set:  nil,
+			want: 0,
+		},
+		{
+			name: "empty set",
+			set:  NewString(),
+			want: 0,
+		},
+		{
+			name: "non-empty set",
+			set:  NewString("foo", "bar"),
+			want: 2,
+		},
+		{
+			name: "empty set with nil inner set",
+			set:  &IgnoreCaseSet{},
+			want: 0,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := tt.set.Len(); got != tt.want {
+				t.Errorf("Len() = %d, want %d", got, tt.want)
+			}
+		})
+	}
+}