Repository: kubernetes-sigs/cloud-provider-azure
Pull request #8781 (2025-04-01 23:21:26)

[release-1.32] feat: Support migration from basic to standard load balancer
This is an automated cherry-pick of #8463

/assign nilo19

```release-note
feat: Support migration from basic to standard load balancer

Introducing a new feature to migrate from basic to standard sku load balancer. To trigger the migration, switch `loadBalancerSKU` in the cloud provider configuration from `basic` to `standard`. The basic load balancer will be removed automatically, and service workloads on it will be migrated to the newly created standard load balancer, with their ip addresses unchanged. This operation may cause downtime.
```

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

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/cloud-provider-azure/pull/8781#" title="Author self-approved">k8s-infra-cherrypick-robot</a>*
**Once this PR has been reviewed and has the lgtm label**, please assign [nilo19](https://github.com/nilo19) for approval. For more information see [the Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

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).

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

- **[OWNERS](https://github.com/kubernetes-sigs/cloud-provider-azure/blob/release-1.32/OWNERS)**

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":["nilo19"]} -->

Patch:
diff --git a/.github/workflows/cleanupcache.yaml b/.github/workflows/cleanupcache.yaml
index ab44af35e..b75aeeeee 100644
--- a/.github/workflows/cleanupcache.yaml
+++ b/.github/workflows/cleanupcache.yaml
@@ -14,7 +14,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
 
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 87f22b3e9..7b5ab4b10 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -55,7 +55,7 @@ jobs:
 
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+      uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
       with:
         egress-policy: audit
 
@@ -70,7 +70,7 @@ jobs:
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
+      uses: github/codeql-action/init@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
       with:
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -92,4 +92,4 @@ jobs:
         format: golang
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
+      uses: github/codeql-action/analyze@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
diff --git a/.github/workflows/codespell.yaml b/.github/workflows/codespell.yaml
index 1320f6288..a4b54247f 100644
--- a/.github/workflows/codespell.yaml
+++ b/.github/workflows/codespell.yaml
@@ -17,7 +17,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
 
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
index 3026b2ed4..243cf1f32 100644
--- a/.github/workflows/dependency-review.yml
+++ b/.github/workflows/dependency-review.yml
@@ -19,11 +19,11 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
 
       - name: 'Checkout Repository'
         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
       - name: 'Dependency Review'
-        uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
+        uses: actions/dependency-review-action@ce3cf9537a52e8119d91fd484ab5b8a807627bf8 # v4.6.0
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
new file mode 100644
index 000000000..1a0275ccb
--- /dev/null
+++ b/.github/workflows/labeler.yml
@@ -0,0 +1,17 @@
+name: "Add Label to Pull Requests"
+on:
+  pull_request_target:
+    types: [opened, reopened, synchronize]
+
+jobs:
+  add-label:
+    permissions:
+      contents: read
+      pull-requests: write
+    runs-on: ubuntu-latest
+    steps:
+      - name: Add Label
+        uses: actions-ecosystem/action-add-labels@v1
+        with:
+          github_token: ${{ secrets.GITHUB_TOKEN }}
+          labels: "tide/merge-method-squash"
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index 801533287..3675b6a77 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -20,7 +20,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
       - name: Checkout
diff --git a/.github/workflows/release-azclient-trace.yml b/.github/workflows/release-azclient-trace.yml
index 64915299b..2d4b8b6a4 100644
--- a/.github/workflows/release-azclient-trace.yml
+++ b/.github/workflows/release-azclient-trace.yml
@@ -15,7 +15,7 @@ jobs:
       contents: write
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+      uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
       with:
         egress-policy: audit
 
diff --git a/.github/workflows/release-azclient.yml b/.github/workflows/release-azclient.yml
index 4301b7598..b6537447f 100644
--- a/.github/workflows/release-azclient.yml
+++ b/.github/workflows/release-azclient.yml
@@ -19,7 +19,7 @@ jobs:
       contents: write
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+      uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
       with:
         egress-policy: audit
 
diff --git a/.github/workflows/release-cache.yml b/.github/workflows/release-cache.yml
index e3865e838..8190552c6 100644
--- a/.github/workflows/release-cache.yml
+++ b/.github/workflows/release-cache.yml
@@ -15,7 +15,7 @@ jobs:
       contents: write
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+      uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
       with:
         egress-policy: audit
 
diff --git a/.github/workflows/release-configloader.yml b/.github/workflows/release-configloader.yml
index aa19212ee..9236709c4 100644
--- a/.github/workflows/release-configloader.yml
+++ b/.github/workflows/release-configloader.yml
@@ -15,7 +15,7 @@ jobs:
       contents: write
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+      uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
       with:
         egress-policy: audit
 
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 18c9e9a2c..3fef3c5eb 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -23,7 +23,7 @@ jobs:
             arch: arm64
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
       - name: Install arm build toolchains
@@ -62,7 +62,7 @@ jobs:
             arch: amd64
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
       - name: Install arm build toolchains
@@ -117,7 +117,7 @@ jobs:
             arch: amd64
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
       - name: Install arm build toolchains
@@ -166,7 +166,7 @@ jobs:
       contents: write
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
       - name: Install arm build toolchains
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index 2ea3b71da..3fa2853b6 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -32,7 +32,7 @@ jobs:
 
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
 
@@ -72,6 +72,6 @@ jobs:
 
       # Upload the results to GitHub's code scanning dashboard.
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
+        uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
         with:
           sarif_file: results.sarif
diff --git a/.github/workflows/trivy.yaml b/.github/workflows/trivy.yaml
index a548266c1..75bef070b 100644
--- a/.github/workflows/trivy.yaml
+++ b/.github/workflows/trivy.yaml
@@ -35,7 +35,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
 
@@ -67,7 +67,7 @@ jobs:
         env:
           TRIVY_SKIP_DB_UPDATE: true
       - name: Upload Trivy scan results to GitHub Security tab
-        uses: github/codeql-action/upload-sarif@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
+        uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
         with:
           sarif_file: 'trivy-ccm-results.sarif'
           category: azure-cloud-controller-manager-image
@@ -83,7 +83,7 @@ jobs:
         env:
           TRIVY_SKIP_DB_UPDATE: true
       - name: Upload Trivy scan results to GitHub Security tab
-        uses: github/codeql-action/upload-sarif@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
+        uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
         with:
           sarif_file: 'trivy-cnm-linux-results.sarif'
           category: azure-cloud-node-manager-linux-image
@@ -99,7 +99,7 @@ jobs:
         env:
           TRIVY_SKIP_DB_UPDATE: true
       - name: Upload Trivy scan results to GitHub Security tab
-        uses: github/codeql-action/upload-sarif@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
+        uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
         with:
           sarif_file: 'trivy-health-probe-proxy-linux-results.sarif'
           category: health-probe-proxy-linux-image
diff --git a/.github/workflows/update-trivy-db.yaml b/.github/workflows/update-trivy-db.yaml
index 446f661a4..5a1d833ff 100644
--- a/.github/workflows/update-trivy-db.yaml
+++ b/.github/workflows/update-trivy-db.yaml
@@ -12,7 +12,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
 
diff --git a/.github/workflows/update-vendor-license.yml b/.github/workflows/update-vendor-license.yml
index cfee11abd..05739816a 100644
--- a/.github/workflows/update-vendor-license.yml
+++ b/.github/workflows/update-vendor-license.yml
@@ -37,7 +37,7 @@ jobs:
 
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
+      uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
       with:
         egress-policy: audit
 
diff --git a/Dockerfile b/Dockerfile
index 46a795eb2..0621499f6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -14,7 +14,7 @@
 
 # syntax=docker/dockerfile:1
 
-FROM --platform=linux/amd64 mcr.microsoft.com/oss/go/microsoft/golang:1.23-bullseye@sha256:68a909a48c0615d50a12f5ed2f68c57e0826d1e9ce571326f81424720a98f4d4 AS builder
+FROM --platform=linux/amd64 mcr.microsoft.com/oss/go/microsoft/golang:1.23-bullseye@sha256:2ce8af802c1bc8e49df50f99376f906108240233501c950108db039639889085 AS builder
 
 ARG ENABLE_GIT_COMMAND=true
 ARG ARCH=amd64
diff --git a/cloud-node-manager.Dockerfile b/cloud-node-manager.Dockerfile
index 0e9730be0..5072e0f99 100644
--- a/cloud-node-manager.Dockerfile
+++ b/cloud-node-manager.Dockerfile
@@ -14,7 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-FROM --platform=linux/amd64 mcr.microsoft.com/oss/go/microsoft/golang:1.23@sha256:ecf14ac8de682086952aca5b8f2c036f85e2f1d2233b05159db61fa0e3181005 AS builder
+FROM --platform=linux/amd64 mcr.microsoft.com/oss/go/microsoft/golang:1.23@sha256:744d9f57788ff7225c6994b3845e668a4da394bd8afcb6d3c42b2bf5b9ee6af5 AS builder
 
 ARG ENABLE_GIT_COMMAND=true
 ARG ARCH=amd64
diff --git a/pkg/provider/azure_loadbalancer.go b/pkg/provider/azure_loadbalancer.go
index 8ebeb6db8..24fa1504f 100644
--- a/pkg/provider/azure_loadbalancer.go
+++ b/pkg/provider/azure_loadbalancer.go
@@ -667,13 +667,12 @@ func (az *Cloud) cleanOrphanedLoadBalancer(ctx context.Context, lb *armnetwork.L
 		klog.V(2).Infof("cleanOrphanedLoadBalancer(%s, %s, %s): deleting the LB since there are no remaining frontendIPConfigurations", lbName, serviceName, clusterName)
 
 		// Remove backend pools from vmSets. This is required for virtual machine scale sets before removing the LB.
-		vmSetName := az.mapLoadBalancerNameToVMSet(lbName, clusterName)
 		if _, ok := az.VMSet.(*availabilitySet); ok {
 			// do nothing for availability set
 			lb.Properties.BackendAddressPools = nil
 		}
 
-		if deleteErr := az.safeDeleteLoadBalancer(ctx, *lb, clusterName, vmSetName, service); deleteErr != nil {
+		if deleteErr := az.safeDeleteLoadBalancer(ctx, *lb, clusterName, service); deleteErr != nil {
 			klog.Warningf("cleanOrphanedLoadBalancer(%s, %s, %s): failed to DeleteLB: %v", lbName, serviceName, clusterName, deleteErr)
 
 			rgName, vmssName, parseErr := errutils.GetVMSSMetadataByRawError(deleteErr)
@@ -713,7 +712,8 @@ func (az *Cloud) cleanOrphanedLoadBalancer(ctx context.Context, lb *armnetwork.L
 }
 
 // safeDeleteLoadBalancer deletes the load balancer after decoupling it from the vmSet
-func (az *Cloud) safeDeleteLoadBalancer(ctx context.Context, lb armnetwork.LoadBalancer, _, vmSetName string, service *v1.Service) error {
+func (az *Cloud) safeDeleteLoadBalancer(ctx context.Context, lb armnetwork.LoadBalancer, clusterName string, service *v1.Service) error {
+	vmSetName := az.mapLoadBalancerNameToVMSet(ptr.Deref(lb.Name, ""), clusterName)
 	lbBackendPoolIDsToDelete := []string{}
 	if lb.Properties != nil && lb.Properties.BackendAddressPools != nil {
 		for _, bp := range lb.Properties.BackendAddressPools {
@@ -1141,6 +1141,18 @@ func (az *Cloud) ensurePublicIPExists(ctx context.Context, service *v1.Service,
 			pip.Tags = make(map[string]*string)
 		}
 
+		if az.UseStandardLoadBalancer() {
+			if pip.SKU == nil {
+				pip.SKU = &armnetwork.PublicIPAddressSKU{
+					Name: to.Ptr(armnetwork.PublicIPAddressSKUNameStandard),
+				}
+				changed = true
+			} else if !strings.EqualFold(string(*pip.SKU.Name), string(armnetwork.PublicIPAddressSKUNameStandard)) {
+				pip.SKU.Name = ptr.To(armnetwork.PublicIPAddressSKUNameStandard)
+				changed = true
+			}
+		}
+
 		// return if pip exist and dns label is the same
 		if strings.EqualFold(getDomainNameLabel(pip), domainNameLabel) {
 			if existingServiceName := getServiceFromPIPDNSTags(pip.Tags); existingServiceName != "" && strings.EqualFold(existingServiceName, serviceName) {
@@ -1776,6 +1788,11 @@ func (az *Cloud) reconcileLoadBalancer(ctx context.Context, clusterName string,
 		return nil, fmt.Errorf("reconcileLoadBalancer: failed to list managed LB: %w", err)
 	}
 
+	if existingLBs, err = az.cleanupBasicLoadBalancer(ctx, clusterName, service, existingLBs); err != nil {
+		klog.ErrorS(err, "reconcileLoadBalancer: failed to check and remove outdated basic load balancers", "service", serviceName)
+		return nil, err
+	}
+
 	// Delete backend pools for local service if:
 	// 1. the cluster is migrating from multi-slb to single-slb,
 	// 2. the service is changed from local to cluster.
@@ -2614,6 +2631,10 @@ func (az *Cloud) reconcileFrontendIPConfigs(
 					klog.V(4).Infof("reconcileFrontendIPConfigs for service (%s): keep the original private IP %s", serviceName, privateIP)
 					configProperties.PrivateIPAllocationMethod = to.Ptr(armnetwork.IPAllocationMethodStatic)
 					configProperties.PrivateIPAddress = ptr.To(privateIP)
+				} else if len(service.Status.LoadBalancer.Ingress) > 0 && ingressIPInSubnet(service.Status.LoadBalancer.Ingress) {
+					klog.V(4).Infof("reconcileFrontendIPConfigs for service (%s): keep the original private IP %s from service.status.loadbalacner.ingress", serviceName, privateIP)
+					configProperties.PrivateIPAllocationMethod = to.Ptr(armnetwork.IPAllocationMethodStatic)
+					configProperties.PrivateIPAddress = ptr.To(privateIP)
 				} else {
 					// We'll need to call GetLoadBalancer later to retrieve allocated IP.
 					klog.V(4).Infof("reconcileFrontendIPConfigs for service (%s): dynamically allocate the private IP", serviceName)
diff --git a/pkg/provider/azure_loadbalancer_repo.go b/pkg/provider/azure_loadbalancer_repo.go
index 4aac78348..36f367cd4 100644
--- a/pkg/provider/azure_loadbalancer_repo.go
+++ b/pkg/provider/azure_loadbalancer_repo.go
@@ -410,3 +410,41 @@ func isNICPool(bp *armnetwork.BackendAddressPool) bool {
 	}
 	return false
 }
+
+// cleanupBasicLoadBalancer removes outdated basic load balancers
+// when the loadBalancerSkus is `Standard`. It ensures the backend pool of the basic
+// load balancer is empty before deleting the load balancer.
+func (az *Cloud) cleanupBasicLoadBalancer(
+	ctx context.Context, clusterName string, service *v1.Service, existingLBs []*armnetwork.LoadBalancer,
+) ([]*armnetwork.LoadBalancer, error) {
+	if !az.UseStandardLoadBalancer() {
+		return existingLBs, nil
+	}
+
+	var elbRemoved bool
+	for i := len(existingLBs) - 1; i >= 0; i-- {
+		lb := existingLBs[i]
+		if lb != nil && lb.SKU != nil && lb.SKU.Name != nil && *lb.SKU.Name == armnetwork.LoadBalancerSKUNameBasic {
+			klog.V(2).Infof("cleanupBasicLoadBalancer: found basic load balancer %q, removing it", *lb.Name)
+			if err := az.safeDeleteLoadBalancer(ctx, *lb, clusterName, service); err != nil {
+				klog.ErrorS(err, "cleanupBasicLoadBalancer: failed to delete outdated basic load balancer", "loadBalancerName", *lb.Name)
+				return nil, err
+			}
+			existingLBs = append(existingLBs[:i], existingLBs[i+1:]...)
+			if !strings.Contains(strings.ToLower(ptr.Deref(lb.Name, "")), strings.ToLower(consts.InternalLoadBalancerNameSuffix)) {
+				elbRemoved = true
+			}
+		}
+	}
+	// The lb refs in pip will be changed after the removal, so we need to
+	// reinitialize the cache to prevent etag mismatches.
+	if elbRemoved {
+		var err error
+		az.pipCache, err = az.newPIPCache()
+		if err != nil {
+			klog.ErrorS(err, "cleanupBasicLoadBalancer: failed to refresh pip cache")
+			return nil, err
+		}
+	}
+	return existingLBs, nil
+}
diff --git a/pkg/provider/azure_standard.go b/pkg/provider/azure_standard.go
index c8fbe12da..10bca88b7 100644
--- a/pkg/provider/azure_standard.go
+++ b/pkg/provider/azure_standard.go
@@ -122,7 +122,7 @@ func (az *Cloud) getNetworkResourceSubscriptionID() string {
 	return az.SubscriptionID
 }
 
-func (az *Cloud) mapLoadBalancerNameToVMSet(lbName string, clusterName string) (vmSetName string) {
+func (az *Cloud) mapLoadBalancerNameToVMSet(lbName, clusterName string) (vmSetName string) {
 	vmSetName = trimSuffixIgnoreCase(lbName, consts.InternalLoadBalancerNameSuffix)
 	if strings.EqualFold(clusterName, vmSetName) {
 		vmSetName = az.VMSet.GetPrimaryVMSetName()


Test patch:
diff --git a/e2e.Dockerfile b/e2e.Dockerfile
index 2d9257560..1d78c4826 100644
--- a/e2e.Dockerfile
+++ b/e2e.Dockerfile
@@ -14,7 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-FROM mcr.microsoft.com/oss/go/microsoft/golang:1.23-bullseye@sha256:68a909a48c0615d50a12f5ed2f68c57e0826d1e9ce571326f81424720a98f4d4
+FROM mcr.microsoft.com/oss/go/microsoft/golang:1.23-bullseye@sha256:2ce8af802c1bc8e49df50f99376f906108240233501c950108db039639889085
 
 WORKDIR /go/src/sigs.k8s.io/cloud-provider-azure
 
diff --git a/pkg/provider/azure_loadbalancer_repo_test.go b/pkg/provider/azure_loadbalancer_repo_test.go
index 64470bc83..a5b1774a6 100644
--- a/pkg/provider/azure_loadbalancer_repo_test.go
+++ b/pkg/provider/azure_loadbalancer_repo_test.go
@@ -550,3 +550,200 @@ func TestIsNICPool(t *testing.T) {
 		})
 	}
 }
+
+func TestCleanupBasicLoadBalancer(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	testCases := []struct {
+		desc                 string
+		useStandardLB        bool
+		existingLBs          []*armnetwork.LoadBalancer
+		expectedErr          bool
+		expectedDeleteCalled bool
+	}{
+		{
+			desc:                 "UseStandardLoadBalancer=false should skip deletion",
+			useStandardLB:        false,
+			existingLBs:          []*armnetwork.LoadBalancer{},
+			expectedErr:          false,
+			expectedDeleteCalled: false,
+		},
+		{
+			desc:          "Basic LB should be deleted when UseStandardLoadBalancer=true",
+			useStandardLB: true,
+			existingLBs: []*armnetwork.LoadBalancer{
+				{
+					Name: to.Ptr("test-lb"),
+					SKU: &armnetwork.LoadBalancerSKU{
+						Name: to.Ptr(armnetwork.LoadBalancerSKUNameBasic),
+					},
+					Properties: &armnetwork.LoadBalancerPropertiesFormat{
+						BackendAddressPools: []*armnetwork.BackendAddressPool{
+							{
+								ID: to.Ptr("pool-id-1"),
+							},
+						},
+					},
+				},
+			},
+			expectedErr:          false,
+			expectedDeleteCalled: true,
+		},
+		{
+			desc:          "Internal basic LB should be deleted but not reinitialize pip cache",
+			useStandardLB: true,
+			existingLBs: []*armnetwork.LoadBalancer{
+				{
+					Name: to.Ptr("test-lb-" + consts.InternalLoadBalancerNameSuffix),
+					SKU: &armnetwork.LoadBalancerSKU{
+						Name: to.Ptr(armnetwork.LoadBalancerSKUNameBasic),
+					},
+					Properties: &armnetwork.LoadBalancerPropertiesFormat{
+						BackendAddressPools: []*armnetwork.BackendAddressPool{
+							{
+								ID: to.Ptr("pool-id-1"),
+							},
+						},
+					},
+				},
+			},
+			expectedErr:          false,
+			expectedDeleteCalled: true,
+		},
+		{
+			desc:          "Standard LB should not be deleted",
+			useStandardLB: true,
+			existingLBs: []*armnetwork.LoadBalancer{
+				{
+					Name: to.Ptr("test-lb"),
+					SKU: &armnetwork.LoadBalancerSKU{
+						Name: to.Ptr(armnetwork.LoadBalancerSKUNameStandard),
+					},
+					Properties: &armnetwork.LoadBalancerPropertiesFormat{},
+				},
+			},
+			expectedErr:          false,
+			expectedDeleteCalled: false,
+		},
+		{
+			desc:          "Mix of basic and standard LBs should only delete basic",
+			useStandardLB: true,
+			existingLBs: []*armnetwork.LoadBalancer{
+				{
+					Name: to.Ptr("test-lb-standard"),
+					SKU: &armnetwork.LoadBalancerSKU{
+						Name: to.Ptr(armnetwork.LoadBalancerSKUNameStandard),
+					},
+					Properties: &armnetwork.LoadBalancerPropertiesFormat{},
+				},
+				{
+					Name: to.Ptr("test-lb-basic"),
+					SKU: &armnetwork.LoadBalancerSKU{
+						Name: to.Ptr(armnetwork.LoadBalancerSKUNameBasic),
+					},
+					Properties: &armnetwork.LoadBalancerPropertiesFormat{
+						BackendAddressPools: []*armnetwork.BackendAddressPool{
+							{
+								ID: to.Ptr("pool-id-1"),
+							},
+						},
+					},
+				},
+			},
+			expectedErr:          false,
+			expectedDeleteCalled: true,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			// Print initial test case info
+			t.Logf("Test case: %s", tc.desc)
+			t.Logf("Initial LBs:")
+			for i, lb := range tc.existingLBs {
+				t.Logf("  LB[%d]: Name=%s, SKU=%s", i, *lb.Name, *lb.SKU.Name)
+			}
+
+			az := GetTestCloud(ctrl)
+			az.Config.LoadBalancerSKU = consts.LoadBalancerSKUStandard
+
+			if !tc.useStandardLB {
+				az.Config.LoadBalancerSKU = consts.LoadBalancerSKUBasic
+			}
+
+			service := &v1.Service{}
+			clusterName := "testCluster"
+			ctx := context.Background()
+
+			// Setup mocks
+			mockVMSet := NewMockVMSet(ctrl)
+			az.VMSet = mockVMSet
+			mockLBClient := az.NetworkClientFactory.GetLoadBalancerClient().(*mock_loadbalancerclient.MockInterface)
+
+			// Set up mocks for safeDeleteLoadBalancer if we expect it to be called
+			if tc.expectedDeleteCalled {
+				for _, lb := range tc.existingLBs {
+					if lb.SKU != nil && lb.SKU.Name != nil && *lb.SKU.Name == armnetwork.LoadBalancerSKUNameBasic {
+						// Mock the EnsureBackendPoolDeleted call
+						mockVMSet.EXPECT().EnsureBackendPoolDeleted(
+							gomock.Any(), // context
+							gomock.Any(), // service
+							gomock.Any(), // backendPoolIDs
+							gomock.Any(), // vmSetName
+							gomock.Any(), // backendAddressPools
+							true,         // deleteEmptyPool
+						).Return(true, nil).AnyTimes()
+
+						// Mock the DeleteLB call
+						mockLBClient.EXPECT().Delete(
+							gomock.Any(), // context
+							az.ResourceGroup,
+							*lb.Name,
+						).Return(nil).AnyTimes()
+					}
+				}
+			}
+
+			// Call the function under test
+			result, err := az.cleanupBasicLoadBalancer(ctx, clusterName, service, tc.existingLBs)
+
+			// Debugging output
+			t.Logf("Original LBs: %d, Result LBs: %d", len(tc.existingLBs), len(result))
+			for i, lb := range tc.existingLBs {
+				t.Logf("Original LB[%d]: Name=%s, SKU=%s", i, *lb.Name, *lb.SKU.Name)
+			}
+			for i, lb := range result {
+				t.Logf("Result LB[%d]: Name=%s, SKU=%s", i, *lb.Name, *lb.SKU.Name)
+			}
+
+			// Check for errors if we're expecting them
+			if tc.expectedErr {
+				assert.Error(t, err)
+				return
+			}
+			assert.NoError(t, err)
+
+			// Verify the number of remaining LBs
+			expectedLBCount := 0
+			if !tc.expectedDeleteCalled {
+				expectedLBCount = len(tc.existingLBs)
+			} else {
+				// Count standard LBs (which should not be deleted)
+				for _, lb := range tc.existingLBs {
+					if lb.SKU != nil && lb.SKU.Name != nil && *lb.SKU.Name == armnetwork.LoadBalancerSKUNameStandard {
+						expectedLBCount++
+					}
+				}
+			}
+			assert.Equal(t, expectedLBCount, len(result), "Expected %d load balancers after deletion, got %d", expectedLBCount, len(result))
+
+			// If we expect LBs to remain, verify they are not basic
+			if len(result) > 0 {
+				for _, lb := range result {
+					assert.NotEqual(t, armnetwork.LoadBalancerSKUNameBasic, *lb.SKU.Name, "Found a basic load balancer that should have been deleted")
+				}
+			}
+		})
+	}
+}
diff --git a/pkg/provider/azure_loadbalancer_test.go b/pkg/provider/azure_loadbalancer_test.go
index 865c584f6..c8650c0e2 100644
--- a/pkg/provider/azure_loadbalancer_test.go
+++ b/pkg/provider/azure_loadbalancer_test.go
@@ -5309,6 +5309,9 @@ func TestEnsurePublicIPExistsCommon(t *testing.T) {
 					PublicIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodStatic),
 				},
 				Tags: map[string]*string{},
+				SKU: &armnetwork.PublicIPAddressSKU{
+					Name: to.Ptr(armnetwork.PublicIPAddressSKUNameStandard),
+				},
 			},
 			shouldPutPIP: true,
 		},
@@ -6763,6 +6766,9 @@ func TestReconcileFrontendIPConfigs(t *testing.T) {
 						PublicIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodStatic),
 						IPAddress:                ptr.To("fe::1"),
 					},
+					SKU: &armnetwork.PublicIPAddressSKU{
+						Name: to.Ptr(armnetwork.PublicIPAddressSKUNameStandard),
+					},
 				},
 				{
 					Name: ptr.To("pipV4"),
@@ -6770,6 +6776,9 @@ func TestReconcileFrontendIPConfigs(t *testing.T) {
 						PublicIPAddressVersion: to.Ptr(armnetwork.IPVersionIPv4),
 						IPAddress:              ptr.To("1.2.3.4"),
 					},
+					SKU: &armnetwork.PublicIPAddressSKU{
+						Name: to.Ptr(armnetwork.PublicIPAddressSKUNameStandard),
+					},
 				},
 			},
 			status:        nil,
@@ -7106,7 +7115,7 @@ func TestSafeDeleteLoadBalancer(t *testing.T) {
 					BackendAddressPools: []*armnetwork.BackendAddressPool{},
 				},
 			}
-			err := cloud.safeDeleteLoadBalancer(context.TODO(), lb, "cluster", "vmss", &svc)
+			err := cloud.safeDeleteLoadBalancer(context.TODO(), lb, "vmss", &svc)
 			if tc.expectedErr != nil {
 				assert.Contains(t, err.Error(), tc.expectedErr.Error())
 			}