Repository: kubernetes-sigs/cloud-provider-azure
Pull request #1719 (2022-05-13 08:05:53)

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


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

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/cloud-provider-azure/pull/1719#" title="Author self-approved">lzhecheng</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.1/OWNERS)~~ [lzhecheng]

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

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cloud-provider-azure-unit-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-unit-1-22/1525024525626904576) | true | `/test pull-cloud-provider-azure-unit-1-22`

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

<details>

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

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cloud-provider-azure-unit-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-unit-1-22/1525024525626904576) | true | `/test pull-cloud-provider-azure-unit-1-22`
pull-cloud-provider-azure-check-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-check-1-22/1525024525433966592) | true | `/test pull-cloud-provider-azure-check-1-22`

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

<details>

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

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cloud-provider-azure-unit-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-unit-1-22/1525024525626904576) | true | `/test pull-cloud-provider-azure-unit-1-22`
pull-cloud-provider-azure-check-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-check-1-22/1525024525433966592) | true | `/test pull-cloud-provider-azure-check-1-22`
pull-cloud-provider-azure-e2e-ccm-capz-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-e2e-ccm-capz-1-22/1525024525597544448) | true | `/test pull-cloud-provider-azure-e2e-ccm-capz-1-22`

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

<details>

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

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cloud-provider-azure-unit-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-unit-1-22/1525024525626904576) | true | `/test pull-cloud-provider-azure-unit-1-22`
pull-cloud-provider-azure-check-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-check-1-22/1525024525433966592) | true | `/test pull-cloud-provider-azure-check-1-22`
pull-cloud-provider-azure-e2e-ccm-capz-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-e2e-ccm-capz-1-22/1525024525597544448) | true | `/test pull-cloud-provider-azure-e2e-ccm-capz-1-22`
pull-cloud-provider-azure-e2e-ccm-vmss-basic-lb-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-e2e-ccm-vmss-basic-lb-1-22/1525024525467521024) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-basic-lb-1-22`

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

<details>

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

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cloud-provider-azure-unit-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-unit-1-22/1525024525626904576) | true | `/test pull-cloud-provider-azure-unit-1-22`
pull-cloud-provider-azure-check-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-check-1-22/1525024525433966592) | true | `/test pull-cloud-provider-azure-check-1-22`
pull-cloud-provider-azure-e2e-ccm-capz-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-e2e-ccm-capz-1-22/1525024525597544448) | true | `/test pull-cloud-provider-azure-e2e-ccm-capz-1-22`
pull-cloud-provider-azure-e2e-ccm-vmss-basic-lb-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-e2e-ccm-vmss-basic-lb-1-22/1525024525467521024) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-basic-lb-1-22`
pull-cloud-provider-azure-e2e-capz-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-e2e-capz-1-22/1525024525559795712) | true | `/test pull-cloud-provider-azure-e2e-capz-1-22`

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

<details>

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

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cloud-provider-azure-unit-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-unit-1-22/1525024525626904576) | true | `/test pull-cloud-provider-azure-unit-1-22`
pull-cloud-provider-azure-check-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-check-1-22/1525024525433966592) | true | `/test pull-cloud-provider-azure-check-1-22`
pull-cloud-provider-azure-e2e-ccm-capz-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-e2e-ccm-capz-1-22/1525024525597544448) | true | `/test pull-cloud-provider-azure-e2e-ccm-capz-1-22`
pull-cloud-provider-azure-e2e-ccm-vmss-basic-lb-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-e2e-ccm-vmss-basic-lb-1-22/1525024525467521024) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-basic-lb-1-22`
pull-cloud-provider-azure-e2e-capz-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-e2e-capz-1-22/1525024525559795712) | true | `/test pull-cloud-provider-azure-e2e-capz-1-22`
pull-cloud-provider-azure-e2e-ccm-vmss-1-22 | 9797968d75c42ec9cd97c30a76b19825198d1471 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/1719/pull-cloud-provider-azure-e2e-ccm-vmss-1-22/1525024525517852672) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-1-22`

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

<details>

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

