Repository: kubernetes-sigs/cloud-provider-azure
Pull request #7589 (2024-11-14 17:29:51)

build(deps): bump golang from `d4387d8` to `bc1b90c`
Bumps golang from `d4387d8` to `bc1b90c`.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang&package-manager=docker&previous-version=1.23-bullseye&new-version=1.23-bullseye)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

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

Approval requirements bypassed by manually added approval.

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/cloud-provider-azure/pull/7589#" title="Author self-approved">dependabot[bot]</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/release-1.31/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":["jwtty"]} -->
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

Approval requirements bypassed by manually added approval.

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/cloud-provider-azure/pull/7589#" title="Author self-approved">dependabot[bot]</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/release-1.31/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":["jwtty"]} -->
k8s-ci-robot: Hi @dependabot[bot]. Thanks for your PR.

I'm waiting for a [kubernetes-sigs](https://github.com/orgs/kubernetes-sigs/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://git.k8s.io/community/community-membership.md#member) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fcloud-provider-azure).

<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-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>


Patch:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 9e1395b45..1888ce46f 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -70,7 +70,7 @@ jobs:
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3
+      uses: github/codeql-action/init@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
       with:
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -93,4 +93,4 @@ jobs:
         $(go env GOPATH)/bin/goveralls -coverprofile=profile.cov -service=github
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3
+      uses: github/codeql-action/analyze@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index 2e3a1066f..57eb6a5ff 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -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@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3
+        uses: github/codeql-action/upload-sarif@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
         with:
           sarif_file: results.sarif
diff --git a/.github/workflows/trivy.yaml b/.github/workflows/trivy.yaml
index 1658abdb5..817f3317d 100644
--- a/.github/workflows/trivy.yaml
+++ b/.github/workflows/trivy.yaml
@@ -67,7 +67,7 @@ jobs:
           vuln-type: 'os,library'
           severity: 'CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN'
       - name: Upload Trivy scan results to GitHub Security tab
-        uses: github/codeql-action/upload-sarif@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3
+        uses: github/codeql-action/upload-sarif@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
         with:
           sarif_file: 'trivy-ccm-results.sarif'
           category: azure-cloud-controller-manager-image
@@ -82,7 +82,7 @@ jobs:
           severity: 'CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN'
           skip-setup-trivy: true
       - name: Upload Trivy scan results to GitHub Security tab
-        uses: github/codeql-action/upload-sarif@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3
+        uses: github/codeql-action/upload-sarif@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
         with:
           sarif_file: 'trivy-cnm-linux-results.sarif'
           category: azure-cloud-node-manager-linux-image
@@ -97,7 +97,7 @@ jobs:
           severity: 'CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN'
           skip-setup-trivy: true
       - name: Upload Trivy scan results to GitHub Security tab
-        uses: github/codeql-action/upload-sarif@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3
+        uses: github/codeql-action/upload-sarif@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
         if: ${{ github.ref == 'refs/heads/main' }}
         with:
           sarif_file: 'trivy-health-probe-proxy-linux-results.sarif'
diff --git a/Dockerfile b/Dockerfile
index 9e73ee082..a387e3991 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -14,7 +14,7 @@
 
 # syntax=docker/dockerfile:1
 
-FROM --platform=linux/amd64 golang:1.23-bullseye@sha256:d4387d8b2ee5b78a199fae168a761d0fb0f33f172642d20856f29a67204af16b AS builder
+FROM --platform=linux/amd64 golang:1.23-bullseye@sha256:bc1b90c2a8eb0ffb62325e02a85d51031ad3afae15b3df4b6a48b7929b00badb AS builder
 
 ARG ENABLE_GIT_COMMAND=true
 ARG ARCH=amd64
diff --git a/cloud-node-manager.Dockerfile b/cloud-node-manager.Dockerfile
index c4fc398b7..8ad757d07 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 golang:1.23-bullseye@sha256:d4387d8b2ee5b78a199fae168a761d0fb0f33f172642d20856f29a67204af16b AS builder
+FROM --platform=linux/amd64 golang:1.23-bullseye@sha256:bc1b90c2a8eb0ffb62325e02a85d51031ad3afae15b3df4b6a48b7929b00badb AS builder
 
 ARG ENABLE_GIT_COMMAND=true
 ARG ARCH=amd64
