Repository: kubernetes-sigs/cloud-provider-azure
Pull request #5368 (2024-02-05 05:46:28)

chore: Add case-insensitive string set utility
A new structure called IgnoreCaseSet has been introduced into the pkg/util/sets package. This constructs a set of strings that is case-insensitive, providing methods for applying standard set operations. It's beneficial for handling inputs where case sensitivity isn't important or might cause issues.

<!--  Thanks for sending a pull request!  Here are some tips for you:

1. If this is your first time, please read our contributor guidelines: https://git.k8s.io/community/contributors/guide/first-contribution.md#your-first-contribution and developer guide https://git.k8s.io/community/contributors/devel/development.md#development-guide
2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. For reference on required PR/issue labels, read here:
https://git.k8s.io/community/contributors/devel/sig-release/release.md#issuepr-kind-label
3. Ensure you have added or ran the appropriate tests for your PR: https://git.k8s.io/community/contributors/devel/sig-testing/testing.md
4. If you want *faster* PR reviews, read how: https://git.k8s.io/community/contributors/guide/pull-requests.md#best-practices-for-faster-reviews
5. If the PR is unfinished, see how to mark it: https://git.k8s.io/community/contributors/guide/pull-requests.md#marking-unfinished-pull-requests
-->

#### What type of PR is this?

<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind design

/kind cleanup

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->

#### What this PR does / why we need it:

chore: Add case-insensitive string set utility

A new structure called IgnoreCaseSet has been introduced into the pkg/util/sets package. This constructs a set of strings that is case-insensitive, providing methods for applying standard set operations. It's beneficial for handling inputs where case sensitivity isn't important or might cause issues.

#### Which issue(s) this PR fixes:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Fixes #

#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

For more information on release notes see: https://git.k8s.io/community/contributors/guide/release-notes.md
-->
```release-note
NONE
```

#### Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.:

<!--
This section can be blank if this pull request does not require a release note.

When adding links which point to resources within git repositories, like
KEPs or supporting documentation, please reference a specific commit and avoid
linking directly to the master branch. This ensures that links reference a
specific point in time, rather than a document that may change over time.

See here for guidance on getting permanent links to files: https://help.github.com/en/articles/getting-permanent-links-to-files

Please use the following format for linking documentation:
- [KEP]: <link>
- [Usage]: <link>
- [Other doc]: <link>
-->
```docs

```


k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/cloud-provider-azure/pull/5368#" title="Author self-approved">nilo19</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fcloud-provider-azure).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes-sigs/cloud-provider-azure/blob/master/OWNERS)~~ [nilo19]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
k8s-ci-robot: @nilo19: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cloud-provider-azure-check | a8331c324ca8feac3626d192c2a33a5056cdeef9 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/5368/pull-cloud-provider-azure-check/1754380960910020608) | true | `/test pull-cloud-provider-azure-check`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=5368). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Anilo19). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cloud-provider-azure/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @nilo19: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cloud-provider-azure-check | a8331c324ca8feac3626d192c2a33a5056cdeef9 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/5368/pull-cloud-provider-azure-check/1754380960910020608) | true | `/test pull-cloud-provider-azure-check`
pull-cloud-provider-azure-unit | a8331c324ca8feac3626d192c2a33a5056cdeef9 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/5368/pull-cloud-provider-azure-unit/1754380961337839616) | true | `/test pull-cloud-provider-azure-unit`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=5368). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Anilo19). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cloud-provider-azure/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
nilo19: /kind cleanup
k8s-ci-robot: @nilo19: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cloud-provider-azure-check | 85d210a8c0096912daf875fbe27f4773780f93e8 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/5368/pull-cloud-provider-azure-check/1754710863849197568) | true | `/test pull-cloud-provider-azure-check`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=5368). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Anilo19). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cloud-provider-azure/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->

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)
+			}
+		})
+	}
+}
diff --git a/tests/k8s-azure/manifest/cluster-api/cluster-template-prow-ipv6-mp.yaml b/tests/k8s-azure/manifest/cluster-api/cluster-template-prow-ipv6-mp.yaml
index d2fb9b15d..b17ea3a8d 100644
--- a/tests/k8s-azure/manifest/cluster-api/cluster-template-prow-ipv6-mp.yaml
+++ b/tests/k8s-azure/manifest/cluster-api/cluster-template-prow-ipv6-mp.yaml
@@ -137,7 +137,11 @@ spec:
     mounts:
     - - LABEL=etcd_disk
       - /var/lib/etcddisk
-    postKubeadmCommands: []
+    postKubeadmCommands:
+    - echo "DNSStubListener=no" >> /etc/systemd/resolved.conf
+    - mv /etc/resolv.conf /etc/resolv.conf.OLD && ln -s /run/systemd/resolve/resolv.conf
+      /etc/resolv.conf
+    - systemctl restart systemd-resolved
     preKubeadmCommands: []
   machineTemplate:
     infrastructureRef:
@@ -256,7 +260,11 @@ spec:
             cloud-provider: external
             cluster-dns: '[fd00::10]'
           name: '{{ ds.meta_data["local_hostname"] }}'
-      postKubeadmCommands: []
+      postKubeadmCommands:
+      - echo "DNSStubListener=no" >> /etc/systemd/resolved.conf
+      - mv /etc/resolv.conf /etc/resolv.conf.OLD && ln -s /run/systemd/resolve/resolv.conf
+        /etc/resolv.conf
+      - systemctl restart systemd-resolved
 ---
 apiVersion: cluster.x-k8s.io/v1beta1
 kind: MachinePool
@@ -334,7 +342,11 @@ spec:
         cloud-provider: azure
         cluster-dns: '[fd00::10]'
       name: '{{ ds.meta_data["local_hostname"] }}'
-  postKubeadmCommands: []
+  postKubeadmCommands:
+  - echo "DNSStubListener=no" >> /etc/systemd/resolved.conf
+  - mv /etc/resolv.conf /etc/resolv.conf.OLD && ln -s /run/systemd/resolve/resolv.conf
+    /etc/resolv.conf
+  - systemctl restart systemd-resolved
 ---
 apiVersion: addons.cluster.x-k8s.io/v1alpha1
 kind: HelmChartProxy