Patch:
diff --git a/pkg/azureclients/armclient/azure_armclient.go b/pkg/azureclients/armclient/azure_armclient.go
index 8ac954cf9..907072e13 100644
--- a/pkg/azureclients/armclient/azure_armclient.go
+++ b/pkg/azureclients/armclient/azure_armclient.go
@@ -494,6 +494,15 @@ func (c *Client) PutResourcesInBatches(ctx context.Context, resources map[string
 
 // PutResourceWithDecorators puts a resource by resource ID
 func (c *Client) PutResourceWithDecorators(ctx context.Context, resourceID string, parameters interface{}, decorators []autorest.PrepareDecorator) (*http.Response, *retry.Error) {
+	return c.putResourceWithDecorators(ctx, resourceID, parameters, decorators, true)
+}
+
+// PutResourceWithDecoratorsAsync puts a resource by resource ID in async mode.
+func (c *Client) PutResourceWithDecoratorsAsync(ctx context.Context, resourceID string, parameters interface{}, decorators []autorest.PrepareDecorator) (*http.Response, *retry.Error) {
+	return c.putResourceWithDecorators(ctx, resourceID, parameters, decorators, false)
+}
+
+func (c *Client) putResourceWithDecorators(ctx context.Context, resourceID string, parameters interface{}, decorators []autorest.PrepareDecorator, wait bool) (*http.Response, *retry.Error) {
 	request, err := c.PreparePutRequest(ctx, decorators...)
 	if err != nil {
 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "put.prepare", resourceID, err)
@@ -508,6 +517,9 @@ func (c *Client) PutResourceWithDecorators(ctx context.Context, resourceID strin
 		return nil, clientErr
 	}
 
+	if !wait {
+		return resp, nil
+	}
 	response, err := c.WaitForAsyncOperationResult(ctx, future, "armclient.PutResource")
 	if err != nil {
 		if response != nil {
@@ -764,6 +776,14 @@ func GetResourceID(subscriptionID, resourceGroupName, resourceType, resourceName
 		autorest.Encode("path", resourceName))
 }
 
+// GetResourceListID gets Azure resource list ID
+func GetResourceListID(subscriptionID, resourceGroupName, resourceType string) string {
+	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/%s",
+		autorest.Encode("path", subscriptionID),
+		autorest.Encode("path", resourceGroupName),
+		resourceType)
+}
+
 // GetChildResourceID gets Azure child resource ID
 func GetChildResourceID(subscriptionID, resourceGroupName, resourceType, resourceName, childResourceType, childResourceName string) string {
 	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/%s/%s/%s/%s",
diff --git a/pkg/azureclients/armclient/interface.go b/pkg/azureclients/armclient/interface.go
index 4ce0ab085..6e38d6846 100644
--- a/pkg/azureclients/armclient/interface.go
+++ b/pkg/azureclients/armclient/interface.go
@@ -76,6 +76,9 @@ type Interface interface {
 	// PutResourceWithDecorators puts a resource with decorators by resource ID
 	PutResourceWithDecorators(ctx context.Context, resourceID string, parameters interface{}, decorators []autorest.PrepareDecorator) (*http.Response, *retry.Error)
 
+	// PutResourceWithDecoratorsAsync puts a resource with decorators by resource ID
+	PutResourceWithDecoratorsAsync(ctx context.Context, resourceID string, parameters interface{}, decorators []autorest.PrepareDecorator) (*http.Response, *retry.Error)
+
 	// PatchResource patches a resource by resource ID
 	PatchResource(ctx context.Context, resourceID string, parameters interface{}) (*http.Response, *retry.Error)
 
diff --git a/pkg/azureclients/armclient/mockarmclient/interface.go b/pkg/azureclients/armclient/mockarmclient/interface.go
index adcb549ba..0af9b7507 100644
--- a/pkg/azureclients/armclient/mockarmclient/interface.go
+++ b/pkg/azureclients/armclient/mockarmclient/interface.go
@@ -337,6 +337,21 @@ func (mr *MockInterfaceMockRecorder) PutResourceWithDecorators(ctx, resourceID,
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutResourceWithDecorators", reflect.TypeOf((*MockInterface)(nil).PutResourceWithDecorators), ctx, resourceID, parameters, decorators)
 }
 
+// PutResourceWithDecoratorsAsync mocks base method.
+func (m *MockInterface) PutResourceWithDecoratorsAsync(ctx context.Context, resourceID string, parameters interface{}, decorators []autorest.PrepareDecorator) (*http.Response, *retry.Error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "PutResourceWithDecoratorsAsync", ctx, resourceID, parameters, decorators)
+	ret0, _ := ret[0].(*http.Response)
+	ret1, _ := ret[1].(*retry.Error)
+	return ret0, ret1
+}
+
+// PutResourceWithDecoratorsAsync indicates an expected call of PutResourceWithDecoratorsAsync.
+func (mr *MockInterfaceMockRecorder) PutResourceWithDecoratorsAsync(ctx, resourceID, parameters, decorators interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutResourceWithDecoratorsAsync", reflect.TypeOf((*MockInterface)(nil).PutResourceWithDecoratorsAsync), ctx, resourceID, parameters, decorators)
+}
+
 // PutResources mocks base method.
 func (m *MockInterface) PutResources(ctx context.Context, resources map[string]interface{}) map[string]*armclient.PutResourcesResponse {
 	m.ctrl.T.Helper()
diff --git a/pkg/azureclients/containerserviceclient/azure_containerserviceclient.go b/pkg/azureclients/containerserviceclient/azure_containerserviceclient.go
index 22f9a45e4..31d095469 100644
--- a/pkg/azureclients/containerserviceclient/azure_containerserviceclient.go
+++ b/pkg/azureclients/containerserviceclient/azure_containerserviceclient.go
@@ -18,7 +18,6 @@ package containerserviceclient
 
 import (
 	"context"
-	"fmt"
 	"net/http"
 	"time"
 
@@ -38,6 +37,8 @@ import (
 
 var _ Interface = &Client{}
 
+const managedClusterResourceType = "Microsoft.ContainerService/managedClusters"
+
 // Client implements ContainerService client Interface.
 type Client struct {
 	armClient      armclient.Interface
@@ -121,7 +122,7 @@ func (c *Client) getManagedCluster(ctx context.Context, resourceGroupName string
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.ContainerService/managedClusters",
+		managedClusterResourceType,
 		managedClusterName,
 	)
 	result := containerservice.ManagedCluster{}
@@ -179,9 +180,7 @@ func (c *Client) List(ctx context.Context, resourceGroupName string) ([]containe
 
 // listManagedCluster gets a list of ManagedClusters in the resource group.
 func (c *Client) listManagedCluster(ctx context.Context, resourceGroupName string) ([]containerservice.ManagedCluster, *retry.Error) {
-	resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.ContainerService/managedClusters",
-		autorest.Encode("path", c.subscriptionID),
-		autorest.Encode("path", resourceGroupName))
+	resourceID := armclient.GetResourceListID(c.subscriptionID, resourceGroupName, managedClusterResourceType)
 	result := make([]containerservice.ManagedCluster, 0)
 	page := &ManagedClusterResultPage{}
 	page.fn = c.listNextResults
@@ -343,7 +342,7 @@ func (c *Client) createOrUpdateManagedCluster(ctx context.Context, resourceGroup
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.ContainerService/managedClusters",
+		managedClusterResourceType,
 		managedClusterName,
 	)
 	decorators := []autorest.PrepareDecorator{
@@ -418,7 +417,7 @@ func (c *Client) deleteManagedCluster(ctx context.Context, resourceGroupName str
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.ContainerService/managedClusters",
+		managedClusterResourceType,
 		managedClusterName,
 	)
 
diff --git a/pkg/azureclients/deploymentclient/azure_deploymentclient.go b/pkg/azureclients/deploymentclient/azure_deploymentclient.go
index c0bffa7ee..737b83722 100644
--- a/pkg/azureclients/deploymentclient/azure_deploymentclient.go
+++ b/pkg/azureclients/deploymentclient/azure_deploymentclient.go
@@ -18,7 +18,6 @@ package deploymentclient
 
 import (
 	"context"
-	"fmt"
 	"net/http"
 	"time"
 
@@ -38,6 +37,8 @@ import (
 
 var _ Interface = &Client{}
 
+const deploymentResourceType = "Microsoft.Resources/deployments"
+
 // Client implements ContainerService client Interface.
 type Client struct {
 	armClient      armclient.Interface
@@ -114,7 +115,7 @@ func (c *Client) getDeployment(ctx context.Context, resourceGroupName string, de
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Resources/deployments",
+		deploymentResourceType,
 		deploymentName,
 	)
 	result := resources.DeploymentExtended{}
@@ -172,9 +173,7 @@ func (c *Client) List(ctx context.Context, resourceGroupName string) ([]resource
 
 // listDeployment gets a list of deployments in the resource group.
 func (c *Client) listDeployment(ctx context.Context, resourceGroupName string) ([]resources.DeploymentExtended, *retry.Error) {
-	resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Resources/deployments",
-		autorest.Encode("path", c.subscriptionID),
-		autorest.Encode("path", resourceGroupName))
+	resourceID := armclient.GetResourceListID(c.subscriptionID, resourceGroupName, deploymentResourceType)
 	result := make([]resources.DeploymentExtended, 0)
 	page := &DeploymentResultPage{}
 	page.fn = c.listNextResults
@@ -335,7 +334,7 @@ func (c *Client) createOrUpdateDeployment(ctx context.Context, resourceGroupName
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Resources/deployments",
+		deploymentResourceType,
 		deploymentName,
 	)
 	decorators := []autorest.PrepareDecorator{
@@ -410,7 +409,7 @@ func (c *Client) deleteDeployment(ctx context.Context, resourceGroupName string,
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Resources/deployments",
+		deploymentResourceType,
 		deploymentName,
 	)
 
@@ -434,10 +433,12 @@ func (c *Client) ExportTemplate(ctx context.Context, resourceGroupName string, d
 		return resources.DeploymentExportResult{}, rerr
 	}
 
-	resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Resources/deployments/%s/exportTemplate",
-		autorest.Encode("path", c.subscriptionID),
-		autorest.Encode("path", resourceGroupName),
-		autorest.Encode("path", deploymentName))
+	resourceID := armclient.GetResourceID(
+		c.subscriptionID,
+		resourceGroupName,
+		deploymentResourceType,
+		deploymentName,
+	)
 	response, rerr := c.armClient.PostResource(ctx, resourceID, "exportTemplate", struct{}{}, map[string]interface{}{})
 	defer c.armClient.CloseResponse(ctx, response)
 	if rerr != nil {
diff --git a/pkg/azureclients/diskclient/azure_diskclient.go b/pkg/azureclients/diskclient/azure_diskclient.go
index 5c9911aca..62b6b57f7 100644
--- a/pkg/azureclients/diskclient/azure_diskclient.go
+++ b/pkg/azureclients/diskclient/azure_diskclient.go
@@ -18,7 +18,6 @@ package diskclient
 
 import (
 	"context"
-	"fmt"
 	"net/http"
 	"strings"
 	"time"
@@ -39,6 +38,8 @@ import (
 
 var _ Interface = &Client{}
 
+const diskResourceType = "Microsoft.Compute/disks"
+
 // Client implements Disk client Interface.
 type Client struct {
 	armClient      armclient.Interface
@@ -124,7 +125,7 @@ func (c *Client) getDisk(ctx context.Context, resourceGroupName string, diskName
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/disks",
+		diskResourceType,
 		diskName,
 	)
 	result := compute.Disk{}
@@ -185,7 +186,7 @@ func (c *Client) createOrUpdateDisk(ctx context.Context, resourceGroupName strin
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/disks",
+		diskResourceType,
 		diskName,
 	)
 
@@ -253,7 +254,7 @@ func (c *Client) updateDisk(ctx context.Context, resourceGroupName string, diskN
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/disks",
+		diskResourceType,
 		diskName,
 	)
 
@@ -321,7 +322,7 @@ func (c *Client) deleteDisk(ctx context.Context, resourceGroupName string, diskN
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/disks",
+		diskResourceType,
 		diskName,
 	)
 
@@ -330,9 +331,7 @@ func (c *Client) deleteDisk(ctx context.Context, resourceGroupName string, diskN
 
 // ListByResourceGroup lists all the disks under a resource group.
 func (c *Client) ListByResourceGroup(ctx context.Context, resourceGroupName string) ([]compute.Disk, *retry.Error) {
-	resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks",
-		autorest.Encode("path", c.subscriptionID),
-		autorest.Encode("path", resourceGroupName))
+	resourceID := armclient.GetResourceListID(c.subscriptionID, resourceGroupName, diskResourceType)
 
 	result := make([]compute.Disk, 0)
 	page := &DiskListPage{}
diff --git a/pkg/azureclients/interfaceclient/azure_interfaceclient.go b/pkg/azureclients/interfaceclient/azure_interfaceclient.go
index 4909e88ea..8faa5b734 100644
--- a/pkg/azureclients/interfaceclient/azure_interfaceclient.go
+++ b/pkg/azureclients/interfaceclient/azure_interfaceclient.go
@@ -39,6 +39,8 @@ import (
 
 var _ Interface = &Client{}
 
+const netInterfaceResourceType = "Microsoft.Network/networkInterfaces"
+
 // Client implements network interface client.
 type Client struct {
 	armClient              armclient.Interface
@@ -123,7 +125,7 @@ func (c *Client) getNetworkInterface(ctx context.Context, resourceGroupName stri
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/networkInterfaces",
+		netInterfaceResourceType,
 		networkInterfaceName,
 	)
 	result := network.Interface{}
@@ -259,7 +261,7 @@ func (c *Client) createOrUpdateInterface(ctx context.Context, resourceGroupName
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/networkInterfaces",
+		netInterfaceResourceType,
 		networkInterfaceName,
 	)
 	decorators := []autorest.PrepareDecorator{
@@ -334,7 +336,7 @@ func (c *Client) deleteInterface(ctx context.Context, resourceGroupName string,
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/networkInterfaces",
+		netInterfaceResourceType,
 		networkInterfaceName,
 	)
 
diff --git a/pkg/azureclients/loadbalancerclient/azure_loadbalancerclient.go b/pkg/azureclients/loadbalancerclient/azure_loadbalancerclient.go
index 0df1b56d7..1ebde4eb5 100644
--- a/pkg/azureclients/loadbalancerclient/azure_loadbalancerclient.go
+++ b/pkg/azureclients/loadbalancerclient/azure_loadbalancerclient.go
@@ -18,7 +18,6 @@ package loadbalancerclient
 
 import (
 	"context"
-	"fmt"
 	"net/http"
 	"strings"
 	"time"
@@ -39,6 +38,8 @@ import (
 
 var _ Interface = &Client{}
 
+const lbResourceType = "Microsoft.Network/loadBalancers"
+
 // Client implements LoadBalancer client Interface.
 type Client struct {
 	armClient      armclient.Interface
@@ -121,7 +122,7 @@ func (c *Client) getLB(ctx context.Context, resourceGroupName string, loadBalanc
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/loadBalancers",
+		lbResourceType,
 		loadBalancerName,
 	)
 	result := network.LoadBalancer{}
@@ -179,9 +180,7 @@ func (c *Client) List(ctx context.Context, resourceGroupName string) ([]network.
 
 // listLB gets a list of LoadBalancers in the resource group.
 func (c *Client) listLB(ctx context.Context, resourceGroupName string) ([]network.LoadBalancer, *retry.Error) {
-	resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers",
-		autorest.Encode("path", c.subscriptionID),
-		autorest.Encode("path", resourceGroupName))
+	resourceID := armclient.GetResourceListID(c.subscriptionID, resourceGroupName, "Microsoft.Network/loadBalancers")
 	result := make([]network.LoadBalancer, 0)
 	page := &LoadBalancerListResultPage{}
 	page.fn = c.listNextResults
@@ -253,7 +252,7 @@ func (c *Client) createOrUpdateLB(ctx context.Context, resourceGroupName string,
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/loadBalancers",
+		lbResourceType,
 		loadBalancerName,
 	)
 	decorators := []autorest.PrepareDecorator{
@@ -328,7 +327,7 @@ func (c *Client) deleteLB(ctx context.Context, resourceGroupName string, loadBal
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/loadBalancers",
+		lbResourceType,
 		loadBalancerName,
 	)
 
@@ -461,7 +460,7 @@ func (c *Client) createOrUpdateLBBackendPool(ctx context.Context, resourceGroupN
 	resourceID := armclient.GetChildResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/loadBalancers",
+		lbResourceType,
 		loadBalancerName,
 		"backendAddressPools",
 		backendPoolName,
diff --git a/pkg/azureclients/privatednsclient/azure_privatednsclient.go b/pkg/azureclients/privatednsclient/azure_privatednsclient.go
index d160e7c1b..8f4a6290e 100644
--- a/pkg/azureclients/privatednsclient/azure_privatednsclient.go
+++ b/pkg/azureclients/privatednsclient/azure_privatednsclient.go
@@ -18,48 +18,216 @@ package privatednsclient
 
 import (
 	"context"
+	"net/http"
+	"strings"
+	"time"
 
 	"github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns"
+	"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 (
+	privateDNSZoneResourceType = "Microsoft.Network/privateDnsZones"
+)
+
 // Client implements privatednsclient Interface.
 type Client struct {
-	privateDNSClient privatedns.PrivateZonesClient
+	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 privatedns client.
+// New creates a new private dns client with ratelimiting.
 func New(config *azclients.ClientConfig) *Client {
-	privateDNSClient := privatedns.NewPrivateZonesClient(config.SubscriptionID)
-	privateDNSClient.Authorizer = config.Authorizer
+	baseURI := config.ResourceManagerEndpoint
+	authorizer := config.Authorizer
+	apiVersion := APIVersion
+	if strings.EqualFold(config.CloudName, AzureStackCloudName) && !config.DisableAzureStackCloud {
+		klog.Warningf("Azure Stack is not supported for Private DNS Zone API")
+	}
+	armClient := armclient.New(authorizer, *config, baseURI, apiVersion)
+	rateLimiterReader, rateLimiterWriter := azclients.NewRateLimiter(config.RateLimitConfig)
+
+	if azclients.RateLimitEnabled(config.RateLimitConfig) {
+		klog.V(2).Infof("Azure PrivateDNSZoneClient (read ops) using rate limit config: QPS=%g, bucket=%d",
+			config.RateLimitConfig.CloudProviderRateLimitQPS,
+			config.RateLimitConfig.CloudProviderRateLimitBucket)
+		klog.V(2).Infof("Azure PrivateDNSZoneClient (write ops) using rate limit config: QPS=%g, bucket=%d",
+			config.RateLimitConfig.CloudProviderRateLimitQPSWrite,
+			config.RateLimitConfig.CloudProviderRateLimitBucketWrite)
+	}
+
 	client := &Client{
-		privateDNSClient: privateDNSClient,
+		armClient:         armClient,
+		rateLimiterReader: rateLimiterReader,
+		rateLimiterWriter: rateLimiterWriter,
+		subscriptionID:    config.SubscriptionID,
+		cloudName:         config.CloudName,
 	}
+
 	return client
 }
 
-// CreateOrUpdate creates or updates a private dns zone
-func (c *Client) CreateOrUpdate(ctx context.Context, resourceGroupName string, privateZoneName string, parameters privatedns.PrivateZone, waitForCompletion bool) error {
-	createOrUpdateFuture, err := c.privateDNSClient.CreateOrUpdate(ctx, resourceGroupName, privateZoneName, parameters, "", "*")
+// CreateOrUpdate creates or updates a private DNS zone.
+func (c *Client) CreateOrUpdate(ctx context.Context, resourceGroupName, privateZoneName string, parameters privatedns.PrivateZone, etag string, waitForCompletion bool) *retry.Error {
+	mc := metrics.NewMetricContext("private_dns_zone", "create_or_update", resourceGroupName, c.subscriptionID, "")
 
-	if err != nil {
-		klog.V(5).Infof("Received error for %s, resourceGroup: %s, error: %s", "privatedns.put.request", resourceGroupName, err)
-		return err
+	// Report errors if the client is rate limited.
+	if !c.rateLimiterWriter.TryAccept() {
+		mc.RateLimitedCount()
+		return retry.GetRateLimitError(true, "PrivateDNSZoneCreateOrUpdate")
+	}
+
+	// Report errors if the client is throttled.
+	if c.RetryAfterWriter.After(time.Now()) {
+		mc.ThrottledCount()
+		rerr := retry.GetThrottlingError("PrivateDNSZoneCreateOrUpdate", "client throttled", c.RetryAfterWriter)
+		return rerr
+	}
+
+	rerr := c.createOrUpdatePrivateDNSZone(ctx, resourceGroupName, privateZoneName, parameters, etag, waitForCompletion)
+	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
+}
+
+// createOrUpdatePrivateDNSZone creates or updates a private DNS zone.
+func (c *Client) createOrUpdatePrivateDNSZone(ctx context.Context, resourceGroupName, privateDNSZoneName string, parameters privatedns.PrivateZone, etag string, waitForCompletion bool) *retry.Error {
+	resourceID := armclient.GetResourceID(
+		c.subscriptionID,
+		resourceGroupName,
+		privateDNSZoneResourceType,
+		privateDNSZoneName,
+	)
+	decorators := []autorest.PrepareDecorator{
+		autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}),
+		autorest.WithJSON(parameters),
+	}
+	if etag != "" {
+		decorators = append(decorators, autorest.WithHeader("If-Match", autorest.String(etag)))
 	}
 
+	var response *http.Response
+	var rerr *retry.Error
 	if waitForCompletion {
-		err := createOrUpdateFuture.WaitForCompletionRef(ctx, c.privateDNSClient.Client)
-		if err != nil {
-			klog.V(5).Infof("Received error while waiting for completion for %s, resourceGroup: %s, error: %s", "privatedns.put.request", resourceGroupName, err)
-			return err
+		response, rerr = c.armClient.PutResourceWithDecorators(ctx, resourceID, parameters, decorators)
+	} else {
+		response, rerr = c.armClient.PutResourceWithDecoratorsAsync(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", "privatednszone.put.request", resourceID, rerr.Error())
+		return rerr
+	}
+	if !waitForCompletion {
+		return nil
+	}
+
+	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", "privatednszone.put.respond", resourceID, rerr.Error())
+			return rerr
 		}
 	}
+
 	return nil
 }
 
-func (c *Client) Get(ctx context.Context, resourceGroupName string, privateZoneName string) (result privatedns.PrivateZone, err error) {
-	return c.privateDNSClient.Get(ctx, resourceGroupName, privateZoneName)
+func (c *Client) createOrUpdateResponder(resp *http.Response) (*privatedns.PrivateZone, *retry.Error) {
+	result := &privatedns.PrivateZone{}
+	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)
+}
+
+// Get gets a private dns zone.
+func (c *Client) Get(ctx context.Context, resourceGroupName, privateZoneName string) (privatedns.PrivateZone, *retry.Error) {
+	mc := metrics.NewMetricContext("private_dns_zones", "get", resourceGroupName, c.subscriptionID, "")
+
+	// Report errors if the client is rate limited.
+	if !c.rateLimiterReader.TryAccept() {
+		mc.RateLimitedCount()
+		return privatedns.PrivateZone{}, retry.GetRateLimitError(false, "PrivateDNSZoneGet")
+	}
+
+	// Report errors if the client is throttled.
+	if c.RetryAfterReader.After(time.Now()) {
+		mc.ThrottledCount()
+		rerr := retry.GetThrottlingError("PrivateDNSZoneGet", "client throttled", c.RetryAfterReader)
+		return privatedns.PrivateZone{}, rerr
+	}
+
+	result, rerr := c.getPrivateDNSZone(ctx, resourceGroupName, privateZoneName)
+	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
+}
+
+// getPrivateDNSZone gets a private DNS zone.
+func (c *Client) getPrivateDNSZone(ctx context.Context, resourceGroupName, privateDNSZoneName string) (privatedns.PrivateZone, *retry.Error) {
+	resourceID := armclient.GetResourceID(
+		c.subscriptionID,
+		resourceGroupName,
+		privateDNSZoneResourceType,
+		privateDNSZoneName,
+	)
+	result := privatedns.PrivateZone{}
+
+	response, rerr := c.armClient.GetResource(ctx, resourceID)
+	defer c.armClient.CloseResponse(ctx, response)
+	if rerr != nil {
+		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "privatednszone.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", "privatednszone.get.respond", resourceID, err)
+		return result, retry.GetError(response, err)
+	}
+
+	result.Response = autorest.Response{Response: response}
+	return result, nil
 }
diff --git a/pkg/azureclients/privatednsclient/interface.go b/pkg/azureclients/privatednsclient/interface.go
index 628f8dbc4..63ae62a0a 100644
--- a/pkg/azureclients/privatednsclient/interface.go
+++ b/pkg/azureclients/privatednsclient/interface.go
@@ -20,15 +20,23 @@ import (
 	"context"
 
 	"github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns"
+
+	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
+)
+
+const (
+	// APIVersion is the API version.
+	APIVersion = "2018-09-01"
+	// AzureStackCloudName is the cloud name of Azure Stack
+	AzureStackCloudName = "AZURESTACKCLOUD"
 )
 
 // Interface is the client interface for Private DNS Zones
 // Don't forget to run "hack/update-mock-clients.sh" command to generate the mock client.
 type Interface interface {
+	// Get gets a private DNS zone
+	Get(ctx context.Context, resourceGroupName, privateZoneName string) (privatedns.PrivateZone, *retry.Error)
 
-	//Get gets the PrivateDNSZone
-	Get(ctx context.Context, resourceGroupName string, privateZoneName string) (result privatedns.PrivateZone, err error)
-
-	// CreateOrUpdate creates or updates a private dns zone.
-	CreateOrUpdate(ctx context.Context, resourceGroupName string, privateZoneName string, parameters privatedns.PrivateZone, waitForCompletion bool) error
+	// CreateOrUpdate creates or updates a private DNS zone.
+	CreateOrUpdate(ctx context.Context, resourceGroupName, privateZoneName string, parameters privatedns.PrivateZone, etag string, waitForCompletion bool) *retry.Error
 }
diff --git a/pkg/azureclients/privatednsclient/mockprivatednsclient/interface.go b/pkg/azureclients/privatednsclient/mockprivatednsclient/interface.go
index 5999d2356..0bd0dd21d 100644
--- a/pkg/azureclients/privatednsclient/mockprivatednsclient/interface.go
+++ b/pkg/azureclients/privatednsclient/mockprivatednsclient/interface.go
@@ -27,6 +27,7 @@ import (
 
 	privatedns "github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns"
 	gomock "github.com/golang/mock/gomock"
+	retry "sigs.k8s.io/cloud-provider-azure/pkg/retry"
 )
 
 // MockInterface is a mock of Interface interface.
@@ -53,25 +54,25 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
 }
 
 // CreateOrUpdate mocks base method.
-func (m *MockInterface) CreateOrUpdate(ctx context.Context, resourceGroupName, privateZoneName string, parameters privatedns.PrivateZone, waitForCompletion bool) error {
+func (m *MockInterface) CreateOrUpdate(ctx context.Context, resourceGroupName, privateZoneName string, parameters privatedns.PrivateZone, etag string, waitForCompletion bool) *retry.Error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "CreateOrUpdate", ctx, resourceGroupName, privateZoneName, parameters, waitForCompletion)
-	ret0, _ := ret[0].(error)
+	ret := m.ctrl.Call(m, "CreateOrUpdate", ctx, resourceGroupName, privateZoneName, parameters, etag, waitForCompletion)
+	ret0, _ := ret[0].(*retry.Error)
 	return ret0
 }
 
 // CreateOrUpdate indicates an expected call of CreateOrUpdate.
-func (mr *MockInterfaceMockRecorder) CreateOrUpdate(ctx, resourceGroupName, privateZoneName, parameters, waitForCompletion interface{}) *gomock.Call {
+func (mr *MockInterfaceMockRecorder) CreateOrUpdate(ctx, resourceGroupName, privateZoneName, parameters, etag, waitForCompletion interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdate", reflect.TypeOf((*MockInterface)(nil).CreateOrUpdate), ctx, resourceGroupName, privateZoneName, parameters, waitForCompletion)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdate", reflect.TypeOf((*MockInterface)(nil).CreateOrUpdate), ctx, resourceGroupName, privateZoneName, parameters, etag, waitForCompletion)
 }
 
 // Get mocks base method.
-func (m *MockInterface) Get(ctx context.Context, resourceGroupName, privateZoneName string) (privatedns.PrivateZone, error) {
+func (m *MockInterface) Get(ctx context.Context, resourceGroupName, privateZoneName string) (privatedns.PrivateZone, *retry.Error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, privateZoneName)
 	ret0, _ := ret[0].(privatedns.PrivateZone)
-	ret1, _ := ret[1].(error)
+	ret1, _ := ret[1].(*retry.Error)
 	return ret0, ret1
 }
 
diff --git a/pkg/azureclients/privatednszonegroupclient/azure_privatednszonegroupclient.go b/pkg/azureclients/privatednszonegroupclient/azure_privatednszonegroupclient.go
index 71e218643..79719efa8 100644
--- a/pkg/azureclients/privatednszonegroupclient/azure_privatednszonegroupclient.go
+++ b/pkg/azureclients/privatednszonegroupclient/azure_privatednszonegroupclient.go
@@ -18,47 +18,220 @@ package privatednszonegroupclient
 
 import (
 	"context"
+	"net/http"
+	"strings"
+	"time"
 
 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-02-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 (
+	peResourceType                  = "Microsoft.Network/privateEndpoints"
+	privateDNSZoneGroupResourceType = "privateDnsZoneGroups"
+)
+
 // Client implements privatednszonegroupclient client Interface.
 type Client struct {
-	privateDNSZoneGroupClient network.PrivateDNSZoneGroupsClient
+	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 private dns zone group client.
+// New creates a new private dns zone group client with ratelimiting.
 func New(config *azclients.ClientConfig) *Client {
-	privateDNSZoneGroupClient := network.NewPrivateDNSZoneGroupsClient(config.SubscriptionID)
-	privateDNSZoneGroupClient.Authorizer = config.Authorizer
+	baseURI := config.ResourceManagerEndpoint
+	authorizer := config.Authorizer
+	apiVersion := APIVersion
+	if strings.EqualFold(config.CloudName, AzureStackCloudName) && !config.DisableAzureStackCloud {
+		klog.Warningf("Azure Stack is not supported for Private DNS Zone Group API")
+	}
+	armClient := armclient.New(authorizer, *config, baseURI, apiVersion)
+	rateLimiterReader, rateLimiterWriter := azclients.NewRateLimiter(config.RateLimitConfig)
+
+	if azclients.RateLimitEnabled(config.RateLimitConfig) {
+		klog.V(2).Infof("Azure PrivateDNSZoneGroupClient (read ops) using rate limit config: QPS=%g, bucket=%d",
+			config.RateLimitConfig.CloudProviderRateLimitQPS,
+			config.RateLimitConfig.CloudProviderRateLimitBucket)
+		klog.V(2).Infof("Azure PrivateDNSZoneGroupClient (write ops) using rate limit config: QPS=%g, bucket=%d",
+			config.RateLimitConfig.CloudProviderRateLimitQPSWrite,
+			config.RateLimitConfig.CloudProviderRateLimitBucketWrite)
+	}
+
 	client := &Client{
-		privateDNSZoneGroupClient: privateDNSZoneGroupClient,
+		armClient:         armClient,
+		rateLimiterReader: rateLimiterReader,
+		rateLimiterWriter: rateLimiterWriter,
+		subscriptionID:    config.SubscriptionID,
+		cloudName:         config.CloudName,
 	}
+
 	return client
 }
 
-// CreateOrUpdate creates or updates a private dns zone group
-func (c *Client) CreateOrUpdate(ctx context.Context, resourceGroupName string, privateEndpointName string, privateDNSZoneGroupName string, parameters network.PrivateDNSZoneGroup, waitForCompletion bool) error {
-	createOrUpdateFuture, err := c.privateDNSZoneGroupClient.CreateOrUpdate(ctx, resourceGroupName, privateEndpointName, privateDNSZoneGroupName, parameters)
-	if err != nil {
-		klog.V(5).Infof("Received error for %s, resourceGroup: %s, privateEndpointName: %s, error: %s", "privatednszonegroup.put.request", resourceGroupName, privateEndpointName, err)
-		return err
+// CreateOrUpdate creates or updates a private DNS zone group.
+func (c *Client) CreateOrUpdate(ctx context.Context, resourceGroupName, privateEndpointName, privateDNSZoneGroupName string, parameters network.PrivateDNSZoneGroup, etag string, waitForCompletion bool) *retry.Error {
+	mc := metrics.NewMetricContext("private_dns_zone_group", "create_or_update", resourceGroupName, c.subscriptionID, "")
+
+	// Report errors if the client is rate limited.
+	if !c.rateLimiterWriter.TryAccept() {
+		mc.RateLimitedCount()
+		return retry.GetRateLimitError(true, "PrivateDNSZoneGroupCreateOrUpdate")
+	}
+
+	// Report errors if the client is throttled.
+	if c.RetryAfterWriter.After(time.Now()) {
+		mc.ThrottledCount()
+		rerr := retry.GetThrottlingError("PrivateDNSZoneGroupCreateOrUpdate", "client throttled", c.RetryAfterWriter)
+		return rerr
+	}
+
+	rerr := c.createOrUpdatePrivateDNSZoneGroup(ctx, resourceGroupName, privateEndpointName, privateDNSZoneGroupName, parameters, etag, waitForCompletion)
+	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
+}
+
+// createOrUpdatePrivateDNSZoneGroup creates or updates a private DNS zone group.
+func (c *Client) createOrUpdatePrivateDNSZoneGroup(ctx context.Context, resourceGroupName, privateEndpointName, privateDNSZoneGroupName string, parameters network.PrivateDNSZoneGroup, etag string, waitForCompletion bool) *retry.Error {
+	resourceID := armclient.GetChildResourceID(
+		c.subscriptionID,
+		resourceGroupName,
+		peResourceType,
+		privateEndpointName,
+		privateDNSZoneGroupResourceType,
+		privateDNSZoneGroupName)
+	decorators := []autorest.PrepareDecorator{
+		autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}),
+		autorest.WithJSON(parameters),
+	}
+	if etag != "" {
+		decorators = append(decorators, autorest.WithHeader("If-Match", autorest.String(etag)))
 	}
+
+	var response *http.Response
+	var rerr *retry.Error
 	if waitForCompletion {
-		err = createOrUpdateFuture.WaitForCompletionRef(ctx, c.privateDNSZoneGroupClient.Client)
-		if err != nil {
-			klog.V(5).Infof("Received error while waiting for completion for %s, resourceGroup: %s, privateEndpointName: %s, error: %s", "privatednszonegroup.put.request", resourceGroupName, privateEndpointName, err)
-			return err
+		response, rerr = c.armClient.PutResourceWithDecorators(ctx, resourceID, parameters, decorators)
+	} else {
+		response, rerr = c.armClient.PutResourceWithDecoratorsAsync(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", "privatednszonegroup.put.request", resourceID, rerr.Error())
+		return rerr
+	}
+	if !waitForCompletion {
+		return nil
+	}
+
+	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", "privatednszonegroup.put.respond", resourceID, rerr.Error())
+			return rerr
 		}
 	}
+
 	return nil
 }
 
-// Get gets the private dns zone group
-func (c *Client) Get(ctx context.Context, resourceGroupName string, privateEndpointName string, privateDNSZoneGroupName string) (result network.PrivateDNSZoneGroup, err error) {
-	return c.privateDNSZoneGroupClient.Get(ctx, resourceGroupName, privateEndpointName, privateDNSZoneGroupName)
+func (c *Client) createOrUpdateResponder(resp *http.Response) (*network.PrivateDNSZoneGroup, *retry.Error) {
+	result := &network.PrivateDNSZoneGroup{}
+	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)
+}
+
+// Get gets a private dns zone group.
+func (c *Client) Get(ctx context.Context, resourceGroupName, privateEndpointName, privateDNSZoneGroupName string) (network.PrivateDNSZoneGroup, *retry.Error) {
+	mc := metrics.NewMetricContext("private_dns_zone_group", "get", resourceGroupName, c.subscriptionID, "")
+
+	// Report errors if the client is rate limited.
+	if !c.rateLimiterReader.TryAccept() {
+		mc.RateLimitedCount()
+		return network.PrivateDNSZoneGroup{}, retry.GetRateLimitError(false, "PrivateDNSZoneGroupGet")
+	}
+
+	// Report errors if the client is throttled.
+	if c.RetryAfterReader.After(time.Now()) {
+		mc.ThrottledCount()
+		rerr := retry.GetThrottlingError("PrivateDNSZoneGroupGet", "client throttled", c.RetryAfterReader)
+		return network.PrivateDNSZoneGroup{}, rerr
+	}
+
+	result, rerr := c.getPrivateDNSZoneGroup(ctx, resourceGroupName, privateEndpointName, privateDNSZoneGroupName)
+	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
+}
+
+// getPrivateDNSZoneGroup gets a private DNS zone group.
+func (c *Client) getPrivateDNSZoneGroup(ctx context.Context, resourceGroupName, privateEndpointName, privateDNSZoneGroupName string) (network.PrivateDNSZoneGroup, *retry.Error) {
+	resourceID := armclient.GetChildResourceID(
+		c.subscriptionID,
+		resourceGroupName,
+		peResourceType,
+		privateEndpointName,
+		privateDNSZoneGroupResourceType,
+		privateDNSZoneGroupName,
+	)
+	result := network.PrivateDNSZoneGroup{}
+
+	response, rerr := c.armClient.GetResource(ctx, resourceID)
+	defer c.armClient.CloseResponse(ctx, response)
+	if rerr != nil {
+		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "privatednszonegroup.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", "privatednszonegroup.get.respond", resourceID, err)
+		return result, retry.GetError(response, err)
+	}
+
+	result.Response = autorest.Response{Response: response}
+	return result, nil
 }
diff --git a/pkg/azureclients/privatednszonegroupclient/interface.go b/pkg/azureclients/privatednszonegroupclient/interface.go
index fae7df643..72c531b18 100644
--- a/pkg/azureclients/privatednszonegroupclient/interface.go
+++ b/pkg/azureclients/privatednszonegroupclient/interface.go
@@ -20,15 +20,23 @@ import (
 	"context"
 
 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-02-01/network"
+
+	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
+)
+
+const (
+	// APIVersion is the API version.
+	APIVersion = "2021-02-01"
+	// AzureStackCloudName is the cloud name of Azure Stack
+	AzureStackCloudName = "AZURESTACKCLOUD"
 )
 
 // Interface is the client interface for Private DNS Zone Group.
 // Don't forget to run "hack/update-mock-clients.sh" command to generate the mock client.
 type Interface interface {
-
 	// Get gets the private dns zone group
-	Get(ctx context.Context, resourceGroupName string, privateEndpointName string, privateDNSZoneGroupName string) (result network.PrivateDNSZoneGroup, err error)
+	Get(ctx context.Context, resourceGroupName, privateEndpointName, privateDNSZoneGroupName string) (network.PrivateDNSZoneGroup, *retry.Error)
 
-	// CreateOrUpdate creates or updates a private dns zone group endpoint.
-	CreateOrUpdate(ctx context.Context, resourceGroupName string, privateEndpointName string, privateDNSZoneGroupName string, parameters network.PrivateDNSZoneGroup, waitForCompletion bool) error
+	// CreateOrUpdate creates or updates a private dns zone group.
+	CreateOrUpdate(ctx context.Context, resourceGroupName, privateEndpointName, privateDNSZoneGroupName string, parameters network.PrivateDNSZoneGroup, etag string, waitForCompletion bool) *retry.Error
 }
diff --git a/pkg/azureclients/privatednszonegroupclient/mockprivatednszonegroupclient/interface.go b/pkg/azureclients/privatednszonegroupclient/mockprivatednszonegroupclient/interface.go
index f752205a2..bd6fbc10d 100644
--- a/pkg/azureclients/privatednszonegroupclient/mockprivatednszonegroupclient/interface.go
+++ b/pkg/azureclients/privatednszonegroupclient/mockprivatednszonegroupclient/interface.go
@@ -27,6 +27,7 @@ import (
 
 	network "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-02-01/network"
 	gomock "github.com/golang/mock/gomock"
+	retry "sigs.k8s.io/cloud-provider-azure/pkg/retry"
 )
 
 // MockInterface is a mock of Interface interface.
@@ -53,25 +54,25 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
 }
 
 // CreateOrUpdate mocks base method.
-func (m *MockInterface) CreateOrUpdate(ctx context.Context, resourceGroupName, privateEndpointName, privateDNSZoneGroupName string, parameters network.PrivateDNSZoneGroup, waitForCompletion bool) error {
+func (m *MockInterface) CreateOrUpdate(ctx context.Context, resourceGroupName, privateEndpointName, privateDNSZoneGroupName string, parameters network.PrivateDNSZoneGroup, etag string, waitForCompletion bool) *retry.Error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "CreateOrUpdate", ctx, resourceGroupName, privateEndpointName, privateDNSZoneGroupName, parameters, waitForCompletion)
-	ret0, _ := ret[0].(error)
+	ret := m.ctrl.Call(m, "CreateOrUpdate", ctx, resourceGroupName, privateEndpointName, privateDNSZoneGroupName, parameters, etag, waitForCompletion)
+	ret0, _ := ret[0].(*retry.Error)
 	return ret0
 }
 
 // CreateOrUpdate indicates an expected call of CreateOrUpdate.
-func (mr *MockInterfaceMockRecorder) CreateOrUpdate(ctx, resourceGroupName, privateEndpointName, privateDNSZoneGroupName, parameters, waitForCompletion interface{}) *gomock.Call {
+func (mr *MockInterfaceMockRecorder) CreateOrUpdate(ctx, resourceGroupName, privateEndpointName, privateDNSZoneGroupName, parameters, etag, waitForCompletion interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdate", reflect.TypeOf((*MockInterface)(nil).CreateOrUpdate), ctx, resourceGroupName, privateEndpointName, privateDNSZoneGroupName, parameters, waitForCompletion)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdate", reflect.TypeOf((*MockInterface)(nil).CreateOrUpdate), ctx, resourceGroupName, privateEndpointName, privateDNSZoneGroupName, parameters, etag, waitForCompletion)
 }
 
 // Get mocks base method.
-func (m *MockInterface) Get(ctx context.Context, resourceGroupName, privateEndpointName, privateDNSZoneGroupName string) (network.PrivateDNSZoneGroup, error) {
+func (m *MockInterface) Get(ctx context.Context, resourceGroupName, privateEndpointName, privateDNSZoneGroupName string) (network.PrivateDNSZoneGroup, *retry.Error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, privateEndpointName, privateDNSZoneGroupName)
 	ret0, _ := ret[0].(network.PrivateDNSZoneGroup)
-	ret1, _ := ret[1].(error)
+	ret1, _ := ret[1].(*retry.Error)
 	return ret0, ret1
 }
 
diff --git a/pkg/azureclients/privatelinkserviceclient/azure_privatelinkserviceclient.go b/pkg/azureclients/privatelinkserviceclient/azure_privatelinkserviceclient.go
index 5b6bee721..5f92c8a20 100644
--- a/pkg/azureclients/privatelinkserviceclient/azure_privatelinkserviceclient.go
+++ b/pkg/azureclients/privatelinkserviceclient/azure_privatelinkserviceclient.go
@@ -19,7 +19,6 @@ package privatelinkserviceclient
 
 import (
 	"context"
-	"fmt"
 	"net/http"
 	"strings"
 	"time"
@@ -251,10 +250,7 @@ func (c *Client) List(ctx context.Context, resourceGroupName string) ([]network.
 
 // listPLS gets a list of PrivateLinkServices in the resource group.
 func (c *Client) listPLS(ctx context.Context, resourceGroupName string) ([]network.PrivateLinkService, *retry.Error) {
-	resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/%s",
-		autorest.Encode("path", c.subscriptionID),
-		autorest.Encode("path", resourceGroupName),
-		PLSResourceType)
+	resourceID := armclient.GetResourceListID(c.subscriptionID, resourceGroupName, PLSResourceType)
 	result := make([]network.PrivateLinkService, 0)
 	page := &PrivateLinkServiceListResultPage{}
 	page.fn = c.listNextResults
diff --git a/pkg/azureclients/publicipclient/azure_publicipclient.go b/pkg/azureclients/publicipclient/azure_publicipclient.go
index a440af3eb..7f12b717b 100644
--- a/pkg/azureclients/publicipclient/azure_publicipclient.go
+++ b/pkg/azureclients/publicipclient/azure_publicipclient.go
@@ -39,6 +39,8 @@ import (
 
 var _ Interface = &Client{}
 
+const publicIPResourceType = "Microsoft.Network/publicIPAddresses"
+
 // Client implements PublicIPAddress client Interface.
 type Client struct {
 	armClient              armclient.Interface
@@ -123,7 +125,7 @@ func (c *Client) getPublicIPAddress(ctx context.Context, resourceGroupName strin
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/publicIPAddresses",
+		publicIPResourceType,
 		publicIPAddressName,
 	)
 	result := network.PublicIPAddress{}
@@ -258,9 +260,7 @@ func (c *Client) List(ctx context.Context, resourceGroupName string) ([]network.
 
 // listPublicIPAddress gets a list of PublicIPAddress in the resource group.
 func (c *Client) listPublicIPAddress(ctx context.Context, resourceGroupName string) ([]network.PublicIPAddress, *retry.Error) {
-	resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/publicIPAddresses",
-		autorest.Encode("path", c.subscriptionID),
-		autorest.Encode("path", resourceGroupName))
+	resourceID := armclient.GetResourceListID(c.subscriptionID, resourceGroupName, publicIPResourceType)
 	result := make([]network.PublicIPAddress, 0)
 	page := &PublicIPAddressListResultPage{}
 	page.fn = c.listNextResults
@@ -332,7 +332,7 @@ func (c *Client) createOrUpdatePublicIP(ctx context.Context, resourceGroupName s
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/publicIPAddresses",
+		publicIPResourceType,
 		publicIPAddressName,
 	)
 
@@ -400,7 +400,7 @@ func (c *Client) deletePublicIP(ctx context.Context, resourceGroupName string, p
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/publicIPAddresses",
+		publicIPResourceType,
 		publicIPAddressName,
 	)
 
@@ -525,8 +525,7 @@ func (c *Client) ListAll(ctx context.Context) ([]network.PublicIPAddress, *retry
 
 // listAllPublicIPAddress gets all of PublicIPAddress in the subscription.
 func (c *Client) listAllPublicIPAddress(ctx context.Context) ([]network.PublicIPAddress, *retry.Error) {
-	resourceID := fmt.Sprintf("/subscriptions/%s/providers/Microsoft.Network/publicIPAddresses",
-		autorest.Encode("path", c.subscriptionID))
+	resourceID := armclient.GetProviderResourceID(c.subscriptionID, publicIPResourceType)
 	result := make([]network.PublicIPAddress, 0)
 	page := &PublicIPAddressListResultPage{}
 	page.fn = c.listNextResults
diff --git a/pkg/azureclients/routeclient/azure_routeclient.go b/pkg/azureclients/routeclient/azure_routeclient.go
index e70801760..2c4f65243 100644
--- a/pkg/azureclients/routeclient/azure_routeclient.go
+++ b/pkg/azureclients/routeclient/azure_routeclient.go
@@ -37,6 +37,8 @@ import (
 
 var _ Interface = &Client{}
 
+const routeTablesResourceType = "Microsoft.Network/routeTables"
+
 // Client implements Route client Interface.
 type Client struct {
 	armClient      armclient.Interface
@@ -119,7 +121,7 @@ func (c *Client) createOrUpdateRoute(ctx context.Context, resourceGroupName stri
 	resourceID := armclient.GetChildResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/routeTables",
+		routeTablesResourceType,
 		routeTableName,
 		"routes",
 		routeName,
@@ -196,7 +198,7 @@ func (c *Client) deleteRoute(ctx context.Context, resourceGroupName string, rout
 	resourceID := armclient.GetChildResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/routeTables",
+		routeTablesResourceType,
 		routeTableName,
 		"routes",
 		routeName,
diff --git a/pkg/azureclients/routetableclient/azure_routetableclient.go b/pkg/azureclients/routetableclient/azure_routetableclient.go
index 2ba1fbb99..b66ac9ff9 100644
--- a/pkg/azureclients/routetableclient/azure_routetableclient.go
+++ b/pkg/azureclients/routetableclient/azure_routetableclient.go
@@ -37,6 +37,8 @@ import (
 
 var _ Interface = &Client{}
 
+const routeTablesResourceType = "Microsoft.Network/routeTables"
+
 // Client implements RouteTable client Interface.
 type Client struct {
 	armClient      armclient.Interface
@@ -119,7 +121,7 @@ func (c *Client) getRouteTable(ctx context.Context, resourceGroupName string, ro
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/routeTables",
+		routeTablesResourceType,
 		routeTableName,
 	)
 	result := network.RouteTable{}
@@ -180,7 +182,7 @@ func (c *Client) createOrUpdateRouteTable(ctx context.Context, resourceGroupName
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/routeTables",
+		routeTablesResourceType,
 		routeTableName,
 	)
 	decorators := []autorest.PrepareDecorator{
diff --git a/pkg/azureclients/securitygroupclient/azure_securitygroupclient.go b/pkg/azureclients/securitygroupclient/azure_securitygroupclient.go
index f625e7e4d..9180a2b40 100644
--- a/pkg/azureclients/securitygroupclient/azure_securitygroupclient.go
+++ b/pkg/azureclients/securitygroupclient/azure_securitygroupclient.go
@@ -18,7 +18,6 @@ package securitygroupclient
 
 import (
 	"context"
-	"fmt"
 	"net/http"
 	"strings"
 	"time"
@@ -39,6 +38,8 @@ import (
 
 var _ Interface = &Client{}
 
+const nsgResourceType = "Microsoft.Network/networkSecurityGroups"
+
 // Client implements SecurityGroup client Interface.
 type Client struct {
 	armClient      armclient.Interface
@@ -121,7 +122,7 @@ func (c *Client) getSecurityGroup(ctx context.Context, resourceGroupName string,
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/networkSecurityGroups",
+		nsgResourceType,
 		networkSecurityGroupName,
 	)
 	result := network.SecurityGroup{}
@@ -179,9 +180,7 @@ func (c *Client) List(ctx context.Context, resourceGroupName string) ([]network.
 
 // listSecurityGroup gets a list of SecurityGroups in the resource group.
 func (c *Client) listSecurityGroup(ctx context.Context, resourceGroupName string) ([]network.SecurityGroup, *retry.Error) {
-	resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/networkSecurityGroups",
-		autorest.Encode("path", c.subscriptionID),
-		autorest.Encode("path", resourceGroupName))
+	resourceID := armclient.GetResourceListID(c.subscriptionID, resourceGroupName, nsgResourceType)
 	result := make([]network.SecurityGroup, 0)
 	page := &SecurityGroupListResultPage{}
 	page.fn = c.listNextResults
@@ -253,7 +252,7 @@ func (c *Client) createOrUpdateNSG(ctx context.Context, resourceGroupName string
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/networkSecurityGroups",
+		nsgResourceType,
 		networkSecurityGroupName,
 	)
 	decorators := []autorest.PrepareDecorator{
@@ -328,7 +327,7 @@ func (c *Client) deleteNSG(ctx context.Context, resourceGroupName string, networ
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/networkSecurityGroups",
+		nsgResourceType,
 		networkSecurityGroupName,
 	)
 
diff --git a/pkg/azureclients/snapshotclient/azure_snapshotclient.go b/pkg/azureclients/snapshotclient/azure_snapshotclient.go
index 256be5518..a6bb82253 100644
--- a/pkg/azureclients/snapshotclient/azure_snapshotclient.go
+++ b/pkg/azureclients/snapshotclient/azure_snapshotclient.go
@@ -18,7 +18,6 @@ package snapshotclient
 
 import (
 	"context"
-	"fmt"
 	"net/http"
 	"strings"
 	"time"
@@ -39,6 +38,8 @@ import (
 
 var _ Interface = &Client{}
 
+const snapshotsResourceType = "Microsoft.Compute/snapshots"
+
 // Client implements Snapshot client Interface.
 type Client struct {
 	armClient      armclient.Interface
@@ -121,7 +122,7 @@ func (c *Client) getSnapshot(ctx context.Context, resourceGroupName string, snap
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/snapshots",
+		snapshotsResourceType,
 		snapshotName,
 	)
 	result := compute.Snapshot{}
@@ -182,7 +183,7 @@ func (c *Client) deleteSnapshot(ctx context.Context, resourceGroupName string, s
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/snapshots",
+		snapshotsResourceType,
 		snapshotName,
 	)
 
@@ -225,7 +226,7 @@ func (c *Client) createOrUpdateSnapshot(ctx context.Context, resourceGroupName s
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/snapshots",
+		snapshotsResourceType,
 		snapshotName,
 	)
 
@@ -290,9 +291,7 @@ func (c *Client) ListByResourceGroup(ctx context.Context, resourceGroupName stri
 
 // listSnapshotsByResourceGroup gets a list of snapshots in the resource group.
 func (c *Client) listSnapshotsByResourceGroup(ctx context.Context, resourceGroupName string) ([]compute.Snapshot, *retry.Error) {
-	resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/snapshots",
-		autorest.Encode("path", c.subscriptionID),
-		autorest.Encode("path", resourceGroupName))
+	resourceID := armclient.GetResourceListID(c.subscriptionID, resourceGroupName, snapshotsResourceType)
 	result := make([]compute.Snapshot, 0)
 	page := &SnapshotListPage{}
 	page.fn = c.listNextResults
diff --git a/pkg/azureclients/storageaccountclient/azure_storageaccountclient.go b/pkg/azureclients/storageaccountclient/azure_storageaccountclient.go
index 34bc3c1d7..2a6e4cebe 100644
--- a/pkg/azureclients/storageaccountclient/azure_storageaccountclient.go
+++ b/pkg/azureclients/storageaccountclient/azure_storageaccountclient.go
@@ -18,7 +18,6 @@ package storageaccountclient
 
 import (
 	"context"
-	"fmt"
 	"net/http"
 	"strings"
 	"time"
@@ -39,6 +38,8 @@ import (
 
 var _ Interface = &Client{}
 
+const saResourceType = "Microsoft.Storage/storageAccounts"
+
 // Client implements StorageAccount client Interface.
 type Client struct {
 	armClient      armclient.Interface
@@ -121,7 +122,7 @@ func (c *Client) getStorageAccount(ctx context.Context, resourceGroupName string
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Storage/storageAccounts",
+		saResourceType,
 		accountName,
 	)
 	result := storage.Account{}
@@ -182,7 +183,7 @@ func (c *Client) listStorageAccountKeys(ctx context.Context, resourceGroupName s
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Storage/storageAccounts",
+		saResourceType,
 		accountName,
 	)
 
@@ -243,7 +244,7 @@ func (c *Client) createStorageAccount(ctx context.Context, resourceGroupName str
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Storage/storageAccounts",
+		saResourceType,
 		accountName,
 	)
 
@@ -309,7 +310,7 @@ func (c *Client) updateStorageAccount(ctx context.Context, resourceGroupName str
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Storage/storageAccounts",
+		saResourceType,
 		accountName,
 	)
 
@@ -377,7 +378,7 @@ func (c *Client) deleteStorageAccount(ctx context.Context, resourceGroupName str
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Storage/storageAccounts",
+		saResourceType,
 		accountName,
 	)
 
@@ -417,9 +418,7 @@ func (c *Client) ListByResourceGroup(ctx context.Context, resourceGroupName stri
 
 // ListStorageAccountByResourceGroup get a list storage accounts by resourceGroup.
 func (c *Client) ListStorageAccountByResourceGroup(ctx context.Context, resourceGroupName string) ([]storage.Account, *retry.Error) {
-	resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Storage/storageAccounts",
-		autorest.Encode("path", c.subscriptionID),
-		autorest.Encode("path", resourceGroupName))
+	resourceID := armclient.GetResourceListID(c.subscriptionID, resourceGroupName, saResourceType)
 	result := make([]storage.Account, 0)
 	page := &AccountListResultPage{}
 	page.fn = c.listNextResults
diff --git a/pkg/azureclients/subnetclient/azure_subnetclient.go b/pkg/azureclients/subnetclient/azure_subnetclient.go
index 05ebfa0b4..e5d63401d 100644
--- a/pkg/azureclients/subnetclient/azure_subnetclient.go
+++ b/pkg/azureclients/subnetclient/azure_subnetclient.go
@@ -38,6 +38,8 @@ import (
 
 var _ Interface = &Client{}
 
+const vnetResourceType = "Microsoft.Network/virtualNetworks"
+
 // Client implements Subnet client Interface.
 type Client struct {
 	armClient      armclient.Interface
@@ -120,7 +122,7 @@ func (c *Client) getSubnet(ctx context.Context, resourceGroupName string, virtua
 	resourceID := armclient.GetChildResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/virtualNetworks",
+		vnetResourceType,
 		virtualNetworkName,
 		"subnets",
 		subnetName,
@@ -183,7 +185,7 @@ func (c *Client) listSubnet(ctx context.Context, resourceGroupName string, virtu
 	resourceID := armclient.GetChildResourcesListID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/virtualNetworks",
+		vnetResourceType,
 		virtualNetworkName,
 		"subnets")
 
@@ -258,7 +260,7 @@ func (c *Client) createOrUpdateSubnet(ctx context.Context, resourceGroupName str
 	resourceID := armclient.GetChildResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/virtualNetworks",
+		vnetResourceType,
 		virtualNetworkName,
 		"subnets",
 		subnetName)
@@ -327,7 +329,7 @@ func (c *Client) deleteSubnet(ctx context.Context, resourceGroupName string, vir
 	resourceID := armclient.GetChildResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Network/virtualNetworks",
+		vnetResourceType,
 		virtualNetworkName,
 		"subnets",
 		subnetName)
diff --git a/pkg/azureclients/vmasclient/azure_vmasclient.go b/pkg/azureclients/vmasclient/azure_vmasclient.go
index 4a9751b3f..59372fb25 100644
--- a/pkg/azureclients/vmasclient/azure_vmasclient.go
+++ b/pkg/azureclients/vmasclient/azure_vmasclient.go
@@ -18,7 +18,6 @@ package vmasclient
 
 import (
 	"context"
-	"fmt"
 	"net/http"
 	"strings"
 	"time"
@@ -37,7 +36,9 @@ import (
 	"sigs.k8s.io/cloud-provider-azure/pkg/retry"
 )
 
-//var _ Interface = &Client{}
+var _ Interface = &Client{}
+
+const vmasResourceType = "Microsoft.Compute/availabilitySets"
 
 // Client implements VMAS client Interface.
 type Client struct {
@@ -121,7 +122,7 @@ func (c *Client) getVMAS(ctx context.Context, resourceGroupName string, vmasName
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/availabilitySets",
+		vmasResourceType,
 		vmasName,
 	)
 	result := compute.AvailabilitySet{}
@@ -179,9 +180,7 @@ func (c *Client) List(ctx context.Context, resourceGroupName string) ([]compute.
 
 // listVMAS gets a list of AvailabilitySets in the resource group.
 func (c *Client) listVMAS(ctx context.Context, resourceGroupName string) ([]compute.AvailabilitySet, *retry.Error) {
-	resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/availabilitySets",
-		autorest.Encode("path", c.subscriptionID),
-		autorest.Encode("path", resourceGroupName))
+	resourceID := armclient.GetResourceListID(c.subscriptionID, resourceGroupName, vmasResourceType)
 	result := make([]compute.AvailabilitySet, 0)
 	page := &AvailabilitySetListResultPage{}
 	page.fn = c.listNextResults
diff --git a/pkg/azureclients/vmclient/azure_vmclient.go b/pkg/azureclients/vmclient/azure_vmclient.go
index f46fff651..1856d1c60 100644
--- a/pkg/azureclients/vmclient/azure_vmclient.go
+++ b/pkg/azureclients/vmclient/azure_vmclient.go
@@ -18,7 +18,6 @@ package vmclient
 
 import (
 	"context"
-	"fmt"
 	"net/http"
 	"strings"
 	"time"
@@ -39,6 +38,8 @@ import (
 
 var _ Interface = &Client{}
 
+const vmResourceType = "Microsoft.Compute/virtualMachines"
+
 // Client implements VirtualMachine client Interface.
 type Client struct {
 	armClient      armclient.Interface
@@ -121,7 +122,7 @@ func (c *Client) getVM(ctx context.Context, resourceGroupName string, VMName str
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/virtualMachines",
+		vmResourceType,
 		VMName,
 	)
 	result := compute.VirtualMachine{}
@@ -179,10 +180,7 @@ func (c *Client) List(ctx context.Context, resourceGroupName string) ([]compute.
 
 // listVM gets a list of VirtualMachines in the resourceGroupName.
 func (c *Client) listVM(ctx context.Context, resourceGroupName string) ([]compute.VirtualMachine, *retry.Error) {
-	resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/virtualMachines",
-		autorest.Encode("path", c.subscriptionID),
-		autorest.Encode("path", resourceGroupName),
-	)
+	resourceID := armclient.GetResourceListID(c.subscriptionID, resourceGroupName, vmResourceType)
 
 	result := make([]compute.VirtualMachine, 0)
 	page := &VirtualMachineListResultPage{}
@@ -270,7 +268,7 @@ func (c *Client) UpdateAsync(ctx context.Context, resourceGroupName string, VMNa
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/virtualMachines",
+		vmResourceType,
 		VMName,
 	)
 
@@ -310,7 +308,7 @@ func (c *Client) updateVM(ctx context.Context, resourceGroupName string, VMName
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/virtualMachines",
+		vmResourceType,
 		VMName,
 	)
 
@@ -471,7 +469,7 @@ func (c *Client) createOrUpdateVM(ctx context.Context, resourceGroupName string,
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/virtualMachines",
+		vmResourceType,
 		VMName,
 	)
 
@@ -540,7 +538,7 @@ func (c *Client) deleteVM(ctx context.Context, resourceGroupName string, VMName
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/virtualMachines",
+		vmResourceType,
 		VMName,
 	)
 
diff --git a/pkg/azureclients/vmssclient/azure_vmssclient.go b/pkg/azureclients/vmssclient/azure_vmssclient.go
index be47b251a..1bb17abaf 100644
--- a/pkg/azureclients/vmssclient/azure_vmssclient.go
+++ b/pkg/azureclients/vmssclient/azure_vmssclient.go
@@ -18,7 +18,6 @@ package vmssclient
 
 import (
 	"context"
-	"fmt"
 	"net/http"
 	"strings"
 	"time"
@@ -39,6 +38,8 @@ import (
 
 var _ Interface = &Client{}
 
+const vmssResourceType = "Microsoft.Compute/virtualMachineScaleSets"
+
 // Client implements VMSS client Interface.
 type Client struct {
 	armClient      armclient.Interface
@@ -121,7 +122,7 @@ func (c *Client) getVMSS(ctx context.Context, resourceGroupName string, VMScaleS
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/virtualMachineScaleSets",
+		vmssResourceType,
 		VMScaleSetName,
 	)
 	result := compute.VirtualMachineScaleSet{}
@@ -179,9 +180,11 @@ func (c *Client) List(ctx context.Context, resourceGroupName string) ([]compute.
 
 // listVMSS gets a list of VirtualMachineScaleSets in the resource group.
 func (c *Client) listVMSS(ctx context.Context, resourceGroupName string) ([]compute.VirtualMachineScaleSet, *retry.Error) {
-	resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/virtualMachineScaleSets",
-		autorest.Encode("path", c.subscriptionID),
-		autorest.Encode("path", resourceGroupName))
+	resourceID := armclient.GetResourceListID(
+		c.subscriptionID,
+		resourceGroupName,
+		vmssResourceType,
+	)
 	result := make([]compute.VirtualMachineScaleSet, 0)
 	page := &VirtualMachineScaleSetListResultPage{}
 	page.fn = c.listNextResults
@@ -268,7 +271,7 @@ func (c *Client) CreateOrUpdateAsync(ctx context.Context, resourceGroupName stri
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/virtualMachineScaleSets",
+		vmssResourceType,
 		VMScaleSetName,
 	)
 
@@ -319,7 +322,7 @@ func (c *Client) createOrUpdateVMSS(ctx context.Context, resourceGroupName strin
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/virtualMachineScaleSets",
+		vmssResourceType,
 		VMScaleSetName,
 	)
 	response, rerr := c.armClient.PutResource(ctx, resourceID, parameters)
@@ -491,7 +494,7 @@ func (c *Client) DeleteInstancesAsync(ctx context.Context, resourceGroupName str
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/virtualMachineScaleSets",
+		vmssResourceType,
 		vmScaleSetName,
 	)
 
@@ -540,7 +543,7 @@ func (c *Client) DeallocateInstancesAsync(ctx context.Context, resourceGroupName
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/virtualMachineScaleSets",
+		vmssResourceType,
 		vmScaleSetName,
 	)
 
@@ -589,7 +592,7 @@ func (c *Client) StartInstancesAsync(ctx context.Context, resourceGroupName stri
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/virtualMachineScaleSets",
+		vmssResourceType,
 		vmScaleSetName,
 	)
 
@@ -623,7 +626,7 @@ func (c *Client) deleteVMSSInstances(ctx context.Context, resourceGroupName stri
 	resourceID := armclient.GetResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/virtualMachineScaleSets",
+		vmssResourceType,
 		vmScaleSetName,
 	)
 	response, rerr := c.armClient.PostResource(ctx, resourceID, "delete", vmInstanceIDs, map[string]interface{}{})
diff --git a/pkg/azureclients/vmssvmclient/azure_vmssvmclient.go b/pkg/azureclients/vmssvmclient/azure_vmssvmclient.go
index a65615eb3..04f47ac68 100644
--- a/pkg/azureclients/vmssvmclient/azure_vmssvmclient.go
+++ b/pkg/azureclients/vmssvmclient/azure_vmssvmclient.go
@@ -18,7 +18,6 @@ package vmssvmclient
 
 import (
 	"context"
-	"fmt"
 	"net/http"
 	"strings"
 	"time"
@@ -40,6 +39,11 @@ import (
 
 var _ Interface = &Client{}
 
+const (
+	vmssResourceType = "Microsoft.Compute/virtualMachineScaleSets"
+	vmResourceType   = "virtualMachines"
+)
+
 // Client implements VMSS client Interface.
 type Client struct {
 	armClient      armclient.Interface
@@ -122,9 +126,9 @@ func (c *Client) getVMSSVM(ctx context.Context, resourceGroupName string, VMScal
 	resourceID := armclient.GetChildResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/virtualMachineScaleSets",
+		vmssResourceType,
 		VMScaleSetName,
-		"virtualMachines",
+		vmResourceType,
 		instanceID,
 	)
 	result := compute.VirtualMachineScaleSetVM{}
@@ -182,10 +186,12 @@ func (c *Client) List(ctx context.Context, resourceGroupName string, virtualMach
 
 // listVMSSVM gets a list of VirtualMachineScaleSetVMs in the virtualMachineScaleSet.
 func (c *Client) listVMSSVM(ctx context.Context, resourceGroupName string, virtualMachineScaleSetName string, expand string) ([]compute.VirtualMachineScaleSetVM, *retry.Error) {
-	resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/virtualMachineScaleSets/%s/virtualMachines",
-		autorest.Encode("path", c.subscriptionID),
-		autorest.Encode("path", resourceGroupName),
-		autorest.Encode("path", virtualMachineScaleSetName),
+	resourceID := armclient.GetChildResourcesListID(
+		c.subscriptionID,
+		resourceGroupName,
+		vmssResourceType,
+		virtualMachineScaleSetName,
+		vmResourceType,
 	)
 
 	result := make([]compute.VirtualMachineScaleSetVM, 0)
@@ -274,9 +280,9 @@ func (c *Client) UpdateAsync(ctx context.Context, resourceGroupName string, VMSc
 	resourceID := armclient.GetChildResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/virtualMachineScaleSets",
+		vmssResourceType,
 		VMScaleSetName,
-		"virtualMachines",
+		vmResourceType,
 		instanceID,
 	)
 
@@ -315,9 +321,9 @@ func (c *Client) updateVMSSVM(ctx context.Context, resourceGroupName string, VMS
 	resourceID := armclient.GetChildResourceID(
 		c.subscriptionID,
 		resourceGroupName,
-		"Microsoft.Compute/virtualMachineScaleSets",
+		vmssResourceType,
 		VMScaleSetName,
-		"virtualMachines",
+		vmResourceType,
 		instanceID,
 	)
 
@@ -479,9 +485,9 @@ func (c *Client) updateVMSSVMs(ctx context.Context, resourceGroupName string, VM
 		resourceID := armclient.GetChildResourceID(
 			c.subscriptionID,
 			resourceGroupName,
-			"Microsoft.Compute/virtualMachineScaleSets",
+			vmssResourceType,
 			VMScaleSetName,
-			"virtualMachines",
+			vmResourceType,
 			instanceID,
 		)
 		resources[resourceID] = parameter
diff --git a/pkg/azureclients/zoneclient/azure_zoneclient.go b/pkg/azureclients/zoneclient/azure_zoneclient.go
index ac197a641..9a226d0b0 100644
--- a/pkg/azureclients/zoneclient/azure_zoneclient.go
+++ b/pkg/azureclients/zoneclient/azure_zoneclient.go
@@ -18,7 +18,6 @@ package zoneclient
 
 import (
 	"context"
-	"fmt"
 	"net/http"
 	"strings"
 
@@ -34,6 +33,8 @@ import (
 
 var _ Interface = &Client{}
 
+const computeResourceProvider = "Microsoft.Compute"
+
 type resourceTypeMetadata struct {
 	ResourceType string         `json:"resourceType"`
 	ZoneMappings []zoneMappings `json:"zoneMappings"`
@@ -112,7 +113,10 @@ func (c *Client) getZones(ctx context.Context, subscriptionID string) (map[strin
 	}
 
 	regionZoneMap := make(map[string][]string)
-	expectedID := fmt.Sprintf("/subscriptions/%s/providers/Microsoft.Compute", subscriptionID)
+	expectedID := armclient.GetProviderResourceID(
+		subscriptionID,
+		computeResourceProvider,
+	)
 	if len(result.ProviderListDataProperties) != 0 {
 		for _, property := range result.ProviderListDataProperties {
 			if strings.EqualFold(property.ID, expectedID) {
diff --git a/pkg/consts/helpers.go b/pkg/consts/helpers.go
index 44296f3d9..f86236218 100644
--- a/pkg/consts/helpers.go
+++ b/pkg/consts/helpers.go
@@ -23,7 +23,6 @@ import (
 	"strings"
 
 	v1 "k8s.io/api/core/v1"
-	"k8s.io/utils/net"
 )
 
 // IsK8sServiceHasHAModeEnabled return if HA Mode is enabled in kuberntes service annotations
@@ -36,10 +35,6 @@ func IsK8sServiceUsingInternalLoadBalancer(service *v1.Service) bool {
 	return expectAttributeInSvcAnnotationBeEqualTo(service.Annotations, ServiceAnnotationLoadBalancerInternal, TrueAnnotationValue)
 }
 
-func IsK8sServiceInternalIPv6(service *v1.Service) bool {
-	return IsK8sServiceUsingInternalLoadBalancer(service) && net.IsIPv6String(service.Spec.ClusterIP)
-}
-
 // GetHealthProbeConfigOfPortFromK8sSvcAnnotation get health probe configuration for port
 func GetHealthProbeConfigOfPortFromK8sSvcAnnotation(annotations map[string]string, port int32, key HealthProbeParams, validators ...BusinessValidator) (*string, error) {
 	return GetAttributeValueInSvcAnnotation(annotations, BuildHealthProbeAnnotationKeyForPort(port, key), validators...)
diff --git a/pkg/provider/azure_loadbalancer.go b/pkg/provider/azure_loadbalancer.go
index e186d8eea..e77cfef1c 100644
--- a/pkg/provider/azure_loadbalancer.go
+++ b/pkg/provider/azure_loadbalancer.go
@@ -2366,7 +2366,7 @@ func (az *Cloud) getExpectedLBRules(
 			if err != nil {
 				return expectedProbes, expectedRules, fmt.Errorf("failed to parse transport protocol: %w", err)
 			}
-			props, err := az.getExpectedLoadBalancingRulePropertiesForPort(service, lbFrontendIPConfigID, lbBackendPoolID, port, *transportProto)
+			props, err := az.getExpectedLoadBalancingRulePropertiesForPort(service, lbFrontendIPConfigID, lbBackendPoolID, to.Int32Ptr(port.Port), *transportProto)
 			if err != nil {
 				return expectedProbes, expectedRules, fmt.Errorf("error generate lb rule for ha mod loadbalancer. err: %w", err)
 			}
@@ -2404,7 +2404,7 @@ func (az *Cloud) getExpectedLBRules(
 func (az *Cloud) getExpectedLoadBalancingRulePropertiesForPort(
 	service *v1.Service,
 	lbFrontendIPConfigID string,
-	lbBackendPoolID string, servicePort v1.ServicePort, transportProto network.TransportProtocol) (*network.LoadBalancingRulePropertiesFormat, error) {
+	lbBackendPoolID string, port *int32, transportProto network.TransportProtocol) (*network.LoadBalancingRulePropertiesFormat, error) {
 	var err error
 
 	loadDistribution := network.LoadDistributionDefault
@@ -2430,8 +2430,8 @@ func (az *Cloud) getExpectedLoadBalancingRulePropertiesForPort(
 
 	props := &network.LoadBalancingRulePropertiesFormat{
 		Protocol:            transportProto,
-		FrontendPort:        to.Int32Ptr(servicePort.Port),
-		BackendPort:         to.Int32Ptr(servicePort.Port),
+		FrontendPort:        port,
+		BackendPort:         port,
 		DisableOutboundSnat: to.BoolPtr(az.disableLoadBalancerOutboundSNAT()),
 		EnableFloatingIP:    to.BoolPtr(true),
 		LoadDistribution:    loadDistribution,
@@ -2446,13 +2446,6 @@ func (az *Cloud) getExpectedLoadBalancingRulePropertiesForPort(
 	if strings.EqualFold(string(transportProto), string(network.TransportProtocolTCP)) && az.useStandardLoadBalancer() {
 		props.EnableTCPReset = to.BoolPtr(true)
 	}
-
-	// Azure ILB does not support secondary IPs as floating IPs on the LB. Therefore, floating IP needs to be turned
-	// off and the rule should point to the nodeIP:nodePort.
-	if consts.IsK8sServiceInternalIPv6(service) {
-		props.BackendPort = to.Int32Ptr(servicePort.NodePort)
-		props.EnableFloatingIP = to.BoolPtr(false)
-	}
 	return props, nil
 }
 
@@ -2461,7 +2454,7 @@ func (az *Cloud) getExpectedHAModeLoadBalancingRuleProperties(
 	service *v1.Service,
 	lbFrontendIPConfigID string,
 	lbBackendPoolID string) (*network.LoadBalancingRulePropertiesFormat, error) {
-	props, err := az.getExpectedLoadBalancingRulePropertiesForPort(service, lbFrontendIPConfigID, lbBackendPoolID, v1.ServicePort{}, network.TransportProtocolAll)
+	props, err := az.getExpectedLoadBalancingRulePropertiesForPort(service, lbFrontendIPConfigID, lbBackendPoolID, to.Int32Ptr(0), network.TransportProtocolAll)
 	if err != nil {
 		return nil, fmt.Errorf("error generate lb rule for ha mod loadbalancer. err: %w", err)
 	}
diff --git a/pkg/provider/azure_routes.go b/pkg/provider/azure_routes.go
index 53a9f655c..a669e1be8 100644
--- a/pkg/provider/azure_routes.go
+++ b/pkg/provider/azure_routes.go
@@ -383,6 +383,10 @@ func (az *Cloud) CreateRoute(ctx context.Context, clusterName string, nameHint s
 		return err
 	}
 	if unmanaged {
+		if az.ipv6DualStackEnabled {
+			//TODO (khenidak) add support for unmanaged nodes when the feature reaches beta
+			return fmt.Errorf("unmanaged nodes are not supported in dual stack mode")
+		}
 		klog.V(2).Infof("CreateRoute: omitting unmanaged node %q", kubeRoute.TargetNode)
 		az.routeCIDRsLock.Lock()
 		defer az.routeCIDRsLock.Unlock()
diff --git a/pkg/provider/azure_storageaccount.go b/pkg/provider/azure_storageaccount.go
index d1c0fae57..8357b45ed 100644
--- a/pkg/provider/azure_storageaccount.go
+++ b/pkg/provider/azure_storageaccount.go
@@ -315,12 +315,12 @@ func (az *Cloud) createPrivateDNSZone(ctx context.Context, vnetResourceGroup str
 	klog.V(2).Infof("Creating private dns zone(%s) in resourceGroup (%s)", PrivateDNSZoneName, vnetResourceGroup)
 	location := LocationGlobal
 	privateDNSZone := privatedns.PrivateZone{Location: &location}
-	if err := az.privatednsclient.CreateOrUpdate(ctx, vnetResourceGroup, PrivateDNSZoneName, privateDNSZone, true); err != nil {
-		if strings.Contains(err.Error(), "exists already") {
+	if err := az.privatednsclient.CreateOrUpdate(ctx, vnetResourceGroup, PrivateDNSZoneName, privateDNSZone, "", true); err != nil {
+		if strings.Contains(err.Error().Error(), "exists already") {
 			klog.V(2).Infof("private dns zone(%s) in resourceGroup (%s) already exists", PrivateDNSZoneName, vnetResourceGroup)
 			return nil
 		}
-		return err
+		return err.Error()
 	}
 	return nil
 }
@@ -353,7 +353,7 @@ func (az *Cloud) createPrivateDNSZoneGroup(ctx context.Context, dnsZoneGroupName
 			PrivateDNSZoneConfigs: &privateDNSZoneConfigs,
 		},
 	}
-	return az.privatednszonegroupclient.CreateOrUpdate(ctx, vnetResourceGroup, privateEndpointName, dnsZoneGroupName, privateDNSZoneGroup, false)
+	return az.privatednszonegroupclient.CreateOrUpdate(ctx, vnetResourceGroup, privateEndpointName, dnsZoneGroupName, privateDNSZoneGroup, "", false).Error()
 }
 
 // AddStorageAccountTags add tags to storage account


Test patch:
diff --git a/pkg/azureclients/armclient/azure_armclient_test.go b/pkg/azureclients/armclient/azure_armclient_test.go
index 6427cb639..ac4a9d589 100644
--- a/pkg/azureclients/armclient/azure_armclient_test.go
+++ b/pkg/azureclients/armclient/azure_armclient_test.go
@@ -595,6 +595,11 @@ func TestGetResourceID(t *testing.T) {
 			resourceID:         GetResourceID("sub", "rg", "type", "name"),
 			expectedResourceID: "/subscriptions/sub/resourceGroups/rg/providers/type/name",
 		},
+		{
+			description:        "resource list ID",
+			resourceID:         GetResourceListID("sub", "rg", "type"),
+			expectedResourceID: "/subscriptions/sub/resourceGroups/rg/providers/type",
+		},
 		{
 			description:        "child resource ID",
 			resourceID:         GetChildResourceID("sub", "rg", "type", "name-1", "childType", "name-3"),
diff --git a/pkg/azureclients/containerserviceclient/azure_containerserviceclient_test.go b/pkg/azureclients/containerserviceclient/azure_containerserviceclient_test.go
index f27f564a2..98733c7ae 100644
--- a/pkg/azureclients/containerserviceclient/azure_containerserviceclient_test.go
+++ b/pkg/azureclients/containerserviceclient/azure_containerserviceclient_test.go
@@ -138,10 +138,7 @@ func TestGetNeverRateLimiter(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	mcGetErr := &retry.Error{
-		RawError:  fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "read", "GetManagedCluster"),
-		Retriable: true,
-	}
+	mcGetErr := retry.GetRateLimitError(false, "GetManagedCluster")
 
 	armClient := mockarmclient.NewMockInterface(ctrl)
 
@@ -156,11 +153,7 @@ func TestGetRetryAfterReader(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	mcGetErr := &retry.Error{
-		RawError:   fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "GetManagedCluster", "client throttled"),
-		Retriable:  true,
-		RetryAfter: getFutureTime(),
-	}
+	mcGetErr := retry.GetThrottlingError("GetManagedCluster", "client throttled", getFutureTime())
 
 	armClient := mockarmclient.NewMockInterface(ctrl)
 
diff --git a/pkg/azureclients/deploymentclient/azure_deploymentclient_test.go b/pkg/azureclients/deploymentclient/azure_deploymentclient_test.go
index e558ec6a8..c358b06c5 100644
--- a/pkg/azureclients/deploymentclient/azure_deploymentclient_test.go
+++ b/pkg/azureclients/deploymentclient/azure_deploymentclient_test.go
@@ -136,10 +136,7 @@ func TestGetNeverRateLimiter(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	dpGetErr := &retry.Error{
-		RawError:  fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "read", "GetDeployment"),
-		Retriable: true,
-	}
+	dpGetErr := retry.GetRateLimitError(false, "GetDeployment")
 
 	armClient := mockarmclient.NewMockInterface(ctrl)
 
@@ -154,11 +151,7 @@ func TestGetRetryAfterReader(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	dpGetErr := &retry.Error{
-		RawError:   fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "GetDeployment", "client throttled"),
-		Retriable:  true,
-		RetryAfter: getFutureTime(),
-	}
+	dpGetErr := retry.GetThrottlingError("GetDeployment", "client throttled", getFutureTime())
 
 	armClient := mockarmclient.NewMockInterface(ctrl)
 
diff --git a/pkg/azureclients/privatednsclient/azure_privatednsclient_test.go b/pkg/azureclients/privatednsclient/azure_privatednsclient_test.go
new file mode 100644
index 000000000..7c498ecd8
--- /dev/null
+++ b/pkg/azureclients/privatednsclient/azure_privatednsclient_test.go
@@ -0,0 +1,272 @@
+/*
+Copyright 2022 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 privatednsclient
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns"
+	"github.com/Azure/go-autorest/autorest"
+	"github.com/Azure/go-autorest/autorest/to"
+
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/assert"
+	"k8s.io/client-go/util/flowcontrol"
+
+	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 (
+	sub0 = "sub0"
+	rg0  = "rg0"
+	pz0  = "pz0"
+
+	testResourceID     = "/subscriptions/sub0/resourceGroups/rg0/providers/" + privateDNSZoneResourceType + "/" + pz0
+	testResourcePrefix = "/subscriptions/sub0/resourceGroups/rg0/providers/" + privateDNSZoneResourceType
+)
+
+func TestNew(t *testing.T) {
+	config := &azclients.ClientConfig{
+		SubscriptionID:          sub0,
+		ResourceManagerEndpoint: "endpoint",
+		Location:                "eastus",
+		RateLimitConfig: &azclients.RateLimitConfig{
+			CloudProviderRateLimit:            true,
+			CloudProviderRateLimitQPS:         0.5,
+			CloudProviderRateLimitBucket:      1,
+			CloudProviderRateLimitQPSWrite:    0.5,
+			CloudProviderRateLimitBucketWrite: 1,
+		},
+		Backoff: &retry.Backoff{Steps: 1},
+	}
+
+	pzClient := New(config)
+	assert.Equal(t, sub0, pzClient.subscriptionID)
+	assert.NotEmpty(t, pzClient.rateLimiterReader)
+	assert.NotEmpty(t, pzClient.rateLimiterWriter)
+}
+
+func TestCreateOrUpdate(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	pz := getTestPrivateDNSZone(pz0)
+	armClient := mockarmclient.NewMockInterface(ctrl)
+	response := &http.Response{
+		StatusCode: http.StatusOK,
+		Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+	}
+	armClient.EXPECT().PutResourceWithDecorators(gomock.Any(), to.String(pz.ID), pz, gomock.Any()).Return(response, nil).Times(1)
+	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
+
+	pzClient := getTestPrivateDNSZoneClient(armClient)
+	rerr := pzClient.CreateOrUpdate(context.TODO(), rg0, pz0, pz, "", true)
+	assert.Nil(t, rerr)
+}
+
+func TestCreateOrUpdateWithNeverRateLimiter(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	rcCreateOrUpdateErr := retry.GetRateLimitError(true, "PrivateDNSZoneCreateOrUpdate")
+
+	pz := getTestPrivateDNSZone(pz0)
+	armClient := mockarmclient.NewMockInterface(ctrl)
+
+	pzClient := getTestPrivateDNSZoneClientWithNeverRateLimiter(armClient)
+	rerr := pzClient.CreateOrUpdate(context.TODO(), rg0, pz0, pz, "", true)
+	assert.Equal(t, rcCreateOrUpdateErr, rerr)
+}
+
+func TestCreateOrUpdateRetryAfterReader(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	rcCreateOrUpdateErr := retry.GetThrottlingError("PrivateDNSZoneCreateOrUpdate", "client throttled", getFutureTime())
+
+	pz := getTestPrivateDNSZone(pz0)
+	armClient := mockarmclient.NewMockInterface(ctrl)
+
+	pzClient := getTestPrivateDNSZoneClientWithRetryAfterReader(armClient)
+	rerr := pzClient.CreateOrUpdate(context.TODO(), rg0, pz0, pz, "", true)
+	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:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
+	}
+	throttleErr := &retry.Error{
+		HTTPStatusCode: http.StatusTooManyRequests,
+		RawError:       fmt.Errorf("error"),
+		Retriable:      true,
+		RetryAfter:     time.Unix(100, 0),
+	}
+
+	pz := getTestPrivateDNSZone(pz0)
+	armClient := mockarmclient.NewMockInterface(ctrl)
+	armClient.EXPECT().PutResourceWithDecorators(gomock.Any(), to.String(pz.ID), pz, gomock.Any()).Return(response, throttleErr).Times(1)
+	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
+
+	pzClient := getTestPrivateDNSZoneClient(armClient)
+	rerr := pzClient.CreateOrUpdate(context.TODO(), rg0, pz0, pz, "", true)
+	assert.Equal(t, throttleErr, rerr)
+}
+
+func TestCreateOrUpdateWithCreateOrUpdateResponderError(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	pz := getTestPrivateDNSZone(pz0)
+	armClient := mockarmclient.NewMockInterface(ctrl)
+	response := &http.Response{
+		StatusCode: http.StatusNotFound,
+		Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+	}
+
+	armClient.EXPECT().PutResourceWithDecorators(gomock.Any(), to.String(pz.ID), pz, gomock.Any()).Return(response, nil).Times(1)
+	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
+
+	pzClient := getTestPrivateDNSZoneClient(armClient)
+	rerr := pzClient.CreateOrUpdate(context.TODO(), rg0, pz0, pz, "", true)
+	assert.NotNil(t, rerr)
+}
+
+func getTestPrivateDNSZone(name string) privatedns.PrivateZone {
+	return privatedns.PrivateZone{
+		ID:       to.StringPtr(fmt.Sprintf("%s/%s", testResourcePrefix, name)),
+		Name:     to.StringPtr(name),
+		Location: to.StringPtr("eastus"),
+	}
+}
+
+func getTestPrivateDNSZoneClient(armClient armclient.Interface) *Client {
+	rateLimiterReader, rateLimiterWriter := azclients.NewRateLimiter(&azclients.RateLimitConfig{})
+	return &Client{
+		armClient:         armClient,
+		subscriptionID:    sub0,
+		rateLimiterReader: rateLimiterReader,
+		rateLimiterWriter: rateLimiterWriter,
+	}
+}
+
+func getTestPrivateDNSZoneClientWithNeverRateLimiter(armClient armclient.Interface) *Client {
+	rateLimiterReader := flowcontrol.NewFakeNeverRateLimiter()
+	rateLimiterWriter := flowcontrol.NewFakeNeverRateLimiter()
+	return &Client{
+		armClient:         armClient,
+		subscriptionID:    sub0,
+		rateLimiterReader: rateLimiterReader,
+		rateLimiterWriter: rateLimiterWriter,
+	}
+}
+
+func getTestPrivateDNSZoneClientWithRetryAfterReader(armClient armclient.Interface) *Client {
+	rateLimiterReader := flowcontrol.NewFakeAlwaysRateLimiter()
+	rateLimiterWriter := flowcontrol.NewFakeAlwaysRateLimiter()
+	return &Client{
+		armClient:         armClient,
+		subscriptionID:    sub0,
+		rateLimiterReader: rateLimiterReader,
+		rateLimiterWriter: rateLimiterWriter,
+		RetryAfterReader:  getFutureTime(),
+		RetryAfterWriter:  getFutureTime(),
+	}
+}
+
+func TestGetNotFound(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	response := &http.Response{
+		StatusCode: http.StatusNotFound,
+		Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
+	}
+	armClient := mockarmclient.NewMockInterface(ctrl)
+	armClient.EXPECT().GetResource(gomock.Any(), testResourceID).Return(response, nil).Times(1)
+	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
+
+	pzClient := getTestPrivateDNSZoneClient(armClient)
+	expected := privatedns.PrivateZone{Response: autorest.Response{}}
+	result, rerr := pzClient.Get(context.TODO(), rg0, pz0)
+	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:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
+	}
+	armClient := mockarmclient.NewMockInterface(ctrl)
+	armClient.EXPECT().GetResource(gomock.Any(), testResourceID).Return(response, nil).Times(1)
+	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
+
+	pzClient := getTestPrivateDNSZoneClient(armClient)
+	expected := privatedns.PrivateZone{Response: autorest.Response{}}
+	result, rerr := pzClient.Get(context.TODO(), rg0, pz0)
+	assert.Equal(t, expected, result)
+	assert.NotNil(t, rerr)
+	assert.Equal(t, http.StatusInternalServerError, rerr.HTTPStatusCode)
+}
+
+func TestGetThrottle(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	response := &http.Response{
+		StatusCode: http.StatusTooManyRequests,
+		Body:       ioutil.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().GetResource(gomock.Any(), testResourceID).Return(response, throttleErr).Times(1)
+	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
+
+	pzClient := getTestPrivateDNSZoneClient(armClient)
+	result, rerr := pzClient.Get(context.TODO(), rg0, pz0)
+	assert.Empty(t, result)
+	assert.Equal(t, throttleErr, rerr)
+}
+
+// 2065-01-24 05:20:00 +0000 UTC
+func getFutureTime() time.Time {
+	return time.Unix(3000000000, 0)
+}
diff --git a/pkg/azureclients/privatednszonegroupclient/azure_privatednszonegroupclient_test.go b/pkg/azureclients/privatednszonegroupclient/azure_privatednszonegroupclient_test.go
new file mode 100644
index 000000000..e3406b32a
--- /dev/null
+++ b/pkg/azureclients/privatednszonegroupclient/azure_privatednszonegroupclient_test.go
@@ -0,0 +1,272 @@
+/*
+Copyright 2022 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 privatednszonegroupclient
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-02-01/network"
+	"github.com/Azure/go-autorest/autorest"
+	"github.com/Azure/go-autorest/autorest/to"
+
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/assert"
+	"k8s.io/client-go/util/flowcontrol"
+
+	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 (
+	sub0 = "sub0"
+	rg0  = "rg0"
+	pe0  = "pe0"
+	pzg0 = "pzg0"
+
+	testResourceID     = "/subscriptions/sub0/resourceGroups/rg0/providers/Microsoft.Network/privateEndpoints/pe0/privateDnsZoneGroups/" + pzg0
+	testResourcePrefix = "/subscriptions/sub0/resourceGroups/rg0/providers/Microsoft.Network/privateEndpoints/pe0/privateDnsZoneGroups"
+)
+
+func TestNew(t *testing.T) {
+	config := &azclients.ClientConfig{
+		SubscriptionID:          sub0,
+		ResourceManagerEndpoint: "endpoint",
+		Location:                "eastus",
+		RateLimitConfig: &azclients.RateLimitConfig{
+			CloudProviderRateLimit:            true,
+			CloudProviderRateLimitQPS:         0.5,
+			CloudProviderRateLimitBucket:      1,
+			CloudProviderRateLimitQPSWrite:    0.5,
+			CloudProviderRateLimitBucketWrite: 1,
+		},
+		Backoff: &retry.Backoff{Steps: 1},
+	}
+
+	pzgClient := New(config)
+	assert.Equal(t, sub0, pzgClient.subscriptionID)
+	assert.NotEmpty(t, pzgClient.rateLimiterReader)
+	assert.NotEmpty(t, pzgClient.rateLimiterWriter)
+}
+
+func TestCreateOrUpdate(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	pzg := getTestPrivateDNSZoneGroup(pzg0)
+	armClient := mockarmclient.NewMockInterface(ctrl)
+	response := &http.Response{
+		StatusCode: http.StatusOK,
+		Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+	}
+	armClient.EXPECT().PutResourceWithDecorators(gomock.Any(), to.String(pzg.ID), pzg, gomock.Any()).Return(response, nil).Times(1)
+	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
+
+	pzgClient := getTestPrivateDNSZoneGroupClient(armClient)
+	rerr := pzgClient.CreateOrUpdate(context.TODO(), rg0, pe0, pzg0, pzg, "", true)
+	assert.Nil(t, rerr)
+}
+
+func TestCreateOrUpdateWithNeverRateLimiter(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	rcCreateOrUpdateErr := retry.GetRateLimitError(true, "PrivateDNSZoneGroupCreateOrUpdate")
+
+	pzg := getTestPrivateDNSZoneGroup(pzg0)
+	armClient := mockarmclient.NewMockInterface(ctrl)
+
+	pzgClient := getTestPrivateDNSZoneGroupClientWithNeverRateLimiter(armClient)
+	rerr := pzgClient.CreateOrUpdate(context.TODO(), rg0, pe0, pzg0, pzg, "", true)
+	assert.Equal(t, rcCreateOrUpdateErr, rerr)
+}
+
+func TestCreateOrUpdateRetryAfterReader(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	rcCreateOrUpdateErr := retry.GetThrottlingError("PrivateDNSZoneGroupCreateOrUpdate", "client throttled", getFutureTime())
+
+	pzg := getTestPrivateDNSZoneGroup(pzg0)
+	armClient := mockarmclient.NewMockInterface(ctrl)
+
+	pzgClient := getTestPrivateDNSZoneGroupClientWithRetryAfterReader(armClient)
+	rerr := pzgClient.CreateOrUpdate(context.TODO(), rg0, pe0, pzg0, pzg, "", true)
+	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:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
+	}
+	throttleErr := &retry.Error{
+		HTTPStatusCode: http.StatusTooManyRequests,
+		RawError:       fmt.Errorf("error"),
+		Retriable:      true,
+		RetryAfter:     time.Unix(100, 0),
+	}
+
+	pzg := getTestPrivateDNSZoneGroup(pzg0)
+	armClient := mockarmclient.NewMockInterface(ctrl)
+	armClient.EXPECT().PutResourceWithDecorators(gomock.Any(), to.String(pzg.ID), pzg, gomock.Any()).Return(response, throttleErr).Times(1)
+	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
+
+	pzgClient := getTestPrivateDNSZoneGroupClient(armClient)
+	rerr := pzgClient.CreateOrUpdate(context.TODO(), rg0, pe0, pzg0, pzg, "", true)
+	assert.Equal(t, throttleErr, rerr)
+}
+
+func TestCreateOrUpdateWithCreateOrUpdateResponderError(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	pzg := getTestPrivateDNSZoneGroup(pzg0)
+	armClient := mockarmclient.NewMockInterface(ctrl)
+	response := &http.Response{
+		StatusCode: http.StatusNotFound,
+		Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+	}
+
+	armClient.EXPECT().PutResourceWithDecorators(gomock.Any(), to.String(pzg.ID), pzg, gomock.Any()).Return(response, nil).Times(1)
+	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
+
+	pzgClient := getTestPrivateDNSZoneGroupClient(armClient)
+	rerr := pzgClient.CreateOrUpdate(context.TODO(), rg0, pe0, pzg0, pzg, "", true)
+	assert.NotNil(t, rerr)
+}
+
+func getTestPrivateDNSZoneGroup(name string) network.PrivateDNSZoneGroup {
+	return network.PrivateDNSZoneGroup{
+		ID:   to.StringPtr(fmt.Sprintf("%s/%s", testResourcePrefix, name)),
+		Name: to.StringPtr(name),
+	}
+}
+
+func getTestPrivateDNSZoneGroupClient(armClient armclient.Interface) *Client {
+	rateLimiterReader, rateLimiterWriter := azclients.NewRateLimiter(&azclients.RateLimitConfig{})
+	return &Client{
+		armClient:         armClient,
+		subscriptionID:    sub0,
+		rateLimiterReader: rateLimiterReader,
+		rateLimiterWriter: rateLimiterWriter,
+	}
+}
+
+func getTestPrivateDNSZoneGroupClientWithNeverRateLimiter(armClient armclient.Interface) *Client {
+	rateLimiterReader := flowcontrol.NewFakeNeverRateLimiter()
+	rateLimiterWriter := flowcontrol.NewFakeNeverRateLimiter()
+	return &Client{
+		armClient:         armClient,
+		subscriptionID:    sub0,
+		rateLimiterReader: rateLimiterReader,
+		rateLimiterWriter: rateLimiterWriter,
+	}
+}
+
+func getTestPrivateDNSZoneGroupClientWithRetryAfterReader(armClient armclient.Interface) *Client {
+	rateLimiterReader := flowcontrol.NewFakeAlwaysRateLimiter()
+	rateLimiterWriter := flowcontrol.NewFakeAlwaysRateLimiter()
+	return &Client{
+		armClient:         armClient,
+		subscriptionID:    sub0,
+		rateLimiterReader: rateLimiterReader,
+		rateLimiterWriter: rateLimiterWriter,
+		RetryAfterReader:  getFutureTime(),
+		RetryAfterWriter:  getFutureTime(),
+	}
+}
+
+func TestGetNotFound(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	response := &http.Response{
+		StatusCode: http.StatusNotFound,
+		Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
+	}
+	armClient := mockarmclient.NewMockInterface(ctrl)
+	armClient.EXPECT().GetResource(gomock.Any(), testResourceID).Return(response, nil).Times(1)
+	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
+
+	pzgClient := getTestPrivateDNSZoneGroupClient(armClient)
+	expected := network.PrivateDNSZoneGroup{Response: autorest.Response{}}
+	result, rerr := pzgClient.Get(context.TODO(), rg0, pe0, pzg0)
+	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:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
+	}
+	armClient := mockarmclient.NewMockInterface(ctrl)
+	armClient.EXPECT().GetResource(gomock.Any(), testResourceID).Return(response, nil).Times(1)
+	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
+
+	pzgClient := getTestPrivateDNSZoneGroupClient(armClient)
+	expected := network.PrivateDNSZoneGroup{Response: autorest.Response{}}
+	result, rerr := pzgClient.Get(context.TODO(), rg0, pe0, pzg0)
+	assert.Equal(t, expected, result)
+	assert.NotNil(t, rerr)
+	assert.Equal(t, http.StatusInternalServerError, rerr.HTTPStatusCode)
+}
+
+func TestGetThrottle(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	response := &http.Response{
+		StatusCode: http.StatusTooManyRequests,
+		Body:       ioutil.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().GetResource(gomock.Any(), testResourceID).Return(response, throttleErr).Times(1)
+	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
+
+	pzgClient := getTestPrivateDNSZoneGroupClient(armClient)
+	result, rerr := pzgClient.Get(context.TODO(), rg0, pe0, pzg0)
+	assert.Empty(t, result)
+	assert.Equal(t, throttleErr, rerr)
+}
+
+// 2065-01-24 05:20:00 +0000 UTC
+func getFutureTime() time.Time {
+	return time.Unix(3000000000, 0)
+}
diff --git a/pkg/azureclients/publicipclient/azure_publicipclient_test.go b/pkg/azureclients/publicipclient/azure_publicipclient_test.go
index b5dd3318c..8715cb437 100644
--- a/pkg/azureclients/publicipclient/azure_publicipclient_test.go
+++ b/pkg/azureclients/publicipclient/azure_publicipclient_test.go
@@ -641,10 +641,7 @@ func TestCreateOrUpdateNeverRateLimiter(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	pipCreateOrUpdateErr := &retry.Error{
-		RawError:  fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "write", "PublicIPCreateOrUpdate"),
-		Retriable: true,
-	}
+	pipCreateOrUpdateErr := retry.GetRateLimitError(true, "PublicIPCreateOrUpdate")
 
 	armClient := mockarmclient.NewMockInterface(ctrl)
 	pipClient := getTestPublicIPAddressClientWithNeverRateLimiter(armClient)
@@ -658,11 +655,7 @@ func TestCreateOrUpdateRetryAfterReader(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	pipCreateOrUpdateErr := &retry.Error{
-		RawError:   fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "PublicIPCreateOrUpdate", "client throttled"),
-		Retriable:  true,
-		RetryAfter: getFutureTime(),
-	}
+	pipCreateOrUpdateErr := retry.GetThrottlingError("PublicIPCreateOrUpdate", "client throttled", getFutureTime())
 
 	pip := getTestPublicIPAddress("pip1")
 	armClient := mockarmclient.NewMockInterface(ctrl)
diff --git a/pkg/azureclients/routeclient/azure_routeclient_test.go b/pkg/azureclients/routeclient/azure_routeclient_test.go
index 2e2d4f725..e2706a46a 100644
--- a/pkg/azureclients/routeclient/azure_routeclient_test.go
+++ b/pkg/azureclients/routeclient/azure_routeclient_test.go
@@ -107,10 +107,7 @@ func TestCreateOrUpdateWithNeverRateLimiter(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	rcCreateOrUpdateErr := &retry.Error{
-		RawError:  fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "write", "RouteCreateOrUpdate"),
-		Retriable: true,
-	}
+	rcCreateOrUpdateErr := retry.GetRateLimitError(true, "RouteCreateOrUpdate")
 
 	r := getTestRoute("r1")
 	armClient := mockarmclient.NewMockInterface(ctrl)
@@ -124,11 +121,7 @@ func TestCreateOrUpdateRetryAfterReader(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	rcCreateOrUpdateErr := &retry.Error{
-		RawError:   fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "RouteCreateOrUpdate", "client throttled"),
-		Retriable:  true,
-		RetryAfter: getFutureTime(),
-	}
+	rcCreateOrUpdateErr := retry.GetThrottlingError("RouteCreateOrUpdate", "client throttled", getFutureTime())
 
 	r := getTestRoute("r1")
 	armClient := mockarmclient.NewMockInterface(ctrl)
diff --git a/pkg/azureclients/routetableclient/azure_routetableclient_test.go b/pkg/azureclients/routetableclient/azure_routetableclient_test.go
index 78bd69da1..d6685e9c2 100644
--- a/pkg/azureclients/routetableclient/azure_routetableclient_test.go
+++ b/pkg/azureclients/routetableclient/azure_routetableclient_test.go
@@ -234,10 +234,7 @@ func TestCreateOrUpdateWithNeverRateLimiter(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	rcCreateOrUpdateErr := &retry.Error{
-		RawError:  fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "write", "RouteTableCreateOrUpdate"),
-		Retriable: true,
-	}
+	rcCreateOrUpdateErr := retry.GetRateLimitError(true, "RouteTableCreateOrUpdate")
 
 	rt1 := getTestRouteTable("rt1")
 	armClient := mockarmclient.NewMockInterface(ctrl)
@@ -251,11 +248,7 @@ func TestCreateOrUpdateRetryAfterReader(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	rcCreateOrUpdateErr := &retry.Error{
-		RawError:   fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "RouteTableCreateOrUpdate", "client throttled"),
-		Retriable:  true,
-		RetryAfter: getFutureTime(),
-	}
+	rcCreateOrUpdateErr := retry.GetThrottlingError("RouteTableCreateOrUpdate", "client throttled", getFutureTime())
 
 	rt1 := getTestRouteTable("rt1")
 	armClient := mockarmclient.NewMockInterface(ctrl)
diff --git a/pkg/azureclients/securitygroupclient/azure_securitygroupclient_test.go b/pkg/azureclients/securitygroupclient/azure_securitygroupclient_test.go
index 8365cd508..1c3fd68cd 100644
--- a/pkg/azureclients/securitygroupclient/azure_securitygroupclient_test.go
+++ b/pkg/azureclients/securitygroupclient/azure_securitygroupclient_test.go
@@ -477,10 +477,7 @@ func TestCreateOrUpdateNeverRateLimiter(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	nsgCreateOrUpdateErr := &retry.Error{
-		RawError:  fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "write", "NSGCreateOrUpdate"),
-		Retriable: true,
-	}
+	nsgCreateOrUpdateErr := retry.GetRateLimitError(true, "NSGCreateOrUpdate")
 
 	armClient := mockarmclient.NewMockInterface(ctrl)
 
@@ -495,11 +492,7 @@ func TestCreateOrUpdateRetryAfterReader(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	nsgCreateOrUpdateErr := &retry.Error{
-		RawError:   fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "NSGCreateOrUpdate", "client throttled"),
-		Retriable:  true,
-		RetryAfter: getFutureTime(),
-	}
+	nsgCreateOrUpdateErr := retry.GetThrottlingError("NSGCreateOrUpdate", "client throttled", getFutureTime())
 
 	nsg := getTestSecurityGroup("nsg1")
 	armClient := mockarmclient.NewMockInterface(ctrl)
diff --git a/pkg/azureclients/snapshotclient/azure_snapshotclient_test.go b/pkg/azureclients/snapshotclient/azure_snapshotclient_test.go
index 8f6e0658c..8f492288d 100644
--- a/pkg/azureclients/snapshotclient/azure_snapshotclient_test.go
+++ b/pkg/azureclients/snapshotclient/azure_snapshotclient_test.go
@@ -506,10 +506,7 @@ func TestCreateOrUpdateNeverRateLimiter(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	snCreateOrUpdateErr := &retry.Error{
-		RawError:  fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "write", "SnapshotCreateOrUpdate"),
-		Retriable: true,
-	}
+	snCreateOrUpdateErr := retry.GetRateLimitError(true, "SnapshotCreateOrUpdate")
 
 	armClient := mockarmclient.NewMockInterface(ctrl)
 	snClient := getTestSnapshotClientWithNeverRateLimiter(armClient)
@@ -523,11 +520,7 @@ func TestCreateOrUpdateRetryAfterReader(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	snCreateOrUpdateErr := &retry.Error{
-		RawError:   fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "SnapshotCreateOrUpdate", "client throttled"),
-		Retriable:  true,
-		RetryAfter: getFutureTime(),
-	}
+	snCreateOrUpdateErr := retry.GetThrottlingError("SnapshotCreateOrUpdate", "client throttled", getFutureTime())
 
 	sn := getTestSnapshot("sn1")
 	armClient := mockarmclient.NewMockInterface(ctrl)
diff --git a/pkg/azureclients/subnetclient/azure_subnetclient_test.go b/pkg/azureclients/subnetclient/azure_subnetclient_test.go
index e7504819f..30453921b 100644
--- a/pkg/azureclients/subnetclient/azure_subnetclient_test.go
+++ b/pkg/azureclients/subnetclient/azure_subnetclient_test.go
@@ -517,10 +517,7 @@ func TestCreateOrUpdateNeverRateLimiter(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	subnetCreateOrUpdateErr := &retry.Error{
-		RawError:  fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "write", "SubnetCreateOrUpdate"),
-		Retriable: true,
-	}
+	subnetCreateOrUpdateErr := retry.GetRateLimitError(true, "SubnetCreateOrUpdate")
 
 	armClient := mockarmclient.NewMockInterface(ctrl)
 	subnetClient := getTestSubnetClientWithNeverRateLimiter(armClient)
@@ -534,11 +531,7 @@ func TestCreateOrUpdateRetryAfterReader(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	subnetCreateOrUpdateErr := &retry.Error{
-		RawError:   fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "SubnetCreateOrUpdate", "client throttled"),
-		Retriable:  true,
-		RetryAfter: getFutureTime(),
-	}
+	subnetCreateOrUpdateErr := retry.GetThrottlingError("SubnetCreateOrUpdate", "client throttled", getFutureTime())
 
 	subnet := getTestSubnet("subnet1")
 	armClient := mockarmclient.NewMockInterface(ctrl)
diff --git a/pkg/azureclients/vmclient/azure_vmclient_test.go b/pkg/azureclients/vmclient/azure_vmclient_test.go
index 5fd044f08..f3cda3a6f 100644
--- a/pkg/azureclients/vmclient/azure_vmclient_test.go
+++ b/pkg/azureclients/vmclient/azure_vmclient_test.go
@@ -696,10 +696,7 @@ func TestCreateOrUpdateNeverRateLimiter(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	vmCreateOrUpdateErr := &retry.Error{
-		RawError:  fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "write", "VMCreateOrUpdate"),
-		Retriable: true,
-	}
+	vmCreateOrUpdateErr := retry.GetRateLimitError(true, "VMCreateOrUpdate")
 
 	armClient := mockarmclient.NewMockInterface(ctrl)
 	vmClient := getTestVMClientWithNeverRateLimiter(armClient)
@@ -713,11 +710,7 @@ func TestCreateOrUpdateRetryAfterReader(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	vmCreateOrUpdateErr := &retry.Error{
-		RawError:   fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "VMCreateOrUpdate", "client throttled"),
-		Retriable:  true,
-		RetryAfter: getFutureTime(),
-	}
+	vmCreateOrUpdateErr := retry.GetThrottlingError("VMCreateOrUpdate", "client throttled", getFutureTime())
 
 	testVM := getTestVM("vm1")
 	armClient := mockarmclient.NewMockInterface(ctrl)
diff --git a/pkg/azureclients/vmssclient/azure_vmssclient_test.go b/pkg/azureclients/vmssclient/azure_vmssclient_test.go
index 40518e7b4..04860568f 100644
--- a/pkg/azureclients/vmssclient/azure_vmssclient_test.go
+++ b/pkg/azureclients/vmssclient/azure_vmssclient_test.go
@@ -533,10 +533,7 @@ func TestCreateOrUpdateNeverRateLimiter(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	vmssCreateOrUpdateErr := &retry.Error{
-		RawError:  fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "write", "VMSSCreateOrUpdate"),
-		Retriable: true,
-	}
+	vmssCreateOrUpdateErr := retry.GetRateLimitError(true, "VMSSCreateOrUpdate")
 
 	armClient := mockarmclient.NewMockInterface(ctrl)
 	vmssClient := getTestVMSSClientWithNeverRateLimiter(armClient)
@@ -550,11 +547,7 @@ func TestCreateOrUpdateRetryAfterReader(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	vmssCreateOrUpdateErr := &retry.Error{
-		RawError:   fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "VMSSCreateOrUpdate", "client throttled"),
-		Retriable:  true,
-		RetryAfter: getFutureTime(),
-	}
+	vmssCreateOrUpdateErr := retry.GetThrottlingError("VMSSCreateOrUpdate", "client throttled", getFutureTime())
 
 	vmss := getTestVMSS("vmss1")
 	armClient := mockarmclient.NewMockInterface(ctrl)
diff --git a/pkg/provider/azure_loadbalancer_test.go b/pkg/provider/azure_loadbalancer_test.go
index ba8410d08..d1af3aa12 100644
--- a/pkg/provider/azure_loadbalancer_test.go
+++ b/pkg/provider/azure_loadbalancer_test.go
@@ -1880,15 +1880,6 @@ func TestReconcileLoadBalancerRule(t *testing.T) {
 			expectedProbes:  getDefaultTestProbes("Https", "/healthy1"),
 			expectedRules:   getDefaultTestRules(true),
 		},
-		{
-			desc: "getExpectedLBRules shall return corresponding probe and lbRule (slb with IPv6)",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
-				"service.beta.kubernetes.io/azure-load-balancer-internal": "true",
-			}, true, 80),
-			loadBalancerSku: "standard",
-			expectedProbes:  getDefaultTestProbes("Tcp", ""),
-			expectedRules:   getDefaultInternalIPv6Rules(true),
-		},
 		{
 			desc: "getExpectedLBRules shall return corresponding probe and lbRule (slb with HA enabled)",
 			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
@@ -2203,15 +2194,6 @@ func getDefaultTestRules(enableTCPReset bool) []network.LoadBalancingRule {
 	}
 }
 
-func getDefaultInternalIPv6Rules(enableTCPReset bool) []network.LoadBalancingRule {
-	rules := getDefaultTestRules(true)
-	for _, rule := range rules {
-		rule.EnableFloatingIP = to.BoolPtr(false)
-		rule.BackendPort = to.Int32Ptr(getBackendPort(*rule.FrontendPort))
-	}
-	return rules
-}
-
 func getTestRule(enableTCPReset bool, port int32) network.LoadBalancingRule {
 	expectedRules := network.LoadBalancingRule{
 		Name: to.StringPtr(fmt.Sprintf("atest1-TCP-%d", port)),
diff --git a/pkg/provider/azure_routes_test.go b/pkg/provider/azure_routes_test.go
index 7a765f80f..1d4e3776e 100644
--- a/pkg/provider/azure_routes_test.go
+++ b/pkg/provider/azure_routes_test.go
@@ -308,6 +308,13 @@ func TestCreateRoute(t *testing.T) {
 			routeCIDRs:         map[string]string{},
 			expectedRouteCIDRs: map[string]string{"node": "1.2.3.4/24"},
 		},
+		{
+			name:                 "CreateRoute should report error if node is unmanaged and cloud.ipv6DualStackEnabled is true",
+			hasUnmanagedNodes:    true,
+			ipv6DualStackEnabled: true,
+			unmanagedNodeName:    "node",
+			expectedErrMsg:       fmt.Errorf("unmanaged nodes are not supported in dual stack mode"),
+		},
 		{
 			name:           "CreateRoute should create route if cloud.ipv6DualStackEnabled is true and route doesn't exist",
 			routeTableName: "rt9",
diff --git a/pkg/provider/azure_storageaccount_test.go b/pkg/provider/azure_storageaccount_test.go
index bf91c957c..ea51b3016 100644
--- a/pkg/provider/azure_storageaccount_test.go
+++ b/pkg/provider/azure_storageaccount_test.go
@@ -367,11 +367,11 @@ func TestEnsureStorageAccountWithPrivateEndpoint(t *testing.T) {
 	cloud.SubnetsClient = mockSubnetsClient
 
 	mockPrivateDNSClient := mockprivatednsclient.NewMockInterface(ctrl)
-	mockPrivateDNSClient.EXPECT().CreateOrUpdate(gomock.Any(), vnetResourceGroup, gomock.Any(), gomock.Any(), true).Return(nil).Times(1)
+	mockPrivateDNSClient.EXPECT().CreateOrUpdate(gomock.Any(), vnetResourceGroup, gomock.Any(), gomock.Any(), "", true).Return(nil).Times(1)
 	cloud.privatednsclient = mockPrivateDNSClient
 
 	mockPrivateDNSZoneGroup := mockprivatednszonegroupclient.NewMockInterface(ctrl)
-	mockPrivateDNSZoneGroup.EXPECT().CreateOrUpdate(gomock.Any(), vnetResourceGroup, gomock.Any(), gomock.Any(), gomock.Any(), false).Return(nil).Times(1)
+	mockPrivateDNSZoneGroup.EXPECT().CreateOrUpdate(gomock.Any(), vnetResourceGroup, gomock.Any(), gomock.Any(), gomock.Any(), "", false).Return(nil).Times(1)
 	cloud.privatednszonegroupclient = mockPrivateDNSZoneGroup
 
 	mockPrivateEndpointClient := mockprivateendpointclient.NewMockInterface(ctrl)
diff --git a/tests/e2e/network/private_link_service.go b/tests/e2e/network/private_link_service.go
deleted file mode 100644
index f02f64057..000000000
--- a/tests/e2e/network/private_link_service.go
+++ /dev/null
@@ -1,459 +0,0 @@
-/*
-Copyright 2022 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 network
-
-import (
-	"context"
-	"fmt"
-	"os"
-	"strconv"
-	"strings"
-	"time"
-
-	v1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/util/intstr"
-	"k8s.io/apimachinery/pkg/util/wait"
-	clientset "k8s.io/client-go/kubernetes"
-	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
-	"sigs.k8s.io/cloud-provider-azure/tests/e2e/utils"
-
-	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-02-01/network"
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-)
-
-var _ = Describe("Private link service", func() {
-	basename := "pls"
-	serviceName := "pls-test"
-
-	var cs clientset.Interface
-	var ns *v1.Namespace
-	var tc *utils.AzureTestClient
-
-	labels := map[string]string{
-		"app": serviceName,
-	}
-	ports := []v1.ServicePort{{
-		Port:       nginxPort,
-		TargetPort: intstr.FromInt(nginxPort),
-	}}
-
-	BeforeEach(func() {
-		if !strings.EqualFold(os.Getenv(utils.LoadBalancerSkuEnv), "standard") {
-			Skip("private link service only works with standard load balancer")
-		}
-		var err error
-		cs, err = utils.CreateKubeClientSet()
-		Expect(err).NotTo(HaveOccurred())
-
-		ns, err = utils.CreateTestingNamespace(basename, cs)
-		Expect(err).NotTo(HaveOccurred())
-
-		tc, err = utils.CreateAzureTestClient()
-		Expect(err).NotTo(HaveOccurred())
-
-		utils.Logf("Creating deployment " + serviceName)
-		deployment := createNginxDeploymentManifest(serviceName, labels)
-		_, err = cs.AppsV1().Deployments(ns.Name).Create(context.TODO(), deployment, metav1.CreateOptions{})
-		Expect(err).NotTo(HaveOccurred())
-	})
-
-	AfterEach(func() {
-		err := cs.AppsV1().Deployments(ns.Name).Delete(context.TODO(), serviceName, metav1.DeleteOptions{})
-		Expect(err).NotTo(HaveOccurred())
-
-		err = utils.DeleteNamespace(cs, ns.Name)
-		Expect(err).NotTo(HaveOccurred())
-
-		cs = nil
-		ns = nil
-		tc = nil
-	})
-
-	It("should support service annotation 'service.beta.kubernetes.io/azure-pls-create'", func() {
-		annotation := map[string]string{
-			consts.ServiceAnnotationLoadBalancerInternal: "true",
-			consts.ServiceAnnotationPLSCreation:          "true",
-		}
-
-		// create service with given annotation and wait it to expose
-		ip := createAndExposeDefaultServiceWithAnnotation(cs, serviceName, ns.Name, labels, annotation, ports)
-		defer func() {
-			utils.Logf("cleaning up test service %s", serviceName)
-			err := utils.DeleteService(cs, ns.Name, serviceName)
-			Expect(err).NotTo(HaveOccurred())
-		}()
-		utils.Logf("Get Internal IP: %s", ip)
-
-		// get pls from azure client
-		pls := getPrivateLinkServiceFromIP(tc, ip, "", "", "")
-		Expect(pls.IPConfigurations).NotTo(BeNil())
-		Expect(len(*pls.IPConfigurations)).To(Equal(1))
-		Expect((*pls.IPConfigurations)[0].PrivateIPAllocationMethod).To(Equal(network.IPAllocationMethodDynamic))
-		Expect(pls.Fqdns == nil || len(*pls.Fqdns) == 0).To(BeTrue())
-		Expect(pls.EnableProxyProtocol == nil || !*pls.EnableProxyProtocol).To(BeTrue())
-		Expect(pls.Visibility == nil || len(*pls.Visibility.Subscriptions) == 0).To(BeTrue())
-		Expect(pls.AutoApproval == nil || len(*pls.AutoApproval.Subscriptions) == 0).To(BeTrue())
-	})
-
-	It("should support service annotation 'service.beta.kubernetes.io/azure-pls-name'", func() {
-		plsName := "testpls"
-		annotation := map[string]string{
-			consts.ServiceAnnotationLoadBalancerInternal: "true",
-			consts.ServiceAnnotationPLSCreation:          "true",
-			consts.ServiceAnnotationPLSName:              plsName,
-		}
-
-		// create service with given annotation and wait it to expose
-		ip := createAndExposeDefaultServiceWithAnnotation(cs, serviceName, ns.Name, labels, annotation, ports)
-		defer func() {
-			utils.Logf("cleaning up test service %s", serviceName)
-			err := utils.DeleteService(cs, ns.Name, serviceName)
-			Expect(err).NotTo(HaveOccurred())
-		}()
-		utils.Logf("Get Internal IP: %s", ip)
-
-		// get pls from azure client
-		pls := getPrivateLinkServiceFromIP(tc, ip, "", "", plsName)
-		Expect(*pls.Name).To(Equal(plsName))
-	})
-
-	It("should support service annotation 'service.beta.kubernetes.io/azure-pls-ip-configuration-subnet'", func() {
-		subnetName := "pls-subnet"
-
-		vNet, err := tc.GetClusterVirtualNetwork()
-		Expect(err).NotTo(HaveOccurred())
-
-		var newSubnetID string
-		for _, existingSubnet := range *vNet.Subnets {
-			if *existingSubnet.Name == subnetName {
-				By("Test subnet have existed, skip creating")
-				newSubnetID = *existingSubnet.ID
-				break
-			}
-		}
-
-		if newSubnetID == "" {
-			By("Test subnet doesn't exist. Creating a new one...")
-			newSubnetCIDR, err := utils.GetNextSubnetCIDR(vNet)
-			Expect(err).NotTo(HaveOccurred())
-			newSubnet, err := tc.CreateSubnet(vNet, &subnetName, &newSubnetCIDR, true)
-			newSubnetID = *newSubnet.ID
-			Expect(err).NotTo(HaveOccurred())
-			defer func() {
-				utils.Logf("cleaning up test subnet %s", subnetName)
-				err = tc.DeleteSubnet(*vNet.Name, subnetName)
-				Expect(err).NotTo(HaveOccurred())
-			}()
-		}
-
-		annotation := map[string]string{
-			consts.ServiceAnnotationLoadBalancerInternal:     "true",
-			consts.ServiceAnnotationPLSCreation:              "true",
-			consts.ServiceAnnotationPLSIpConfigurationSubnet: subnetName,
-		}
-
-		// create service with given annotation and wait it to expose
-		ip := createAndExposeDefaultServiceWithAnnotation(cs, serviceName, ns.Name, labels, annotation, ports)
-		defer func() {
-			utils.Logf("cleaning up test service %s", serviceName)
-			err := utils.DeleteService(cs, ns.Name, serviceName)
-			Expect(err).NotTo(HaveOccurred())
-		}()
-		utils.Logf("Get Internal IP: %s", ip)
-
-		// get pls from azure client
-		pls := getPrivateLinkServiceFromIP(tc, ip, "", "", "")
-		Expect(pls.IPConfigurations).NotTo(BeNil())
-		Expect(len(*pls.IPConfigurations)).To(Equal(1))
-		Expect(strings.EqualFold(*(*pls.IPConfigurations)[0].Subnet.ID, newSubnetID)).To(BeTrue())
-	})
-
-	It("should support service annotation 'service.beta.kubernetes.io/azure-pls-ip-configuration-ip-address-count'", func() {
-		ipConfigCount := 3
-		annotation := map[string]string{
-			consts.ServiceAnnotationLoadBalancerInternal:             "true",
-			consts.ServiceAnnotationPLSCreation:                      "true",
-			consts.ServiceAnnotationPLSIpConfigurationIPAddressCount: strconv.Itoa(ipConfigCount),
-		}
-
-		// create service with given annotation and wait it to expose
-		ip := createAndExposeDefaultServiceWithAnnotation(cs, serviceName, ns.Name, labels, annotation, ports)
-		defer func() {
-			utils.Logf("cleaning up test service %s", serviceName)
-			err := utils.DeleteService(cs, ns.Name, serviceName)
-			Expect(err).NotTo(HaveOccurred())
-		}()
-		utils.Logf("Get Internal IP: %s", ip)
-
-		// get pls from azure client
-		pls := getPrivateLinkServiceFromIP(tc, ip, "", "", "")
-		Expect(pls.IPConfigurations).NotTo(BeNil())
-		Expect(len(*pls.IPConfigurations)).To(Equal(ipConfigCount))
-	})
-
-	It("should support service annotation 'service.beta.kubernetes.io/azure-pls-ip-configuration-ip-address'", func() {
-		annotation := map[string]string{
-			consts.ServiceAnnotationLoadBalancerInternal: "true",
-			consts.ServiceAnnotationPLSCreation:          "true",
-		}
-
-		// create service with given annotation and wait it to expose
-		ip := createAndExposeDefaultServiceWithAnnotation(cs, serviceName, ns.Name, labels, annotation, ports)
-		defer func() {
-			utils.Logf("cleaning up test service %s", serviceName)
-			err := utils.DeleteService(cs, ns.Name, serviceName)
-			Expect(err).NotTo(HaveOccurred())
-		}()
-		utils.Logf("Get Internal IP: %s", ip)
-
-		selectedip, err := utils.SelectAvailablePrivateIP(tc)
-		Expect(err).NotTo(HaveOccurred())
-		annotation[consts.ServiceAnnotationPLSIpConfigurationIPAddress] = selectedip
-		utils.Logf("Now update private link service's static ip to %s", selectedip)
-
-		service, err := cs.CoreV1().Services(ns.Name).Get(context.TODO(), serviceName, metav1.GetOptions{})
-		Expect(err).NotTo(HaveOccurred())
-		service = updateServiceAnnotation(service, annotation)
-		utils.Logf("service's annotations: %v", annotation)
-		_, err = cs.CoreV1().Services(ns.Name).Update(context.TODO(), service, metav1.UpdateOptions{})
-		Expect(err).NotTo(HaveOccurred())
-
-		ip, err = utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, serviceName, "")
-		Expect(err).NotTo(HaveOccurred())
-
-		// wait and check pls is updated also
-		err = wait.PollImmediate(10*time.Second, 5*time.Minute, func() (bool, error) {
-			pls := getPrivateLinkServiceFromIP(tc, ip, "", "", "")
-			return pls.IPConfigurations != nil &&
-				len(*pls.IPConfigurations) == 1 &&
-				(*pls.IPConfigurations)[0].PrivateIPAllocationMethod == network.IPAllocationMethodStatic &&
-				*(*pls.IPConfigurations)[0].PrivateIPAddress == selectedip, nil
-		})
-		Expect(err).NotTo(HaveOccurred())
-	})
-
-	It("should support service annotation 'service.beta.kubernetes.io/azure-pls-fqdns'", func() {
-		fqdns := "fqdns1 fqdns2"
-		expectedFqdns := map[string]bool{
-			"fqdns1": true,
-			"fqdns2": true,
-		}
-		annotation := map[string]string{
-			consts.ServiceAnnotationLoadBalancerInternal: "true",
-			consts.ServiceAnnotationPLSCreation:          "true",
-			consts.ServiceAnnotationPLSFqdns:             fqdns,
-		}
-
-		// create service with given annotation and wait it to expose
-		ip := createAndExposeDefaultServiceWithAnnotation(cs, serviceName, ns.Name, labels, annotation, ports)
-		defer func() {
-			utils.Logf("cleaning up test service %s", serviceName)
-			err := utils.DeleteService(cs, ns.Name, serviceName)
-			Expect(err).NotTo(HaveOccurred())
-		}()
-		utils.Logf("Get Internal IP: %s", ip)
-
-		// get pls from azure client
-		pls := getPrivateLinkServiceFromIP(tc, ip, "", "", "")
-		Expect(pls.Fqdns).NotTo(BeNil())
-		actualFqdns := make(map[string]bool)
-		for _, f := range *pls.Fqdns {
-			actualFqdns[f] = true
-		}
-		Expect(actualFqdns).To(Equal(expectedFqdns))
-	})
-
-	It("should support service annotation 'service.beta.kubernetes.io/azure-pls-proxy-protocol'", func() {
-		annotation := map[string]string{
-			consts.ServiceAnnotationLoadBalancerInternal: "true",
-			consts.ServiceAnnotationPLSCreation:          "true",
-			consts.ServiceAnnotationPLSProxyProtocol:     "true",
-		}
-
-		// create service with given annotation and wait it to expose
-		ip := createAndExposeDefaultServiceWithAnnotation(cs, serviceName, ns.Name, labels, annotation, ports)
-		defer func() {
-			utils.Logf("cleaning up test service %s", serviceName)
-			err := utils.DeleteService(cs, ns.Name, serviceName)
-			Expect(err).NotTo(HaveOccurred())
-		}()
-		utils.Logf("Get Internal IP: %s", ip)
-
-		// get pls from azure client
-		pls := getPrivateLinkServiceFromIP(tc, ip, "", "", "")
-		Expect(pls.EnableProxyProtocol).NotTo(BeNil())
-		Expect(*pls.EnableProxyProtocol).To(BeTrue())
-	})
-
-	It("should support service annotation 'service.beta.kubernetes.io/azure-pls-visibility'", func() {
-		vis := "00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000001"
-		expectedVis := map[string]bool{
-			"00000000-0000-0000-0000-000000000000": true,
-			"00000000-0000-0000-0000-000000000001": true,
-		}
-		annotation := map[string]string{
-			consts.ServiceAnnotationLoadBalancerInternal: "true",
-			consts.ServiceAnnotationPLSCreation:          "true",
-			consts.ServiceAnnotationPLSVisibility:        vis,
-		}
-
-		// create service with given annotation and wait it to expose
-		ip := createAndExposeDefaultServiceWithAnnotation(cs, serviceName, ns.Name, labels, annotation, ports)
-		defer func() {
-			utils.Logf("cleaning up test service %s", serviceName)
-			err := utils.DeleteService(cs, ns.Name, serviceName)
-			Expect(err).NotTo(HaveOccurred())
-		}()
-		utils.Logf("Get Internal IP: %s", ip)
-
-		// get pls from azure client
-		pls := getPrivateLinkServiceFromIP(tc, ip, "", "", "")
-		Expect(pls.Visibility).NotTo(BeNil())
-		Expect(pls.Visibility.Subscriptions).NotTo(BeNil())
-		actualVis := make(map[string]bool)
-		for _, v := range *pls.Visibility.Subscriptions {
-			actualVis[v] = true
-		}
-		Expect(actualVis).To(Equal(expectedVis))
-	})
-
-	It("should support service annotation 'service.beta.kubernetes.io/azure-pls-auto-approval'", func() {
-		autoapp := "00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000001"
-		expectedAutoApp := map[string]bool{
-			"00000000-0000-0000-0000-000000000000": true,
-			"00000000-0000-0000-0000-000000000001": true,
-		}
-		annotation := map[string]string{
-			consts.ServiceAnnotationLoadBalancerInternal: "true",
-			consts.ServiceAnnotationPLSCreation:          "true",
-			consts.ServiceAnnotationPLSVisibility:        "*",
-			consts.ServiceAnnotationPLSAutoApproval:      autoapp,
-		}
-
-		// create service with given annotation and wait it to expose
-		ip := createAndExposeDefaultServiceWithAnnotation(cs, serviceName, ns.Name, labels, annotation, ports)
-		defer func() {
-			utils.Logf("cleaning up test service %s", serviceName)
-			err := utils.DeleteService(cs, ns.Name, serviceName)
-			Expect(err).NotTo(HaveOccurred())
-		}()
-		utils.Logf("Get Internal IP: %s", ip)
-
-		// get pls from azure client
-		pls := getPrivateLinkServiceFromIP(tc, ip, "", "", "")
-		Expect(pls.AutoApproval).NotTo(BeNil())
-		Expect(pls.AutoApproval.Subscriptions).NotTo(BeNil())
-		actualAutoApp := make(map[string]bool)
-		for _, v := range *pls.AutoApproval.Subscriptions {
-			actualAutoApp[v] = true
-		}
-		Expect(actualAutoApp).To(Equal(expectedAutoApp))
-	})
-
-	It("should support multiple internal services sharing one private link service", func() {
-		ipAddrCount := 2
-		annotation := map[string]string{
-			consts.ServiceAnnotationLoadBalancerInternal:             "true",
-			consts.ServiceAnnotationPLSCreation:                      "true",
-			consts.ServiceAnnotationPLSIpConfigurationIPAddressCount: strconv.Itoa(ipAddrCount),
-		}
-		ip := createAndExposeDefaultServiceWithAnnotation(cs, "service1", ns.Name, labels, annotation, ports)
-		utils.Logf("Successfully created service1 in namespace %s with IP %s", ns.Name, ip)
-
-		ports2 := []v1.ServicePort{{
-			Port:       testingPort,
-			TargetPort: intstr.FromInt(testingPort),
-		}}
-		delete(annotation, consts.ServiceAnnotationPLSIpConfigurationIPAddressCount)
-		service2 := utils.CreateLoadBalancerServiceManifest("service2", annotation, labels, ns.Name, ports2)
-		service2.Spec.LoadBalancerIP = ip
-		_, err := cs.CoreV1().Services(ns.Name).Create(context.TODO(), service2, metav1.CreateOptions{})
-		Expect(err).NotTo(HaveOccurred())
-		_, err = utils.WaitServiceExposureAndValidateConnectivity(cs, ns.Name, "service2", ip)
-		Expect(err).NotTo(HaveOccurred())
-		utils.Logf("Successfully created service2 in namespace %s with IP %s", ns.Name, ip)
-
-		// get pls from azure client
-		pls := getPrivateLinkServiceFromIP(tc, ip, "", "", "")
-		Expect(pls.IPConfigurations).NotTo(BeNil())
-		// Verify it's still the configuration from service1
-		Expect(len(*pls.IPConfigurations)).To(Equal(ipAddrCount))
-
-		defer func() {
-			By("Cleaning up")
-			err = utils.DeleteService(cs, ns.Name, "service1")
-			Expect(err).NotTo(HaveOccurred())
-			err = utils.DeleteService(cs, ns.Name, "service2")
-			Expect(err).NotTo(HaveOccurred())
-		}()
-	})
-})
-
-func updateServiceAnnotation(service *v1.Service, annotation map[string]string) (result *v1.Service) {
-	result = service
-	if result == nil {
-		return
-	}
-	result.Annotations = annotation
-	return
-}
-
-func getPrivateLinkServiceFromIP(tc *utils.AzureTestClient, ip, plsResourceGroup, lbResourceGroup, plsName string) *network.PrivateLinkService {
-	if lbResourceGroup == "" {
-		lbResourceGroup = tc.GetResourceGroup()
-	}
-	utils.Logf("Getting load balancers in the resourceGroup " + lbResourceGroup)
-	lbList, err := tc.ListLoadBalancers(lbResourceGroup)
-	Expect(err).NotTo(HaveOccurred())
-
-	utils.Logf("Looking for internal load balancer frontend config ID with private ip as frontend")
-	var lbFipConfigName string
-	for _, lb := range lbList {
-		for _, fipconfig := range *lb.FrontendIPConfigurations {
-			if fipconfig.PrivateIPAddress != nil &&
-				*fipconfig.PrivateIPAddress == ip {
-				lbFipConfigName = *fipconfig.Name
-				break
-			}
-		}
-	}
-	Expect(lbFipConfigName).NotTo(Equal(""))
-	utils.Logf("Successfully obtained LB frontend config: %v", lbFipConfigName)
-
-	if plsName == "" {
-		plsName = fmt.Sprintf("%s-%s", "pls", lbFipConfigName)
-	}
-	if plsResourceGroup == "" {
-		plsResourceGroup = tc.GetResourceGroup()
-	}
-
-	utils.Logf("Getting private link service(%s) from rg(%s)", plsName, plsResourceGroup)
-	var pls network.PrivateLinkService
-	err = wait.PollImmediate(10*time.Second, 10*time.Minute, func() (bool, error) {
-		pls, err = tc.GetPrivateLinkService(plsResourceGroup, plsName)
-		if err != nil {
-			return false, err
-		}
-		return *pls.Name == plsName, nil
-	})
-	Expect(err).NotTo(HaveOccurred())
-
-	utils.Logf("Successfully obtained private link service")
-	return &pls
-}
diff --git a/tests/e2e/network/service_annotations.go b/tests/e2e/network/service_annotations.go
index df2fb031f..64d1e88fa 100644
--- a/tests/e2e/network/service_annotations.go
+++ b/tests/e2e/network/service_annotations.go
@@ -188,7 +188,7 @@ var _ = Describe("Service with annotation", func() {
 			By("Test subnet doesn't exist. Creating a new one...")
 			newSubnetCIDR, err = utils.GetNextSubnetCIDR(vNet)
 			Expect(err).NotTo(HaveOccurred())
-			_, err = tc.CreateSubnet(vNet, &subnetName, &newSubnetCIDR, false)
+			err = tc.CreateSubnet(vNet, &subnetName, &newSubnetCIDR)
 			Expect(err).NotTo(HaveOccurred())
 			defer func() {
 				utils.Logf("cleaning up test subnet %s", subnetName)
diff --git a/tests/e2e/utils/azure_test_client.go b/tests/e2e/utils/azure_test_client.go
index b51d92925..8cf7613c9 100644
--- a/tests/e2e/utils/azure_test_client.go
+++ b/tests/e2e/utils/azure_test_client.go
@@ -143,16 +143,11 @@ func (tc *AzureTestClient) createPublicIPAddressesClient() *aznetwork.PublicIPAd
 	return &aznetwork.PublicIPAddressesClient{BaseClient: tc.networkClient}
 }
 
-// createLoadBalancerClient generates loadbalancer client with the same baseclient as azure test client
-func (tc *AzureTestClient) createLoadBalancerClient() *aznetwork.LoadBalancersClient {
+// creteLoadBalancerClient generates loadbalancer client with the same baseclient as azure test client
+func (tc *AzureTestClient) creteLoadBalancerClient() *aznetwork.LoadBalancersClient {
 	return &aznetwork.LoadBalancersClient{BaseClient: tc.networkClient}
 }
 
-// createPrivateLinkServiceClient generates private link service client with the same baseclient as azure test client
-func (tc *AzureTestClient) createPrivateLinkServiceClient() *aznetwork.PrivateLinkServicesClient {
-	return &aznetwork.PrivateLinkServicesClient{BaseClient: tc.networkClient}
-}
-
 // createInterfacesClient generates network interface client with the same baseclient as azure test client
 func (tc *AzureTestClient) createInterfacesClient() *aznetwork.InterfacesClient {
 	return &aznetwork.InterfacesClient{BaseClient: tc.networkClient}
diff --git a/tests/e2e/utils/network_utils.go b/tests/e2e/utils/network_utils.go
index 45dd1153c..77503c674 100644
--- a/tests/e2e/utils/network_utils.go
+++ b/tests/e2e/utils/network_utils.go
@@ -22,7 +22,6 @@ import (
 	"net"
 	"strings"
 
-	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-02-01/network"
 	aznetwork "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-02-01/network"
 	"github.com/Azure/go-autorest/autorest/to"
 
@@ -69,28 +68,14 @@ func (azureTestClient *AzureTestClient) GetClusterVirtualNetwork() (virtualNetwo
 }
 
 // CreateSubnet creates a new subnet in the specified virtual network.
-func (azureTestClient *AzureTestClient) CreateSubnet(vnet aznetwork.VirtualNetwork, subnetName *string, prefix *string, waitUntilComplete bool) (network.Subnet, error) {
+func (azureTestClient *AzureTestClient) CreateSubnet(vnet aznetwork.VirtualNetwork, subnetName *string, prefix *string) error {
 	Logf("creating a new subnet %s, %s", *subnetName, *prefix)
 	subnetParameter := (*vnet.Subnets)[0]
 	subnetParameter.Name = subnetName
 	subnetParameter.AddressPrefix = prefix
 	subnetsClient := azureTestClient.createSubnetsClient()
 	_, err := subnetsClient.CreateOrUpdate(context.Background(), azureTestClient.GetResourceGroup(), *vnet.Name, *subnetName, subnetParameter)
-	var subnet network.Subnet
-	if err != nil || !waitUntilComplete {
-		return subnet, err
-	}
-	err = wait.PollImmediate(poll, singleCallTimeout, func() (bool, error) {
-		subnet, err = subnetsClient.Get(context.Background(), azureTestClient.GetResourceGroup(), *vnet.Name, *subnetName, "")
-		if err != nil {
-			if !IsRetryableAPIError(err) {
-				return false, err
-			}
-			return false, nil
-		}
-		return subnet.ID != nil, nil
-	})
-	return subnet, err
+	return err
 }
 
 // DeleteSubnet deletes a subnet with retry.
@@ -302,56 +287,8 @@ func (azureTestClient *AzureTestClient) ListPublicIPs(resourceGroupName string)
 	return result, nil
 }
 
-// ListLoadBalancers lists all the load balancers active
-func (azureTestClient *AzureTestClient) ListLoadBalancers(resourceGroupName string) ([]aznetwork.LoadBalancer, error) {
-	lbClient := azureTestClient.createLoadBalancerClient()
-
-	iterator, err := lbClient.ListComplete(context.Background(), resourceGroupName)
-	if err != nil {
-		return nil, err
-	}
-
-	result := make([]aznetwork.LoadBalancer, 0)
-	for ; iterator.NotDone(); err = iterator.Next() {
-		if err != nil {
-			return nil, err
-		}
-
-		result = append(result, iterator.Value())
-	}
-
-	return result, nil
-}
-
 // GetLoadBalancer gets aznetwork.LoadBalancer by loadBalancer name.
 func (azureTestClient *AzureTestClient) GetLoadBalancer(resourceGroupName, lbName string) (aznetwork.LoadBalancer, error) {
-	lbClient := azureTestClient.createLoadBalancerClient()
+	lbClient := azureTestClient.creteLoadBalancerClient()
 	return lbClient.Get(context.Background(), resourceGroupName, lbName, "")
 }
-
-// GetPrivateLinkService gets aznetwork.PrivateLinkService by privateLinkService name.
-func (azureTestClient *AzureTestClient) GetPrivateLinkService(resourceGroupName, plsName string) (aznetwork.PrivateLinkService, error) {
-	plsClient := azureTestClient.createPrivateLinkServiceClient()
-	return plsClient.Get(context.Background(), resourceGroupName, plsName, "")
-}
-
-// ListPrivateLinkServices lists all the private link services active
-func (azureTestClient *AzureTestClient) ListPrivateLinkServices(resourceGroupName string) ([]aznetwork.PrivateLinkService, error) {
-	plsClient := azureTestClient.createPrivateLinkServiceClient()
-
-	iterator, err := plsClient.ListComplete(context.Background(), resourceGroupName)
-	if err != nil {
-		return nil, err
-	}
-
-	result := make([]aznetwork.PrivateLinkService, 0)
-	for ; iterator.NotDone(); err = iterator.Next() {
-		if err != nil {
-			return nil, err
-		}
-
-		result = append(result, iterator.Value())
-	}
-
-	return result, nil
-}