diff --git a/pkg/azureclients/routetableclient/azure_routetableclient.go b/pkg/azureclients/routetableclient/azure_routetableclient.go
deleted file mode 100644
index 1eea86106..000000000
--- a/pkg/azureclients/routetableclient/azure_routetableclient.go
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
-Copyright 2020 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 routetableclient
-
-import (
-	"context"
-	"net/http"
-	"strings"
-	"time"
-
-	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2022-07-01/network"
-	"github.com/Azure/go-autorest/autorest"
-	"github.com/Azure/go-autorest/autorest/azure"
-
-	"k8s.io/client-go/util/flowcontrol"
-	"k8s.io/klog/v2"
-
-	azclients "sigs.k8s.io/cloud-provider-azure/pkg/azureclients"
-	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/armclient"
-	"sigs.k8s.io/cloud-provider-azure/pkg/metrics"
-	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
-)
-
-var _ Interface = &Client{}
-
-const routeTablesResourceType = "Microsoft.Network/routeTables"
-
-// Client implements RouteTable client Interface.
-type Client struct {
-	armClient      armclient.Interface
-	subscriptionID string
-	cloudName      string
-
-	// Rate limiting configures.
-	rateLimiterReader flowcontrol.RateLimiter
-	rateLimiterWriter flowcontrol.RateLimiter
-
-	// ARM throttling configures.
-	RetryAfterReader time.Time
-	RetryAfterWriter time.Time
-}
-
-// New creates a new RouteTable client with ratelimiting.
-func New(config *azclients.ClientConfig) *Client {
-	baseURI := config.ResourceManagerEndpoint
-	authorizer := config.Authorizer
-	apiVersion := APIVersion
-	if strings.EqualFold(config.CloudName, AzureStackCloudName) && !config.DisableAzureStackCloud {
-		apiVersion = AzureStackCloudAPIVersion
-	}
-	armClient := armclient.New(authorizer, *config, baseURI, apiVersion)
-	rateLimiterReader, rateLimiterWriter := azclients.NewRateLimiter(config.RateLimitConfig)
-
-	if azclients.RateLimitEnabled(config.RateLimitConfig) {
-		klog.V(2).Infof("Azure RouteTablesClient (read ops) using rate limit config: QPS=%g, bucket=%d",
-			config.RateLimitConfig.CloudProviderRateLimitQPS,
-			config.RateLimitConfig.CloudProviderRateLimitBucket)
-		klog.V(2).Infof("Azure RouteTablesClient (write ops) using rate limit config: QPS=%g, bucket=%d",
-			config.RateLimitConfig.CloudProviderRateLimitQPSWrite,
-			config.RateLimitConfig.CloudProviderRateLimitBucketWrite)
-	}
-
-	client := &Client{
-		armClient:         armClient,
-		rateLimiterReader: rateLimiterReader,
-		rateLimiterWriter: rateLimiterWriter,
-		subscriptionID:    config.SubscriptionID,
-		cloudName:         config.CloudName,
-	}
-
-	return client
-}
-
-// Get gets a RouteTable.
-func (c *Client) Get(ctx context.Context, resourceGroupName string, routeTableName string, expand string) (network.RouteTable, *retry.Error) {
-	mc := metrics.NewMetricContext("route_tables", "get", resourceGroupName, c.subscriptionID, "")
-
-	// Report errors if the client is rate limited.
-	if !c.rateLimiterReader.TryAccept() {
-		mc.RateLimitedCount()
-		return network.RouteTable{}, retry.GetRateLimitError(false, "RouteTableGet")
-	}
-
-	// Report errors if the client is throttled.
-	if c.RetryAfterReader.After(time.Now()) {
-		mc.ThrottledCount()
-		rerr := retry.GetThrottlingError("RouteTableGet", "client throttled", c.RetryAfterReader)
-		return network.RouteTable{}, rerr
-	}
-
-	result, rerr := c.getRouteTable(ctx, resourceGroupName, routeTableName, expand)
-	mc.Observe(rerr)
-	if rerr != nil {
-		if rerr.IsThrottled() {
-			// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires.
-			c.RetryAfterReader = rerr.RetryAfter
-		}
-
-		return result, rerr
-	}
-
-	return result, nil
-}
-
-// getRouteTable gets a RouteTable.
-func (c *Client) getRouteTable(ctx context.Context, resourceGroupName string, routeTableName string, expand string) (network.RouteTable, *retry.Error) {
-	resourceID := armclient.GetResourceID(
-		c.subscriptionID,
-		resourceGroupName,
-		routeTablesResourceType,
-		routeTableName,
-	)
-	result := network.RouteTable{}
-
-	response, rerr := c.armClient.GetResourceWithExpandQuery(ctx, resourceID, expand)
-	defer c.armClient.CloseResponse(ctx, response)
-	if rerr != nil {
-		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "routetable.get.request", resourceID, rerr.Error())
-		return result, rerr
-	}
-
-	err := autorest.Respond(
-		response,
-		azure.WithErrorUnlessStatusCode(http.StatusOK),
-		autorest.ByUnmarshallingJSON(&result))
-	if err != nil {
-		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "routetable.get.respond", resourceID, err)
-		return result, retry.GetError(response, err)
-	}
-
-	result.Response = autorest.Response{Response: response}
-	return result, nil
-}
-
-// CreateOrUpdate creates or updates a RouteTable.
-func (c *Client) CreateOrUpdate(ctx context.Context, resourceGroupName string, routeTableName string, parameters network.RouteTable, etag string) *retry.Error {
-	mc := metrics.NewMetricContext("route_tables", "create_or_update", resourceGroupName, c.subscriptionID, "")
-
-	// Report errors if the client is rate limited.
-	if !c.rateLimiterWriter.TryAccept() {
-		mc.RateLimitedCount()
-		return retry.GetRateLimitError(true, "RouteTableCreateOrUpdate")
-	}
-
-	// Report errors if the client is throttled.
-	if c.RetryAfterWriter.After(time.Now()) {
-		mc.ThrottledCount()
-		rerr := retry.GetThrottlingError("RouteTableCreateOrUpdate", "client throttled", c.RetryAfterWriter)
-		return rerr
-	}
-
-	rerr := c.createOrUpdateRouteTable(ctx, resourceGroupName, routeTableName, parameters, etag)
-	mc.Observe(rerr)
-	if rerr != nil {
-		if rerr.IsThrottled() {
-			// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires.
-			c.RetryAfterWriter = rerr.RetryAfter
-		}
-
-		return rerr
-	}
-
-	return nil
-}
-
-// createOrUpdateRouteTable creates or updates a RouteTable.
-func (c *Client) createOrUpdateRouteTable(ctx context.Context, resourceGroupName string, routeTableName string, parameters network.RouteTable, etag string) *retry.Error {
-	resourceID := armclient.GetResourceID(
-		c.subscriptionID,
-		resourceGroupName,
-		routeTablesResourceType,
-		routeTableName,
-	)
-	decorators := []autorest.PrepareDecorator{}
-	if etag != "" {
-		decorators = append(decorators, autorest.WithHeader("If-Match", autorest.String(etag)))
-	}
-
-	response, rerr := c.armClient.PutResource(ctx, resourceID, parameters, decorators...)
-	defer c.armClient.CloseResponse(ctx, response)
-	if rerr != nil {
-		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "routetable.put.request", resourceID, rerr.Error())
-		return rerr
-	}
-
-	if response != nil && response.StatusCode != http.StatusNoContent {
-		_, rerr = c.createOrUpdateResponder(response)
-		if rerr != nil {
-			klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "routetable.put.respond", resourceID, rerr.Error())
-			return rerr
-		}
-	}
-
-	return nil
-}
-
-func (c *Client) createOrUpdateResponder(resp *http.Response) (*network.RouteTable, *retry.Error) {
-	result := &network.RouteTable{}
-	err := autorest.Respond(
-		resp,
-		azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusCreated),
-		autorest.ByUnmarshallingJSON(&result))
-	result.Response = autorest.Response{Response: resp}
-	return result, retry.GetError(resp, err)
-}
diff --git a/pkg/azureclients/routetableclient/doc.go b/pkg/azureclients/routetableclient/doc.go
deleted file mode 100644
index 49d8ed616..000000000
--- a/pkg/azureclients/routetableclient/doc.go
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
-Copyright 2020 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 routetableclient implements the client for RouteTable.
-package routetableclient // import "sigs.k8s.io/cloud-provider-azure/pkg/azureclients/routetableclient"
diff --git a/pkg/azureclients/routetableclient/interface.go b/pkg/azureclients/routetableclient/interface.go
deleted file mode 100644
index 1ef8cfc08..000000000
--- a/pkg/azureclients/routetableclient/interface.go
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
-Copyright 2020 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 routetableclient
-
-import (
-	"context"
-
-	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2022-07-01/network"
-
-	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
-)
-
-const (
-	// APIVersion is the API version for network.
-	APIVersion = "2022-07-01"
-	// AzureStackCloudAPIVersion is the API version for Azure Stack
-	AzureStackCloudAPIVersion = "2018-11-01"
-	// AzureStackCloudName is the cloud name of Azure Stack
-	AzureStackCloudName = "AZURESTACKCLOUD"
-)
-
-// Interface is the client interface for RouteTable.
-// Don't forget to run "hack/update-mock-clients.sh" command to generate the mock client.
-type Interface interface {
-	// Get gets a RouteTable.
-	Get(ctx context.Context, resourceGroupName string, routeTableName string, expand string) (result network.RouteTable, rerr *retry.Error)
-
-	// CreateOrUpdate creates or updates a RouteTable.
-	CreateOrUpdate(ctx context.Context, resourceGroupName string, routeTableName string, parameters network.RouteTable, etag string) *retry.Error
-}
diff --git a/pkg/azureclients/routetableclient/mockroutetableclient/doc.go b/pkg/azureclients/routetableclient/mockroutetableclient/doc.go
deleted file mode 100644
index c2aa4cc39..000000000
--- a/pkg/azureclients/routetableclient/mockroutetableclient/doc.go
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
-Copyright 2020 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 mockroutetableclient implements the mock client for RouteTable.
-package mockroutetableclient // import "sigs.k8s.io/cloud-provider-azure/pkg/azureclients/routetableclient/mockroutetableclient"
diff --git a/pkg/provider/azure.go b/pkg/provider/azure.go
index 725b6cdce..7d027adfc 100644
--- a/pkg/provider/azure.go
+++ b/pkg/provider/azure.go
@@ -48,6 +48,7 @@ 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"
@@ -60,7 +61,6 @@ import (
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/privatednszonegroupclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/privateendpointclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/publicipclient"
-	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/routetableclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/storageaccountclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/subnetclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/vmasclient"
@@ -69,15 +69,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/provider/privatelinkservice"
+	"sigs.k8s.io/cloud-provider-azure/pkg/provider/routetable"
+	"sigs.k8s.io/cloud-provider-azure/pkg/provider/securitygroup"
 	"sigs.k8s.io/cloud-provider-azure/pkg/provider/subnet"
 	"sigs.k8s.io/cloud-provider-azure/pkg/provider/zone"
 
-	"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/provider/securitygroup"
 	"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"
@@ -358,7 +357,6 @@ type Cloud struct {
 
 	SubnetsClient                   subnetclient.Interface
 	InterfacesClient                interfaceclient.Interface
-	RouteTablesClient               routetableclient.Interface
 	LoadBalancerClient              loadbalancerclient.Interface
 	PublicIPAddressesClient         publicipclient.Interface
 	VirtualMachinesClient           vmclient.Interface
@@ -415,13 +413,13 @@ type Cloud struct {
 	routeUpdater       batchProcessor
 	backendPoolUpdater batchProcessor
 
-	vmCache    azcache.Resource
-	lbCache    azcache.Resource
-	nsgRepo    securitygroup.Repository
-	zoneRepo   zone.Repository
-	plsRepo    privatelinkservice.Repository
-	subnetRepo subnet.Repository
-	rtCache    azcache.Resource
+	vmCache        azcache.Resource
+	lbCache        azcache.Resource
+	nsgRepo        securitygroup.Repository
+	zoneRepo       zone.Repository
+	plsRepo        privatelinkservice.Repository
+	subnetRepo     subnet.Repository
+	routeTableRepo routetable.Repository
 	// public ip cache
 	// key: [resourceGroupName]
 	// Value: sync.Map of [pipName]*PublicIPAddress
@@ -752,10 +750,16 @@ func (az *Cloud) InitializeCloudFromConfig(ctx context.Context, config *Config,
 		if err != nil {
 			return err
 		}
+
 		az.subnetRepo, err = subnet.NewRepo(networkClientFactory.GetSubnetClient())
 		if err != nil {
 			return err
 		}
+
+		az.routeTableRepo, err = routetable.NewRepo(networkClientFactory.GetRouteTableClient(), az.RouteTableResourceGroup, time.Duration(az.RouteTableCacheTTLInSeconds)*time.Second, az.DisableAPICallCache)
+		if err != nil {
+			return err
+		}
 	}
 	err = az.initCaches()
 	if err != nil {
@@ -869,11 +873,6 @@ func (az *Cloud) initCaches() (err error) {
 		return err
 	}
 
-	az.rtCache, err = az.newRouteTableCache()
-	if err != nil {
-		return err
-	}
-
 	az.pipCache, err = az.newPIPCache()
 	if err != nil {
 		return err
@@ -1036,7 +1035,6 @@ func (az *Cloud) configAzureClients(
 	az.VirtualMachineScaleSetsClient = vmssclient.New(vmssClientConfig)
 	az.VirtualMachineScaleSetVMsClient = vmssvmclient.New(vmssVMClientConfig)
 	az.SubnetsClient = subnetclient.New(subnetClientConfig)
-	az.RouteTablesClient = routetableclient.New(routeTableClientConfig)
 	az.LoadBalancerClient = loadbalancerclient.New(loadBalancerClientConfig)
 	az.PublicIPAddressesClient = publicipclient.New(publicIPClientConfig)
 	az.FileClient = fileclient.New(fileClientConfig)
diff --git a/pkg/provider/azure_fakes.go b/pkg/provider/azure_fakes.go
index 030128a85..0b1db9dfd 100644
--- a/pkg/provider/azure_fakes.go
+++ b/pkg/provider/azure_fakes.go
@@ -36,7 +36,6 @@ import (
 	"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/publicipclient/mockpublicipclient"
-	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/routetableclient/mockroutetableclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/subnetclient/mocksubnetclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/vmclient/mockvmclient"
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/vmssclient/mockvmssclient"
@@ -45,6 +44,7 @@ import (
 	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
 	"sigs.k8s.io/cloud-provider-azure/pkg/provider/config"
 	"sigs.k8s.io/cloud-provider-azure/pkg/provider/privatelinkservice"
+	"sigs.k8s.io/cloud-provider-azure/pkg/provider/routetable"
 	"sigs.k8s.io/cloud-provider-azure/pkg/provider/securitygroup"
 	"sigs.k8s.io/cloud-provider-azure/pkg/provider/subnet"
 	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
@@ -116,7 +116,6 @@ func GetTestCloud(ctrl *gomock.Controller) (az *Cloud) {
 	az.InterfacesClient = mockinterfaceclient.NewMockInterface(ctrl)
 	az.LoadBalancerClient = mockloadbalancerclient.NewMockInterface(ctrl)
 	az.PublicIPAddressesClient = mockpublicipclient.NewMockInterface(ctrl)
-	az.RouteTablesClient = mockroutetableclient.NewMockInterface(ctrl)
 	az.SubnetsClient = mocksubnetclient.NewMockInterface(ctrl)
 	az.VirtualMachineScaleSetsClient = mockvmssclient.NewMockInterface(ctrl)
 	az.VirtualMachineScaleSetVMsClient = mockvmssvmclient.NewMockInterface(ctrl)
@@ -140,11 +139,11 @@ func GetTestCloud(ctrl *gomock.Controller) (az *Cloud) {
 	az.lbCache, _ = az.newLBCache()
 	az.nsgRepo, _ = securitygroup.NewSecurityGroupRepo(az.SecurityGroupResourceGroup, az.SecurityGroupName, az.NsgCacheTTLInSeconds, az.Config.DisableAPICallCache, securtyGrouptrack2Client)
 	az.subnetRepo = subnet.NewMockRepository(ctrl)
-	az.rtCache, _ = az.newRouteTableCache()
 	az.pipCache, _ = az.newPIPCache()
 	az.LoadBalancerBackendPool = NewMockBackendPool(ctrl)
 
 	az.plsRepo = privatelinkservice.NewMockRepository(ctrl)
+	az.routeTableRepo = routetable.NewMockRepository(ctrl)
 
 	getter := func(_ context.Context, _ string) (interface{}, error) { return nil, nil }
 	az.storageAccountCache, _ = azcache.NewTimedCache(time.Minute, getter, az.Config.DisableAPICallCache)
diff --git a/pkg/provider/azure_routes.go b/pkg/provider/azure_routes.go
index c32c7a3cc..4527908ef 100644
--- a/pkg/provider/azure_routes.go
+++ b/pkg/provider/azure_routes.go
@@ -23,8 +23,7 @@ import (
 	"sync"
 	"time"
 
-	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2022-07-01/network"
-
+	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/wait"
 	cloudprovider "k8s.io/cloud-provider"
@@ -53,7 +52,7 @@ const (
 
 // delayedRouteOperation defines a delayed route operation which is used in delayedRouteUpdater.
 type delayedRouteOperation struct {
-	route          network.Route
+	route          *armnetwork.Route
 	routeTableTags map[string]*string
 	operation      routeOperation
 	result         chan batchOperationResult
@@ -119,24 +118,23 @@ func (d *delayedRouteUpdater) updateRoutes(ctx context.Context) {
 	}()
 
 	var (
-		routeTable       network.RouteTable
-		existsRouteTable bool
+		routeTable *armnetwork.RouteTable
 	)
-	routeTable, existsRouteTable, err = d.az.getRouteTable(ctx, azcache.CacheReadTypeDefault)
+	routeTable, err = d.az.routeTableRepo.Get(ctx, d.az.RouteTableName, azcache.CacheReadTypeDefault)
 	if err != nil {
 		klog.Errorf("getRouteTable() failed with error: %v", err)
 		return
 	}
 
 	// create route table if it doesn't exists yet.
-	if !existsRouteTable {
-		err = d.az.createRouteTable()
+	if routeTable == nil {
+		err = d.az.createRouteTable(ctx)
 		if err != nil {
 			klog.Errorf("createRouteTable() failed with error: %v", err)
 			return
 		}
 
-		routeTable, _, err = d.az.getRouteTable(ctx, azcache.CacheReadTypeDefault)
+		routeTable, err = d.az.routeTableRepo.Get(ctx, d.az.RouteTableName, azcache.CacheReadTypeDefault)
 		if err != nil {
 			klog.Errorf("getRouteTable() failed with error: %v", err)
 			return
@@ -145,9 +143,9 @@ func (d *delayedRouteUpdater) updateRoutes(ctx context.Context) {
 
 	// reconcile routes.
 	dirty, onlyUpdateTags := false, true
-	routes := []network.Route{}
-	if routeTable.RouteTablePropertiesFormat != nil && routeTable.RouteTablePropertiesFormat.Routes != nil {
-		routes = *routeTable.Routes
+	var routes []*armnetwork.Route
+	if routeTable.Properties != nil {
+		routes = routeTable.Properties.Routes
 	}
 
 	routes, dirty = d.cleanupOutdatedRoutes(routes)
@@ -169,10 +167,10 @@ func (d *delayedRouteUpdater) updateRoutes(ctx context.Context) {
 			if strings.EqualFold(ptr.Deref(existingRoute.Name, ""), ptr.Deref(rt.route.Name, "")) {
 				// delete the name-matched routes here (missing routes would be added later if the operation is add).
 				routes = append(routes[:i], routes[i+1:]...)
-				if existingRoute.RoutePropertiesFormat != nil &&
-					rt.route.RoutePropertiesFormat != nil &&
-					strings.EqualFold(ptr.Deref(existingRoute.AddressPrefix, ""), ptr.Deref(rt.route.AddressPrefix, "")) &&
-					strings.EqualFold(ptr.Deref(existingRoute.NextHopIPAddress, ""), ptr.Deref(rt.route.NextHopIPAddress, "")) {
+				if existingRoute.Properties != nil &&
+					rt.route.Properties != nil &&
+					strings.EqualFold(ptr.Deref(existingRoute.Properties.AddressPrefix, ""), ptr.Deref(rt.route.Properties.AddressPrefix, "")) &&
+					strings.EqualFold(ptr.Deref(existingRoute.Properties.NextHopIPAddress, ""), ptr.Deref(rt.route.Properties.NextHopIPAddress, "")) {
 					routeMatch = true
 				}
 				if rt.operation == routeOperationDelete {
@@ -198,9 +196,9 @@ func (d *delayedRouteUpdater) updateRoutes(ctx context.Context) {
 	if dirty {
 		if !onlyUpdateTags {
 			klog.V(2).Infof("updateRoutes: updating routes")
-			routeTable.Routes = &routes
+			routeTable.Properties.Routes = routes
 		}
-		err = d.az.CreateOrUpdateRouteTable(routeTable)
+		_, err := d.az.routeTableRepo.CreateOrUpdate(ctx, *routeTable)
 		if err != nil {
 			klog.Errorf("CreateOrUpdateRouteTable() failed with error: %v", err)
 			return
@@ -213,7 +211,7 @@ func (d *delayedRouteUpdater) updateRoutes(ctx context.Context) {
 
 // cleanupOutdatedRoutes deletes all non-dualstack routes when dualstack is enabled,
 // and deletes all dualstack routes when dualstack is not enabled.
-func (d *delayedRouteUpdater) cleanupOutdatedRoutes(existingRoutes []network.Route) (routes []network.Route, changed bool) {
+func (d *delayedRouteUpdater) cleanupOutdatedRoutes(existingRoutes []*armnetwork.Route) (routes []*armnetwork.Route, changed bool) {
 	for i := len(existingRoutes) - 1; i >= 0; i-- {
 		existingRouteName := ptr.Deref(existingRoutes[i].Name, "")
 		split := strings.Split(existingRouteName, consts.RouteNameSeparator)
@@ -241,7 +239,7 @@ func (d *delayedRouteUpdater) cleanupOutdatedRoutes(existingRoutes []network.Rou
 	return existingRoutes, changed
 }
 
-func getAddRouteOperation(route network.Route) batchOperation {
+func getAddRouteOperation(route *armnetwork.Route) batchOperation {
 	return &delayedRouteOperation{
 		route:     route,
 		operation: routeOperationAdd,
@@ -249,7 +247,7 @@ func getAddRouteOperation(route network.Route) batchOperation {
 	}
 }
 
-func getDeleteRouteOperation(route network.Route) batchOperation {
+func getDeleteRouteOperation(route *armnetwork.Route) batchOperation {
 	return &delayedRouteOperation{
 		route:     route,
 		operation: routeOperationDelete,
@@ -280,8 +278,8 @@ func (d *delayedRouteUpdater) removeOperation(_ string) {}
 // implements cloudprovider.Routes.ListRoutes
 func (az *Cloud) ListRoutes(ctx context.Context, clusterName string) ([]*cloudprovider.Route, error) {
 	klog.V(10).Infof("ListRoutes: START clusterName=%q", clusterName)
-	routeTable, existsRouteTable, err := az.getRouteTable(ctx, azcache.CacheReadTypeDefault)
-	routes, err := processRoutes(az.ipv6DualStackEnabled, routeTable, existsRouteTable, err)
+	routeTable, err := az.routeTableRepo.Get(ctx, az.RouteTableName, azcache.CacheReadTypeDefault)
+	routes, err := processRoutes(az.ipv6DualStackEnabled, routeTable, err)
 	if err != nil {
 		return nil, err
 	}
@@ -304,7 +302,7 @@ func (az *Cloud) ListRoutes(ctx context.Context, clusterName string) ([]*cloudpr
 	}
 
 	// ensure the route table is tagged as configured
-	tags, changed := az.ensureRouteTableTagged(&routeTable)
+	tags, changed := az.ensureRouteTableTagged(routeTable)
 	if changed {
 		klog.V(2).Infof("ListRoutes: updating tags on route table %s", ptr.Deref(routeTable.Name, ""))
 		op := az.routeUpdater.addOperation(getUpdateRouteTableTagsOperation(tags))
@@ -321,20 +319,20 @@ func (az *Cloud) ListRoutes(ctx context.Context, clusterName string) ([]*cloudpr
 }
 
 // Injectable for testing
-func processRoutes(ipv6DualStackEnabled bool, routeTable network.RouteTable, exists bool, err error) ([]*cloudprovider.Route, error) {
+func processRoutes(ipv6DualStackEnabled bool, routeTable *armnetwork.RouteTable, err error) ([]*cloudprovider.Route, error) {
 	if err != nil {
 		return nil, err
 	}
-	if !exists {
+	if routeTable == nil {
 		return []*cloudprovider.Route{}, nil
 	}
 
 	var kubeRoutes []*cloudprovider.Route
-	if routeTable.RouteTablePropertiesFormat != nil && routeTable.Routes != nil {
-		kubeRoutes = make([]*cloudprovider.Route, len(*routeTable.Routes))
-		for i, route := range *routeTable.Routes {
+	if routeTable.Properties != nil {
+		kubeRoutes = make([]*cloudprovider.Route, len(routeTable.Properties.Routes))
+		for i, route := range routeTable.Properties.Routes {
 			instance := MapRouteNameToNodeName(ipv6DualStackEnabled, *route.Name)
-			cidr := *route.AddressPrefix
+			cidr := *route.Properties.AddressPrefix
 			klog.V(10).Infof("ListRoutes: * instance=%q, cidr=%q", instance, cidr)
 
 			kubeRoutes[i] = &cloudprovider.Route{
@@ -349,22 +347,16 @@ func processRoutes(ipv6DualStackEnabled bool, routeTable network.RouteTable, exi
 	return kubeRoutes, nil
 }
 
-func (az *Cloud) createRouteTable() error {
-	routeTable := network.RouteTable{
-		Name:                       ptr.To(az.RouteTableName),
-		Location:                   ptr.To(az.Location),
-		RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{},
+func (az *Cloud) createRouteTable(ctx context.Context) error {
+	routeTable := armnetwork.RouteTable{
+		Name:       ptr.To(az.RouteTableName),
+		Location:   ptr.To(az.Location),
+		Properties: &armnetwork.RouteTablePropertiesFormat{},
 	}
 
 	klog.V(3).Infof("createRouteTableIfNotExists: creating routetable. routeTableName=%q", az.RouteTableName)
-	err := az.CreateOrUpdateRouteTable(routeTable)
-	if err != nil {
-		return err
-	}
-
-	// Invalidate the cache right after updating
-	_ = az.rtCache.Delete(az.RouteTableName)
-	return nil
+	_, err := az.routeTableRepo.CreateOrUpdate(ctx, routeTable)
+	return err
 }
 
 // CreateRoute creates the described managed route
@@ -419,11 +411,11 @@ func (az *Cloud) CreateRoute(ctx context.Context, clusterName string, _ string,
 		}
 	}
 	routeName := mapNodeNameToRouteName(az.ipv6DualStackEnabled, kubeRoute.TargetNode, kubeRoute.DestinationCIDR)
-	route := network.Route{
+	route := &armnetwork.Route{
 		Name: ptr.To(routeName),
-		RoutePropertiesFormat: &network.RoutePropertiesFormat{
+		Properties: &armnetwork.RoutePropertiesFormat{
 			AddressPrefix:    ptr.To(kubeRoute.DestinationCIDR),
-			NextHopType:      network.RouteNextHopTypeVirtualAppliance,
+			NextHopType:      ptr.To(armnetwork.RouteNextHopTypeVirtualAppliance),
 			NextHopIPAddress: ptr.To(targetIP),
 		},
 	}
@@ -470,9 +462,9 @@ func (az *Cloud) DeleteRoute(_ context.Context, clusterName string, kubeRoute *c
 
 	routeName := mapNodeNameToRouteName(az.ipv6DualStackEnabled, kubeRoute.TargetNode, kubeRoute.DestinationCIDR)
 	klog.V(2).Infof("DeleteRoute: deleting route. clusterName=%q instance=%q cidr=%q routeName=%q", clusterName, kubeRoute.TargetNode, kubeRoute.DestinationCIDR, routeName)
-	route := network.Route{
-		Name:                  ptr.To(routeName),
-		RoutePropertiesFormat: &network.RoutePropertiesFormat{},
+	route := &armnetwork.Route{
+		Name:       ptr.To(routeName),
+		Properties: &armnetwork.RoutePropertiesFormat{},
 	}
 	op := az.routeUpdater.addOperation(getDeleteRouteOperation(route))
 
@@ -487,9 +479,9 @@ func (az *Cloud) DeleteRoute(_ context.Context, clusterName string, kubeRoute *c
 	if az.ipv6DualStackEnabled {
 		routeNameWithoutIPV6Suffix := strings.Split(routeName, consts.RouteNameSeparator)[0]
 		klog.V(2).Infof("DeleteRoute: deleting route. clusterName=%q instance=%q cidr=%q routeName=%q", clusterName, kubeRoute.TargetNode, kubeRoute.DestinationCIDR, routeNameWithoutIPV6Suffix)
-		route := network.Route{
-			Name:                  ptr.To(routeNameWithoutIPV6Suffix),
-			RoutePropertiesFormat: &network.RoutePropertiesFormat{},
+		route := &armnetwork.Route{
+			Name:       ptr.To(routeNameWithoutIPV6Suffix),
+			Properties: &armnetwork.RoutePropertiesFormat{},
 		}
 		op := az.routeUpdater.addOperation(getDeleteRouteOperation(route))
 
@@ -553,7 +545,7 @@ func cidrtoRfc1035(cidr string) string {
 }
 
 // ensureRouteTableTagged ensures the route table is tagged as configured
-func (az *Cloud) ensureRouteTableTagged(rt *network.RouteTable) (map[string]*string, bool) {
+func (az *Cloud) ensureRouteTableTagged(rt *armnetwork.RouteTable) (map[string]*string, bool) {
 	if !strings.EqualFold(az.RouteTableResourceGroup, az.ResourceGroup) {
 		return nil, false
 	}
diff --git a/pkg/provider/azure_routetable_repo.go b/pkg/provider/azure_routetable_repo.go
deleted file mode 100644
index 116c9138f..000000000
--- a/pkg/provider/azure_routetable_repo.go
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
-Copyright 2023 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 provider
-
-import (
-	"context"
-	"encoding/json"
-	"net/http"
-	"strings"
-	"time"
-
-	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2022-07-01/network"
-
-	"k8s.io/klog/v2"
-	"k8s.io/utils/ptr"
-
-	azcache "sigs.k8s.io/cloud-provider-azure/pkg/cache"
-	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
-)
-
-// CreateOrUpdateRouteTable invokes az.RouteTablesClient.CreateOrUpdate with exponential backoff retry
-func (az *Cloud) CreateOrUpdateRouteTable(routeTable network.RouteTable) error {
-	ctx, cancel := getContextWithCancel()
-	defer cancel()
-
-	rerr := az.RouteTablesClient.CreateOrUpdate(ctx, az.RouteTableResourceGroup, az.RouteTableName, routeTable, ptr.Deref(routeTable.Etag, ""))
-	if rerr == nil {
-		// Invalidate the cache right after updating
-		_ = az.rtCache.Delete(*routeTable.Name)
-		return nil
-	}
-
-	rtJSON, _ := json.Marshal(routeTable)
-	klog.Warningf("RouteTablesClient.CreateOrUpdate(%s) failed: %v, RouteTable request: %s", ptr.Deref(routeTable.Name, ""), rerr.Error(), string(rtJSON))
-
-	// Invalidate the cache because etag mismatch.
-	if rerr.HTTPStatusCode == http.StatusPreconditionFailed {
-		klog.V(3).Infof("Route table cache for %s is cleanup because of http.StatusPreconditionFailed", *routeTable.Name)
-		_ = az.rtCache.Delete(*routeTable.Name)
-	}
-	// Invalidate the cache because another new operation has canceled the current request.
-	if strings.Contains(strings.ToLower(rerr.Error().Error()), consts.OperationCanceledErrorMessage) {
-		klog.V(3).Infof("Route table cache for %s is cleanup because CreateOrUpdateRouteTable is canceled by another operation", *routeTable.Name)
-		_ = az.rtCache.Delete(*routeTable.Name)
-	}
-	klog.Errorf("RouteTablesClient.CreateOrUpdate(%s) failed: %v", az.RouteTableName, rerr.Error())
-	return rerr.Error()
-}
-
-func (az *Cloud) newRouteTableCache() (azcache.Resource, error) {
-	getter := func(ctx context.Context, key string) (interface{}, error) {
-		rt, err := az.RouteTablesClient.Get(ctx, az.RouteTableResourceGroup, key, "")
-		exists, rerr := checkResourceExistsFromError(err)
-		if rerr != nil {
-			return nil, rerr.Error()
-		}
-
-		if !exists {
-			klog.V(2).Infof("Route table %q not found", key)
-			return nil, nil
-		}
-
-		return &rt, nil
-	}
-
-	if az.RouteTableCacheTTLInSeconds == 0 {
-		az.RouteTableCacheTTLInSeconds = routeTableCacheTTLDefaultInSeconds
-	}
-	return azcache.NewTimedCache(time.Duration(az.RouteTableCacheTTLInSeconds)*time.Second, getter, az.Config.DisableAPICallCache)
-}
diff --git a/pkg/provider/azure_vmsets_repo.go b/pkg/provider/azure_vmsets_repo.go
index ec7106c95..00c13e6a6 100644
--- a/pkg/provider/azure_vmsets_repo.go
+++ b/pkg/provider/azure_vmsets_repo.go
@@ -19,13 +19,10 @@ package provider
 import (
 	"context"
 	"errors"
-	"fmt"
 	"strings"
 	"time"
 
 	"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"
-
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/wait"
 	cloudprovider "k8s.io/cloud-provider"
@@ -171,20 +168,3 @@ func (az *Cloud) getVirtualMachine(ctx context.Context, nodeName types.NodeName,
 
 	return *(cachedVM.(*compute.VirtualMachine)), nil
 }
-
-func (az *Cloud) getRouteTable(ctx context.Context, crt azcache.AzureCacheReadType) (routeTable network.RouteTable, exists bool, err error) {
-	if len(az.RouteTableName) == 0 {
-		return routeTable, false, fmt.Errorf("route table name is not configured")
-	}
-
-	cachedRt, err := az.rtCache.GetWithDeepCopy(ctx, az.RouteTableName, crt)
-	if err != nil {
-		return routeTable, false, err
-	}
-
-	if cachedRt == nil {
-		return routeTable, false, nil
-	}
-
-	return *(cachedRt.(*network.RouteTable)), true, nil
-}
diff --git a/pkg/provider/azure_wrap.go b/pkg/provider/azure_wrap.go
index d599ddbd2..614c01233 100644
--- a/pkg/provider/azure_wrap.go
+++ b/pkg/provider/azure_wrap.go
@@ -29,7 +29,6 @@ import (
 var (
 	vmCacheTTLDefaultInSeconds           = 60
 	loadBalancerCacheTTLDefaultInSeconds = 120
-	routeTableCacheTTLDefaultInSeconds   = 120
 	publicIPCacheTTLDefaultInSeconds     = 120
 
 	azureNodeProviderIDRE    = regexp.MustCompile(`^azure:///subscriptions/(?:.*)/resourceGroups/(?:.*)/providers/Microsoft.Compute/(?:.*)`)
diff --git a/pkg/provider/routetable/cache.go b/pkg/provider/routetable/cache.go
new file mode 100644
index 000000000..96927a51c
--- /dev/null
+++ b/pkg/provider/routetable/cache.go
@@ -0,0 +1,55 @@
+/*
+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 routetable
+
+import (
+	"context"
+	"time"
+
+	"sigs.k8s.io/cloud-provider-azure/pkg/azclient/routetableclient"
+	"sigs.k8s.io/cloud-provider-azure/pkg/cache"
+	"sigs.k8s.io/cloud-provider-azure/pkg/util/errutils"
+)
+
+const (
+	DefaultCacheTTL = 120 * time.Second
+)
+
+func NewCache(
+	client routetableclient.Interface,
+	resourceGroup string,
+	cacheTTL time.Duration,
+	disableAPICallCache bool,
+) (cache.Resource, error) {
+	getter := func(ctx context.Context, key string) (interface{}, error) {
+		rt, err := client.Get(ctx, resourceGroup, key)
+		found, err := errutils.CheckResourceExistsFromAzcoreError(err)
+		if err != nil {
+			return nil, err
+		}
+		if !found {
+			return nil, nil
+		}
+
+		return rt, nil
+	}
+
+	if cacheTTL == 0 {
+		cacheTTL = DefaultCacheTTL
+	}
+	return cache.NewTimedCache(cacheTTL, getter, disableAPICallCache)
+}
diff --git a/pkg/provider/routetable/mock_repo.go b/pkg/provider/routetable/mock_repo.go
new file mode 100644
index 000000000..76988b62c
--- /dev/null
+++ b/pkg/provider/routetable/mock_repo.go
@@ -0,0 +1,88 @@
+// /*
+// Copyright 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.
+// */
+//
+
+// Code generated by MockGen. DO NOT EDIT.
+// Source: repo.go
+//
+// Generated by this command:
+//
+//	mockgen -destination=./mock_repo.go -package=routetable -copyright_file ../../../hack/boilerplate/boilerplate.generatego.txt -source=repo.go Repository
+//
+// Package routetable is a generated GoMock package.
+package routetable
+
+import (
+	context "context"
+	reflect "reflect"
+
+	armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6"
+	gomock "go.uber.org/mock/gomock"
+	cache "sigs.k8s.io/cloud-provider-azure/pkg/cache"
+)
+
+// MockRepository is a mock of Repository interface.
+type MockRepository struct {
+	ctrl     *gomock.Controller
+	recorder *MockRepositoryMockRecorder
+}
+
+// MockRepositoryMockRecorder is the mock recorder for MockRepository.
+type MockRepositoryMockRecorder struct {
+	mock *MockRepository
+}
+
+// NewMockRepository creates a new mock instance.
+func NewMockRepository(ctrl *gomock.Controller) *MockRepository {
+	mock := &MockRepository{ctrl: ctrl}
+	mock.recorder = &MockRepositoryMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder {
+	return m.recorder
+}
+
+// CreateOrUpdate mocks base method.
+func (m *MockRepository) CreateOrUpdate(ctx context.Context, routeTable armnetwork.RouteTable) (*armnetwork.RouteTable, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CreateOrUpdate", ctx, routeTable)
+	ret0, _ := ret[0].(*armnetwork.RouteTable)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// CreateOrUpdate indicates an expected call of CreateOrUpdate.
+func (mr *MockRepositoryMockRecorder) CreateOrUpdate(ctx, routeTable any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdate", reflect.TypeOf((*MockRepository)(nil).CreateOrUpdate), ctx, routeTable)
+}
+
+// Get mocks base method.
+func (m *MockRepository) Get(ctx context.Context, routeTableName string, crt cache.AzureCacheReadType) (*armnetwork.RouteTable, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Get", ctx, routeTableName, crt)
+	ret0, _ := ret[0].(*armnetwork.RouteTable)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockRepositoryMockRecorder) Get(ctx, routeTableName, crt any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockRepository)(nil).Get), ctx, routeTableName, crt)
+}
diff --git a/pkg/provider/routetable/repo.go b/pkg/provider/routetable/repo.go
new file mode 100644
index 000000000..fb5400df1
--- /dev/null
+++ b/pkg/provider/routetable/repo.go
@@ -0,0 +1,87 @@
+/*
+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 routetable
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6"
+
+	"sigs.k8s.io/cloud-provider-azure/pkg/azclient/routetableclient"
+	"sigs.k8s.io/cloud-provider-azure/pkg/cache"
+)
+
+// Generate mocks for the repository interface
+//go:generate mockgen -destination=./mock_repo.go -package=routetable -copyright_file ../../../hack/boilerplate/boilerplate.generatego.txt -source=repo.go Repository
+
+var (
+	ErrMissingRouteTableName = fmt.Errorf("missing RouteTable name")
+)
+
+type Repository interface {
+	Get(ctx context.Context, routeTableName string, crt cache.AzureCacheReadType) (*armnetwork.RouteTable, error)
+	CreateOrUpdate(ctx context.Context, routeTable armnetwork.RouteTable) (*armnetwork.RouteTable, error)
+}
+
+type repo struct {
+	resourceGroup string
+	client        routetableclient.Interface
+	cache         cache.Resource
+}
+
+func NewRepo(
+	client routetableclient.Interface,
+	resourceGroup string,
+	cacheTTL time.Duration,
+	disableAPICallCache bool,
+) (Repository, error) {
+	c, err := NewCache(client, resourceGroup, cacheTTL, disableAPICallCache)
+	if err != nil {
+		return nil, fmt.Errorf("new RouteTable cache: %w", err)
+	}
+
+	return &repo{
+		resourceGroup: resourceGroup,
+		client:        client,
+		cache:         c,
+	}, nil
+}
+
+func (r *repo) Get(ctx context.Context, routeTableName string, crt cache.AzureCacheReadType) (*armnetwork.RouteTable, error) {
+	rt, err := r.cache.GetWithDeepCopy(ctx, routeTableName, crt)
+	if err != nil {
+		return nil, fmt.Errorf("get RouteTable: %w", err)
+	}
+
+	return rt.(*armnetwork.RouteTable), nil
+}
+
+func (r *repo) CreateOrUpdate(ctx context.Context, routeTable armnetwork.RouteTable) (*armnetwork.RouteTable, error) {
+	if routeTable.Name == nil {
+		return nil, ErrMissingRouteTableName
+	}
+
+	rv, err := r.client.CreateOrUpdate(ctx, r.resourceGroup, *routeTable.Name, routeTable)
+	if err != nil {
+		return nil, fmt.Errorf("create or update RouteTable: %w", err)
+	}
+	_ = r.cache.Delete(*routeTable.Name)
+
+	return rv, nil
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 7fe1d339f..b47aafef8 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -1571,6 +1571,7 @@ sigs.k8s.io/cloud-provider-azure/pkg/azclient/registryclient
 sigs.k8s.io/cloud-provider-azure/pkg/azclient/resourcegroupclient
 sigs.k8s.io/cloud-provider-azure/pkg/azclient/roleassignmentclient
 sigs.k8s.io/cloud-provider-azure/pkg/azclient/routetableclient
+sigs.k8s.io/cloud-provider-azure/pkg/azclient/routetableclient/mock_routetableclient
 sigs.k8s.io/cloud-provider-azure/pkg/azclient/secretclient
 sigs.k8s.io/cloud-provider-azure/pkg/azclient/securitygroupclient
 sigs.k8s.io/cloud-provider-azure/pkg/azclient/securitygroupclient/mock_securitygroupclient
diff --git a/pkg/azureclients/routetableclient/mockroutetableclient/interface.go b/vendor/sigs.k8s.io/cloud-provider-azure/pkg/azclient/routetableclient/mock_routetableclient/interface.go
similarity index 52%
rename from pkg/azureclients/routetableclient/mockroutetableclient/interface.go
rename to vendor/sigs.k8s.io/cloud-provider-azure/pkg/azclient/routetableclient/mock_routetableclient/interface.go
index 200ff014c..ef6fedb42 100644
--- a/pkg/azureclients/routetableclient/mockroutetableclient/interface.go
+++ b/vendor/sigs.k8s.io/cloud-provider-azure/pkg/azclient/routetableclient/mock_routetableclient/interface.go
@@ -13,32 +13,31 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 // */
-//
 
 // Code generated by MockGen. DO NOT EDIT.
-// Source: pkg/azureclients/routetableclient/interface.go
+// Source: routetableclient/interface.go
 //
 // Generated by this command:
 //
-//	mockgen -copyright_file=/home/runner/work/cloud-provider-azure/cloud-provider-azure/hack/boilerplate/boilerplate.generatego.txt -source=pkg/azureclients/routetableclient/interface.go -package=mockroutetableclient Interface
+//	mockgen -package mock_routetableclient -source routetableclient/interface.go
 //
 
-// Package mockroutetableclient is a generated GoMock package.
-package mockroutetableclient
+// Package mock_routetableclient is a generated GoMock package.
+package mock_routetableclient
 
 import (
 	context "context"
 	reflect "reflect"
 
-	network "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2022-07-01/network"
+	armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6"
 	gomock "go.uber.org/mock/gomock"
-	retry "sigs.k8s.io/cloud-provider-azure/pkg/retry"
 )
 
 // MockInterface is a mock of Interface interface.
 type MockInterface struct {
 	ctrl     *gomock.Controller
 	recorder *MockInterfaceMockRecorder
+	isgomock struct{}
 }
 
 // MockInterfaceMockRecorder is the mock recorder for MockInterface.
@@ -59,30 +58,60 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
 }
 
 // CreateOrUpdate mocks base method.
-func (m *MockInterface) CreateOrUpdate(ctx context.Context, resourceGroupName, routeTableName string, parameters network.RouteTable, etag string) *retry.Error {
+func (m *MockInterface) CreateOrUpdate(ctx context.Context, resourceGroupName, resourceName string, resourceParam armnetwork.RouteTable) (*armnetwork.RouteTable, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "CreateOrUpdate", ctx, resourceGroupName, routeTableName, parameters, etag)
-	ret0, _ := ret[0].(*retry.Error)
-	return ret0
+	ret := m.ctrl.Call(m, "CreateOrUpdate", ctx, resourceGroupName, resourceName, resourceParam)
+	ret0, _ := ret[0].(*armnetwork.RouteTable)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
 }
 
 // CreateOrUpdate indicates an expected call of CreateOrUpdate.
-func (mr *MockInterfaceMockRecorder) CreateOrUpdate(ctx, resourceGroupName, routeTableName, parameters, etag any) *gomock.Call {
+func (mr *MockInterfaceMockRecorder) CreateOrUpdate(ctx, resourceGroupName, resourceName, resourceParam any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdate", reflect.TypeOf((*MockInterface)(nil).CreateOrUpdate), ctx, resourceGroupName, routeTableName, parameters, etag)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdate", reflect.TypeOf((*MockInterface)(nil).CreateOrUpdate), ctx, resourceGroupName, resourceName, resourceParam)
+}
+
+// Delete mocks base method.
+func (m *MockInterface) Delete(ctx context.Context, resourceGroupName, resourceName string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Delete", ctx, resourceGroupName, resourceName)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Delete indicates an expected call of Delete.
+func (mr *MockInterfaceMockRecorder) Delete(ctx, resourceGroupName, resourceName any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockInterface)(nil).Delete), ctx, resourceGroupName, resourceName)
 }
 
 // Get mocks base method.
-func (m *MockInterface) Get(ctx context.Context, resourceGroupName, routeTableName, expand string) (network.RouteTable, *retry.Error) {
+func (m *MockInterface) Get(ctx context.Context, resourceGroupName, resourceName string) (*armnetwork.RouteTable, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, routeTableName, expand)
-	ret0, _ := ret[0].(network.RouteTable)
-	ret1, _ := ret[1].(*retry.Error)
+	ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, resourceName)
+	ret0, _ := ret[0].(*armnetwork.RouteTable)
+	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // Get indicates an expected call of Get.
-func (mr *MockInterfaceMockRecorder) Get(ctx, resourceGroupName, routeTableName, expand any) *gomock.Call {
+func (mr *MockInterfaceMockRecorder) Get(ctx, resourceGroupName, resourceName any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInterface)(nil).Get), ctx, resourceGroupName, resourceName)
+}
+
+// List mocks base method.
+func (m *MockInterface) List(ctx context.Context, resourceGroupName string) ([]*armnetwork.RouteTable, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "List", ctx, resourceGroupName)
+	ret0, _ := ret[0].([]*armnetwork.RouteTable)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// List indicates an expected call of List.
+func (mr *MockInterfaceMockRecorder) List(ctx, resourceGroupName any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInterface)(nil).Get), ctx, resourceGroupName, routeTableName, expand)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockInterface)(nil).List), ctx, resourceGroupName)
 }


Test patch:
diff --git a/e2e.Dockerfile b/e2e.Dockerfile
index 2c221c5a5..86fc035ea 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 golang:1.23-bullseye@sha256:d4387d8b2ee5b78a199fae168a761d0fb0f33f172642d20856f29a67204af16b
+FROM golang:1.23-bullseye@sha256:bc1b90c2a8eb0ffb62325e02a85d51031ad3afae15b3df4b6a48b7929b00badb
 
 WORKDIR /go/src/sigs.k8s.io/cloud-provider-azure
 
diff --git a/pkg/azureclients/routetableclient/azure_routetableclient_test.go b/pkg/azureclients/routetableclient/azure_routetableclient_test.go
deleted file mode 100644
index ee1d6d9d4..000000000
--- a/pkg/azureclients/routetableclient/azure_routetableclient_test.go
+++ /dev/null
@@ -1,347 +0,0 @@
-/*
-Copyright 2020 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 routetableclient
-
-import (
-	"bytes"
-	"context"
-	"fmt"
-	"io"
-	"net/http"
-	"testing"
-	"time"
-
-	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2022-07-01/network"
-	"github.com/Azure/go-autorest/autorest"
-	"github.com/stretchr/testify/assert"
-
-	"go.uber.org/mock/gomock"
-
-	"k8s.io/client-go/util/flowcontrol"
-	"k8s.io/utils/ptr"
-
-	azclients "sigs.k8s.io/cloud-provider-azure/pkg/azureclients"
-	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/armclient"
-	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/armclient/mockarmclient"
-	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
-)
-
-const (
-	resourceID = "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Network/routeTables/rt1"
-)
-
-// 2065-01-24 05:20:00 +0000 UTC
-func getFutureTime() time.Time {
-	return time.Unix(3000000000, 0)
-}
-
-func TestNew(t *testing.T) {
-	config := &azclients.ClientConfig{
-		SubscriptionID:          "sub",
-		ResourceManagerEndpoint: "endpoint",
-		Location:                "eastus",
-		RateLimitConfig: &azclients.RateLimitConfig{
-			CloudProviderRateLimit:            true,
-			CloudProviderRateLimitQPS:         0.5,
-			CloudProviderRateLimitBucket:      1,
-			CloudProviderRateLimitQPSWrite:    0.5,
-			CloudProviderRateLimitBucketWrite: 1,
-		},
-		Backoff: &retry.Backoff{Steps: 1},
-	}
-
-	routetableClient := New(config)
-	assert.Equal(t, "sub", routetableClient.subscriptionID)
-	assert.NotEmpty(t, routetableClient.rateLimiterReader)
-	assert.NotEmpty(t, routetableClient.rateLimiterWriter)
-}
-
-func TestNewAzureStack(t *testing.T) {
-	config := &azclients.ClientConfig{
-		CloudName:               "AZURESTACKCLOUD",
-		SubscriptionID:          "sub",
-		ResourceManagerEndpoint: "endpoint",
-		Location:                "eastus",
-		RateLimitConfig: &azclients.RateLimitConfig{
-			CloudProviderRateLimit:            true,
-			CloudProviderRateLimitQPS:         0.5,
-			CloudProviderRateLimitBucket:      1,
-			CloudProviderRateLimitQPSWrite:    0.5,
-			CloudProviderRateLimitBucketWrite: 1,
-		},
-		Backoff: &retry.Backoff{Steps: 1},
-	}
-
-	routetableClient := New(config)
-	assert.Equal(t, "AZURESTACKCLOUD", routetableClient.cloudName)
-	assert.Equal(t, "sub", routetableClient.subscriptionID)
-}
-
-func TestGet(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-
-	response := &http.Response{
-		StatusCode: http.StatusOK,
-		Body:       io.NopCloser(bytes.NewReader([]byte("{}"))),
-	}
-
-	armClient := mockarmclient.NewMockInterface(ctrl)
-	armClient.EXPECT().GetResourceWithExpandQuery(gomock.Any(), resourceID, "").Return(response, nil).Times(1)
-	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
-
-	routetableClient := getTestRouteTableClient(armClient)
-	expected := network.RouteTable{}
-	expected.Response = autorest.Response{Response: response}
-	result, rerr := routetableClient.Get(context.TODO(), "rg", "rt1", "")
-	assert.Equal(t, expected, result)
-	assert.Nil(t, rerr)
-}
-
-func TestGetNotFound(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-
-	response := &http.Response{
-		StatusCode: http.StatusNotFound,
-		Body:       io.NopCloser(bytes.NewReader([]byte("{}"))),
-	}
-	armClient := mockarmclient.NewMockInterface(ctrl)
-	armClient.EXPECT().GetResourceWithExpandQuery(gomock.Any(), resourceID, "").Return(response, nil).Times(1)
-	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
-
-	rtClient := getTestRouteTableClient(armClient)
-	expected := network.RouteTable{Response: autorest.Response{}}
-	result, rerr := rtClient.Get(context.TODO(), "rg", "rt1", "")
-	assert.Equal(t, expected, result)
-	assert.NotNil(t, rerr)
-	assert.Equal(t, http.StatusNotFound, rerr.HTTPStatusCode)
-}
-
-func TestGetInternalError(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-
-	response := &http.Response{
-		StatusCode: http.StatusInternalServerError,
-		Body:       io.NopCloser(bytes.NewReader([]byte("{}"))),
-	}
-	armClient := mockarmclient.NewMockInterface(ctrl)
-	armClient.EXPECT().GetResourceWithExpandQuery(gomock.Any(), resourceID, "").Return(response, nil).Times(1)
-	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
-
-	rtClient := getTestRouteTableClient(armClient)
-	expected := network.RouteTable{Response: autorest.Response{}}
-	result, rerr := rtClient.Get(context.TODO(), "rg", "rt1", "")
-	assert.Equal(t, expected, result)
-	assert.NotNil(t, rerr)
-	assert.Equal(t, http.StatusInternalServerError, rerr.HTTPStatusCode)
-}
-
-func TestGetNeverRateLimiter(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-
-	rtGetErr := &retry.Error{
-		RawError:  fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "read", "RouteTableGet"),
-		Retriable: true,
-	}
-
-	armClient := mockarmclient.NewMockInterface(ctrl)
-
-	routetableClient := getTestRouteTableClientWithNeverRateLimiter(armClient)
-	expected := network.RouteTable{}
-	result, rerr := routetableClient.Get(context.TODO(), "rg", "rt1", "")
-	assert.Equal(t, expected, result)
-	assert.Equal(t, rtGetErr, rerr)
-}
-
-func TestGetRetryAfterReader(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-
-	rtGetErr := &retry.Error{
-		RawError:   fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "RouteTableGet", "client throttled"),
-		Retriable:  true,
-		RetryAfter: getFutureTime(),
-	}
-
-	armClient := mockarmclient.NewMockInterface(ctrl)
-
-	routetableClient := getTestRouteTableClientWithRetryAfterReader(armClient)
-	expected := network.RouteTable{}
-	result, rerr := routetableClient.Get(context.TODO(), "rg", "rt1", "")
-	assert.Equal(t, expected, result)
-	assert.Equal(t, rtGetErr, rerr)
-}
-
-func TestGetThrottle(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-
-	response := &http.Response{
-		StatusCode: http.StatusTooManyRequests,
-		Body:       io.NopCloser(bytes.NewReader([]byte("{}"))),
-	}
-	throttleErr := &retry.Error{
-		HTTPStatusCode: http.StatusTooManyRequests,
-		RawError:       fmt.Errorf("error"),
-		Retriable:      true,
-		RetryAfter:     time.Unix(100, 0),
-	}
-	armClient := mockarmclient.NewMockInterface(ctrl)
-	armClient.EXPECT().GetResourceWithExpandQuery(gomock.Any(), resourceID, "").Return(response, throttleErr).Times(1)
-	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
-
-	routetableClient := getTestRouteTableClient(armClient)
-	result, rerr := routetableClient.Get(context.TODO(), "rg", "rt1", "")
-	assert.Empty(t, result)
-	assert.Equal(t, throttleErr, rerr)
-}
-
-func TestCreateOrUpdate(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-
-	rt1 := getTestRouteTable("rt1")
-	armClient := mockarmclient.NewMockInterface(ctrl)
-	response := &http.Response{
-		StatusCode: http.StatusOK,
-		Body:       io.NopCloser(bytes.NewReader([]byte(""))),
-	}
-	armClient.EXPECT().PutResource(gomock.Any(), ptr.Deref(rt1.ID, ""), rt1, gomock.Any()).Return(response, nil).Times(1)
-	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
-
-	rtClient := getTestRouteTableClient(armClient)
-	rerr := rtClient.CreateOrUpdate(context.TODO(), "rg", "rt1", rt1, "*")
-	assert.Nil(t, rerr)
-}
-
-func TestCreateOrUpdateWithNeverRateLimiter(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-
-	rcCreateOrUpdateErr := retry.GetRateLimitError(true, "RouteTableCreateOrUpdate")
-
-	rt1 := getTestRouteTable("rt1")
-	armClient := mockarmclient.NewMockInterface(ctrl)
-
-	routetableClient := getTestRouteTableClientWithNeverRateLimiter(armClient)
-	rerr := routetableClient.CreateOrUpdate(context.TODO(), "rg", "rt1", rt1, "")
-	assert.Equal(t, rcCreateOrUpdateErr, rerr)
-}
-
-func TestCreateOrUpdateRetryAfterReader(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-
-	rcCreateOrUpdateErr := retry.GetThrottlingError("RouteTableCreateOrUpdate", "client throttled", getFutureTime())
-
-	rt1 := getTestRouteTable("rt1")
-	armClient := mockarmclient.NewMockInterface(ctrl)
-
-	routetableClient := getTestRouteTableClientWithRetryAfterReader(armClient)
-	rerr := routetableClient.CreateOrUpdate(context.TODO(), "rg", "rt1", rt1, "")
-	assert.NotNil(t, rerr)
-	assert.Equal(t, rcCreateOrUpdateErr, rerr)
-}
-
-func TestCreateOrUpdateThrottle(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-
-	response := &http.Response{
-		StatusCode: http.StatusTooManyRequests,
-		Body:       io.NopCloser(bytes.NewReader([]byte("{}"))),
-	}
-	throttleErr := &retry.Error{
-		HTTPStatusCode: http.StatusTooManyRequests,
-		RawError:       fmt.Errorf("error"),
-		Retriable:      true,
-		RetryAfter:     time.Unix(100, 0),
-	}
-
-	rt1 := getTestRouteTable("rt1")
-	armClient := mockarmclient.NewMockInterface(ctrl)
-	armClient.EXPECT().PutResource(gomock.Any(), ptr.Deref(rt1.ID, ""), rt1, gomock.Any()).Return(response, throttleErr).Times(1)
-	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
-
-	routetableClient := getTestRouteTableClient(armClient)
-	rerr := routetableClient.CreateOrUpdate(context.TODO(), "rg", "rt1", rt1, "")
-	assert.Equal(t, throttleErr, rerr)
-}
-
-func TestCreateOrUpdateWithCreateOrUpdateResponderError(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-
-	rt1 := getTestRouteTable("rt1")
-	armClient := mockarmclient.NewMockInterface(ctrl)
-	response := &http.Response{
-		StatusCode: http.StatusNotFound,
-		Body:       io.NopCloser(bytes.NewReader([]byte(""))),
-	}
-
-	armClient.EXPECT().PutResource(gomock.Any(), ptr.Deref(rt1.ID, ""), rt1, gomock.Any()).Return(response, nil).Times(1)
-	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
-
-	routetableClient := getTestRouteTableClient(armClient)
-	rerr := routetableClient.CreateOrUpdate(context.TODO(), "rg", "rt1", rt1, "")
-	assert.NotNil(t, rerr)
-}
-
-func getTestRouteTable(name string) network.RouteTable {
-	return network.RouteTable{
-		ID:       ptr.To(fmt.Sprintf("/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Network/routeTables/%s", name)),
-		Name:     ptr.To(name),
-		Location: ptr.To("eastus"),
-	}
-}
-
-func getTestRouteTableClient(armClient armclient.Interface) *Client {
-	rateLimiterReader, rateLimiterWriter := azclients.NewRateLimiter(&azclients.RateLimitConfig{})
-	return &Client{
-		armClient:         armClient,
-		subscriptionID:    "subscriptionID",
-		rateLimiterReader: rateLimiterReader,
-		rateLimiterWriter: rateLimiterWriter,
-	}
-}
-
-func getTestRouteTableClientWithNeverRateLimiter(armClient armclient.Interface) *Client {
-	rateLimiterReader := flowcontrol.NewFakeNeverRateLimiter()
-	rateLimiterWriter := flowcontrol.NewFakeNeverRateLimiter()
-	return &Client{
-		armClient:         armClient,
-		subscriptionID:    "subscriptionID",
-		rateLimiterReader: rateLimiterReader,
-		rateLimiterWriter: rateLimiterWriter,
-	}
-}
-
-func getTestRouteTableClientWithRetryAfterReader(armClient armclient.Interface) *Client {
-	rateLimiterReader := flowcontrol.NewFakeAlwaysRateLimiter()
-	rateLimiterWriter := flowcontrol.NewFakeAlwaysRateLimiter()
-	return &Client{
-		armClient:         armClient,
-		subscriptionID:    "subscriptionID",
-		rateLimiterReader: rateLimiterReader,
-		rateLimiterWriter: rateLimiterWriter,
-		RetryAfterReader:  getFutureTime(),
-		RetryAfterWriter:  getFutureTime(),
-	}
-}
diff --git a/pkg/provider/azure_routes_test.go b/pkg/provider/azure_routes_test.go
index b95165892..719dff4f1 100644
--- a/pkg/provider/azure_routes_test.go
+++ b/pkg/provider/azure_routes_test.go
@@ -18,13 +18,13 @@ package provider
 
 import (
 	"context"
+	"errors"
 	"fmt"
-	"net/http"
 	"reflect"
 	"testing"
 	"time"
 
-	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2022-07-01/network"
+	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6"
 	"github.com/stretchr/testify/assert"
 	"go.uber.org/mock/gomock"
 
@@ -32,18 +32,18 @@ import (
 	cloudprovider "k8s.io/cloud-provider"
 	"k8s.io/utils/ptr"
 
-	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/routetableclient/mockroutetableclient"
-	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
+	"sigs.k8s.io/cloud-provider-azure/pkg/provider/routetable"
 	utilsets "sigs.k8s.io/cloud-provider-azure/pkg/util/sets"
 )
 
 func TestDeleteRoute(t *testing.T) {
+	t.Parallel()
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
-	routeTableClient := mockroutetableclient.NewMockInterface(ctrl)
 
+	mockRTRepo := routetable.NewMockRepository(ctrl)
 	cloud := &Cloud{
-		RouteTablesClient: routeTableClient,
+		routeTableRepo: mockRTRepo,
 		Config: Config{
 			RouteTableResourceGroup: "foo",
 			RouteTableName:          "bar",
@@ -52,8 +52,6 @@ func TestDeleteRoute(t *testing.T) {
 		unmanagedNodes:     utilsets.NewString(),
 		nodeInformerSynced: func() bool { return true },
 	}
-	cache, _ := cloud.newRouteTableCache()
-	cloud.rtCache = cache
 	cloud.routeUpdater = newDelayedRouteUpdater(cloud, 100*time.Millisecond)
 	go cloud.routeUpdater.run(context.Background())
 	route := cloudprovider.Route{
@@ -61,26 +59,33 @@ func TestDeleteRoute(t *testing.T) {
 		DestinationCIDR: "1.2.3.4/24",
 	}
 	routeName := mapNodeNameToRouteName(false, route.TargetNode, route.DestinationCIDR)
-	routeTables := network.RouteTable{
+	routeTables := &armnetwork.RouteTable{
 		Name:     &cloud.RouteTableName,
 		Location: &cloud.Location,
-		RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
-			Routes: &[]network.Route{
+		Properties: &armnetwork.RouteTablePropertiesFormat{
+			Routes: []*armnetwork.Route{
 				{
 					Name: &routeName,
 				},
 			},
 		},
 	}
-	routeTablesAfterDeletion := network.RouteTable{
+	routeTablesAfterDeletion := armnetwork.RouteTable{
 		Name:     &cloud.RouteTableName,
 		Location: &cloud.Location,
-		RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
-			Routes: &[]network.Route{},
+		Properties: &armnetwork.RouteTablePropertiesFormat{
+			Routes: []*armnetwork.Route{},
 		},
 	}
-	routeTableClient.EXPECT().Get(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, "").Return(routeTables, nil).AnyTimes()
-	routeTableClient.EXPECT().CreateOrUpdate(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, routeTablesAfterDeletion, "").Return(nil)
+
+	mockRTRepo.EXPECT().Get(gomock.Any(), cloud.RouteTableName, gomock.Any()).Return(routeTables, nil).AnyTimes()
+	mockRTRepo.EXPECT().CreateOrUpdate(gomock.Any(), gomock.Any()).DoAndReturn(
+		func(_ context.Context, routeTable armnetwork.RouteTable) (*armnetwork.RouteTable, error) {
+			assert.Equal(t, routeTablesAfterDeletion, routeTable)
+			return nil, nil
+		},
+	).AnyTimes()
+
 	err := cloud.DeleteRoute(context.TODO(), "cluster", &route)
 	if err != nil {
 		t.Errorf("unexpected error deleting route: %v", err)
@@ -110,12 +115,13 @@ func TestDeleteRoute(t *testing.T) {
 }
 
 func TestDeleteRouteDualStack(t *testing.T) {
+	t.Parallel()
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
-	routeTableClient := mockroutetableclient.NewMockInterface(ctrl)
 
+	mockRTRepo := routetable.NewMockRepository(ctrl)
 	cloud := &Cloud{
-		RouteTablesClient: routeTableClient,
+		routeTableRepo: mockRTRepo,
 		Config: Config{
 			RouteTableResourceGroup: "foo",
 			RouteTableName:          "bar",
@@ -125,8 +131,6 @@ func TestDeleteRouteDualStack(t *testing.T) {
 		nodeInformerSynced:   func() bool { return true },
 		ipv6DualStackEnabled: true,
 	}
-	cache, _ := cloud.newRouteTableCache()
-	cloud.rtCache = cache
 	cloud.routeUpdater = newDelayedRouteUpdater(cloud, 100*time.Millisecond)
 	go cloud.routeUpdater.run(context.Background())
 
@@ -136,11 +140,11 @@ func TestDeleteRouteDualStack(t *testing.T) {
 	}
 	routeName := mapNodeNameToRouteName(true, route.TargetNode, route.DestinationCIDR)
 	routeNameIPV4 := mapNodeNameToRouteName(false, route.TargetNode, route.DestinationCIDR)
-	routeTables := network.RouteTable{
+	routeTables := &armnetwork.RouteTable{
 		Name:     &cloud.RouteTableName,
 		Location: &cloud.Location,
-		RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
-			Routes: &[]network.Route{
+		Properties: &armnetwork.RouteTablePropertiesFormat{
+			Routes: []*armnetwork.Route{
 				{
 					Name: &routeName,
 				},
@@ -150,28 +154,38 @@ func TestDeleteRouteDualStack(t *testing.T) {
 			},
 		},
 	}
-	routeTablesAfterFirstDeletion := network.RouteTable{
+	routeTablesAfterFirstDeletion := armnetwork.RouteTable{
 		Name:     &cloud.RouteTableName,
 		Location: &cloud.Location,
-		RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
-			Routes: &[]network.Route{
+		Properties: &armnetwork.RouteTablePropertiesFormat{
+			Routes: []*armnetwork.Route{
 				{
 					Name: &routeNameIPV4,
 				},
 			},
 		},
 	}
-	routeTablesAfterSecondDeletion := network.RouteTable{
+	routeTablesAfterSecondDeletion := armnetwork.RouteTable{
 		Name:     &cloud.RouteTableName,
 		Location: &cloud.Location,
-		RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
-			Routes: &[]network.Route{},
+		Properties: &armnetwork.RouteTablePropertiesFormat{
+			Routes: []*armnetwork.Route{},
 		},
 	}
-	routeTableClient.EXPECT().Get(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, "").Return(routeTables, nil)
-	routeTableClient.EXPECT().Get(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, "").Return(routeTablesAfterFirstDeletion, nil)
-	routeTableClient.EXPECT().CreateOrUpdate(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, routeTablesAfterFirstDeletion, "").Return(nil)
-	routeTableClient.EXPECT().CreateOrUpdate(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, routeTablesAfterSecondDeletion, "").Return(nil)
+	mockRTRepo.EXPECT().Get(gomock.Any(), cloud.RouteTableName, gomock.Any()).Return(routeTables, nil)
+	mockRTRepo.EXPECT().Get(gomock.Any(), cloud.RouteTableName, gomock.Any()).Return(&routeTablesAfterFirstDeletion, nil)
+	mockRTRepo.EXPECT().CreateOrUpdate(gomock.Any(), gomock.Any()).DoAndReturn(
+		func(_ context.Context, routeTable armnetwork.RouteTable) (*armnetwork.RouteTable, error) {
+			assert.Equal(t, routeTablesAfterFirstDeletion, routeTable)
+			return nil, nil
+		},
+	)
+	mockRTRepo.EXPECT().CreateOrUpdate(gomock.Any(), gomock.Any()).DoAndReturn(
+		func(_ context.Context, routeTable armnetwork.RouteTable) (*armnetwork.RouteTable, error) {
+			assert.Equal(t, routeTablesAfterSecondDeletion, routeTable)
+			return nil, nil
+		},
+	)
 	err := cloud.DeleteRoute(context.TODO(), "cluster", &route)
 	if err != nil {
 		t.Errorf("unexpected error deleting route: %v", err)
@@ -181,45 +195,28 @@ func TestDeleteRouteDualStack(t *testing.T) {
 }
 
 func TestCreateRoute(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-	routeTableClient := mockroutetableclient.NewMockInterface(ctrl)
-	mockVMSet := NewMockVMSet(ctrl)
-
-	cloud := &Cloud{
-		RouteTablesClient: routeTableClient,
-		VMSet:             mockVMSet,
-		Config: Config{
-			RouteTableResourceGroup: "foo",
-			RouteTableName:          "bar",
-			Location:                "location",
-		},
-		unmanagedNodes:     utilsets.NewString(),
-		nodeInformerSynced: func() bool { return true },
-	}
-	cache, _ := cloud.newRouteTableCache()
-	cloud.rtCache = cache
-	cloud.routeUpdater = newDelayedRouteUpdater(cloud, 100*time.Millisecond)
-	go cloud.routeUpdater.run(context.Background())
+	t.Parallel()
 
-	route := cloudprovider.Route{TargetNode: "node", DestinationCIDR: "1.2.3.4/24"}
 	nodePrivateIP := "2.4.6.8"
-	networkRoute := &[]network.Route{
-		{
-			Name: ptr.To("node"),
-			RoutePropertiesFormat: &network.RoutePropertiesFormat{
-				AddressPrefix:    ptr.To("1.2.3.4/24"),
-				NextHopIPAddress: &nodePrivateIP,
-				NextHopType:      network.RouteNextHopTypeVirtualAppliance,
+
+	defaultNetworkRoute := func() []*armnetwork.Route {
+		return []*armnetwork.Route{
+			{
+				Name: ptr.To("node"),
+				Properties: &armnetwork.RoutePropertiesFormat{
+					AddressPrefix:    ptr.To("1.2.3.4/24"),
+					NextHopIPAddress: &nodePrivateIP,
+					NextHopType:      ptr.To(armnetwork.RouteNextHopTypeVirtualAppliance),
+				},
 			},
-		},
+		}
 	}
 
-	testCases := []struct {
+	tests := []struct {
 		name                  string
 		routeTableName        string
-		initialRoute          *[]network.Route
-		updatedRoute          *[]network.Route
+		initialRoute          []*armnetwork.Route
+		updatedRoute          []*armnetwork.Route
 		hasUnmanagedNodes     bool
 		nodeInformerNotSynced bool
 		ipv6DualStackEnabled  bool
@@ -229,71 +226,49 @@ func TestCreateRoute(t *testing.T) {
 		expectedRouteCIDRs    map[string]string
 
 		getIPError        error
-		getErr            *retry.Error
-		secondGetErr      *retry.Error
-		createOrUpdateErr *retry.Error
+		firstGetNotFound  bool // FIXME: it's very confusing; should refactor the whole test
+		getErr            error
+		secondGetErr      error
+		createOrUpdateErr error
 		expectedErrMsg    error
 	}{
-		{
-			name:           "CreateRoute should report an error if route table name is not configured",
-			routeTableName: "",
-			expectedErrMsg: fmt.Errorf("route table name is not configured"),
-		},
 		{
 			name:           "CreateRoute should create route if route doesn't exist",
 			routeTableName: "rt1",
-			updatedRoute:   networkRoute,
+			updatedRoute:   defaultNetworkRoute(),
 		},
 		{
-			name:           "CreateRoute should report error if error occurs when invoke CreateOrUpdateRouteTable",
-			routeTableName: "rt2",
-			updatedRoute:   networkRoute,
-			createOrUpdateErr: &retry.Error{
-				HTTPStatusCode: http.StatusInternalServerError,
-				RawError:       fmt.Errorf("CreateOrUpdate error"),
-			},
-			expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: CreateOrUpdate error"),
+			name:              "CreateRoute should report error if error occurs when invoke CreateOrUpdateRouteTable",
+			routeTableName:    "rt2",
+			updatedRoute:      defaultNetworkRoute(),
+			createOrUpdateErr: errors.New("dummy"),
+			expectedErrMsg:    errors.New("dummy"),
 		},
 		{
 			name:           "CreateRoute should do nothing if route already exists",
 			routeTableName: "rt3",
-			initialRoute:   networkRoute,
-			updatedRoute:   networkRoute,
+			initialRoute:   defaultNetworkRoute(),
+			updatedRoute:   defaultNetworkRoute(),
 		},
 		{
-			name:           "CreateRoute should report error if error occurs when invoke createRouteTable",
-			routeTableName: "rt4",
-			getErr: &retry.Error{
-				HTTPStatusCode: http.StatusNotFound,
-				RawError:       cloudprovider.InstanceNotFound,
-			},
-			createOrUpdateErr: &retry.Error{
-				HTTPStatusCode: http.StatusInternalServerError,
-				RawError:       fmt.Errorf("CreateOrUpdate error"),
-			},
-			expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: CreateOrUpdate error"),
+			name:              "CreateRoute should report error if error occurs when invoke createRouteTable",
+			routeTableName:    "rt4",
+			firstGetNotFound:  true,
+			createOrUpdateErr: errors.New("create or update dummy"),
+			expectedErrMsg:    errors.New("create or update dummy"),
 		},
 		{
-			name:           "CreateRoute should report error if error occurs when invoke getRouteTable for the second time",
-			routeTableName: "rt5",
-			getErr: &retry.Error{
-				HTTPStatusCode: http.StatusNotFound,
-				RawError:       cloudprovider.InstanceNotFound,
-			},
-			secondGetErr: &retry.Error{
-				HTTPStatusCode: http.StatusInternalServerError,
-				RawError:       fmt.Errorf("Get error"),
-			},
-			expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: Get error"),
+			name:             "CreateRoute should report error if error occurs when invoke getRouteTable for the second time",
+			routeTableName:   "rt5",
+			firstGetNotFound: true,
+			secondGetErr:     errors.New("second get dummy"),
+			expectedErrMsg:   errors.New("second get dummy"),
 		},
 		{
 			name:           "CreateRoute should report error if error occurs when invoke routeTableClient.Get",
 			routeTableName: "rt6",
-			getErr: &retry.Error{
-				HTTPStatusCode: http.StatusInternalServerError,
-				RawError:       fmt.Errorf("Get error"),
-			},
-			expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: Get error"),
+			getErr:         errors.New("first get dummy"),
+			expectedErrMsg: errors.New("first get dummy"),
 		},
 		{
 			name:           "CreateRoute should report error if error occurs when invoke GetIPByNodeName",
@@ -312,13 +287,13 @@ func TestCreateRoute(t *testing.T) {
 		{
 			name:           "CreateRoute should create route if cloud.ipv6DualStackEnabled is true and route doesn't exist",
 			routeTableName: "rt9",
-			updatedRoute: &[]network.Route{
+			updatedRoute: []*armnetwork.Route{
 				{
 					Name: ptr.To("node____123424"),
-					RoutePropertiesFormat: &network.RoutePropertiesFormat{
+					Properties: &armnetwork.RoutePropertiesFormat{
 						AddressPrefix:    ptr.To("1.2.3.4/24"),
 						NextHopIPAddress: &nodePrivateIP,
-						NextHopType:      network.RouteNextHopTypeVirtualAppliance,
+						NextHopType:      ptr.To(armnetwork.RouteNextHopTypeVirtualAppliance),
 					},
 				},
 			},
@@ -331,76 +306,111 @@ func TestCreateRoute(t *testing.T) {
 		},
 	}
 
-	for _, test := range testCases {
-		initialTable := network.RouteTable{
-			Name:     ptr.To(test.routeTableName),
-			Location: &cloud.Location,
-			RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
-				Routes: test.initialRoute,
-			},
-		}
-		updatedTable := network.RouteTable{
-			Name:     ptr.To(test.routeTableName),
-			Location: &cloud.Location,
-			RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
-				Routes: test.updatedRoute,
-			},
-		}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
 
-		cloud.RouteTableName = test.routeTableName
-		cloud.ipv6DualStackEnabled = test.ipv6DualStackEnabled
-		if test.hasUnmanagedNodes {
-			cloud.unmanagedNodes.Insert(test.unmanagedNodeName)
-			cloud.routeCIDRs = test.routeCIDRs
-		} else {
-			cloud.unmanagedNodes = utilsets.NewString()
-			cloud.routeCIDRs = nil
-		}
-		if test.nodeInformerNotSynced {
-			cloud.nodeInformerSynced = func() bool { return false }
-		} else {
-			cloud.nodeInformerSynced = func() bool { return true }
-		}
+			ctrl := gomock.NewController(t)
+			defer ctrl.Finish()
+			mockVMSet := NewMockVMSet(ctrl)
 
-		mockVMSet.EXPECT().GetIPByNodeName(gomock.Any(), gomock.Any()).Return(nodePrivateIP, "", test.getIPError).MaxTimes(1)
-		mockVMSet.EXPECT().GetPrivateIPsByNodeName(gomock.Any(), "node").Return([]string{nodePrivateIP, "10.10.10.10"}, nil).MaxTimes(1)
-		routeTableClient.EXPECT().Get(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, "").Return(initialTable, test.getErr).MaxTimes(1)
-		routeTableClient.EXPECT().CreateOrUpdate(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, updatedTable, "").Return(test.createOrUpdateErr).MaxTimes(1)
+			mockRTRepo := routetable.NewMockRepository(ctrl)
+			cloud := &Cloud{
+				routeTableRepo: mockRTRepo,
+				VMSet:          mockVMSet,
+				Config: Config{
+					RouteTableResourceGroup: "foo",
+					RouteTableName:          "bar",
+					Location:                "location",
+				},
+				unmanagedNodes:     utilsets.NewString(),
+				nodeInformerSynced: func() bool { return true },
+			}
+			cloud.routeUpdater = newDelayedRouteUpdater(cloud, 100*time.Millisecond)
+			go cloud.routeUpdater.run(context.Background())
 
-		//Here is the second invocation when route table doesn't exist
-		routeTableClient.EXPECT().Get(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, "").Return(initialTable, test.secondGetErr).MaxTimes(1)
+			initialTable := &armnetwork.RouteTable{
+				Name:     ptr.To(tt.routeTableName),
+				Location: &cloud.Location,
+				Properties: &armnetwork.RouteTablePropertiesFormat{
+					Routes: tt.initialRoute,
+				},
+			}
+			updatedTable := armnetwork.RouteTable{
+				Name:     ptr.To(tt.routeTableName),
+				Location: &cloud.Location,
+				Properties: &armnetwork.RouteTablePropertiesFormat{
+					Routes: tt.updatedRoute,
+				},
+			}
 
-		err := cloud.CreateRoute(context.TODO(), "cluster", "unused", &route)
-		assert.Equal(t, cloud.routeCIDRs, test.expectedRouteCIDRs, test.name)
-		if err != nil {
-			assert.EqualError(t, test.expectedErrMsg, err.Error(), test.name)
-		}
+			cloud.RouteTableName = tt.routeTableName
+			cloud.ipv6DualStackEnabled = tt.ipv6DualStackEnabled
+			if tt.hasUnmanagedNodes {
+				cloud.unmanagedNodes.Insert(tt.unmanagedNodeName)
+				cloud.routeCIDRs = tt.routeCIDRs
+			} else {
+				cloud.unmanagedNodes = utilsets.NewString()
+				cloud.routeCIDRs = nil
+			}
+			if tt.nodeInformerNotSynced {
+				cloud.nodeInformerSynced = func() bool { return false }
+			} else {
+				cloud.nodeInformerSynced = func() bool { return true }
+			}
+
+			mockVMSet.EXPECT().GetIPByNodeName(gomock.Any(), gomock.Any()).Return(nodePrivateIP, "", tt.getIPError).MaxTimes(1)
+			mockVMSet.EXPECT().GetPrivateIPsByNodeName(gomock.Any(), "node").Return([]string{nodePrivateIP, "10.10.10.10"}, nil).MaxTimes(1)
+			if tt.firstGetNotFound {
+				mockRTRepo.EXPECT().Get(gomock.Any(), cloud.RouteTableName, gomock.Any()).Return(nil, nil).MaxTimes(1)
+			} else {
+				mockRTRepo.EXPECT().Get(gomock.Any(), cloud.RouteTableName, gomock.Any()).Return(initialTable, tt.getErr).MaxTimes(1)
+			}
+
+			mockRTRepo.EXPECT().CreateOrUpdate(gomock.Any(), gomock.Any()).DoAndReturn(
+				func(_ context.Context, routeTable armnetwork.RouteTable) (*armnetwork.RouteTable, error) {
+					assert.Equal(t, updatedTable, routeTable)
+					return nil, tt.createOrUpdateErr
+				},
+			).MaxTimes(1)
+
+			//Here is the second invocation when route table doesn't exist
+			mockRTRepo.EXPECT().Get(gomock.Any(), cloud.RouteTableName, gomock.Any()).Return(initialTable, tt.secondGetErr).MaxTimes(1)
+
+			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+			defer cancel()
+			route := cloudprovider.Route{TargetNode: "node", DestinationCIDR: "1.2.3.4/24"}
+			err := cloud.CreateRoute(ctx, "cluster", "unused", &route)
+			assert.Equal(t, cloud.routeCIDRs, tt.expectedRouteCIDRs, tt.name)
+			if err != nil {
+				assert.EqualError(t, tt.expectedErrMsg, err.Error(), tt.name)
+			}
+		})
 	}
 }
 
 func TestCreateRouteTable(t *testing.T) {
+	t.Parallel()
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
-	routeTableClient := mockroutetableclient.NewMockInterface(ctrl)
 
+	mockRTRepo := routetable.NewMockRepository(ctrl)
 	cloud := &Cloud{
-		RouteTablesClient: routeTableClient,
+		routeTableRepo: mockRTRepo,
 		Config: Config{
 			RouteTableResourceGroup: "foo",
 			RouteTableName:          "bar",
 			Location:                "location",
 		},
 	}
-	cache, _ := cloud.newRouteTableCache()
-	cloud.rtCache = cache
 
-	expectedTable := network.RouteTable{
-		Name:                       &cloud.RouteTableName,
-		Location:                   &cloud.Location,
-		RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{},
+	expectedTable := armnetwork.RouteTable{
+		Name:       &cloud.RouteTableName,
+		Location:   &cloud.Location,
+		Properties: &armnetwork.RouteTablePropertiesFormat{},
 	}
-	routeTableClient.EXPECT().CreateOrUpdate(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, expectedTable, "").Return(nil)
-	err := cloud.createRouteTable()
+	mockRTRepo.EXPECT().CreateOrUpdate(gomock.Any(), expectedTable).Return(nil, nil)
+	err := cloud.createRouteTable(context.Background())
 	if err != nil {
 		t.Errorf("unexpected error in creating route table: %v", err)
 		t.FailNow()
@@ -409,9 +419,8 @@ func TestCreateRouteTable(t *testing.T) {
 
 func TestProcessRoutes(t *testing.T) {
 	tests := []struct {
-		rt            network.RouteTable
+		rt            *armnetwork.RouteTable
 		expectedRoute []cloudprovider.Route
-		exists        bool
 		expectErr     bool
 		name          string
 		expectedError string
@@ -423,35 +432,31 @@ func TestProcessRoutes(t *testing.T) {
 			expectedError: "test error",
 		},
 		{
-			exists: false,
-			name:   "doesn't exist",
+			name: "doesn't exist",
 		},
 		{
-			rt:     network.RouteTable{},
-			exists: true,
-			name:   "nil routes",
+			rt:   &armnetwork.RouteTable{},
+			name: "nil routes",
 		},
 		{
-			rt: network.RouteTable{
-				RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{},
+			rt: &armnetwork.RouteTable{
+				Properties: &armnetwork.RouteTablePropertiesFormat{},
 			},
-			exists: true,
-			name:   "no routes",
+			name: "no routes",
 		},
 		{
-			rt: network.RouteTable{
-				RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
-					Routes: &[]network.Route{
+			rt: &armnetwork.RouteTable{
+				Properties: &armnetwork.RouteTablePropertiesFormat{
+					Routes: []*armnetwork.Route{
 						{
 							Name: ptr.To("name"),
-							RoutePropertiesFormat: &network.RoutePropertiesFormat{
+							Properties: &armnetwork.RoutePropertiesFormat{
 								AddressPrefix: ptr.To("1.2.3.4/16"),
 							},
 						},
 					},
 				},
 			},
-			exists: true,
 			expectedRoute: []cloudprovider.Route{
 				{
 					Name:            "name",
@@ -462,25 +467,24 @@ func TestProcessRoutes(t *testing.T) {
 			name: "one route",
 		},
 		{
-			rt: network.RouteTable{
-				RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
-					Routes: &[]network.Route{
+			rt: &armnetwork.RouteTable{
+				Properties: &armnetwork.RouteTablePropertiesFormat{
+					Routes: []*armnetwork.Route{
 						{
 							Name: ptr.To("name"),
-							RoutePropertiesFormat: &network.RoutePropertiesFormat{
+							Properties: &armnetwork.RoutePropertiesFormat{
 								AddressPrefix: ptr.To("1.2.3.4/16"),
 							},
 						},
 						{
 							Name: ptr.To("name2"),
-							RoutePropertiesFormat: &network.RoutePropertiesFormat{
+							Properties: &armnetwork.RoutePropertiesFormat{
 								AddressPrefix: ptr.To("5.6.7.8/16"),
 							},
 						},
 					},
 				},
 			},
-			exists: true,
 			expectedRoute: []cloudprovider.Route{
 				{
 					Name:            "name",
@@ -497,7 +501,7 @@ func TestProcessRoutes(t *testing.T) {
 		},
 	}
 	for _, test := range tests {
-		routes, err := processRoutes(false, test.rt, test.exists, test.err)
+		routes, err := processRoutes(false, test.rt, test.err)
 		if test.expectErr {
 			if err == nil {
 				t.Errorf("%s: unexpected non-error", test.name)
@@ -590,12 +594,12 @@ func TestRouteNameFuncs(t *testing.T) {
 func TestListRoutes(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
-	routeTableClient := mockroutetableclient.NewMockInterface(ctrl)
 	mockVMSet := NewMockVMSet(ctrl)
 
+	mockRTRepo := routetable.NewMockRepository(ctrl)
 	cloud := &Cloud{
-		RouteTablesClient: routeTableClient,
-		VMSet:             mockVMSet,
+		routeTableRepo: mockRTRepo,
+		VMSet:          mockVMSet,
 		Config: Config{
 			RouteTableResourceGroup: "foo",
 			RouteTableName:          "bar",
@@ -604,34 +608,32 @@ func TestListRoutes(t *testing.T) {
 		unmanagedNodes:     utilsets.NewString(),
 		nodeInformerSynced: func() bool { return true },
 	}
-	cache, _ := cloud.newRouteTableCache()
-	cloud.rtCache = cache
 	cloud.routeUpdater = newDelayedRouteUpdater(cloud, 100*time.Millisecond)
 	go cloud.routeUpdater.run(context.Background())
 
 	testCases := []struct {
 		name                  string
 		routeTableName        string
-		routeTable            network.RouteTable
+		routeTable            *armnetwork.RouteTable
 		hasUnmanagedNodes     bool
 		nodeInformerNotSynced bool
 		unmanagedNodeName     string
 		routeCIDRs            map[string]string
 		expectedRoutes        []*cloudprovider.Route
-		getErr                *retry.Error
+		getErr                error
 		expectedErrMsg        error
 	}{
 		{
 			name:           "ListRoutes should return correct routes",
 			routeTableName: "rt1",
-			routeTable: network.RouteTable{
+			routeTable: &armnetwork.RouteTable{
 				Name:     ptr.To("rt1"),
 				Location: &cloud.Location,
-				RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
-					Routes: &[]network.Route{
+				Properties: &armnetwork.RouteTablePropertiesFormat{
+					Routes: []*armnetwork.Route{
 						{
 							Name: ptr.To("node"),
-							RoutePropertiesFormat: &network.RoutePropertiesFormat{
+							Properties: &armnetwork.RoutePropertiesFormat{
 								AddressPrefix: ptr.To("1.2.3.4/24"),
 							},
 						},
@@ -652,14 +654,14 @@ func TestListRoutes(t *testing.T) {
 			hasUnmanagedNodes: true,
 			unmanagedNodeName: "unmanaged-node",
 			routeCIDRs:        map[string]string{"unmanaged-node": "2.2.3.4/24"},
-			routeTable: network.RouteTable{
+			routeTable: &armnetwork.RouteTable{
 				Name:     ptr.To("rt2"),
 				Location: &cloud.Location,
-				RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
-					Routes: &[]network.Route{
+				Properties: &armnetwork.RouteTablePropertiesFormat{
+					Routes: []*armnetwork.Route{
 						{
 							Name: ptr.To("node"),
-							RoutePropertiesFormat: &network.RoutePropertiesFormat{
+							Properties: &armnetwork.RoutePropertiesFormat{
 								AddressPrefix: ptr.To("1.2.3.4/24"),
 							},
 						},
@@ -682,28 +684,22 @@ func TestListRoutes(t *testing.T) {
 		{
 			name:           "ListRoutes should return nil if routeTable don't exist",
 			routeTableName: "rt3",
-			routeTable:     network.RouteTable{},
-			getErr: &retry.Error{
-				HTTPStatusCode: http.StatusNotFound,
-				RawError:       cloudprovider.InstanceNotFound,
-			},
+			routeTable:     &armnetwork.RouteTable{},
+			getErr:         errors.New("dummy"),
 			expectedRoutes: []*cloudprovider.Route{},
 		},
 		{
 			name:           "ListRoutes should report error if error occurs when invoke routeTableClient.Get",
 			routeTableName: "rt4",
-			routeTable:     network.RouteTable{},
-			getErr: &retry.Error{
-				HTTPStatusCode: http.StatusInternalServerError,
-				RawError:       fmt.Errorf("Get error"),
-			},
-			expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: Get error"),
+			routeTable:     &armnetwork.RouteTable{},
+			getErr:         errors.New("dummy"),
+			expectedErrMsg: errors.New("dummy"),
 		},
 		{
 			name:                  "ListRoutes should report error if node informer is not synced",
 			routeTableName:        "rt5",
 			nodeInformerNotSynced: true,
-			routeTable:            network.RouteTable{},
+			routeTable:            &armnetwork.RouteTable{},
 			expectedErrMsg:        fmt.Errorf("node informer is not synced when trying to GetUnmanagedNodes"),
 		},
 	}
@@ -724,11 +720,16 @@ func TestListRoutes(t *testing.T) {
 		}
 
 		cloud.RouteTableName = test.routeTableName
-		routeTableClient.EXPECT().Get(gomock.Any(), cloud.RouteTableResourceGroup, test.routeTableName, "").Return(test.routeTable, test.getErr)
+		mockRTRepo.EXPECT().Get(gomock.Any(), test.routeTableName, gomock.Any()).Return(test.routeTable, test.getErr)
 
 		routes, err := cloud.ListRoutes(context.TODO(), "cluster")
-		assert.Equal(t, test.expectedRoutes, routes, test.name)
+		if len(test.expectedRoutes) == 0 {
+			assert.Empty(t, routes, test.name)
+		} else {
+			assert.Equal(t, test.expectedRoutes, routes, test.name)
+		}
 		if test.expectedErrMsg != nil {
+			assert.Error(t, err)
 			assert.EqualError(t, test.expectedErrMsg, err.Error(), test.name)
 		}
 	}
@@ -740,17 +741,17 @@ func TestCleanupOutdatedRoutes(t *testing.T) {
 
 	for _, testCase := range []struct {
 		description                          string
-		existingRoutes, expectedRoutes       []network.Route
+		existingRoutes, expectedRoutes       []*armnetwork.Route
 		existingNodeNames                    *utilsets.IgnoreCaseSet
 		expectedChanged, enableIPV6DualStack bool
 	}{
 		{
 			description: "cleanupOutdatedRoutes should delete outdated non-dualstack routes when dualstack is enabled",
-			existingRoutes: []network.Route{
+			existingRoutes: []*armnetwork.Route{
 				{Name: ptr.To("aks-node1-vmss000000____xxx")},
 				{Name: ptr.To("aks-node1-vmss000000")},
 			},
-			expectedRoutes: []network.Route{
+			expectedRoutes: []*armnetwork.Route{
 				{Name: ptr.To("aks-node1-vmss000000____xxx")},
 			},
 			existingNodeNames:   utilsets.NewString("aks-node1-vmss000000"),
@@ -759,11 +760,11 @@ func TestCleanupOutdatedRoutes(t *testing.T) {
 		},
 		{
 			description: "cleanupOutdatedRoutes should delete outdated dualstack routes when dualstack is disabled",
-			existingRoutes: []network.Route{
+			existingRoutes: []*armnetwork.Route{
 				{Name: ptr.To("aks-node1-vmss000000____xxx")},
 				{Name: ptr.To("aks-node1-vmss000000")},
 			},
-			expectedRoutes: []network.Route{
+			expectedRoutes: []*armnetwork.Route{
 				{Name: ptr.To("aks-node1-vmss000000")},
 			},
 			existingNodeNames: utilsets.NewString("aks-node1-vmss000000"),
@@ -771,11 +772,11 @@ func TestCleanupOutdatedRoutes(t *testing.T) {
 		},
 		{
 			description: "cleanupOutdatedRoutes should not delete unmanaged routes when dualstack is enabled",
-			existingRoutes: []network.Route{
+			existingRoutes: []*armnetwork.Route{
 				{Name: ptr.To("aks-node1-vmss000000____xxx")},
 				{Name: ptr.To("aks-node1-vmss000000")},
 			},
-			expectedRoutes: []network.Route{
+			expectedRoutes: []*armnetwork.Route{
 				{Name: ptr.To("aks-node1-vmss000000____xxx")},
 				{Name: ptr.To("aks-node1-vmss000000")},
 			},
@@ -784,11 +785,11 @@ func TestCleanupOutdatedRoutes(t *testing.T) {
 		},
 		{
 			description: "cleanupOutdatedRoutes should not delete unmanaged routes when dualstack is disabled",
-			existingRoutes: []network.Route{
+			existingRoutes: []*armnetwork.Route{
 				{Name: ptr.To("aks-node1-vmss000000____xxx")},
 				{Name: ptr.To("aks-node1-vmss000000")},
 			},
-			expectedRoutes: []network.Route{
+			expectedRoutes: []*armnetwork.Route{
 				{Name: ptr.To("aks-node1-vmss000000____xxx")},
 				{Name: ptr.To("aks-node1-vmss000000")},
 			},
@@ -823,13 +824,13 @@ func TestEnsureRouteTableTagged(t *testing.T) {
 		"a": ptr.To("b"),
 		"c": ptr.To("d"),
 	}
-	rt := &network.RouteTable{}
+	rt := &armnetwork.RouteTable{}
 	tags, changed := cloud.ensureRouteTableTagged(rt)
 	assert.Equal(t, expectedTags, tags)
 	assert.True(t, changed)
 
 	cloud.RouteTableResourceGroup = "rg1"
-	rt = &network.RouteTable{}
+	rt = &armnetwork.RouteTable{}
 	tags, changed = cloud.ensureRouteTableTagged(rt)
 	assert.Nil(t, tags)
 	assert.False(t, changed)
diff --git a/pkg/provider/azure_routetable_repo_test.go b/pkg/provider/azure_routetable_repo_test.go
deleted file mode 100644
index 68737a969..000000000
--- a/pkg/provider/azure_routetable_repo_test.go
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
-Copyright 2023 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 provider
-
-import (
-	"context"
-	"fmt"
-	"net/http"
-	"testing"
-
-	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2022-07-01/network"
-	"github.com/stretchr/testify/assert"
-
-	"go.uber.org/mock/gomock"
-
-	"k8s.io/utils/ptr"
-
-	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/routetableclient/mockroutetableclient"
-	"sigs.k8s.io/cloud-provider-azure/pkg/cache"
-	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
-	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
-)
-
-func TestCreateOrUpdateRouteTable(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-
-	tests := []struct {
-		clientErr   *retry.Error
-		expectedErr error
-	}{
-		{
-			clientErr:   &retry.Error{HTTPStatusCode: http.StatusPreconditionFailed},
-			expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 412, RawError: %w", error(nil)),
-		},
-		{
-			clientErr:   &retry.Error{RawError: fmt.Errorf(consts.OperationCanceledErrorMessage)},
-			expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf("canceledandsupersededduetoanotheroperation")),
-		},
-	}
-
-	for _, test := range tests {
-		az := GetTestCloud(ctrl)
-		az.rtCache.Set("rt", "test")
-
-		mockRTClient := az.RouteTablesClient.(*mockroutetableclient.MockInterface)
-		mockRTClient.EXPECT().CreateOrUpdate(gomock.Any(), az.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(test.clientErr)
-		mockRTClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "rt", gomock.Any()).Return(network.RouteTable{}, nil)
-
-		err := az.CreateOrUpdateRouteTable(network.RouteTable{
-			Name: ptr.To("rt"),
-			Etag: ptr.To("etag"),
-		})
-		assert.EqualError(t, test.expectedErr, err.Error())
-
-		// route table should be removed from cache if the etag is mismatch or the operation is canceled
-		shouldBeEmpty, err := az.rtCache.GetWithDeepCopy(context.TODO(), "rt", cache.CacheReadTypeDefault)
-		assert.NoError(t, err)
-		assert.Empty(t, shouldBeEmpty)
-	}
-}
diff --git a/pkg/provider/privatelinkservice/repo_test.go b/pkg/provider/privatelinkservice/repo_test.go
index b6b8bee9c..4d881f218 100644
--- a/pkg/provider/privatelinkservice/repo_test.go
+++ b/pkg/provider/privatelinkservice/repo_test.go
@@ -33,7 +33,9 @@ import (
 )
 
 func TestRepo_Get(t *testing.T) {
+	t.Parallel()
 	t.Run("refresh cache", func(t *testing.T) {
+		t.Parallel()
 		ctrl := gomock.NewController(t)
 		defer ctrl.Finish()
 		cli := mock_privatelinkserviceclient.NewMockInterface(ctrl)
@@ -69,6 +71,7 @@ func TestRepo_Get(t *testing.T) {
 	})
 
 	t.Run("refresh cache with not found", func(t *testing.T) {
+		t.Parallel()
 		ctrl := gomock.NewController(t)
 		defer ctrl.Finish()
 		cli := mock_privatelinkserviceclient.NewMockInterface(ctrl)
@@ -89,6 +92,7 @@ func TestRepo_Get(t *testing.T) {
 	})
 
 	t.Run("API error", func(t *testing.T) {
+		t.Parallel()
 		ctrl := gomock.NewController(t)
 		defer ctrl.Finish()
 		cli := mock_privatelinkserviceclient.NewMockInterface(ctrl)
@@ -112,7 +116,9 @@ func TestRepo_Get(t *testing.T) {
 }
 
 func TestRepo_CreateOrUpdate(t *testing.T) {
+	t.Parallel()
 	t.Run("create one", func(t *testing.T) {
+		t.Parallel()
 		ctrl := gomock.NewController(t)
 		defer ctrl.Finish()
 		cli := mock_privatelinkserviceclient.NewMockInterface(ctrl)
@@ -148,6 +154,7 @@ func TestRepo_CreateOrUpdate(t *testing.T) {
 	})
 
 	t.Run("create one with missing PLS name", func(t *testing.T) {
+		t.Parallel()
 		ctrl := gomock.NewController(t)
 		defer ctrl.Finish()
 		cli := mock_privatelinkserviceclient.NewMockInterface(ctrl)
@@ -165,6 +172,7 @@ func TestRepo_CreateOrUpdate(t *testing.T) {
 	})
 
 	t.Run("create one with missing LoadBalancerFrontendIPConfigurations", func(t *testing.T) {
+		t.Parallel()
 		ctrl := gomock.NewController(t)
 		defer ctrl.Finish()
 		cli := mock_privatelinkserviceclient.NewMockInterface(ctrl)
@@ -185,6 +193,7 @@ func TestRepo_CreateOrUpdate(t *testing.T) {
 	})
 
 	t.Run("should clear cache", func(t *testing.T) {
+		t.Parallel()
 		ctrl := gomock.NewController(t)
 		defer ctrl.Finish()
 		cli := mock_privatelinkserviceclient.NewMockInterface(ctrl)
@@ -242,7 +251,9 @@ func TestRepo_CreateOrUpdate(t *testing.T) {
 }
 
 func TestRepo_Delete(t *testing.T) {
+	t.Parallel()
 	t.Run("delete one", func(t *testing.T) {
+		t.Parallel()
 		ctrl := gomock.NewController(t)
 		defer ctrl.Finish()
 		cli := mock_privatelinkserviceclient.NewMockInterface(ctrl)
@@ -263,6 +274,7 @@ func TestRepo_Delete(t *testing.T) {
 	})
 
 	t.Run("API error", func(t *testing.T) {
+		t.Parallel()
 		ctrl := gomock.NewController(t)
 		defer ctrl.Finish()
 		cli := mock_privatelinkserviceclient.NewMockInterface(ctrl)
@@ -286,7 +298,9 @@ func TestRepo_Delete(t *testing.T) {
 }
 
 func TestRepo_DeletePEConnection(t *testing.T) {
+	t.Parallel()
 	t.Run("delete one", func(t *testing.T) {
+		t.Parallel()
 		ctrl := gomock.NewController(t)
 		defer ctrl.Finish()
 		cli := mock_privatelinkserviceclient.NewMockInterface(ctrl)
@@ -307,6 +321,7 @@ func TestRepo_DeletePEConnection(t *testing.T) {
 	})
 
 	t.Run("API error", func(t *testing.T) {
+		t.Parallel()
 		ctrl := gomock.NewController(t)
 		defer ctrl.Finish()
 		cli := mock_privatelinkserviceclient.NewMockInterface(ctrl)
diff --git a/pkg/provider/routetable/repo_test.go b/pkg/provider/routetable/repo_test.go
new file mode 100644
index 000000000..ac6fe1648
--- /dev/null
+++ b/pkg/provider/routetable/repo_test.go
@@ -0,0 +1,196 @@
+/*
+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 routetable
+
+import (
+	"context"
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
+	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6"
+	"github.com/stretchr/testify/assert"
+	"go.uber.org/mock/gomock"
+	"k8s.io/utils/ptr"
+
+	"sigs.k8s.io/cloud-provider-azure/pkg/azclient/routetableclient/mock_routetableclient"
+	"sigs.k8s.io/cloud-provider-azure/pkg/cache"
+)
+
+func TestRepo_Get(t *testing.T) {
+	t.Parallel()
+
+	const ResourceGroup = "testing-rg"
+
+	t.Run("refresh cache", func(t *testing.T) {
+		t.Parallel()
+		ctrl := gomock.NewController(t)
+		defer ctrl.Finish()
+		cli := mock_routetableclient.NewMockInterface(ctrl)
+		repo, err := NewRepo(cli, ResourceGroup, 60*time.Second, false)
+		assert.NoError(t, err)
+		ctx := context.Background()
+
+		const (
+			RouteTableName = "route-table-name"
+		)
+
+		cli.EXPECT().Get(gomock.Any(), ResourceGroup, gomock.Any()).Return(&armnetwork.RouteTable{
+			Name: ptr.To(RouteTableName),
+		}, nil)
+
+		v, err := repo.Get(ctx, RouteTableName, cache.CacheReadTypeDefault)
+		assert.NoError(t, err)
+		assert.Equal(t, RouteTableName, *v.Name)
+
+		// cache hit
+		v, err = repo.Get(ctx, RouteTableName, cache.CacheReadTypeDefault)
+		assert.NoError(t, err)
+		assert.Equal(t, RouteTableName, *v.Name)
+	})
+
+	t.Run("refresh cache with not found", func(t *testing.T) {
+		t.Parallel()
+		ctrl := gomock.NewController(t)
+		defer ctrl.Finish()
+		cli := mock_routetableclient.NewMockInterface(ctrl)
+		repo, err := NewRepo(cli, ResourceGroup, 60*time.Second, false)
+		assert.NoError(t, err)
+		ctx := context.Background()
+
+		const (
+			RouteTableName = "route-table-name"
+		)
+
+		cli.EXPECT().Get(gomock.Any(), ResourceGroup, gomock.Any()).Return(nil, nil)
+
+		v, err := repo.Get(ctx, RouteTableName, cache.CacheReadTypeDefault)
+		assert.NoError(t, err)
+		assert.Nil(t, v)
+	})
+
+	t.Run("API error", func(t *testing.T) {
+		t.Parallel()
+		ctrl := gomock.NewController(t)
+		defer ctrl.Finish()
+		cli := mock_routetableclient.NewMockInterface(ctrl)
+		repo, err := NewRepo(cli, ResourceGroup, 60*time.Second, false)
+		assert.NoError(t, err)
+		ctx := context.Background()
+
+		const (
+			RouteTableName = "route-table-name"
+		)
+
+		expectedErr := fmt.Errorf("API error")
+		cli.EXPECT().Get(gomock.Any(), ResourceGroup, gomock.Any()).Return(nil, expectedErr)
+
+		_, err = repo.Get(ctx, RouteTableName, cache.CacheReadTypeDefault)
+		assert.Error(t, err)
+		assert.ErrorIs(t, err, expectedErr)
+	})
+}
+
+func TestRepo_CreateOrUpdate(t *testing.T) {
+	t.Parallel()
+
+	const ResourceGroup = "testing-rg"
+
+	t.Run("create one", func(t *testing.T) {
+		t.Parallel()
+		ctrl := gomock.NewController(t)
+		defer ctrl.Finish()
+		cli := mock_routetableclient.NewMockInterface(ctrl)
+		repo, err := NewRepo(cli, ResourceGroup, 60*time.Second, false)
+		assert.NoError(t, err)
+		ctx := context.Background()
+
+		const (
+			RouteTableName = "route-table-name"
+		)
+
+		cli.EXPECT().CreateOrUpdate(gomock.Any(), ResourceGroup, RouteTableName, armnetwork.RouteTable{
+			Name: to.Ptr(RouteTableName),
+		}).Return(&armnetwork.RouteTable{
+			Name: to.Ptr(RouteTableName),
+		}, nil).Times(1)
+
+		v, err := repo.CreateOrUpdate(ctx, armnetwork.RouteTable{
+			Name: to.Ptr(RouteTableName),
+		})
+		assert.NoError(t, err)
+		assert.Equal(t, RouteTableName, *v.Name)
+	})
+
+	t.Run("create one with missing RouteTable name", func(t *testing.T) {
+		t.Parallel()
+		ctrl := gomock.NewController(t)
+		defer ctrl.Finish()
+		cli := mock_routetableclient.NewMockInterface(ctrl)
+		repo, err := NewRepo(cli, ResourceGroup, 60*time.Second, false)
+		assert.NoError(t, err)
+		ctx := context.Background()
+
+		_, err = repo.CreateOrUpdate(ctx, armnetwork.RouteTable{})
+		assert.Error(t, err)
+		assert.ErrorIs(t, err, ErrMissingRouteTableName)
+	})
+
+	t.Run("should clear cache", func(t *testing.T) {
+
+		t.Parallel()
+		ctrl := gomock.NewController(t)
+		defer ctrl.Finish()
+		cli := mock_routetableclient.NewMockInterface(ctrl)
+		repo, err := NewRepo(cli, ResourceGroup, 60*time.Second, false)
+		assert.NoError(t, err)
+		ctx := context.Background()
+
+		const (
+			RouteTableName = "route-table-name"
+		)
+
+		cli.EXPECT().Get(gomock.Any(), ResourceGroup, RouteTableName).Return(&armnetwork.RouteTable{
+			Name: to.Ptr(RouteTableName),
+		}, nil).Times(2)
+
+		cli.EXPECT().CreateOrUpdate(gomock.Any(), ResourceGroup, RouteTableName, armnetwork.RouteTable{
+			Name: to.Ptr(RouteTableName),
+		}).Return(&armnetwork.RouteTable{
+			Name: to.Ptr(RouteTableName),
+		}, nil).Times(1)
+
+		v, err := repo.Get(ctx, RouteTableName, cache.CacheReadTypeDefault)
+		assert.NoError(t, err)
+		assert.Equal(t, RouteTableName, *v.Name)
+
+		v, err = repo.CreateOrUpdate(ctx, armnetwork.RouteTable{
+			Name: to.Ptr(RouteTableName),
+		})
+		assert.NoError(t, err)
+		assert.Equal(t, RouteTableName, *v.Name)
+
+		v, err = repo.Get(ctx, RouteTableName, cache.CacheReadTypeDefault)
+		assert.NoError(t, err)
+		assert.Equal(t, RouteTableName, *v.Name)
+
+		v, err = repo.Get(ctx, RouteTableName, cache.CacheReadTypeDefault)
+		assert.NoError(t, err)
+		assert.Equal(t, RouteTableName, *v.Name)
+	})
+}