Repository: kubernetes-sigs/cloud-provider-azure
Pull request #3898 (2023-05-18 10:55:50)

[DualStack] Support NSG and clean LBs
<!--  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 feature
<!--
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:
* Support NSG and clean LBs for dualstack
* Support related UTs for dualstack
* Refactor
#### 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
* Support NSG and clean LBs for dualstack
* Support related UTs for dualstack
* Refactor
```

#### 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

```


netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *kubernetes-sigs-cloud-provide-azure* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | acc8ea549e1d49431248a6e1fc18122a2d22cdd6 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/kubernetes-sigs-cloud-provide-azure/deploys/64660438a52b660007a33825 |
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/3898#" 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/master/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-e2e-ccm-vmss-ip-lb-capz | acc8ea549e1d49431248a6e1fc18122a2d22cdd6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz/1659150805913047040) | false | `/test pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-ccm-vmss-ip-lb-capz | acc8ea549e1d49431248a6e1fc18122a2d22cdd6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz/1659150805913047040) | false | `/test pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz`
pull-cloud-provider-azure-e2e-ccm-capz | acc8ea549e1d49431248a6e1fc18122a2d22cdd6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-capz/1659150805850132480) | true | `/test pull-cloud-provider-azure-e2e-ccm-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-ccm-vmss-ip-lb-capz | acc8ea549e1d49431248a6e1fc18122a2d22cdd6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz/1659150805913047040) | false | `/test pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz`
pull-cloud-provider-azure-e2e-ccm-capz | acc8ea549e1d49431248a6e1fc18122a2d22cdd6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-capz/1659150805850132480) | true | `/test pull-cloud-provider-azure-e2e-ccm-capz`
pull-cloud-provider-azure-e2e-ccm-vmssflex-capz | acc8ea549e1d49431248a6e1fc18122a2d22cdd6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1659150805749469184) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-ccm-vmss-ip-lb-capz | acc8ea549e1d49431248a6e1fc18122a2d22cdd6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz/1659150805913047040) | false | `/test pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz`
pull-cloud-provider-azure-e2e-ccm-capz | acc8ea549e1d49431248a6e1fc18122a2d22cdd6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-capz/1659150805850132480) | true | `/test pull-cloud-provider-azure-e2e-ccm-capz`
pull-cloud-provider-azure-e2e-ccm-vmssflex-capz | acc8ea549e1d49431248a6e1fc18122a2d22cdd6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1659150805749469184) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-capz | acc8ea549e1d49431248a6e1fc18122a2d22cdd6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-capz/1659150805699137536) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-ccm-vmssflex-capz | acc8ea549e1d49431248a6e1fc18122a2d22cdd6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1659150805749469184) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-capz | acc8ea549e1d49431248a6e1fc18122a2d22cdd6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-capz/1659150805699137536) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-capz`
pull-cloud-provider-azure-e2e-ccm-capz | c53a81189b5d74204e44102a291bacb4d18d0e5a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-capz/1660947147245228032) | true | `/test pull-cloud-provider-azure-e2e-ccm-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-ccm-vmss-capz | acc8ea549e1d49431248a6e1fc18122a2d22cdd6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-capz/1659150805699137536) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-capz`
pull-cloud-provider-azure-e2e-ccm-capz | c53a81189b5d74204e44102a291bacb4d18d0e5a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-capz/1660947147245228032) | true | `/test pull-cloud-provider-azure-e2e-ccm-capz`
pull-cloud-provider-azure-e2e-ccm-vmssflex-capz | c53a81189b5d74204e44102a291bacb4d18d0e5a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1660947147115204608) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-ccm-capz | c53a81189b5d74204e44102a291bacb4d18d0e5a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-capz/1660947147245228032) | true | `/test pull-cloud-provider-azure-e2e-ccm-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-capz | c53a81189b5d74204e44102a291bacb4d18d0e5a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-capz/1660947147060678656) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-capz`
pull-cloud-provider-azure-e2e-ccm-vmssflex-capz | c53a81189b5d74204e44102a291bacb4d18d0e5a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1661202759858262016) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-ccm-capz | c53a81189b5d74204e44102a291bacb4d18d0e5a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-capz/1660947147245228032) | true | `/test pull-cloud-provider-azure-e2e-ccm-capz`
pull-cloud-provider-azure-e2e-ccm-vmssflex-capz | c53a81189b5d74204e44102a291bacb4d18d0e5a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1661202759858262016) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-capz | c53a81189b5d74204e44102a291bacb4d18d0e5a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-capz/1661202759807930368) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-ccm-vmssflex-capz | c53a81189b5d74204e44102a291bacb4d18d0e5a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1661202759858262016) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-capz | c53a81189b5d74204e44102a291bacb4d18d0e5a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-capz/1661202759807930368) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-capz`
pull-cloud-provider-azure-e2e-ccm-capz | c53a81189b5d74204e44102a291bacb4d18d0e5a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-capz/1661202759954731008) | true | `/test pull-cloud-provider-azure-e2e-ccm-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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 -->
lzhecheng: /retest
k8s-ci-robot: PR needs rebase.

<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.
</details>
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-e2e-ccm-vmssflex-capz | c53a81189b5d74204e44102a291bacb4d18d0e5a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1661202759858262016) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-capz | c53a81189b5d74204e44102a291bacb4d18d0e5a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-capz/1661202759807930368) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-capz`
pull-cloud-provider-azure-e2e-ccm-capz | 141670bd76abdd790bbdb9b0b7a26eb2b7da6924 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-capz/1661565923410055168) | true | `/test pull-cloud-provider-azure-e2e-ccm-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-ccm-vmss-capz | c53a81189b5d74204e44102a291bacb4d18d0e5a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-capz/1661202759807930368) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-capz`
pull-cloud-provider-azure-e2e-ccm-capz | 141670bd76abdd790bbdb9b0b7a26eb2b7da6924 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-capz/1661565923410055168) | true | `/test pull-cloud-provider-azure-e2e-ccm-capz`
pull-cloud-provider-azure-e2e-ccm-vmssflex-capz | 141670bd76abdd790bbdb9b0b7a26eb2b7da6924 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1661565923296808960) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-ccm-capz | 141670bd76abdd790bbdb9b0b7a26eb2b7da6924 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-capz/1661565923410055168) | true | `/test pull-cloud-provider-azure-e2e-ccm-capz`
pull-cloud-provider-azure-e2e-ccm-vmssflex-capz | 141670bd76abdd790bbdb9b0b7a26eb2b7da6924 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1661565923296808960) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-capz | 141670bd76abdd790bbdb9b0b7a26eb2b7da6924 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-capz/1661565923238088704) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-ccm-capz | e0541367a85ad2930792a9909b85ee95670d9444 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-capz/1661651351077130240) | true | `/test pull-cloud-provider-azure-e2e-ccm-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz | d51e7e1c12c148a7a28d4d8194aa05285998b916 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz/1662052263012601856) | false | `/test pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-ccm-vmss-ip-lb-capz | d51e7e1c12c148a7a28d4d8194aa05285998b916 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz/1662052263012601856) | false | `/test pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz`
pull-cloud-provider-azure-check | 7217c25bf55afb753f0b543c562e8c83cccfb363 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-check/1663615032883154944) | true | `/test pull-cloud-provider-azure-check`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-ccm-vmss-ip-lb-capz | d51e7e1c12c148a7a28d4d8194aa05285998b916 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz/1662052263012601856) | false | `/test pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz`
pull-cloud-provider-azure-check | 7217c25bf55afb753f0b543c562e8c83cccfb363 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-check/1663615032883154944) | true | `/test pull-cloud-provider-azure-check`
pull-cloud-provider-azure-e2e-capz | 7217c25bf55afb753f0b543c562e8c83cccfb363 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-capz/1663614671338344448) | true | `/test pull-cloud-provider-azure-e2e-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-check | 7217c25bf55afb753f0b543c562e8c83cccfb363 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-check/1663615032883154944) | true | `/test pull-cloud-provider-azure-check`
pull-cloud-provider-azure-e2e-capz | 7217c25bf55afb753f0b543c562e8c83cccfb363 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-capz/1663614671338344448) | true | `/test pull-cloud-provider-azure-e2e-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz | 7217c25bf55afb753f0b543c562e8c83cccfb363 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz/1663614404593192960) | false | `/test pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-check | 7217c25bf55afb753f0b543c562e8c83cccfb363 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-check/1663615032883154944) | true | `/test pull-cloud-provider-azure-check`
pull-cloud-provider-azure-e2e-capz | 7217c25bf55afb753f0b543c562e8c83cccfb363 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-capz/1663614671338344448) | true | `/test pull-cloud-provider-azure-e2e-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz | 7217c25bf55afb753f0b543c562e8c83cccfb363 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz/1663614404593192960) | false | `/test pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-capz | 7217c25bf55afb753f0b543c562e8c83cccfb363 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-capz/1663614861763940352) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-check | 7217c25bf55afb753f0b543c562e8c83cccfb363 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-check/1663615032883154944) | true | `/test pull-cloud-provider-azure-check`
pull-cloud-provider-azure-e2e-capz | 7217c25bf55afb753f0b543c562e8c83cccfb363 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-capz/1663614671338344448) | true | `/test pull-cloud-provider-azure-e2e-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz | 7217c25bf55afb753f0b543c562e8c83cccfb363 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz/1663614404593192960) | false | `/test pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-capz | 7217c25bf55afb753f0b543c562e8c83cccfb363 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-capz/1663614861763940352) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-capz`
pull-cloud-provider-azure-e2e-ccm-vmssflex-capz | 7217c25bf55afb753f0b543c562e8c83cccfb363 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1663615037928902656) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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 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-e2e-ccm-vmssflex-capz | 669012f3d0e87109b8be1663c1a41c354dd5eb32 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1665527157914341376) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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 -->
lzhecheng: /test pull-cloud-provider-azure-e2e-ccm-ipv6-capz
/test pull-cloud-provider-azure-e2e-ccm-ipv6-vmss-capz
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-e2e-ccm-vmssflex-capz | ed6e4704091b68a57c05222479d0c7c807af7d38 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1666016129841106944) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-ccm-vmssflex-capz | ed6e4704091b68a57c05222479d0c7c807af7d38 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1666016129841106944) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`
pull-cloud-provider-azure-e2e-ccm-ipv6-vmss-capz | ed6e4704091b68a57c05222479d0c7c807af7d38 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-ipv6-vmss-capz/1666016749692129280) | true | `/test pull-cloud-provider-azure-e2e-ccm-ipv6-vmss-capz`
pull-cloud-provider-azure-e2e-ccm-ipv6-capz | ed6e4704091b68a57c05222479d0c7c807af7d38 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-ipv6-capz/1666016749755043840) | true | `/test pull-cloud-provider-azure-e2e-ccm-ipv6-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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 -->
lzhecheng: /test pull-cloud-provider-azure-e2e-ccm-ipv6-capz
/test pull-cloud-provider-azure-e2e-ccm-ipv6-vmss-capz
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-e2e-ccm-vmssflex-capz | ed6e4704091b68a57c05222479d0c7c807af7d38 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1666016129841106944) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`
pull-cloud-provider-azure-e2e-ccm-ipv6-capz | 59de6d336b15f3f1a22aa6411ee1a9d3f91e7b36 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-ipv6-capz/1666271073252937728) | true | `/test pull-cloud-provider-azure-e2e-ccm-ipv6-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-ccm-ipv6-capz | 59de6d336b15f3f1a22aa6411ee1a9d3f91e7b36 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-ipv6-capz/1666271073252937728) | true | `/test pull-cloud-provider-azure-e2e-ccm-ipv6-capz`
pull-cloud-provider-azure-e2e-ccm-vmssflex-capz | 59de6d336b15f3f1a22aa6411ee1a9d3f91e7b36 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1666262300576714752) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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 -->
lzhecheng: /retest
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-e2e-ccm-vmssflex-capz | 59de6d336b15f3f1a22aa6411ee1a9d3f91e7b36 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1666320495370309632) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz | 4192a01bce686f05e2fbbc162830c14f23ef1d87 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz/1666400430202032128) | false | `/test pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-ccm-vmssflex-capz | 59de6d336b15f3f1a22aa6411ee1a9d3f91e7b36 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1666320495370309632) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz | 4192a01bce686f05e2fbbc162830c14f23ef1d87 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz/1666400430202032128) | false | `/test pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-capz | 4192a01bce686f05e2fbbc162830c14f23ef1d87 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-capz/1666400429979734016) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-ccm-vmssflex-capz | 59de6d336b15f3f1a22aa6411ee1a9d3f91e7b36 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1666320495370309632) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz | 4192a01bce686f05e2fbbc162830c14f23ef1d87 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz/1666400430202032128) | false | `/test pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-capz | 4192a01bce686f05e2fbbc162830c14f23ef1d87 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-capz/1666400429979734016) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-capz`
pull-cloud-provider-azure-unit | 4192a01bce686f05e2fbbc162830c14f23ef1d87 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-unit/1666400430252363776) | true | `/test pull-cloud-provider-azure-unit`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-ccm-vmss-ip-lb-capz | 4192a01bce686f05e2fbbc162830c14f23ef1d87 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz/1666400430202032128) | false | `/test pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-capz | 4192a01bce686f05e2fbbc162830c14f23ef1d87 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-capz/1666400429979734016) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-capz`
pull-cloud-provider-azure-unit | 4192a01bce686f05e2fbbc162830c14f23ef1d87 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-unit/1666400430252363776) | true | `/test pull-cloud-provider-azure-unit`
pull-cloud-provider-azure-e2e-ccm-vmssflex-capz | 4192a01bce686f05e2fbbc162830c14f23ef1d87 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1666400430042648576) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`
pull-cloud-provider-azure-e2e-capz | 4192a01bce686f05e2fbbc162830c14f23ef1d87 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-capz/1666400430084591616) | true | `/test pull-cloud-provider-azure-e2e-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-ccm-vmss-ip-lb-capz | 4192a01bce686f05e2fbbc162830c14f23ef1d87 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz/1666400430202032128) | false | `/test pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-capz | 4192a01bce686f05e2fbbc162830c14f23ef1d87 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-capz/1666400429979734016) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmss-capz`
pull-cloud-provider-azure-unit | 4192a01bce686f05e2fbbc162830c14f23ef1d87 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-unit/1666400430252363776) | true | `/test pull-cloud-provider-azure-unit`
pull-cloud-provider-azure-e2e-ccm-vmssflex-capz | 4192a01bce686f05e2fbbc162830c14f23ef1d87 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1666400430042648576) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`
pull-cloud-provider-azure-e2e-capz | 4192a01bce686f05e2fbbc162830c14f23ef1d87 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-capz/1666400430084591616) | true | `/test pull-cloud-provider-azure-e2e-capz`
pull-cloud-provider-azure-e2e-ccm-capz | 4192a01bce686f05e2fbbc162830c14f23ef1d87 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-capz/1666400430151700480) | true | `/test pull-cloud-provider-azure-e2e-ccm-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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 -->
coveralls: 
[![Coverage Status](https://coveralls.io/builds/60582020/badge)](https://coveralls.io/builds/60582020)

coverage: 79.914% (+0.03%) from 79.885% when pulling **98a237d535504844d6dafc53ece7c3e524528341 on lzhecheng:dualstack-nsg** into **e5a3523243e28f0a1c2cf7ee883892a7e323667c on kubernetes-sigs:master**.

lzhecheng: /retest
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-e2e-ccm-vmssflex-capz | e02069b29ae2dc5188c07ddbb78eef146d12b69e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmssflex-capz/1666693449446854656) | true | `/test pull-cloud-provider-azure-e2e-ccm-vmssflex-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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 -->
lzhecheng: /test pull-cloud-provider-azure-e2e-ccm-ipv6-capz
/test pull-cloud-provider-azure-e2e-ccm-ipv6-vmss-capz
/test pull-cloud-provider-azure-e2e-ccm-dualstack-capz
/test pull-cloud-provider-azure-e2e-ccm-dualstack-vmss-capz
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-e2e-ccm-capz | d352258c7a635d6bce424bc6356f24c2403db222 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-capz/1668166814187655168) | true | `/test pull-cloud-provider-azure-e2e-ccm-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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 -->
lzhecheng: /test pull-cloud-provider-azure-e2e-ccm-capz
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-e2e-capz | 27e816a1bad400d9096018088df4de695ab090e5 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-capz/1669210746065522688) | true | `/test pull-cloud-provider-azure-e2e-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-capz | 27e816a1bad400d9096018088df4de695ab090e5 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-capz/1669210746065522688) | true | `/test pull-cloud-provider-azure-e2e-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz | 27e816a1bad400d9096018088df4de695ab090e5 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz/1669210746178768896) | false | `/test pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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-e2e-capz | 27e816a1bad400d9096018088df4de695ab090e5 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-capz/1669210746065522688) | true | `/test pull-cloud-provider-azure-e2e-capz`
pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz | 27e816a1bad400d9096018088df4de695ab090e5 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz/1669210746178768896) | false | `/test pull-cloud-provider-azure-e2e-ccm-vmss-ip-lb-capz`
pull-cloud-provider-azure-e2e-ccm-capz | 27e816a1bad400d9096018088df4de695ab090e5 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cloud-provider-azure/3898/pull-cloud-provider-azure-e2e-ccm-capz/1669210746120048640) | true | `/test pull-cloud-provider-azure-e2e-ccm-capz`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cloud-provider-azure&pr=3898). [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 -->
lzhecheng: /retest

Patch:
diff --git a/pkg/consts/consts.go b/pkg/consts/consts.go
index 9d31d2752..0cbbbb819 100644
--- a/pkg/consts/consts.go
+++ b/pkg/consts/consts.go
@@ -194,6 +194,12 @@ const (
 	BackoffJitterDefault = 1.0
 )
 
+// IP family variables
+const (
+	IPVersionIPv6 bool = true
+	IPVersionIPv4 bool = false
+)
+
 // LB variables for dual-stack
 var (
 	// Service.Spec.LoadBalancerIP has been deprecated and may be removed in a future release. Those two annotations are introduced as alternatives to set IPv4/IPv6 LoadBalancer IPs.
diff --git a/pkg/consts/helpers.go b/pkg/consts/helpers.go
index e8a6a6fa8..9706b55aa 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 kubernetes 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)
-}
-
 // IsK8sServiceDisableLoadBalancerFloatingIP return if floating IP in load balancer is disabled in kubernetes service annotations
 func IsK8sServiceDisableLoadBalancerFloatingIP(service *v1.Service) bool {
 	return expectAttributeInSvcAnnotationBeEqualTo(service.Annotations, ServiceAnnotationDisableLoadBalancerFloatingIP, TrueAnnotationValue)
diff --git a/pkg/provider/azure_loadbalancer.go b/pkg/provider/azure_loadbalancer.go
index 713d99ed8..d1cc4b3b7 100644
--- a/pkg/provider/azure_loadbalancer.go
+++ b/pkg/provider/azure_loadbalancer.go
@@ -68,10 +68,10 @@ func (az *Cloud) existsPip(clusterName string, service *v1.Service) bool {
 		return existingPip
 	}
 
-	if v4Enabled && !existsPipSingleStack(false) {
+	if v4Enabled && !existsPipSingleStack(consts.IPVersionIPv4) {
 		return false
 	}
-	if v6Enabled && !existsPipSingleStack(true) {
+	if v6Enabled && !existsPipSingleStack(consts.IPVersionIPv6) {
 		return false
 	}
 	return true
@@ -85,13 +85,13 @@ func (az *Cloud) GetLoadBalancer(ctx context.Context, clusterName string, servic
 		return nil, az.existsPip(clusterName, service), err
 	}
 
-	_, status, existsLb, err := az.getServiceLoadBalancer(service, clusterName, nil, false, existingLBs)
+	_, status, _, existsLb, err := az.getServiceLoadBalancer(service, clusterName, nil, false, existingLBs)
 	if err != nil || existsLb {
 		return status, existsLb || az.existsPip(clusterName, service), err
 	}
 
 	flippedService := flipServiceInternalAnnotation(service)
-	_, status, existsLb, err = az.getServiceLoadBalancer(flippedService, clusterName, nil, false, existingLBs)
+	_, status, _, existsLb, err = az.getServiceLoadBalancer(flippedService, clusterName, nil, false, existingLBs)
 	if err != nil || existsLb {
 		return status, existsLb || az.existsPip(clusterName, service), err
 	}
@@ -126,7 +126,7 @@ func (az *Cloud) reconcileService(ctx context.Context, clusterName string, servi
 	resourceBaseName := az.GetLoadBalancerName(context.TODO(), "", service)
 	klog.V(2).Infof("reconcileService: Start reconciling Service %q with its resource basename %q", serviceName, resourceBaseName)
 
-	lbStatus, fipConfig, err := az.getServiceLoadBalancerStatus(service, lb)
+	lbStatus, lbIPsPrimaryPIPs, fipConfigs, err := az.getServiceLoadBalancerStatus(service, lb)
 	if err != nil {
 		klog.Errorf("getServiceLoadBalancerStatus(%s) failed: %v", serviceName, err)
 		if !errors.Is(err, ErrorNotVmssInstance) {
@@ -134,25 +134,21 @@ func (az *Cloud) reconcileService(ctx context.Context, clusterName string, servi
 		}
 	}
 
-	var serviceIP *string
-	if lbStatus != nil && len(lbStatus.Ingress) > 0 {
-		serviceIP = &lbStatus.Ingress[0].IP
-	}
-
-	klog.V(2).Infof("reconcileService: reconciling security group for service %q with IP %q, wantLb = true", serviceName, logSafe(serviceIP))
-	if _, err := az.reconcileSecurityGroup(clusterName, service, serviceIP, lb.Name, true /* wantLb */); err != nil {
+	serviceIPs := lbIPsPrimaryPIPs
+	klog.V(2).Infof("reconcileService: reconciling security group for service %q with IPs %q, wantLb = true", serviceName, serviceIPs)
+	if _, err := az.reconcileSecurityGroup(clusterName, service, &serviceIPs, lb.Name, true /* wantLb */); err != nil {
 		klog.Errorf("reconcileSecurityGroup(%s) failed: %#v", serviceName, err)
 		return nil, err
 	}
 
-	if fipConfig != nil {
+	for _, fipConfig := range fipConfigs {
 		if err := az.reconcilePrivateLinkService(clusterName, service, fipConfig, true /* wantPLS */); err != nil {
 			klog.Errorf("reconcilePrivateLinkService(%s) failed: %#v", serviceName, err)
 			return nil, err
 		}
 	}
 
-	updateService := updateServiceLoadBalancerIP(service, pointer.StringDeref(serviceIP, ""))
+	updateService := updateServiceLoadBalancerIPs(service, lbIPsPrimaryPIPs)
 	flippedService := flipServiceInternalAnnotation(updateService)
 	if _, err := az.reconcileLoadBalancer(clusterName, flippedService, nil, false /* wantLb */); err != nil {
 		klog.Errorf("reconcileLoadBalancer(%s) failed: %#v", serviceName, err)
@@ -280,13 +276,13 @@ func (az *Cloud) EnsureLoadBalancerDeleted(ctx context.Context, clusterName stri
 		klog.V(5).InfoS("EnsureLoadBalancerDeleted Finish", "service", serviceName, "cluster", clusterName, "service_spec", service, "error", err)
 	}()
 
-	serviceIPToCleanup, err := az.findServiceIPAddress(ctx, clusterName, service)
+	_, _, lbIPsPrimaryPIPs, _, err := az.getServiceLoadBalancer(service, clusterName, nil, false, []network.LoadBalancer{})
 	if err != nil && !retry.HasStatusForbiddenOrIgnoredError(err) {
 		return err
 	}
-
-	klog.V(2).Infof("EnsureLoadBalancerDeleted: reconciling security group for service %q with IP %q, wantLb = false", serviceName, serviceIPToCleanup)
-	_, err = az.reconcileSecurityGroup(clusterName, service, &serviceIPToCleanup, nil, false /* wantLb */)
+	serviceIPsToCleanup := lbIPsPrimaryPIPs
+	klog.V(2).Infof("EnsureLoadBalancerDeleted: reconciling security group for service %q with IPs %q, wantLb = false", serviceName, serviceIPsToCleanup)
+	_, err = az.reconcileSecurityGroup(clusterName, service, &serviceIPsToCleanup, nil, false /* wantLb */)
 	if err != nil {
 		return err
 	}
@@ -302,8 +298,7 @@ func (az *Cloud) EnsureLoadBalancerDeleted(ctx context.Context, clusterName stri
 		return err
 	}
 
-	_, err = az.reconcilePublicIPs(clusterName, service, "", false /* wantLb */)
-	if err != nil {
+	if _, err = az.reconcilePublicIPs(clusterName, service, "", false /* wantLb */); err != nil {
 		return err
 	}
 
@@ -368,15 +363,17 @@ func (az *Cloud) shouldChangeLoadBalancer(service *v1.Service, currLBName, clust
 	return true
 }
 
-func (az *Cloud) removeFrontendIPConfigurationFromLoadBalancer(lb *network.LoadBalancer, existingLBs []network.LoadBalancer, fip *network.FrontendIPConfiguration, clusterName string, service *v1.Service) error {
+func (az *Cloud) removeFrontendIPConfigurationFromLoadBalancer(lb *network.LoadBalancer, existingLBs []network.LoadBalancer, fips []*network.FrontendIPConfiguration, clusterName string, service *v1.Service) error {
 	if lb == nil || lb.LoadBalancerPropertiesFormat == nil || lb.FrontendIPConfigurations == nil {
 		return nil
 	}
 	fipConfigs := *lb.FrontendIPConfigurations
 	for i, fipConfig := range fipConfigs {
-		if strings.EqualFold(pointer.StringDeref(fipConfig.Name, ""), pointer.StringDeref(fip.Name, "")) {
-			fipConfigs = append(fipConfigs[:i], fipConfigs[i+1:]...)
-			break
+		for _, fip := range fips {
+			if strings.EqualFold(pointer.StringDeref(fipConfig.Name, ""), pointer.StringDeref(fip.Name, "")) {
+				fipConfigs = append(fipConfigs[:i], fipConfigs[i+1:]...)
+				break
+			}
 		}
 	}
 	lb.FrontendIPConfigurations = &fipConfigs
@@ -385,8 +382,10 @@ func (az *Cloud) removeFrontendIPConfigurationFromLoadBalancer(lb *network.LoadB
 	if lb.LoadBalancingRules != nil {
 		lbRules := *lb.LoadBalancingRules
 		for i := len(lbRules) - 1; i >= 0; i-- {
-			if strings.Contains(pointer.StringDeref(lbRules[i].Name, ""), pointer.StringDeref(fip.Name, "")) {
-				lbRules = append(lbRules[:i], lbRules[i+1:]...)
+			for _, fip := range fips {
+				if strings.Contains(pointer.StringDeref(lbRules[i].Name, ""), pointer.StringDeref(fip.Name, "")) {
+					lbRules = append(lbRules[:i], lbRules[i+1:]...)
+				}
 			}
 		}
 		lb.LoadBalancingRules = &lbRules
@@ -394,32 +393,41 @@ func (az *Cloud) removeFrontendIPConfigurationFromLoadBalancer(lb *network.LoadB
 	if lb.Probes != nil {
 		lbProbes := *lb.Probes
 		for i := len(lbProbes) - 1; i >= 0; i-- {
-			if strings.Contains(pointer.StringDeref(lbProbes[i].Name, ""), pointer.StringDeref(fip.Name, "")) {
-				lbProbes = append(lbProbes[:i], lbProbes[i+1:]...)
+			for _, fip := range fips {
+				if strings.Contains(pointer.StringDeref(lbProbes[i].Name, ""), pointer.StringDeref(fip.Name, "")) {
+					lbProbes = append(lbProbes[:i], lbProbes[i+1:]...)
+				}
 			}
 		}
 		lb.Probes = &lbProbes
 	}
 
-	// clean up any private link service associated with the frontEndIPConfig
-	err := az.reconcilePrivateLinkService(clusterName, service, fip, false /* wantPLS */)
-	if err != nil {
-		klog.Errorf("removeFrontendIPConfigurationFromLoadBalancer(%s, %s, %s, %s): failed to clean up PLS: %v", pointer.StringDeref(lb.Name, ""), pointer.StringDeref(fip.Name, ""), clusterName, service.Name, err)
-		return err
+	// PLS does not support IPv6 so there will not be additional API calls.
+	for _, fip := range fips {
+		// clean up any private link service associated with the frontEndIPConfig
+		if err := az.reconcilePrivateLinkService(clusterName, service, fip, false /* wantPLS */); err != nil {
+			klog.Errorf("removeFrontendIPConfigurationFromLoadBalancer(%s, %s, %s, %s): failed to clean up PLS: %v", pointer.StringDeref(lb.Name, ""), pointer.StringDeref(fip.Name, ""), clusterName, service.Name, err)
+			return err
+		}
 	}
 
+	fipNames := []string{}
+	for _, fip := range fips {
+		fipNames = append(fipNames, pointer.StringDeref(fip.Name, ""))
+	}
+	logPrefix := fmt.Sprintf("removeFrontendIPConfigurationFromLoadBalancer(%s, %q, %s, %s)", pointer.StringDeref(lb.Name, ""), fipNames, clusterName, service.Name)
 	if len(fipConfigs) == 0 {
-		klog.V(2).Infof("removeFrontendIPConfigurationFromLoadBalancer(%s, %s, %s, %s): deleting load balancer because there is no remaining frontend IP configurations", pointer.StringDeref(lb.Name, ""), pointer.StringDeref(fip.Name, ""), clusterName, service.Name)
+		klog.V(2).Infof("%s: deleting load balancer because there is no remaining frontend IP configurations", logPrefix)
 		err := az.cleanOrphanedLoadBalancer(lb, existingLBs, service, clusterName)
 		if err != nil {
-			klog.Errorf("removeFrontendIPConfigurationFromLoadBalancer(%s, %s, %s, %s): failed to cleanupOrphanedLoadBalancer: %v", pointer.StringDeref(lb.Name, ""), pointer.StringDeref(fip.Name, ""), clusterName, service.Name, err)
+			klog.Errorf("%s: failed to cleanupOrphanedLoadBalancer: %v", logPrefix, err)
 			return err
 		}
 	} else {
-		klog.V(2).Infof("removeFrontendIPConfigurationFromLoadBalancer(%s, %s, %s, %s): updating the load balancer", pointer.StringDeref(lb.Name, ""), pointer.StringDeref(fip.Name, ""), clusterName, service.Name)
+		klog.V(2).Infof("%s: updating the load balancer", logPrefix)
 		err := az.CreateOrUpdateLB(service, *lb)
 		if err != nil {
-			klog.Errorf("removeFrontendIPConfigurationFromLoadBalancer(%s, %s, %s, %s): failed to CreateOrUpdateLB: %v", pointer.StringDeref(lb.Name, ""), pointer.StringDeref(fip.Name, ""), clusterName, service.Name, err)
+			klog.Errorf("%s: failed to CreateOrUpdateLB: %v", logPrefix, err)
 			return err
 		}
 		_ = az.lbCache.Delete(pointer.StringDeref(lb.Name, ""))
@@ -431,9 +439,15 @@ func (az *Cloud) cleanOrphanedLoadBalancer(lb *network.LoadBalancer, existingLBs
 	lbName := pointer.StringDeref(lb.Name, "")
 	serviceName := getServiceName(service)
 	isBackendPoolPreConfigured := az.isBackendPoolPreConfigured(service)
-	isIPv6 := utilnet.IsIPv6String(service.Spec.ClusterIP)
-	lbBackendPoolName := getBackendPoolName(clusterName, isIPv6)
-	lbBackendPoolID := az.getBackendPoolID(lbName, lbBackendPoolName)
+	v4Enabled, v6Enabled := getIPFamiliesEnabled(service)
+	lbBackendPoolIDs := az.getBackendPoolIDs(clusterName, lbName)
+	lbBackendPoolIDsToDelete := []string{}
+	if v4Enabled {
+		lbBackendPoolIDsToDelete = append(lbBackendPoolIDsToDelete, lbBackendPoolIDs[consts.IPVersionIPv4])
+	}
+	if v6Enabled {
+		lbBackendPoolIDsToDelete = append(lbBackendPoolIDsToDelete, lbBackendPoolIDs[consts.IPVersionIPv6])
+	}
 	if isBackendPoolPreConfigured {
 		klog.V(2).Infof("cleanOrphanedLoadBalancer(%s, %s, %s): ignore cleanup of dirty lb because the lb is pre-configured", lbName, serviceName, clusterName)
 	} else {
@@ -460,8 +474,7 @@ func (az *Cloud) cleanOrphanedLoadBalancer(lb *network.LoadBalancer, existingLBs
 			lb.BackendAddressPools = nil
 		}
 
-		deleteErr := az.safeDeleteLoadBalancer(*lb, clusterName, vmSetName, service)
-		if deleteErr != nil {
+		if deleteErr := az.safeDeleteLoadBalancer(*lb, clusterName, vmSetName, service); deleteErr != nil {
 			klog.Warningf("cleanOrphanedLoadBalancer(%s, %s, %s): failed to DeleteLB: %v", lbName, serviceName, clusterName, deleteErr)
 
 			rgName, vmssName, parseErr := retry.GetVMSSMetadataByRawError(deleteErr)
@@ -485,14 +498,12 @@ func (az *Cloud) cleanOrphanedLoadBalancer(lb *network.LoadBalancer, existingLBs
 			}
 
 			vmssNamesMap := map[string]bool{vmssName: true}
-			err := az.VMSet.EnsureBackendPoolDeletedFromVMSets(vmssNamesMap, []string{lbBackendPoolID})
-			if err != nil {
+			if err := az.VMSet.EnsureBackendPoolDeletedFromVMSets(vmssNamesMap, lbBackendPoolIDsToDelete); err != nil {
 				klog.Errorf("cleanOrphanedLoadBalancer(%s, %s, %s): failed to EnsureBackendPoolDeletedFromVMSets: %v", lbName, serviceName, clusterName, err)
 				return err
 			}
 
-			deleteErr := az.DeleteLB(service, lbName)
-			if deleteErr != nil {
+			if deleteErr := az.DeleteLB(service, lbName); deleteErr != nil {
 				klog.Errorf("cleanOrphanedLoadBalancer(%s, %s, %s): failed delete lb for the second time, stop retrying: %v", lbName, serviceName, clusterName, deleteErr)
 				return deleteErr.Error()
 			}
@@ -504,16 +515,21 @@ func (az *Cloud) cleanOrphanedLoadBalancer(lb *network.LoadBalancer, existingLBs
 
 // safeDeleteLoadBalancer deletes the load balancer after decoupling it from the vmSet
 func (az *Cloud) safeDeleteLoadBalancer(lb network.LoadBalancer, clusterName, vmSetName string, service *v1.Service) *retry.Error {
-	isIPv6 := utilnet.IsIPv6String(service.Spec.ClusterIP)
-	lbBackendPoolID := az.getBackendPoolID(pointer.StringDeref(lb.Name, ""), getBackendPoolName(clusterName, isIPv6))
-	_, err := az.VMSet.EnsureBackendPoolDeleted(service, []string{lbBackendPoolID}, vmSetName, lb.BackendAddressPools, true)
-	if err != nil {
+	lbBackendPoolIDs := az.getBackendPoolIDs(clusterName, pointer.StringDeref(lb.Name, ""))
+	lbBackendPoolIDsToDelete := []string{}
+	v4Enabled, v6Enabled := getIPFamiliesEnabled(service)
+	if v4Enabled {
+		lbBackendPoolIDsToDelete = append(lbBackendPoolIDsToDelete, lbBackendPoolIDs[consts.IPVersionIPv4])
+	}
+	if v6Enabled {
+		lbBackendPoolIDsToDelete = append(lbBackendPoolIDsToDelete, lbBackendPoolIDs[consts.IPVersionIPv6])
+	}
+	if _, err := az.VMSet.EnsureBackendPoolDeleted(service, lbBackendPoolIDsToDelete, vmSetName, lb.BackendAddressPools, true); err != nil {
 		return retry.NewError(false, fmt.Errorf("safeDeleteLoadBalancer: failed to EnsureBackendPoolDeleted: %w", err))
 	}
 
 	klog.V(2).Infof("safeDeleteLoadBalancer: deleting LB %s", pointer.StringDeref(lb.Name, ""))
-	rerr := az.DeleteLB(service, pointer.StringDeref(lb.Name, ""))
-	if rerr != nil {
+	if rerr := az.DeleteLB(service, pointer.StringDeref(lb.Name, "")); rerr != nil {
 		return rerr
 	}
 	_ = az.lbCache.Delete(pointer.StringDeref(lb.Name, ""))
@@ -526,20 +542,20 @@ func (az *Cloud) safeDeleteLoadBalancer(lb network.LoadBalancer, clusterName, vm
 // In case the selected load balancer does not exist it returns network.LoadBalancer struct
 // with added metadata (such as name, location) and existsLB set to FALSE.
 // By default - cluster default LB is returned.
-func (az *Cloud) getServiceLoadBalancer(service *v1.Service, clusterName string, nodes []*v1.Node, wantLb bool, existingLBs []network.LoadBalancer) (lb *network.LoadBalancer, status *v1.LoadBalancerStatus, exists bool, err error) {
+func (az *Cloud) getServiceLoadBalancer(service *v1.Service, clusterName string, nodes []*v1.Node, wantLb bool, existingLBs []network.LoadBalancer) (lb *network.LoadBalancer, status *v1.LoadBalancerStatus, lbIPsPrimaryPIPs []string, exists bool, err error) {
 	isInternal := requiresInternalLoadBalancer(service)
 	var defaultLB *network.LoadBalancer
 	primaryVMSetName := az.VMSet.GetPrimaryVMSetName()
 	defaultLBName, err := az.getAzureLoadBalancerName(service, &existingLBs, clusterName, primaryVMSetName, isInternal)
 	if err != nil {
-		return nil, nil, false, err
+		return nil, nil, nil, false, err
 	}
 
 	// reuse the lb list from reconcileSharedLoadBalancer to reduce the api call
 	if len(existingLBs) == 0 {
 		existingLBs, err = az.ListLB(service)
 		if err != nil {
-			return nil, nil, false, err
+			return nil, nil, nil, false, err
 		}
 	}
 
@@ -553,23 +569,28 @@ func (az *Cloud) getServiceLoadBalancer(service *v1.Service, clusterName string,
 		if isInternalLoadBalancer(&existingLB) != isInternal {
 			continue
 		}
-		var fipConfig *network.FrontendIPConfiguration
-		status, fipConfig, err = az.getServiceLoadBalancerStatus(service, &existingLB)
+
+		var fipConfigs []*network.FrontendIPConfiguration
+		status, lbIPsPrimaryPIPs, fipConfigs, err = az.getServiceLoadBalancerStatus(service, &existingLB)
 		if err != nil {
-			return nil, nil, false, err
+			return nil, nil, nil, false, err
 		}
 		if status == nil {
 			// service is not on this load balancer
 			continue
 		}
-		klog.V(4).Infof("getServiceLoadBalancer(%s, %s, %v): current lb ip: %s", service.Name, clusterName, wantLb, status.Ingress[0].IP)
+		klog.V(4).Infof("getServiceLoadBalancer(%s, %s, %v): current lb IPs: %q", service.Name, clusterName, wantLb, lbIPsPrimaryPIPs)
 
 		// select another load balancer instead of returning
 		// the current one if the change is needed
 		if wantLb && az.shouldChangeLoadBalancer(service, pointer.StringDeref(existingLB.Name, ""), clusterName, defaultLBName) {
-			if err := az.removeFrontendIPConfigurationFromLoadBalancer(&existingLB, existingLBs, fipConfig, clusterName, service); err != nil {
-				klog.Errorf("getServiceLoadBalancer(%s, %s, %v): failed to remove frontend IP configuration from load balancer: %v", service.Name, clusterName, wantLb, err)
-				return nil, nil, false, err
+			fipConfigNames := []string{}
+			for _, fipConfig := range fipConfigs {
+				fipConfigNames = append(fipConfigNames, pointer.StringDeref(fipConfig.Name, ""))
+			}
+			if err := az.removeFrontendIPConfigurationFromLoadBalancer(&existingLB, existingLBs, fipConfigs, clusterName, service); err != nil {
+				klog.Errorf("getServiceLoadBalancer(%s, %s, %v): failed to remove frontend IP configurations %q from load balancer: %v", service.Name, clusterName, wantLb, fipConfigNames, err)
+				return nil, nil, nil, false, err
 			}
 			az.reconcileMultipleStandardLoadBalancerConfigurationStatus(
 				false,
@@ -579,7 +600,7 @@ func (az *Cloud) getServiceLoadBalancer(service *v1.Service, clusterName string,
 			break
 		}
 
-		return &existingLB, status, true, nil
+		return &existingLB, status, lbIPsPrimaryPIPs, true, nil
 	}
 
 	// Service does not have a load balancer, select one.
@@ -589,10 +610,10 @@ func (az *Cloud) getServiceLoadBalancer(service *v1.Service, clusterName string,
 		// select new load balancer for service
 		selectedLB, exists, err := az.selectLoadBalancer(clusterName, service, &existingLBs, nodes)
 		if err != nil {
-			return nil, nil, false, err
+			return nil, nil, nil, false, err
 		}
 
-		return selectedLB, status, exists, err
+		return selectedLB, status, lbIPsPrimaryPIPs, exists, err
 	}
 
 	// create a default LB with meta data if not present
@@ -615,7 +636,7 @@ func (az *Cloud) getServiceLoadBalancer(service *v1.Service, clusterName string,
 		}
 	}
 
-	return defaultLB, nil, false, nil
+	return defaultLB, nil, nil, false, nil
 }
 
 // selectLoadBalancer selects load balancer for the service in the cluster.
@@ -693,19 +714,25 @@ func (az *Cloud) selectLoadBalancer(clusterName string, service *v1.Service, exi
 	return selectedLB, existsLb, nil
 }
 
-func (az *Cloud) getServiceLoadBalancerStatus(service *v1.Service, lb *network.LoadBalancer) (status *v1.LoadBalancerStatus, fipConfig *network.FrontendIPConfiguration, err error) {
+// getServiceLoadBalancerStatus returns LB status for the Service.
+// Before DualStack support, old logic takes the first ingress IP as non-additional one
+// and the second one as additional one. With DualStack support, the second IP may be
+// the IP of another IP family so the new logic returns two variables.
+func (az *Cloud) getServiceLoadBalancerStatus(service *v1.Service, lb *network.LoadBalancer) (status *v1.LoadBalancerStatus, lbIPsPrimaryPIPs []string, fipConfigs []*network.FrontendIPConfiguration, err error) {
 	if lb == nil {
 		klog.V(10).Info("getServiceLoadBalancerStatus: lb is nil")
-		return nil, nil, nil
+		return nil, nil, nil, nil
 	}
 	if lb.FrontendIPConfigurations == nil || len(*lb.FrontendIPConfigurations) == 0 {
 		klog.V(10).Info("getServiceLoadBalancerStatus: lb.FrontendIPConfigurations is nil")
-		return nil, nil, nil
+		return nil, nil, nil, nil
 	}
+
 	isInternal := requiresInternalLoadBalancer(service)
 	serviceName := getServiceName(service)
-	for _, ipConfiguration := range *lb.FrontendIPConfigurations {
-		ipConfiguration := ipConfiguration
+	lbIngresses := []v1.LoadBalancerIngress{}
+	for i := range *lb.FrontendIPConfigurations {
+		ipConfiguration := (*lb.FrontendIPConfigurations)[i]
 		owns, isPrimaryService, _ := az.serviceOwnsFrontendIP(ipConfiguration, service)
 		if owns {
 			klog.V(2).Infof("get(%s): lb(%s) - found frontend IP config, primary service: %v", serviceName, pointer.StringDeref(lb.Name, ""), isPrimaryService)
@@ -715,19 +742,19 @@ func (az *Cloud) getServiceLoadBalancerStatus(service *v1.Service, lb *network.L
 				lbIP = ipConfiguration.PrivateIPAddress
 			} else {
 				if ipConfiguration.PublicIPAddress == nil {
-					return nil, nil, fmt.Errorf("get(%s): lb(%s) - failed to get LB PublicIPAddress is Nil", serviceName, *lb.Name)
+					return nil, nil, nil, fmt.Errorf("get(%s): lb(%s) - failed to get LB PublicIPAddress is Nil", serviceName, *lb.Name)
 				}
 				pipID := ipConfiguration.PublicIPAddress.ID
 				if pipID == nil {
-					return nil, nil, fmt.Errorf("get(%s): lb(%s) - failed to get LB PublicIPAddress ID is Nil", serviceName, *lb.Name)
+					return nil, nil, nil, fmt.Errorf("get(%s): lb(%s) - failed to get LB PublicIPAddress ID is Nil", serviceName, *lb.Name)
 				}
 				pipName, err := getLastSegment(*pipID, "/")
 				if err != nil {
-					return nil, nil, fmt.Errorf("get(%s): lb(%s) - failed to get LB PublicIPAddress Name from ID(%s)", serviceName, *lb.Name, *pipID)
+					return nil, nil, nil, fmt.Errorf("get(%s): lb(%s) - failed to get LB PublicIPAddress Name from ID(%s)", serviceName, *lb.Name, *pipID)
 				}
 				pip, existsPip, err := az.getPublicIPAddress(az.getPublicIPAddressResourceGroup(service), pipName, azcache.CacheReadTypeDefault)
 				if err != nil {
-					return nil, nil, err
+					return nil, nil, nil, err
 				}
 				if existsPip {
 					lbIP = pip.IPAddress
@@ -736,25 +763,28 @@ func (az *Cloud) getServiceLoadBalancerStatus(service *v1.Service, lb *network.L
 
 			klog.V(2).Infof("getServiceLoadBalancerStatus gets ingress IP %q from frontendIPConfiguration %q for service %q", pointer.StringDeref(lbIP, ""), pointer.StringDeref(ipConfiguration.Name, ""), serviceName)
 
-			// set additional public IPs to LoadBalancerStatus, so that kube-proxy would create their iptables rules.
-			lbIngress := []v1.LoadBalancerIngress{{IP: pointer.StringDeref(lbIP, "")}}
-			additionalIPs, err := getServiceAdditionalPublicIPs(service)
-			if err != nil {
-				return &v1.LoadBalancerStatus{Ingress: lbIngress}, &ipConfiguration, err
-			}
-			if len(additionalIPs) > 0 {
-				for _, pip := range additionalIPs {
-					lbIngress = append(lbIngress, v1.LoadBalancerIngress{
-						IP: pip,
-					})
-				}
-			}
-
-			return &v1.LoadBalancerStatus{Ingress: lbIngress}, &ipConfiguration, nil
+			lbIngresses = append(lbIngresses, v1.LoadBalancerIngress{IP: pointer.StringDeref(lbIP, "")})
+			lbIPsPrimaryPIPs = append(lbIPsPrimaryPIPs, pointer.StringDeref(lbIP, ""))
+			fipConfigs = append(fipConfigs, &ipConfiguration)
 		}
 	}
+	if len(lbIngresses) == 0 {
+		return nil, nil, nil, nil
+	}
 
-	return nil, nil, nil
+	// set additional public IPs to LoadBalancerStatus, so that kube-proxy would create their iptables rules.
+	additionalIPs, err := getServiceAdditionalPublicIPs(service)
+	if err != nil {
+		return &v1.LoadBalancerStatus{Ingress: lbIngresses}, lbIPsPrimaryPIPs, fipConfigs, err
+	}
+	if len(additionalIPs) > 0 {
+		for _, pip := range additionalIPs {
+			lbIngresses = append(lbIngresses, v1.LoadBalancerIngress{
+				IP: pip,
+			})
+		}
+	}
+	return &v1.LoadBalancerStatus{Ingress: lbIngresses}, lbIPsPrimaryPIPs, fipConfigs, nil
 }
 
 func (az *Cloud) determinePublicIPName(clusterName string, service *v1.Service, isIPv6 bool) (string, bool, error) {
@@ -839,41 +869,16 @@ func flipServiceInternalAnnotation(service *v1.Service) *v1.Service {
 	return copyService
 }
 
-func updateServiceLoadBalancerIP(service *v1.Service, serviceIP string) *v1.Service {
+func updateServiceLoadBalancerIPs(service *v1.Service, serviceIPs []string) *v1.Service {
 	copyService := service.DeepCopy()
-	if len(serviceIP) > 0 && copyService != nil {
-		setServiceLoadBalancerIP(copyService, serviceIP)
+	if copyService != nil {
+		for _, serviceIP := range serviceIPs {
+			setServiceLoadBalancerIP(copyService, serviceIP)
+		}
 	}
 	return copyService
 }
 
-func (az *Cloud) findServiceIPAddress(ctx context.Context, clusterName string, service *v1.Service) (string, error) {
-	isIPv6 := utilnet.IsIPv6String(service.Spec.ClusterIP)
-	lbIP := getServiceLoadBalancerIP(service, isIPv6)
-	if len(lbIP) > 0 {
-		return lbIP, nil
-	}
-
-	if len(service.Status.LoadBalancer.Ingress) > 0 && len(service.Status.LoadBalancer.Ingress[0].IP) > 0 {
-		return service.Status.LoadBalancer.Ingress[0].IP, nil
-	}
-
-	_, lbStatus, existsLb, err := az.getServiceLoadBalancer(service, clusterName, nil, false, []network.LoadBalancer{})
-	if err != nil {
-		return "", err
-	}
-	if !existsLb {
-		klog.V(2).Infof("Expected to find an IP address for service %s but did not. Assuming it has been removed", service.Name)
-		return "", nil
-	}
-	if len(lbStatus.Ingress) < 1 {
-		klog.V(2).Infof("Expected to find an IP address for service %s but it had no ingresses. Assuming it has been removed", service.Name)
-		return "", nil
-	}
-
-	return lbStatus.Ingress[0].IP, nil
-}
-
 func (az *Cloud) ensurePublicIPExists(service *v1.Service, pipName string, domainNameLabel, clusterName string, shouldPIPExisted, foundDNSLabelAnnotation, isIPv6 bool) (*network.PublicIPAddress, error) {
 	pipResourceGroup := az.getPublicIPAddressResourceGroup(service)
 	pip, existsPip, err := az.getPublicIPAddress(pipResourceGroup, pipName, azcache.CacheReadTypeDefault)
@@ -937,7 +942,7 @@ func (az *Cloud) ensurePublicIPExists(service *v1.Service, pipName string, domai
 		}
 	} else {
 		if shouldPIPExisted {
-			return nil, fmt.Errorf("PublicIP from annotation azure-pip-name=%s for service %s doesn't exist", pipName, serviceName)
+			return nil, fmt.Errorf("PublicIP from annotation azure-pip-name(-IPv6)=%s for service %s doesn't exist", pipName, serviceName)
 		}
 
 		changed = true
@@ -1277,7 +1282,7 @@ func (az *Cloud) isFrontendIPChanged(
 		subnetName := getInternalSubnet(service)
 		if subnetName != nil {
 			if subnet == nil {
-				return false, fmt.Errorf("isFrontendIPChanged: Unexpected nil subnet")
+				return false, fmt.Errorf("isFrontendIPChanged: Unexpected nil subnet %q", pointer.StringDeref(subnetName, ""))
 			}
 			if config.Subnet != nil && !strings.EqualFold(pointer.StringDeref(config.Subnet.ID, ""), pointer.StringDeref(subnet.ID, "")) {
 				return true, nil
@@ -1535,7 +1540,7 @@ func (az *Cloud) reconcileLoadBalancer(clusterName string, service *v1.Service,
 		return nil, err
 	}
 
-	lb, lbStatus, _, err := az.getServiceLoadBalancer(service, clusterName, nodes, wantLb, existingLBs)
+	lb, lbStatus, _, _, err := az.getServiceLoadBalancer(service, clusterName, nodes, wantLb, existingLBs)
 	if err != nil {
 		klog.Errorf("reconcileLoadBalancer: failed to get load balancer for service %q, error: %v", serviceName, err)
 		return nil, err
@@ -1548,8 +1553,8 @@ func (az *Cloud) reconcileLoadBalancer(clusterName string, service *v1.Service,
 		serviceName, lbResourceGroup, lbName, wantLb)
 	lbFrontendIPConfigNames := az.getFrontendIPConfigNames(service)
 	lbFrontendIPConfigIDs := map[bool]string{
-		false: az.getFrontendIPConfigID(lbName, lbFrontendIPConfigNames[false]),
-		true:  az.getFrontendIPConfigID(lbName, lbFrontendIPConfigNames[true]),
+		consts.IPVersionIPv4: az.getFrontendIPConfigID(lbName, lbFrontendIPConfigNames[consts.IPVersionIPv4]),
+		consts.IPVersionIPv6: az.getFrontendIPConfigID(lbName, lbFrontendIPConfigNames[consts.IPVersionIPv6]),
 	}
 	dirtyLb := false
 
@@ -1623,7 +1628,7 @@ func (az *Cloud) reconcileLoadBalancer(clusterName string, service *v1.Service,
 		if err = az.checkLoadBalancerResourcesConflicts(lb, lbFrontendIPConfigIDs[false], service); err != nil {
 			return nil, err
 		}
-		if err := getExpectedLBRule(false); err != nil {
+		if err := getExpectedLBRule(consts.IPVersionIPv4); err != nil {
 			return nil, err
 		}
 	}
@@ -1631,7 +1636,7 @@ func (az *Cloud) reconcileLoadBalancer(clusterName string, service *v1.Service,
 		if err = az.checkLoadBalancerResourcesConflicts(lb, lbFrontendIPConfigIDs[true], service); err != nil {
 			return nil, err
 		}
-		if err := getExpectedLBRule(true); err != nil {
+		if err := getExpectedLBRule(consts.IPVersionIPv6); err != nil {
 			return nil, err
 		}
 	}
@@ -1643,7 +1648,6 @@ func (az *Cloud) reconcileLoadBalancer(clusterName string, service *v1.Service,
 	if changed := az.reconcileLBRules(lb, service, serviceName, wantLb, expectedRules); changed {
 		dirtyLb = true
 	}
-
 	if changed := az.ensureLoadBalancerTagged(lb); changed {
 		dirtyLb = true
 	}
@@ -1946,7 +1950,8 @@ func (az *Cloud) reconcileFrontendIPConfigs(clusterName string,
 		}
 
 		addNewFIPOfService := func(isIPv6 bool) error {
-			klog.V(4).Infof("ensure(%s): lb(%s) - creating a new frontend IP config (isIPv6=%t)", serviceName, lbName, isIPv6)
+			klog.V(4).Infof("ensure(%s): lb(%s) - creating a new frontend IP config %q (isIPv6=%t)",
+				serviceName, lbName, lbFrontendIPConfigNames[isIPv6], isIPv6)
 
 			// construct FrontendIPConfigurationPropertiesFormat
 			var fipConfigurationProperties *network.FrontendIPConfigurationPropertiesFormat
@@ -2565,7 +2570,7 @@ func (az *Cloud) getExpectedLoadBalancingRulePropertiesForPort(
 
 	// 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) {
+	if consts.IsK8sServiceUsingInternalLoadBalancer(service) && isBackendPoolIPv6(lbBackendPoolID) {
 		props.BackendPort = pointer.Int32(servicePort.NodePort)
 		props.EnableFloatingIP = pointer.Bool(false)
 	}
@@ -2587,7 +2592,7 @@ func (az *Cloud) getExpectedHAModeLoadBalancingRuleProperties(
 
 // This reconciles the Network Security Group similar to how the LB is reconciled.
 // This entails adding required, missing SecurityRules and removing stale rules.
-func (az *Cloud) reconcileSecurityGroup(clusterName string, service *v1.Service, lbIP *string, lbName *string, wantLb bool) (*network.SecurityGroup, error) {
+func (az *Cloud) reconcileSecurityGroup(clusterName string, service *v1.Service, lbIPs *[]string, lbName *string, wantLb bool) (*network.SecurityGroup, error) {
 	serviceName := getServiceName(service)
 	klog.V(5).Infof("reconcileSecurityGroup(%s): START clusterName=%q", serviceName, clusterName)
 
@@ -2605,16 +2610,26 @@ func (az *Cloud) reconcileSecurityGroup(clusterName string, service *v1.Service,
 		return nil, err
 	}
 
-	destinationIPAddress := ""
-	if wantLb && lbIP == nil {
+	if wantLb && lbIPs == nil {
 		return nil, fmt.Errorf("no load balancer IP for setting up security rules for service %s", service.Name)
 	}
-	if lbIP != nil {
-		destinationIPAddress = *lbIP
+
+	destinationIPAddresses := map[bool][]string{}
+	if lbIPs != nil {
+		for _, ip := range *lbIPs {
+			if net.ParseIP(ip).To4() != nil {
+				destinationIPAddresses[false] = append(destinationIPAddresses[false], ip)
+			} else {
+				destinationIPAddresses[true] = append(destinationIPAddresses[true], ip)
+			}
+		}
 	}
 
-	if destinationIPAddress == "" {
-		destinationIPAddress = "*"
+	if len(destinationIPAddresses[false]) == 0 {
+		destinationIPAddresses[false] = []string{"*"}
+	}
+	if len(destinationIPAddresses[true]) == 0 {
+		destinationIPAddresses[true] = []string{"*"}
 	}
 
 	disableFloatingIP := false
@@ -2622,7 +2637,7 @@ func (az *Cloud) reconcileSecurityGroup(clusterName string, service *v1.Service,
 		disableFloatingIP = true
 	}
 
-	backendIPAddresses := make([]string, 0)
+	backendIPAddresses := map[bool][]string{}
 	if wantLb && disableFloatingIP {
 		lb, exist, err := az.getAzureLoadBalancer(pointer.StringDeref(lbName, ""), azcache.CacheReadTypeDefault)
 		if err != nil {
@@ -2631,21 +2646,18 @@ func (az *Cloud) reconcileSecurityGroup(clusterName string, service *v1.Service,
 		if !exist {
 			return nil, fmt.Errorf("unable to get lb %s", pointer.StringDeref(lbName, ""))
 		}
-		backendPrivateIPv4s, backendPrivateIPv6s := az.LoadBalancerBackendPool.GetBackendPrivateIPs(clusterName, service, lb)
-		backendIPAddresses = backendPrivateIPv4s
-		if utilnet.IsIPv6String(*lbIP) {
-			backendIPAddresses = backendPrivateIPv6s
-		}
+		backendIPAddresses[false], backendIPAddresses[true] = az.LoadBalancerBackendPool.GetBackendPrivateIPs(clusterName, service, lb)
 	}
 
 	additionalIPs, err := getServiceAdditionalPublicIPs(service)
 	if err != nil {
 		return nil, fmt.Errorf("unable to get additional public IPs, error=%w", err)
 	}
-
-	destinationIPAddresses := []string{destinationIPAddress}
-	if destinationIPAddress != "*" {
-		destinationIPAddresses = append(destinationIPAddresses, additionalIPs...)
+	for _, ip := range additionalIPs {
+		isIPv6 := net.ParseIP(ip).To4() == nil
+		if len(destinationIPAddresses[isIPv6]) != 1 || destinationIPAddresses[isIPv6][0] != "*" {
+			destinationIPAddresses[isIPv6] = append(destinationIPAddresses[isIPv6], ip)
+		}
 	}
 
 	sourceRanges, err := servicehelpers.GetLoadBalancerSourceRanges(service)
@@ -2657,21 +2669,40 @@ func (az *Cloud) reconcileSecurityGroup(clusterName string, service *v1.Service,
 		delete(sourceRanges, consts.DefaultLoadBalancerSourceRanges)
 	}
 
-	var sourceAddressPrefixes []string
+	sourceAddressPrefixes := map[bool][]string{}
 	if (sourceRanges == nil || servicehelpers.IsAllowAll(sourceRanges)) && len(serviceTags) == 0 {
 		if !requiresInternalLoadBalancer(service) || len(service.Spec.LoadBalancerSourceRanges) > 0 {
-			sourceAddressPrefixes = []string{"Internet"}
+			sourceAddressPrefixes[false] = []string{"Internet"}
+			sourceAddressPrefixes[true] = []string{"Internet"}
 		}
 	} else {
 		for _, ip := range sourceRanges {
-			sourceAddressPrefixes = append(sourceAddressPrefixes, ip.String())
+			if ip == nil {
+				continue
+			}
+			isIPv6 := net.ParseIP(ip.IP.String()).To4() == nil
+			sourceAddressPrefixes[isIPv6] = append(sourceAddressPrefixes[isIPv6], ip.String())
 		}
-		sourceAddressPrefixes = append(sourceAddressPrefixes, serviceTags...)
+		sourceAddressPrefixes[false] = append(sourceAddressPrefixes[false], serviceTags...)
+		sourceAddressPrefixes[true] = append(sourceAddressPrefixes[true], serviceTags...)
 	}
 
-	expectedSecurityRules, err := az.getExpectedSecurityRules(wantLb, ports, sourceAddressPrefixes, service, destinationIPAddresses, sourceRanges, backendIPAddresses, disableFloatingIP)
-	if err != nil {
-		return nil, err
+	expectedSecurityRules := []network.SecurityRule{}
+	handleSecurityRules := func(isIPv6 bool) error {
+		expectedSecurityRulesSingleStack, err := az.getExpectedSecurityRules(wantLb, ports, sourceAddressPrefixes[isIPv6], service, destinationIPAddresses[isIPv6], sourceRanges, backendIPAddresses[isIPv6], disableFloatingIP, isIPv6)
+		expectedSecurityRules = append(expectedSecurityRules, expectedSecurityRulesSingleStack...)
+		return err
+	}
+	v4Enabled, v6Enabled := getIPFamiliesEnabled(service)
+	if v4Enabled {
+		if err := handleSecurityRules(false); err != nil {
+			return nil, err
+		}
+	}
+	if v6Enabled {
+		if err := handleSecurityRules(true); err != nil {
+			return nil, err
+		}
 	}
 
 	// update security rules
@@ -2700,7 +2731,14 @@ func (az *Cloud) reconcileSecurityGroup(clusterName string, service *v1.Service,
 	return &sg, nil
 }
 
-func (az *Cloud) reconcileSecurityRules(sg network.SecurityGroup, service *v1.Service, serviceName string, wantLb bool, expectedSecurityRules []network.SecurityRule, ports []v1.ServicePort, sourceAddressPrefixes []string, destinationIPAddresses []string) (bool, []network.SecurityRule, error) {
+func (az *Cloud) reconcileSecurityRules(sg network.SecurityGroup,
+	service *v1.Service,
+	serviceName string,
+	wantLb bool,
+	expectedSecurityRules []network.SecurityRule,
+	ports []v1.ServicePort,
+	sourceAddressPrefixes, destinationIPAddresses map[bool][]string,
+) (bool, []network.SecurityRule, error) {
 	dirtySg := false
 	var updatedRules []network.SecurityRule
 	if sg.SecurityGroupPropertiesFormat != nil && sg.SecurityGroupPropertiesFormat.SecurityRules != nil {
@@ -2732,47 +2770,58 @@ func (az *Cloud) reconcileSecurityRules(sg network.SecurityGroup, service *v1.Se
 
 	// update security rules: if the service uses a shared rule and is being deleted,
 	// then remove it from the shared rule
-	if useSharedSecurityRule(service) && !wantLb {
-		for _, port := range ports {
-			for _, sourceAddressPrefix := range sourceAddressPrefixes {
-				sharedRuleName := az.getSecurityRuleName(service, port, sourceAddressPrefix, utilnet.IsIPv6String(service.Spec.ClusterIP))
-				sharedIndex, sharedRule, sharedRuleFound := findSecurityRuleByName(updatedRules, sharedRuleName)
-				if !sharedRuleFound {
-					klog.V(4).Infof("Didn't find shared rule %s for service %s", sharedRuleName, service.Name)
-					continue
-				}
-				shouldDeleteNSGRule := false
-				if sharedRule.DestinationAddressPrefixes == nil || len(*sharedRule.DestinationAddressPrefixes) == 0 {
-					shouldDeleteNSGRule = true
-				} else {
-					existingPrefixes := *sharedRule.DestinationAddressPrefixes
-					for _, destinationIPAddress := range destinationIPAddresses {
-						addressIndex, found := findIndex(existingPrefixes, destinationIPAddress)
-						if !found {
-							klog.Warningf("Didn't find destination address %v in shared rule %s for service %s", destinationIPAddress, sharedRuleName, service.Name)
-							continue
-						}
-						if len(existingPrefixes) == 1 {
-							shouldDeleteNSGRule = true
-							break //shared nsg rule has only one entry and entry owned by deleted svc has been found. skip the rest of the entries
-						} else {
-							newDestinations := append(existingPrefixes[:addressIndex], existingPrefixes[addressIndex+1:]...)
-							sharedRule.DestinationAddressPrefixes = &newDestinations
-							updatedRules[sharedIndex] = sharedRule
+	handleRule := func(isIPv6 bool) {
+		if useSharedSecurityRule(service) && !wantLb {
+			for _, port := range ports {
+				for _, sourceAddressPrefix := range sourceAddressPrefixes[isIPv6] {
+					sharedRuleName := az.getSecurityRuleName(service, port, sourceAddressPrefix, isIPv6)
+					sharedIndex, sharedRule, sharedRuleFound := findSecurityRuleByName(updatedRules, sharedRuleName)
+					if !sharedRuleFound {
+						klog.V(4).Infof("Didn't find shared rule %s for service %s", sharedRuleName, service.Name)
+						continue
+					}
+					shouldDeleteNSGRule := false
+					if sharedRule.SecurityRulePropertiesFormat == nil ||
+						sharedRule.SecurityRulePropertiesFormat.DestinationAddressPrefixes == nil ||
+						len(*sharedRule.SecurityRulePropertiesFormat.DestinationAddressPrefixes) == 0 {
+						shouldDeleteNSGRule = true
+					} else {
+						existingPrefixes := *sharedRule.DestinationAddressPrefixes
+						for _, destinationIPAddress := range destinationIPAddresses[isIPv6] {
+							addressIndex, found := findIndex(existingPrefixes, destinationIPAddress)
+							if !found {
+								klog.Warningf("Didn't find destination address %v in shared rule %s for service %s", destinationIPAddress, sharedRuleName, service.Name)
+								continue
+							}
+							if len(existingPrefixes) == 1 {
+								shouldDeleteNSGRule = true
+								break //shared nsg rule has only one entry and entry owned by deleted svc has been found. skip the rest of the entries
+							} else {
+								newDestinations := append(existingPrefixes[:addressIndex], existingPrefixes[addressIndex+1:]...)
+								sharedRule.DestinationAddressPrefixes = &newDestinations
+								updatedRules[sharedIndex] = sharedRule
+							}
+							dirtySg = true
 						}
-						dirtySg = true
 					}
-				}
 
-				if shouldDeleteNSGRule {
-					klog.V(4).Infof("shared rule will be deleted because last service %s which refers this rule is deleted.", service.Name)
-					updatedRules = append(updatedRules[:sharedIndex], updatedRules[sharedIndex+1:]...)
-					dirtySg = true
-					continue
+					if shouldDeleteNSGRule {
+						klog.V(4).Infof("shared rule will be deleted because last service %s which refers this rule is deleted.", service.Name)
+						updatedRules = append(updatedRules[:sharedIndex], updatedRules[sharedIndex+1:]...)
+						dirtySg = true
+						continue
+					}
 				}
 			}
 		}
 	}
+	v4Enabled, v6Enabled := getIPFamiliesEnabled(service)
+	if v4Enabled {
+		handleRule(consts.IPVersionIPv4)
+	}
+	if v6Enabled {
+		handleRule(consts.IPVersionIPv6)
+	}
 
 	// update security rules: prepare rules for consolidation
 	for index, rule := range updatedRules {
@@ -2820,10 +2869,11 @@ func (az *Cloud) reconcileSecurityRules(sg network.SecurityGroup, service *v1.Se
 	for _, r := range updatedRules {
 		klog.V(10).Infof("Updated security rule while processing %s: %s:%s -> %s:%s", service.Name, logSafe(r.SourceAddressPrefix), logSafe(r.SourcePortRange), logSafeCollection(r.DestinationAddressPrefix, r.DestinationAddressPrefixes), logSafe(r.DestinationPortRange))
 	}
+
 	return dirtySg, updatedRules, nil
 }
 
-func (az *Cloud) getExpectedSecurityRules(wantLb bool, ports []v1.ServicePort, sourceAddressPrefixes []string, service *v1.Service, destinationIPAddresses []string, sourceRanges utilnet.IPNetSet, backendIPAddresses []string, disableFloatingIP bool) ([]network.SecurityRule, error) {
+func (az *Cloud) getExpectedSecurityRules(wantLb bool, ports []v1.ServicePort, sourceAddressPrefixes []string, service *v1.Service, destinationIPAddresses []string, sourceRanges utilnet.IPNetSet, backendIPAddresses []string, disableFloatingIP, isIPv6 bool) ([]network.SecurityRule, error) {
 	expectedSecurityRules := []network.SecurityRule{}
 
 	if wantLb {
@@ -2840,7 +2890,7 @@ func (az *Cloud) getExpectedSecurityRules(wantLb bool, ports []v1.ServicePort, s
 			}
 			for j := range sourceAddressPrefixes {
 				ix := i*len(sourceAddressPrefixes) + j
-				securityRuleName := az.getSecurityRuleName(service, port, sourceAddressPrefixes[j], utilnet.IsIPv6String(service.Spec.ClusterIP))
+				securityRuleName := az.getSecurityRuleName(service, port, sourceAddressPrefixes[j], isIPv6)
 				nsgRule := network.SecurityRule{
 					Name: pointer.String(securityRuleName),
 					SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
@@ -2877,7 +2927,7 @@ func (az *Cloud) getExpectedSecurityRules(wantLb bool, ports []v1.ServicePort, s
 				if err != nil {
 					return nil, err
 				}
-				securityRuleName := az.getSecurityRuleName(service, port, "deny_all", utilnet.IsIPv6String(service.Spec.ClusterIP))
+				securityRuleName := az.getSecurityRuleName(service, port, "deny_all", isIPv6)
 				nsgRule := network.SecurityRule{
 					Name: pointer.String(securityRuleName),
 					SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
@@ -2912,7 +2962,7 @@ func (az *Cloud) shouldUpdateLoadBalancer(clusterName string, service *v1.Servic
 		return false, fmt.Errorf("shouldUpdateLoadBalancer: failed to list managed load balancers: %w", err)
 	}
 
-	_, _, existsLb, _ := az.getServiceLoadBalancer(service, clusterName, nodes, false, existingManagedLBs)
+	_, _, _, existsLb, _ := az.getServiceLoadBalancer(service, clusterName, nodes, false, existingManagedLBs)
 	return existsLb && service.ObjectMeta.DeletionTimestamp == nil && service.Spec.Type == v1.ServiceTypeLoadBalancer, nil
 }
 
@@ -3285,6 +3335,9 @@ func (az *Cloud) getPublicIPUpdates(
 			}
 		}
 
+		if pip.Name == nil {
+			return false, nil, false, nil, fmt.Errorf("PIP name is empty: %v", pip)
+		}
 		pipName := *pip.Name
 
 		// If we've been told to use a specific public ip by the client, let's track whether or not it actually existed
diff --git a/pkg/provider/azure_loadbalancer_backendpool.go b/pkg/provider/azure_loadbalancer_backendpool.go
index 650e102b5..7a4b7c983 100644
--- a/pkg/provider/azure_loadbalancer_backendpool.go
+++ b/pkg/provider/azure_loadbalancer_backendpool.go
@@ -33,6 +33,7 @@ import (
 	"k8s.io/utils/pointer"
 
 	"sigs.k8s.io/cloud-provider-azure/pkg/cache"
+	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
 	"sigs.k8s.io/cloud-provider-azure/pkg/metrics"
 )
 
@@ -70,11 +71,11 @@ func (bc *backendPoolTypeNodeIPConfig) EnsureHostsInPool(service *v1.Service, no
 }
 
 func isLBBackendPoolsExisting(lbBackendPoolNames map[bool]string, bpName *string) (found, isIPv6 bool) {
-	if strings.EqualFold(pointer.StringDeref(bpName, ""), lbBackendPoolNames[false]) {
+	if strings.EqualFold(pointer.StringDeref(bpName, ""), lbBackendPoolNames[consts.IPVersionIPv4]) {
 		isIPv6 = false
 		found = true
 	}
-	if strings.EqualFold(pointer.StringDeref(bpName, ""), lbBackendPoolNames[true]) {
+	if strings.EqualFold(pointer.StringDeref(bpName, ""), lbBackendPoolNames[consts.IPVersionIPv6]) {
 		isIPv6 = true
 		found = true
 	}
@@ -135,10 +136,10 @@ func (bc *backendPoolTypeNodeIPConfig) CleanupVMSetFromBackendPoolByCondition(sl
 			})
 		}
 		if v4Enabled {
-			findBackendpoolToBeDeleted(false)
+			findBackendpoolToBeDeleted(consts.IPVersionIPv4)
 		}
 		if v6Enabled {
-			findBackendpoolToBeDeleted(true)
+			findBackendpoolToBeDeleted(consts.IPVersionIPv6)
 		}
 		// decouple the backendPool from the node
 		shouldRefreshLB, err := bc.VMSet.EnsureBackendPoolDeleted(service, lbBackendPoolIDsSlice, vmSetName, &backendpoolToBeDeleted, true)
@@ -723,6 +724,7 @@ func newBackendPool(lb *network.LoadBalancer, isBackendPoolPreConfigured bool, p
 		BackendAddressPoolPropertiesFormat: &network.BackendAddressPoolPropertiesFormat{},
 	})
 
+	// Always returns false
 	return isBackendPoolPreConfigured
 }
 
diff --git a/pkg/provider/azure_privatelinkservice.go b/pkg/provider/azure_privatelinkservice.go
index efd3772a5..fc65530ab 100644
--- a/pkg/provider/azure_privatelinkservice.go
+++ b/pkg/provider/azure_privatelinkservice.go
@@ -44,24 +44,27 @@ func (az *Cloud) reconcilePrivateLinkService(
 	isinternal := requiresInternalLoadBalancer(service)
 	pipRG := az.getPublicIPAddressResourceGroup(service)
 	_, _, fipIPVersion := az.serviceOwnsFrontendIP(*fipConfig, service)
+	serviceName := getServiceName(service)
 	var isIPv6 bool
 	var err error
 	if fipIPVersion != "" {
 		isIPv6 = fipIPVersion == network.IPv6
 	} else {
 		if isIPv6, err = az.isFIPIPv6(service, pipRG, fipConfig); err != nil {
-			klog.Errorf("reconcilePrivateLinkService for service(%s): failed to get FIP IP family: %v", service, err)
+			klog.Errorf("reconcilePrivateLinkService for service(%s): failed to get FIP IP family: %v", serviceName, err)
 			return err
 		}
 	}
-
+	createPLS := wantPLS && serviceRequiresPLS(service)
+	isDualStack := isServiceDualStack(service)
 	if isIPv6 {
-		klog.V(2).Infof("IPv6 is not supported for private link service, skip reconcilePrivateLinkService for service(%s)", service)
-		return nil
+		if isDualStack || !createPLS {
+			klog.V(2).Infof("IPv6 is not supported for private link service, skip reconcilePrivateLinkService for service(%s)", serviceName)
+			return nil
+		}
+		return fmt.Errorf("IPv6 is not supported for private link service")
 	}
 
-	createPLS := wantPLS && serviceRequiresPLS(service)
-	serviceName := getServiceName(service)
 	fipConfigID := fipConfig.ID
 	klog.V(2).Infof("reconcilePrivateLinkService for service(%s) - LB fipConfigID(%s) - wantPLS(%t) - createPLS(%t)", serviceName, pointer.StringDeref(fipConfig.Name, ""), wantPLS, createPLS)
 
diff --git a/pkg/provider/azure_standard.go b/pkg/provider/azure_standard.go
index 3217dc1cb..ca8321f41 100644
--- a/pkg/provider/azure_standard.go
+++ b/pkg/provider/azure_standard.go
@@ -104,8 +104,8 @@ func (az *Cloud) getBackendPoolIDWithRG(lbName, rgName, backendPoolName string)
 
 func (az *Cloud) getBackendPoolIDs(clusterName, lbName string) map[bool]string {
 	return map[bool]string{
-		false: az.getBackendPoolID(lbName, getBackendPoolName(clusterName, false)),
-		true:  az.getBackendPoolID(lbName, getBackendPoolName(clusterName, true)),
+		consts.IPVersionIPv4: az.getBackendPoolID(lbName, getBackendPoolName(clusterName, consts.IPVersionIPv4)),
+		consts.IPVersionIPv6: az.getBackendPoolID(lbName, getBackendPoolName(clusterName, consts.IPVersionIPv6)),
 	}
 }
 
@@ -282,8 +282,8 @@ func getBackendPoolName(clusterName string, isIPv6 bool) string {
 // getBackendPoolNames returns the IPv4 and IPv6 backend pool names.
 func getBackendPoolNames(clusterName string) map[bool]string {
 	return map[bool]string{
-		false: getBackendPoolName(clusterName, false),
-		true:  getBackendPoolName(clusterName, true),
+		consts.IPVersionIPv4: getBackendPoolName(clusterName, consts.IPVersionIPv4),
+		consts.IPVersionIPv6: getBackendPoolName(clusterName, consts.IPVersionIPv6),
 	}
 }
 
@@ -320,15 +320,17 @@ func (az *Cloud) getloadbalancerHAmodeRuleName(service *v1.Service, isIPv6 bool)
 }
 
 func (az *Cloud) getSecurityRuleName(service *v1.Service, port v1.ServicePort, sourceAddrPrefix string, isIPv6 bool) string {
+	isDualStack := isServiceDualStack(service)
 	safePrefix := strings.Replace(sourceAddrPrefix, "/", "_", -1)
 	safePrefix = strings.Replace(safePrefix, ":", ".", -1) // Consider IPv6 address
+	var name string
 	if useSharedSecurityRule(service) {
-		return fmt.Sprintf("shared-%s-%d-%s", port.Protocol, port.Port, safePrefix)
+		name = fmt.Sprintf("shared-%s-%d-%s", port.Protocol, port.Port, safePrefix)
+	} else {
+		rulePrefix := az.getRulePrefix(service)
+		name = fmt.Sprintf("%s-%s-%d-%s", rulePrefix, port.Protocol, port.Port, safePrefix)
 	}
-	rulePrefix := az.getRulePrefix(service)
-	name := fmt.Sprintf("%s-%s-%d-%s", rulePrefix, port.Protocol, port.Port, safePrefix)
-	// TODO: Use getResourceByIPFamily
-	return name
+	return getResourceByIPFamily(name, isDualStack, isIPv6)
 }
 
 // This returns a human-readable version of the Service used to tag some resources.
@@ -432,8 +434,8 @@ func (az *Cloud) getFrontendIPConfigNames(service *v1.Service) map[bool]string {
 	isDualStack := isServiceDualStack(service)
 	defaultLBFrontendIPConfigName := az.getDefaultFrontendIPConfigName(service)
 	return map[bool]string{
-		false: getResourceByIPFamily(defaultLBFrontendIPConfigName, isDualStack, false),
-		true:  getResourceByIPFamily(defaultLBFrontendIPConfigName, isDualStack, true),
+		consts.IPVersionIPv4: getResourceByIPFamily(defaultLBFrontendIPConfigName, isDualStack, consts.IPVersionIPv4),
+		consts.IPVersionIPv6: getResourceByIPFamily(defaultLBFrontendIPConfigName, isDualStack, consts.IPVersionIPv6),
 	}
 }
 
diff --git a/pkg/provider/azure_utils.go b/pkg/provider/azure_utils.go
index c05d0f3c1..af0854f5b 100644
--- a/pkg/provider/azure_utils.go
+++ b/pkg/provider/azure_utils.go
@@ -34,6 +34,11 @@ import (
 	"sigs.k8s.io/cloud-provider-azure/pkg/consts"
 )
 
+const (
+	IPVersionIPv6 bool = true
+	IPVersionIPv4 bool = false
+)
+
 var strToExtendedLocationType = map[string]network.ExtendedLocationTypes{
 	"edgezone": network.EdgeZone,
 }
@@ -392,10 +397,20 @@ func getServiceLoadBalancerIPs(service *v1.Service) []string {
 
 // setServiceLoadBalancerIP sets LB IP to a Service
 func setServiceLoadBalancerIP(service *v1.Service, ip string) {
+	if service == nil {
+		klog.Warning("setServiceLoadBalancerIP: Service is nil")
+		return
+	}
+	parsedIP := net.ParseIP(ip)
+	if parsedIP == nil {
+		klog.Warning("setServiceLoadBalancerIP: IP %q is not valid for Service", ip, service.Name)
+		return
+	}
+
+	isIPv6 := parsedIP.To4() == nil
 	if service.Annotations == nil {
 		service.Annotations = map[string]string{}
 	}
-	isIPv6 := net.ParseIP(ip).To4() == nil
 	service.Annotations[consts.ServiceAnnotationLoadBalancerIPDualStack[isIPv6]] = ip
 }
 


Test patch:
diff --git a/pkg/provider/azure_loadbalancer_test.go b/pkg/provider/azure_loadbalancer_test.go
index 8b3004875..df141e7c4 100644
--- a/pkg/provider/azure_loadbalancer_test.go
+++ b/pkg/provider/azure_loadbalancer_test.go
@@ -1742,7 +1742,7 @@ func TestGetServiceTags(t *testing.T) {
 	}
 }
 
-func TestGetServiceLoadBalancer(t *testing.T) {
+func TestGetServiceLoadBalancerCommon(t *testing.T) {
 	testCases := []struct {
 		desc           string
 		sku            string
@@ -1878,7 +1878,7 @@ func TestGetServiceLoadBalancer(t *testing.T) {
 			}
 			az.LoadBalancerSku = test.sku
 			service := test.service
-			lb, status, exists, err := az.getServiceLoadBalancer(&service, testClusterName,
+			lb, status, _, exists, err := az.getServiceLoadBalancer(&service, testClusterName,
 				clusterResources.nodes, test.wantLB, []network.LoadBalancer{})
 			assert.Equal(t, test.expectedLB, lb)
 			assert.Equal(t, test.expectedStatus, status)
@@ -1911,7 +1911,7 @@ func TestGetServiceLoadBalancerWithExtendedLocation(t *testing.T) {
 	mockLBsClient.EXPECT().List(gomock.Any(), "rg").Return(nil, nil)
 	az.LoadBalancerClient = mockLBsClient
 
-	lb, status, exists, err := az.getServiceLoadBalancer(&service, testClusterName,
+	lb, status, _, exists, err := az.getServiceLoadBalancer(&service, testClusterName,
 		clusterResources.nodes, false, []network.LoadBalancer{})
 	assert.Equal(t, expectedLB, lb, "GetServiceLoadBalancer shall return a default LB with expected location.")
 	assert.Nil(t, status, "GetServiceLoadBalancer: Status should be nil for default LB.")
@@ -1936,7 +1936,7 @@ func TestGetServiceLoadBalancerWithExtendedLocation(t *testing.T) {
 	mockLBsClient.EXPECT().List(gomock.Any(), "rg").Return(nil, nil)
 	az.LoadBalancerClient = mockLBsClient
 
-	lb, status, exists, err = az.getServiceLoadBalancer(&service, testClusterName,
+	lb, status, _, exists, err = az.getServiceLoadBalancer(&service, testClusterName,
 		clusterResources.nodes, true, []network.LoadBalancer{})
 	assert.Equal(t, expectedLB, lb, "GetServiceLoadBalancer shall return a new LB with expected location.")
 	assert.Nil(t, status, "GetServiceLoadBalancer: Status should be nil for new LB.")
@@ -2295,7 +2295,7 @@ func TestDeterminePublicIPName(t *testing.T) {
 	}
 }
 
-func TestReconcileLoadBalancerRule(t *testing.T) {
+func TestReconcileLoadBalancerRuleCommon(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
@@ -2311,14 +2311,14 @@ func TestReconcileLoadBalancerRule(t *testing.T) {
 	}{
 		{
 			desc:            "getExpectedLBRules shall return corresponding probe and lbRule(blb)",
-			service:         getTestService("test1", v1.ProtocolTCP, map[string]string{}, false, 80),
+			service:         getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{}, 80),
 			loadBalancerSku: "basic",
 			expectedProbes:  getDefaultTestProbes("Tcp", ""),
 			expectedRules:   getDefaultTestRules(false),
 		},
 		{
 			desc:            "getExpectedLBRules shall return tcp probe on non supported protocols when basic lb sku is used",
-			service:         getTestService("test1", v1.ProtocolTCP, map[string]string{}, false, 80),
+			service:         getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{}, 80),
 			loadBalancerSku: "basic",
 			probeProtocol:   "Mongodb",
 			expectedRules:   getDefaultTestRules(false),
@@ -2326,7 +2326,7 @@ func TestReconcileLoadBalancerRule(t *testing.T) {
 		},
 		{
 			desc:            "getExpectedLBRules shall return tcp probe on https protocols when basic lb sku is used",
-			service:         getTestService("test1", v1.ProtocolTCP, map[string]string{}, false, 80),
+			service:         getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{}, 80),
 			loadBalancerSku: "basic",
 			probeProtocol:   "Https",
 			expectedRules:   getDefaultTestRules(false),
@@ -2334,20 +2334,20 @@ func TestReconcileLoadBalancerRule(t *testing.T) {
 		},
 		{
 			desc:            "getExpectedLBRules shall return error (slb with external mode and SCTP)",
-			service:         getTestService("test1", v1.ProtocolSCTP, map[string]string{}, false, 80),
+			service:         getTestServiceDualStack("test1", v1.ProtocolSCTP, map[string]string{}, 80),
 			loadBalancerSku: "standard",
 			expectedErr:     true,
 		},
 		{
 			desc:            "getExpectedLBRules shall return corresponding probe and lbRule(slb with tcp reset)",
-			service:         getTestService("test1", v1.ProtocolTCP, nil, false, 80),
+			service:         getTestServiceDualStack("test1", v1.ProtocolTCP, nil, 80),
 			loadBalancerSku: "standard",
 			expectedProbes:  getDefaultTestProbes("Tcp", ""),
 			expectedRules:   getDefaultTestRules(true),
 		},
 		{
 			desc:            "getExpectedLBRules shall respect the probe protocol and path configuration in the config file",
-			service:         getTestService("test1", v1.ProtocolTCP, nil, false, 80),
+			service:         getTestServiceDualStack("test1", v1.ProtocolTCP, nil, 80),
 			loadBalancerSku: "standard",
 			probeProtocol:   "Http",
 			probePath:       "/healthy",
@@ -2356,7 +2356,7 @@ func TestReconcileLoadBalancerRule(t *testing.T) {
 		},
 		{
 			desc:            "getExpectedLBRules shall respect the probe protocol and path configuration in the config file",
-			service:         getTestService("test1", v1.ProtocolTCP, nil, false, 80),
+			service:         getTestServiceDualStack("test1", v1.ProtocolTCP, nil, 80),
 			loadBalancerSku: "standard",
 			probeProtocol:   "Https",
 			probePath:       "/healthy1",
@@ -2369,50 +2369,53 @@ func TestReconcileLoadBalancerRule(t *testing.T) {
 				consts.ServiceAnnotationLoadBalancerInternal: "true",
 			}, true, 80),
 			loadBalancerSku: "standard",
-			expectedProbes:  getDefaultTestProbes("Tcp", ""),
-			expectedRules:   getDefaultInternalIPv6Rules(true),
+			expectedProbes: map[bool][]network.Probe{
+				// Use false as IPv6 param but it is a IPv6 probe.
+				true: {getTestProbe("Tcp", "", pointer.Int32(5), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(2), false)},
+			},
+			expectedRules: getDefaultInternalIPv6Rules(true),
 		},
 		{
 			desc: "getExpectedLBRules shall return corresponding probe and lbRule (slb with HA enabled)",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				consts.ServiceAnnotationLoadBalancerEnableHighAvailabilityPorts: "true",
 				consts.ServiceAnnotationLoadBalancerInternal:                    "true",
-			}, false, 80),
+			}, 80),
 			loadBalancerSku: "standard",
 			expectedProbes:  getDefaultTestProbes("Tcp", ""),
 			expectedRules: map[bool][]network.LoadBalancingRule{
-				false: getHATestRules(true, true, v1.ProtocolTCP, false),
-				true:  getHATestRules(true, true, v1.ProtocolTCP, true),
+				consts.IPVersionIPv4: getHATestRules(true, true, v1.ProtocolTCP, consts.IPVersionIPv4, true),
+				consts.IPVersionIPv6: getHATestRules(true, true, v1.ProtocolTCP, consts.IPVersionIPv6, true),
 			},
 		},
 		{
 			desc: "getExpectedLBRules shall return corresponding probe and lbRule (slb with HA mode and SCTP)",
-			service: getTestService("test1", v1.ProtocolSCTP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolSCTP, map[string]string{
 				consts.ServiceAnnotationLoadBalancerEnableHighAvailabilityPorts: "true",
 				consts.ServiceAnnotationLoadBalancerInternal:                    "true",
-			}, false, 80),
+			}, 80),
 			loadBalancerSku: "standard",
 			expectedRules: map[bool][]network.LoadBalancingRule{
-				false: getHATestRules(true, false, v1.ProtocolSCTP, false),
-				true:  getHATestRules(true, false, v1.ProtocolSCTP, true),
+				consts.IPVersionIPv4: getHATestRules(true, false, v1.ProtocolSCTP, consts.IPVersionIPv4, true),
+				consts.IPVersionIPv6: getHATestRules(true, false, v1.ProtocolSCTP, consts.IPVersionIPv6, true),
 			},
 		},
 		{
 			desc: "getExpectedLBRules shall return corresponding probe and lbRule (slb with HA enabled multi-ports services)",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				consts.ServiceAnnotationLoadBalancerEnableHighAvailabilityPorts: "true",
 				consts.ServiceAnnotationLoadBalancerInternal:                    "true",
-			}, false, 80, 8080),
+			}, 80, 8080),
 			loadBalancerSku: "standard",
 			expectedProbes:  getDefaultTestProbes("Tcp", ""),
 			expectedRules: map[bool][]network.LoadBalancingRule{
-				false: getHATestRules(true, true, v1.ProtocolTCP, false),
-				true:  getHATestRules(true, true, v1.ProtocolTCP, true),
+				consts.IPVersionIPv4: getHATestRules(true, true, v1.ProtocolTCP, consts.IPVersionIPv4, true),
+				consts.IPVersionIPv6: getHATestRules(true, true, v1.ProtocolTCP, consts.IPVersionIPv6, true),
 			},
 		},
 		{
 			desc:            "getExpectedLBRules should leave probe path empty when using TCP probe",
-			service:         getTestService("test1", v1.ProtocolTCP, nil, false, 80),
+			service:         getTestServiceDualStack("test1", v1.ProtocolTCP, nil, 80),
 			loadBalancerSku: "standard",
 			probeProtocol:   "Tcp",
 			expectedProbes:  getDefaultTestProbes("Tcp", ""),
@@ -2420,9 +2423,9 @@ func TestReconcileLoadBalancerRule(t *testing.T) {
 		},
 		{
 			desc: "getExpectedLBRules should return tcp probe when invalid protocol is defined",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsRequestPath): "/healthy1",
-			}, false, 80),
+			}, 80),
 			loadBalancerSku: "standard",
 			probeProtocol:   "TCP1",
 			expectedProbes:  getDefaultTestProbes("Tcp", ""),
@@ -2430,9 +2433,9 @@ func TestReconcileLoadBalancerRule(t *testing.T) {
 		},
 		{
 			desc: "getExpectedLBRules should return tcp probe when invalid protocol is defined",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsRequestPath): "/healthy1",
-			}, false, 80),
+			}, 80),
 			loadBalancerSku: "basic",
 			probeProtocol:   "TCP1",
 			expectedProbes:  getDefaultTestProbes("Tcp", ""),
@@ -2440,39 +2443,39 @@ func TestReconcileLoadBalancerRule(t *testing.T) {
 		},
 		{
 			desc: "getExpectedLBRules should return correct rule when deprecated annotations are added",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsRequestPath): "/healthy1",
 				consts.ServiceAnnotationLoadBalancerHealthProbeProtocol:                              "https",
-			}, false, 80),
+			}, 80),
 			loadBalancerSku: "standard",
 			expectedProbes:  getDefaultTestProbes("Https", "/healthy1"),
 			expectedRules:   getDefaultTestRules(true),
 		},
 		{
 			desc: "getExpectedLBRules should return correct rule when deprecated annotations are added",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsRequestPath): "/healthy1",
 				consts.ServiceAnnotationLoadBalancerHealthProbeProtocol:                              "http",
-			}, false, 80),
+			}, 80),
 			loadBalancerSku: "standard",
 			expectedProbes:  getDefaultTestProbes("Http", "/healthy1"),
 			expectedRules:   getDefaultTestRules(true),
 		},
 		{
 			desc: "getExpectedLBRules should return correct rule when deprecated annotations are added",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsRequestPath): "/healthy1",
 				consts.ServiceAnnotationLoadBalancerHealthProbeProtocol:                              "tcp",
-			}, false, 80),
+			}, 80),
 			loadBalancerSku: "standard",
 			expectedProbes:  getDefaultTestProbes("Tcp", ""),
 			expectedRules:   getDefaultTestRules(true),
 		},
 		{
 			desc: "getExpectedLBRules should return correct rule when deprecated annotations are added",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsRequestPath): "/healthy1",
-			}, false, 80),
+			}, 80),
 			loadBalancerSku: "standard",
 			probeProtocol:   "Https",
 			expectedProbes:  getDefaultTestProbes("Https", "/healthy1"),
@@ -2480,10 +2483,10 @@ func TestReconcileLoadBalancerRule(t *testing.T) {
 		},
 		{
 			desc: "getExpectedLBRules should overwrite value defined in deprecated annotation when deprecated annotations and probe path are defined",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				consts.ServiceAnnotationLoadBalancerHealthProbeRequestPath:                           "/healthy1",
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsRequestPath): "/healthy2",
-			}, false, 80),
+			}, 80),
 			loadBalancerSku: "standard",
 			probeProtocol:   "Https",
 			expectedProbes:  getDefaultTestProbes("Https", "/healthy2"),
@@ -2491,32 +2494,32 @@ func TestReconcileLoadBalancerRule(t *testing.T) {
 		},
 		{
 			desc: "getExpectedLBRules should return error when probe interval * num > 120",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsProbeInterval): "10",
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsNumOfProbe):    "20",
 				consts.ServiceAnnotationLoadBalancerHealthProbeProtocol:                                "https",
-			}, false, 80),
+			}, 80),
 			loadBalancerSku: "standard",
 			probeProtocol:   "Tcp",
 			expectedErr:     true,
 		},
 		{
 			desc: "getExpectedLBRules should return error when probe interval * num ==  120",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsProbeInterval): "10",
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsNumOfProbe):    "20",
 				consts.ServiceAnnotationLoadBalancerHealthProbeProtocol:                                "tcp",
-			}, false, 80),
+			}, 80),
 			loadBalancerSku: "standard",
 			probeProtocol:   "Tcp",
 			expectedErr:     true,
 		},
 		{
 			desc: "getExpectedLBRules should return correct rule when health probe annotations are added",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsProbeInterval): "20",
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsNumOfProbe):    "5",
-			}, false, 80),
+			}, 80),
 			loadBalancerSku: "standard",
 			probeProtocol:   "Https",
 			probePath:       "/healthy1",
@@ -2525,10 +2528,10 @@ func TestReconcileLoadBalancerRule(t *testing.T) {
 		},
 		{
 			desc: "getExpectedLBRules should return correct rule when health probe annotations are added,default path should be /",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsProbeInterval): "20",
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsNumOfProbe):    "5",
-			}, false, 80),
+			}, 80),
 			loadBalancerSku: "standard",
 			probeProtocol:   "Http",
 			expectedProbes:  getTestProbes("Http", "/", pointer.Int32(20), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(5)),
@@ -2536,10 +2539,10 @@ func TestReconcileLoadBalancerRule(t *testing.T) {
 		},
 		{
 			desc: "getExpectedLBRules should return correct rule when tcp health probe annotations are added",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsProbeInterval): "20",
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsNumOfProbe):    "5",
-			}, false, 80),
+			}, 80),
 			loadBalancerSku: "standard",
 			probeProtocol:   "Tcp",
 			expectedProbes:  getTestProbes("Tcp", "", pointer.Int32(20), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(5)),
@@ -2547,77 +2550,77 @@ func TestReconcileLoadBalancerRule(t *testing.T) {
 		},
 		{
 			desc: "getExpectedLBRules should return error when invalid tcp health probe annotations are added",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsProbeInterval): "20",
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsNumOfProbe):    "5a",
-			}, false, 80),
+			}, 80),
 			loadBalancerSku: "standard",
 			probeProtocol:   "Tcp",
 			expectedErr:     true,
 		},
 		{
 			desc: "getExpectedLBRules should return error when invalid tcp health probe annotations are added",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsProbeInterval): "1",
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsNumOfProbe):    "5",
-			}, false, 80),
+			}, 80),
 			loadBalancerSku: "standard",
 			probeProtocol:   "Tcp",
 			expectedErr:     true,
 		},
 		{
 			desc: "getExpectedLBRules should return error when invalid tcp health probe annotations are added",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsProbeInterval): "10",
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsNumOfProbe):    "1",
-			}, false, 80),
+			}, 80),
 			loadBalancerSku: "standard",
 			probeProtocol:   "Tcp",
 			expectedErr:     true,
 		},
 		{
 			desc: "getExpectedLBRules should return error when invalid tcp health probe annotations are added",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsProbeInterval): "10",
 				consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsNumOfProbe):    "20",
-			}, false, 80),
+			}, 80),
 			loadBalancerSku: "standard",
 			probeProtocol:   "Tcp",
 			expectedErr:     true,
 		},
 		{
 			desc:            "getExpectedLBRules should return correct rule when floating ip annotations are added",
-			service:         getTestService("test1", v1.ProtocolTCP, map[string]string{consts.ServiceAnnotationDisableLoadBalancerFloatingIP: "true"}, false, 80),
+			service:         getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{consts.ServiceAnnotationDisableLoadBalancerFloatingIP: "true"}, 80),
 			loadBalancerSku: "basic",
 			expectedRules: map[bool][]network.LoadBalancingRule{
-				false: {getFloatingIPTestRule(false, false, 80, false)},
-				true:  {getFloatingIPTestRule(false, false, 80, true)},
+				consts.IPVersionIPv4: {getFloatingIPTestRule(false, false, 80, consts.IPVersionIPv4)},
+				consts.IPVersionIPv6: {getFloatingIPTestRule(false, false, 80, consts.IPVersionIPv6)},
 			},
 			expectedProbes: getDefaultTestProbes("Tcp", ""),
 		},
 		{
 			desc: "getExpectedLBRules should prioritize port specific probe protocol over defaults",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				"service.beta.kubernetes.io/port_80_health-probe_protocol": "HtTp",
-			}, false, 80),
+			}, 80),
 			expectedRules:  getDefaultTestRules(false),
 			expectedProbes: getDefaultTestProbes("Http", "/"),
 		},
 		{
 			desc: "getExpectedLBRules should prioritize port specific probe protocol over appProtocol",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				"service.beta.kubernetes.io/port_80_health-probe_protocol": "HtTp",
-			}, false, 80),
+			}, 80),
 			probeProtocol:  "Mongodb",
 			expectedRules:  getDefaultTestRules(false),
 			expectedProbes: getDefaultTestProbes("Http", "/"),
 		},
 		{
 			desc: "getExpectedLBRules should prioritize port specific probe protocol over deprecated annotation",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				"service.beta.kubernetes.io/port_80_health-probe_protocol":             "HtTpS",
 				"service.beta.kubernetes.io/azure-load-balancer-health-probe-protocol": "TcP",
-			}, false, 80),
+			}, 80),
 			loadBalancerSku: "standard",
 			probeProtocol:   "Https",
 			expectedRules:   getDefaultTestRules(true),
@@ -2625,110 +2628,110 @@ func TestReconcileLoadBalancerRule(t *testing.T) {
 		},
 		{
 			desc: "getExpectedLBRules should default to Tcp on invalid port specific probe protocol",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				"service.beta.kubernetes.io/port_80_health-probe_protocol": "FooBar",
-			}, false, 80),
+			}, 80),
 			probeProtocol:  "Http",
 			expectedRules:  getDefaultTestRules(false),
 			expectedProbes: getDefaultTestProbes("Tcp", ""),
 		},
 		{
 			desc: "getExpectedLBRules should support customize health probe port in multi-port service",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				"service.beta.kubernetes.io/port_8000_health-probe_port": "port-tcp-80",
-			}, false, 80, 8000),
+			}, 80, 8000),
 			expectedRules: map[bool][]network.LoadBalancingRule{
-				false: {getTestRule(false, 80, false), getTestRule(false, 8000, false)},
-				true:  {getTestRule(false, 80, true), getTestRule(false, 8000, true)},
+				consts.IPVersionIPv4: {getTestRule(false, 80, consts.IPVersionIPv4), getTestRule(false, 8000, consts.IPVersionIPv4)},
+				consts.IPVersionIPv6: {getTestRule(false, 80, consts.IPVersionIPv6), getTestRule(false, 8000, consts.IPVersionIPv6)},
 			},
 			expectedProbes: map[bool][]network.Probe{
-				false: {
-					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(2), false),
-					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(8000), pointer.Int32(10080), pointer.Int32(2), false),
+				consts.IPVersionIPv4: {
+					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(2), consts.IPVersionIPv4),
+					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(8000), pointer.Int32(10080), pointer.Int32(2), consts.IPVersionIPv4),
 				},
-				true: {
-					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(2), true),
-					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(8000), pointer.Int32(10080), pointer.Int32(2), true),
+				consts.IPVersionIPv6: {
+					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(2), consts.IPVersionIPv6),
+					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(8000), pointer.Int32(10080), pointer.Int32(2), consts.IPVersionIPv6),
 				},
 			},
 		},
 		{
 			desc: "getExpectedLBRules should support customize health probe port in multi-port service",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				"service.beta.kubernetes.io/port_8000_health-probe_port": "80",
-			}, false, 80, 8000),
+			}, 80, 8000),
 			expectedRules: map[bool][]network.LoadBalancingRule{
-				false: {
-					getTestRule(false, 80, false),
-					getTestRule(false, 8000, false),
+				consts.IPVersionIPv4: {
+					getTestRule(false, 80, consts.IPVersionIPv4),
+					getTestRule(false, 8000, consts.IPVersionIPv4),
 				},
-				true: {
-					getTestRule(false, 80, true),
-					getTestRule(false, 8000, true),
+				consts.IPVersionIPv6: {
+					getTestRule(false, 80, consts.IPVersionIPv6),
+					getTestRule(false, 8000, consts.IPVersionIPv6),
 				},
 			},
 			expectedProbes: map[bool][]network.Probe{
-				false: {
-					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(2), false),
-					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(8000), pointer.Int32(10080), pointer.Int32(2), false),
+				consts.IPVersionIPv4: {
+					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(2), consts.IPVersionIPv4),
+					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(8000), pointer.Int32(10080), pointer.Int32(2), consts.IPVersionIPv4),
 				},
-				true: {
-					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(2), true),
-					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(8000), pointer.Int32(10080), pointer.Int32(2), true),
+				consts.IPVersionIPv6: {
+					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(2), consts.IPVersionIPv6),
+					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(8000), pointer.Int32(10080), pointer.Int32(2), consts.IPVersionIPv6),
 				},
 			},
 		},
 		{
 			desc: "getExpectedLBRules should not generate probe rule when no health probe rule is specified.",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				"service.beta.kubernetes.io/port_8000_no_probe_rule": "true",
-			}, false, 80, 8000),
+			}, 80, 8000),
 			expectedRules: map[bool][]network.LoadBalancingRule{
-				false: {
-					getTestRule(false, 80, false),
+				consts.IPVersionIPv4: {
+					getTestRule(false, 80, consts.IPVersionIPv4),
 					func() network.LoadBalancingRule {
-						rule := getTestRule(false, 8000, false)
+						rule := getTestRule(false, 8000, consts.IPVersionIPv4)
 						rule.Probe = nil
 						return rule
 					}(),
 				},
-				true: {
-					getTestRule(false, 80, true),
+				consts.IPVersionIPv6: {
+					getTestRule(false, 80, consts.IPVersionIPv6),
 					func() network.LoadBalancingRule {
-						rule := getTestRule(false, 8000, true)
+						rule := getTestRule(false, 8000, consts.IPVersionIPv6)
 						rule.Probe = nil
 						return rule
 					}(),
 				},
 			},
 			expectedProbes: map[bool][]network.Probe{
-				false: {
-					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(2), false),
+				consts.IPVersionIPv4: {
+					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(2), consts.IPVersionIPv4),
 				},
-				true: {
-					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(2), true),
+				consts.IPVersionIPv6: {
+					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(2), consts.IPVersionIPv6),
 				},
 			},
 		},
 		{
 			desc: "getExpectedLBRules should not generate lb rule and health probe rule when no lb rule is specified.",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 				"service.beta.kubernetes.io/port_8000_no_lb_rule": "true",
-			}, false, 80, 8000),
+			}, 80, 8000),
 			expectedRules: map[bool][]network.LoadBalancingRule{
-				false: {
-					getTestRule(false, 80, false),
+				consts.IPVersionIPv4: {
+					getTestRule(false, 80, consts.IPVersionIPv4),
 				},
-				true: {
-					getTestRule(false, 80, true),
+				consts.IPVersionIPv6: {
+					getTestRule(false, 80, consts.IPVersionIPv6),
 				},
 			},
 			expectedProbes: map[bool][]network.Probe{
-				false: {
-					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(2), false),
+				consts.IPVersionIPv4: {
+					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(2), consts.IPVersionIPv4),
 				},
-				true: {
-					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(2), true),
+				consts.IPVersionIPv6: {
+					getTestProbe("Tcp", "/", pointer.Int32(5), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(2), consts.IPVersionIPv6),
 				},
 			},
 		},
@@ -2750,32 +2753,32 @@ func TestReconcileLoadBalancerRule(t *testing.T) {
 		expectedErr     bool
 	}{
 		desc: "getExpectedLBRules should expected rules when timeout are added",
-		service: getTestService("test1", v1.ProtocolTCP, map[string]string{
+		service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{
 			consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsProbeInterval): "10",
 			consts.BuildHealthProbeAnnotationKeyForPort(80, consts.HealthProbeParamsNumOfProbe):    "10",
 			consts.ServiceAnnotationLoadBalancerIdleTimeout:                                        "5",
-		}, false, 80),
+		}, 80),
 		loadBalancerSku: "standard",
 		probeProtocol:   "Tcp",
 		expectedProbes:  getTestProbes("Tcp", "", pointer.Int32(10), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(10)),
 		expectedRules:   rulesDualStack,
 	})
 	rules1DualStack := map[bool][]network.LoadBalancingRule{
-		false: {
-			getTestRule(true, 80, false),
-			getTestRule(true, 443, false),
-			getTestRule(true, 421, false),
+		consts.IPVersionIPv4: {
+			getTestRule(true, 80, consts.IPVersionIPv4),
+			getTestRule(true, 443, consts.IPVersionIPv4),
+			getTestRule(true, 421, consts.IPVersionIPv4),
 		},
-		true: {
-			getTestRule(true, 80, true),
-			getTestRule(true, 443, true),
-			getTestRule(true, 421, true),
+		consts.IPVersionIPv6: {
+			getTestRule(true, 80, consts.IPVersionIPv6),
+			getTestRule(true, 443, consts.IPVersionIPv6),
+			getTestRule(true, 421, consts.IPVersionIPv6),
 		},
 	}
-	for _, rule := range rules1DualStack[false] {
+	for _, rule := range rules1DualStack[consts.IPVersionIPv4] {
 		rule.Probe.ID = pointer.String("/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lbname/probes/atest1-TCP-34567")
 	}
-	for _, rule := range rules1DualStack[true] {
+	for _, rule := range rules1DualStack[consts.IPVersionIPv6] {
 		rule.Probe.ID = pointer.String("/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lbname/probes/atest1-TCP-34567-IPv6")
 	}
 
@@ -2809,11 +2812,11 @@ func TestReconcileLoadBalancerRule(t *testing.T) {
 		expectedRules:   rules1DualStack,
 	})
 	rules1DualStack = map[bool][]network.LoadBalancingRule{
-		false: {
-			getTestRule(true, 80, false),
+		consts.IPVersionIPv4: {
+			getTestRule(true, 80, consts.IPVersionIPv4),
 		},
-		true: {
-			getTestRule(true, 80, true),
+		consts.IPVersionIPv6: {
+			getTestRule(true, 80, consts.IPVersionIPv6),
 		},
 	}
 	// When the service spec externalTrafficPolicy is Local and azure-disable-service-health-port-probe is set, should return default
@@ -2852,7 +2855,6 @@ func TestReconcileLoadBalancerRule(t *testing.T) {
 			az := GetTestCloud(ctrl)
 			az.Config.LoadBalancerSku = test.loadBalancerSku
 			service := test.service
-			service.Spec.IPFamilies = []v1.IPFamily{v1.IPv4Protocol, v1.IPv6Protocol}
 			firstPort := service.Spec.Ports[0]
 			probeProtocol := test.probeProtocol
 			if test.probeProtocol != "" {
@@ -2861,29 +2863,43 @@ func TestReconcileLoadBalancerRule(t *testing.T) {
 			if test.probePath != "" {
 				service.Annotations[consts.BuildHealthProbeAnnotationKeyForPort(firstPort.Port, consts.HealthProbeParamsRequestPath)] = test.probePath
 			}
-			probeV4, lbruleV4, errV4 := az.getExpectedLBRules(&service,
-				"frontendIPConfigID", "backendPoolID", "lbname", false)
-			probeV6, lbruleV6, errV6 := az.getExpectedLBRules(&service,
-				"frontendIPConfigID-IPv6", "backendPoolID-IPv6", "lbname", true)
-			if test.expectedErr {
-				assert.Error(t, errV4)
-				assert.Error(t, errV6)
-			} else {
-				assert.Equal(t, test.expectedProbes[false], probeV4)
-				assert.Equal(t, test.expectedProbes[true], probeV6)
-				assert.Equal(t, test.expectedRules[false], lbruleV4)
-				assert.Equal(t, test.expectedRules[true], lbruleV6)
-				assert.NoError(t, errV4)
-				assert.NoError(t, errV6)
+			v4Enabled, v6Enabled := getIPFamiliesEnabled(&service)
+			if v4Enabled {
+				probe, lbrule, err := az.getExpectedLBRules(&service,
+					"frontendIPConfigID", "backendPoolID", "lbname", consts.IPVersionIPv4)
+				if test.expectedErr {
+					assert.Error(t, err)
+				} else {
+					assert.Equal(t, test.expectedProbes[consts.IPVersionIPv4], probe)
+					assert.Equal(t, test.expectedRules[consts.IPVersionIPv4], lbrule)
+					assert.NoError(t, err)
+				}
+			}
+			isDualStack := v4Enabled && v6Enabled
+			if v6Enabled {
+				lbFrontendIPConfigID, lbBackendPoolID := "frontendIPConfigID", "backendPoolID-IPv6"
+				if isDualStack {
+					lbFrontendIPConfigID = "frontendIPConfigID-IPv6"
+				}
+				probe, lbrule, err := az.getExpectedLBRules(&service,
+					lbFrontendIPConfigID, lbBackendPoolID, "lbname", consts.IPVersionIPv6)
+				if test.expectedErr {
+					assert.Error(t, err)
+				} else {
+					assert.Equal(t, test.expectedProbes[consts.IPVersionIPv6], probe)
+					assert.Equal(t, test.expectedRules[consts.IPVersionIPv6], lbrule)
+					assert.NoError(t, err)
+				}
 			}
 		})
 	}
 }
 
+// getTestProbes returns dualStack probes.
 func getTestProbes(protocol, path string, interval, servicePort, probePort, numOfProbe *int32) map[bool][]network.Probe {
 	return map[bool][]network.Probe{
-		false: {getTestProbe(protocol, path, interval, servicePort, probePort, numOfProbe, false)},
-		true:  {getTestProbe(protocol, path, interval, servicePort, probePort, numOfProbe, true)},
+		consts.IPVersionIPv4: {getTestProbe(protocol, path, interval, servicePort, probePort, numOfProbe, consts.IPVersionIPv4)},
+		consts.IPVersionIPv6: {getTestProbe(protocol, path, interval, servicePort, probePort, numOfProbe, consts.IPVersionIPv6)},
 	}
 }
 
@@ -2907,28 +2923,31 @@ func getTestProbe(protocol, path string, interval, servicePort, probePort, numOf
 	return expectedProbes
 }
 
+// getDefaultTestProbes returns dualStack probes.
 func getDefaultTestProbes(protocol, path string) map[bool][]network.Probe {
 	return getTestProbes(protocol, path, pointer.Int32(5), pointer.Int32(80), pointer.Int32(10080), pointer.Int32(2))
 }
 
+// getDefaultTestRules returns dualstack rules.
 func getDefaultTestRules(enableTCPReset bool) map[bool][]network.LoadBalancingRule {
 	return map[bool][]network.LoadBalancingRule{
-		false: {getTestRule(enableTCPReset, 80, false)},
-		true:  {getTestRule(enableTCPReset, 80, true)},
+		consts.IPVersionIPv4: {getTestRule(enableTCPReset, 80, consts.IPVersionIPv4)},
+		consts.IPVersionIPv6: {getTestRule(enableTCPReset, 80, consts.IPVersionIPv6)},
 	}
 }
 
+// getDefaultInternalIPv6Rules returns a rule for IPv6 single stack.
 func getDefaultInternalIPv6Rules(enableTCPReset bool) map[bool][]network.LoadBalancingRule {
-	rulesDualStack := getDefaultTestRules(true)
-	for _, rules := range rulesDualStack {
-		for _, rule := range rules {
-			rule.EnableFloatingIP = pointer.Bool(false)
-			rule.BackendPort = pointer.Int32(getBackendPort(*rule.FrontendPort))
-		}
+	rule := getTestRule(true, 80, false)
+	rule.EnableFloatingIP = pointer.Bool(false)
+	rule.BackendPort = pointer.Int32(getBackendPort(*rule.FrontendPort))
+	rule.BackendAddressPool.ID = pointer.String("backendPoolID-IPv6")
+	return map[bool][]network.LoadBalancingRule{
+		true: {rule},
 	}
-	return rulesDualStack
 }
 
+// getTestRule returns a rule for dualStack.
 func getTestRule(enableTCPReset bool, port int32, isIPv6 bool) network.LoadBalancingRule {
 	suffix := ""
 	if isIPv6 {
@@ -2962,14 +2981,19 @@ func getTestRule(enableTCPReset bool, port int32, isIPv6 bool) network.LoadBalan
 	return expectedRules
 }
 
-func getHATestRules(enableTCPReset, hasProbe bool, protocol v1.Protocol, isIPv6 bool) []network.LoadBalancingRule {
+func getHATestRules(enableTCPReset, hasProbe bool, protocol v1.Protocol, isIPv6, isInternal bool) []network.LoadBalancingRule {
 	suffix := ""
+	enableFloatingIP := true
 	if isIPv6 {
 		suffix = "-" + v6Suffix
+		if isInternal {
+			enableFloatingIP = false
+		}
 	}
+
 	expectedRules := []network.LoadBalancingRule{
 		{
-			Name: pointer.String(fmt.Sprintf("atest1-%s-80", string(protocol)) + suffix),
+			Name: pointer.String(fmt.Sprintf("atest1-%s-80%s", string(protocol), suffix)),
 			LoadBalancingRulePropertiesFormat: &network.LoadBalancingRulePropertiesFormat{
 				Protocol: network.TransportProtocol("All"),
 				FrontendIPConfiguration: &network.SubResource{
@@ -2981,7 +3005,7 @@ func getHATestRules(enableTCPReset, hasProbe bool, protocol v1.Protocol, isIPv6
 				LoadDistribution:     "Default",
 				FrontendPort:         pointer.Int32(0),
 				BackendPort:          pointer.Int32(0),
-				EnableFloatingIP:     pointer.Bool(true),
+				EnableFloatingIP:     pointer.Bool(enableFloatingIP),
 				DisableOutboundSnat:  pointer.Bool(false),
 				IdleTimeoutInMinutes: pointer.Int32(4),
 				EnableTCPReset:       pointer.Bool(true),
@@ -3932,20 +3956,20 @@ func TestGetServiceLoadBalancerStatus(t *testing.T) {
 
 	for _, test := range testCases {
 		t.Run(test.desc, func(t *testing.T) {
-			status, _, err := az.getServiceLoadBalancerStatus(test.service, test.lb)
+			status, _, _, err := az.getServiceLoadBalancerStatus(test.service, test.lb)
 			assert.Equal(t, test.expectedStatus, status)
 			assert.Equal(t, test.expectedError, err != nil)
 		})
 	}
 }
 
-func TestReconcileSecurityGroup(t *testing.T) {
+func TestReconcileSecurityGroupCommon(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
 	testCases := []struct {
 		desc          string
-		lbIP          *string
+		lbIPs         *[]string
 		lbName        *string
 		service       v1.Service
 		existingSgs   map[string]network.SecurityGroup
@@ -3966,25 +3990,25 @@ func TestReconcileSecurityGroup(t *testing.T) {
 		},
 		{
 			desc:          "reconcileSecurityGroup shall report error if no such sg can be found",
-			service:       getTestService("test1", v1.ProtocolTCP, nil, false, 80),
+			service:       getTestServiceDualStack("test1", v1.ProtocolTCP, nil, 80),
 			expectedError: true,
 		},
 		{
-			desc:          "reconcileSecurityGroup shall report error if wantLb is true and lbIP is nil",
-			service:       getTestService("test1", v1.ProtocolTCP, nil, false, 80),
+			desc:          "reconcileSecurityGroup shall report error if wantLb is true and lbIPs is nil",
+			service:       getTestServiceDualStack("test1", v1.ProtocolTCP, nil, 80),
 			wantLb:        true,
 			existingSgs:   map[string]network.SecurityGroup{"nsg": {}},
 			expectedError: true,
 		},
 		{
 			desc:        "reconcileSecurityGroup shall remain the existingSgs intact if nothing needs to be modified",
-			service:     getTestService("test1", v1.ProtocolTCP, nil, false, 80),
+			service:     getTestServiceDualStack("test1", v1.ProtocolTCP, nil, 80),
 			existingSgs: map[string]network.SecurityGroup{"nsg": {}},
 			expectedSg:  &network.SecurityGroup{},
 		},
 		{
-			desc:    "reconcileSecurityGroup shall delete unwanted sg if wantLb is false and lbIP is nil",
-			service: getTestService("test1", v1.ProtocolTCP, nil, false, 80),
+			desc:    "reconcileSecurityGroup shall delete unwanted sg if wantLb is false and lbIPs is nil",
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, nil, 80),
 			existingSgs: map[string]network.SecurityGroup{"nsg": {
 				Name: pointer.String("nsg"),
 				SecurityGroupPropertiesFormat: &network.SecurityGroupPropertiesFormat{
@@ -4011,7 +4035,7 @@ func TestReconcileSecurityGroup(t *testing.T) {
 		},
 		{
 			desc:    "reconcileSecurityGroup shall delete unwanted sgs and create needed ones",
-			service: getTestService("test1", v1.ProtocolTCP, nil, false, 80),
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, nil, 80),
 			existingSgs: map[string]network.SecurityGroup{"nsg": {
 				Name: pointer.String("nsg"),
 				SecurityGroupPropertiesFormat: &network.SecurityGroupPropertiesFormat{
@@ -4028,7 +4052,7 @@ func TestReconcileSecurityGroup(t *testing.T) {
 					},
 				},
 			}},
-			lbIP:   pointer.String("1.1.1.1"),
+			lbIPs:  &[]string{"1.1.1.1", "fd00::eef0"},
 			wantLb: true,
 			expectedSg: &network.SecurityGroup{
 				Name: pointer.String("nsg"),
@@ -4047,18 +4071,31 @@ func TestReconcileSecurityGroup(t *testing.T) {
 								Direction:                network.SecurityRuleDirection("Inbound"),
 							},
 						},
+						{
+							Name: pointer.String("atest1-TCP-80-Internet-IPv6"),
+							SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
+								Protocol:                 network.SecurityRuleProtocol("Tcp"),
+								SourcePortRange:          pointer.String("*"),
+								DestinationPortRange:     pointer.String("80"),
+								SourceAddressPrefix:      pointer.String("Internet"),
+								DestinationAddressPrefix: pointer.String("fd00::eef0"),
+								Access:                   network.SecurityRuleAccess("Allow"),
+								Priority:                 pointer.Int32(501),
+								Direction:                network.SecurityRuleDirection("Inbound"),
+							},
+						},
 					},
 				},
 			},
 		},
 		{
-			desc:    "reconcileSecurityGroup shall create sgs with correct destinationPrefix for IPv6",
+			desc:    "reconcileSecurityGroup shall create sgs with correct destinationPrefix for IPv6 only",
 			service: getTestService("test1", v1.ProtocolTCP, nil, true, 80),
 			existingSgs: map[string]network.SecurityGroup{"nsg": {
 				Name:                          pointer.String("nsg"),
 				SecurityGroupPropertiesFormat: &network.SecurityGroupPropertiesFormat{},
 			}},
-			lbIP:   pointer.String("fd00::eef0"),
+			lbIPs:  &[]string{"fd00::eef0"},
 			wantLb: true,
 			expectedSg: &network.SecurityGroup{
 				Name: pointer.String("nsg"),
@@ -4081,14 +4118,57 @@ func TestReconcileSecurityGroup(t *testing.T) {
 				},
 			},
 		},
+		{
+			desc:    "reconcileSecurityGroup shall create sgs with correct destinationPrefix for Dual-stack",
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, nil, 80),
+			existingSgs: map[string]network.SecurityGroup{"nsg": {
+				Name:                          pointer.String("nsg"),
+				SecurityGroupPropertiesFormat: &network.SecurityGroupPropertiesFormat{},
+			}},
+			lbIPs:  &[]string{"1.1.1.1", "fd00::eef0"},
+			wantLb: true,
+			expectedSg: &network.SecurityGroup{
+				Name: pointer.String("nsg"),
+				SecurityGroupPropertiesFormat: &network.SecurityGroupPropertiesFormat{
+					SecurityRules: &[]network.SecurityRule{
+						{
+							Name: pointer.String("atest1-TCP-80-Internet"),
+							SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
+								Protocol:                 network.SecurityRuleProtocol("Tcp"),
+								SourcePortRange:          pointer.String("*"),
+								DestinationPortRange:     pointer.String("80"),
+								SourceAddressPrefix:      pointer.String("Internet"),
+								DestinationAddressPrefix: pointer.String("1.1.1.1"),
+								Access:                   network.SecurityRuleAccess("Allow"),
+								Priority:                 pointer.Int32(500),
+								Direction:                network.SecurityRuleDirection("Inbound"),
+							},
+						},
+						{
+							Name: pointer.String("atest1-TCP-80-Internet-IPv6"),
+							SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
+								Protocol:                 network.SecurityRuleProtocol("Tcp"),
+								SourcePortRange:          pointer.String("*"),
+								DestinationPortRange:     pointer.String("80"),
+								SourceAddressPrefix:      pointer.String("Internet"),
+								DestinationAddressPrefix: pointer.String("fd00::eef0"),
+								Access:                   network.SecurityRuleAccess("Allow"),
+								Priority:                 pointer.Int32(501),
+								Direction:                network.SecurityRuleDirection("Inbound"),
+							},
+						},
+					},
+				},
+			},
+		},
 		{
 			desc:    "reconcileSecurityGroup shall create sgs with correct destinationPrefix with additional public IPs",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{consts.ServiceAnnotationAdditionalPublicIPs: "2.3.4.5"}, true, 80),
+			service: getTestServiceDualStack("test1", v1.ProtocolTCP, map[string]string{consts.ServiceAnnotationAdditionalPublicIPs: "2.3.4.5,fd00::eef1"}, 80),
 			existingSgs: map[string]network.SecurityGroup{"nsg": {
 				Name:                          pointer.String("nsg"),
 				SecurityGroupPropertiesFormat: &network.SecurityGroupPropertiesFormat{},
 			}},
-			lbIP:   pointer.String("1.2.3.4"),
+			lbIPs:  &[]string{"1.2.3.4", "fd00::eef0"},
 			wantLb: true,
 			expectedSg: &network.SecurityGroup{
 				Name: pointer.String("nsg"),
@@ -4107,15 +4187,28 @@ func TestReconcileSecurityGroup(t *testing.T) {
 								Direction:                  network.SecurityRuleDirection("Inbound"),
 							},
 						},
+						{
+							Name: pointer.String("atest1-TCP-80-Internet-IPv6"),
+							SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
+								Protocol:                   network.SecurityRuleProtocol("Tcp"),
+								SourcePortRange:            pointer.String("*"),
+								DestinationPortRange:       pointer.String("80"),
+								SourceAddressPrefix:        pointer.String("Internet"),
+								DestinationAddressPrefixes: &([]string{"fd00::eef0", "fd00::eef1"}),
+								Access:                     network.SecurityRuleAccess("Allow"),
+								Priority:                   pointer.Int32(501),
+								Direction:                  network.SecurityRuleDirection("Inbound"),
+							},
+						},
 					},
 				},
 			},
 		},
 		{
 			desc:    "reconcileSecurityGroup shall not create unwanted security rules if there is service tags",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{consts.ServiceAnnotationAllowedServiceTag: "tag"}, true, 80),
+			service: getTestService("test1", v1.ProtocolTCP, map[string]string{consts.ServiceAnnotationAllowedServiceTag: "tag"}, false, 80),
 			wantLb:  true,
-			lbIP:    pointer.String("1.1.1.1"),
+			lbIPs:   &[]string{"1.1.1.1"},
 			existingSgs: map[string]network.SecurityGroup{"nsg": {
 				Name: pointer.String("nsg"),
 				SecurityGroupPropertiesFormat: &network.SecurityGroupPropertiesFormat{
@@ -4155,12 +4248,12 @@ func TestReconcileSecurityGroup(t *testing.T) {
 		},
 		{
 			desc:    "reconcileSecurityGroup shall create shared sgs for service with azure-shared-securityrule annotations",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{consts.ServiceAnnotationSharedSecurityRule: "true"}, true, 80),
+			service: getTestService("test1", v1.ProtocolTCP, map[string]string{consts.ServiceAnnotationSharedSecurityRule: "true"}, false, 80),
 			existingSgs: map[string]network.SecurityGroup{"nsg": {
 				Name:                          pointer.String("nsg"),
 				SecurityGroupPropertiesFormat: &network.SecurityGroupPropertiesFormat{},
 			}},
-			lbIP:   pointer.String("1.2.3.4"),
+			lbIPs:  &[]string{"1.2.3.4"},
 			wantLb: true,
 			expectedSg: &network.SecurityGroup{
 				Name: pointer.String("nsg"),
@@ -4185,7 +4278,7 @@ func TestReconcileSecurityGroup(t *testing.T) {
 		},
 		{
 			desc:    "reconcileSecurityGroup shall delete shared sgs for service with azure-shared-securityrule annotations",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{consts.ServiceAnnotationSharedSecurityRule: "true"}, true, 80),
+			service: getTestService("test1", v1.ProtocolTCP, map[string]string{consts.ServiceAnnotationSharedSecurityRule: "true"}, false, 80),
 			existingSgs: map[string]network.SecurityGroup{"nsg": {
 				Name: pointer.String("nsg"),
 				SecurityGroupPropertiesFormat: &network.SecurityGroupPropertiesFormat{
@@ -4206,7 +4299,7 @@ func TestReconcileSecurityGroup(t *testing.T) {
 					},
 				},
 			}},
-			lbIP:   pointer.String("1.2.3.4"),
+			lbIPs:  &[]string{"1.2.3.4"},
 			wantLb: false,
 			expectedSg: &network.SecurityGroup{
 				Name: pointer.String("nsg"),
@@ -4217,7 +4310,7 @@ func TestReconcileSecurityGroup(t *testing.T) {
 		},
 		{
 			desc:    "reconcileSecurityGroup shall delete shared sgs destination for service with azure-shared-securityrule annotations",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{consts.ServiceAnnotationSharedSecurityRule: "true"}, true, 80),
+			service: getTestService("test1", v1.ProtocolTCP, map[string]string{consts.ServiceAnnotationSharedSecurityRule: "true"}, false, 80),
 			existingSgs: map[string]network.SecurityGroup{"nsg": {
 				Name: pointer.String("nsg"),
 				SecurityGroupPropertiesFormat: &network.SecurityGroupPropertiesFormat{
@@ -4238,7 +4331,7 @@ func TestReconcileSecurityGroup(t *testing.T) {
 					},
 				},
 			}},
-			lbIP:   pointer.String("1.2.3.4"),
+			lbIPs:  &[]string{"1.2.3.4"},
 			wantLb: false,
 			expectedSg: &network.SecurityGroup{
 				Name: pointer.String("nsg"),
@@ -4268,7 +4361,7 @@ func TestReconcileSecurityGroup(t *testing.T) {
 				Name:                          pointer.String("nsg"),
 				SecurityGroupPropertiesFormat: &network.SecurityGroupPropertiesFormat{},
 			}},
-			lbIP:   pointer.String("1.2.3.4"),
+			lbIPs:  &[]string{"1.2.3.4"},
 			lbName: pointer.String("lb"),
 			wantLb: true,
 			expectedSg: &network.SecurityGroup{
@@ -4294,12 +4387,12 @@ func TestReconcileSecurityGroup(t *testing.T) {
 		},
 		{
 			desc:    "reconcileSecurityGroup shall create sgs with only IPv6 destination addresses for IPv6 services with floating IP disabled",
-			service: getTestService("test1", v1.ProtocolTCP, map[string]string{consts.ServiceAnnotationDisableLoadBalancerFloatingIP: "true"}, false, 80),
+			service: getTestService("test1", v1.ProtocolTCP, map[string]string{consts.ServiceAnnotationDisableLoadBalancerFloatingIP: "true"}, true, 80),
 			existingSgs: map[string]network.SecurityGroup{"nsg": {
 				Name:                          pointer.String("nsg"),
 				SecurityGroupPropertiesFormat: &network.SecurityGroupPropertiesFormat{},
 			}},
-			lbIP:   pointer.String("1234::5"),
+			lbIPs:  &[]string{"1234::5"},
 			lbName: pointer.String("lb"),
 			wantLb: true,
 			expectedSg: &network.SecurityGroup{
@@ -4345,7 +4438,7 @@ func TestReconcileSecurityGroup(t *testing.T) {
 				mockLBClient.EXPECT().Get(gomock.Any(), "rg", *test.lbName, gomock.Any()).Return(network.LoadBalancer{}, nil)
 			}
 			service := test.service
-			sg, err := az.reconcileSecurityGroup("testCluster", &service, test.lbIP, test.lbName, test.wantLb)
+			sg, err := az.reconcileSecurityGroup("testCluster", &service, test.lbIPs, test.lbName, test.wantLb)
 			assert.Equal(t, test.expectedSg, sg)
 			assert.Equal(t, test.expectedError, err != nil)
 		})
@@ -4365,7 +4458,7 @@ func TestReconcileSecurityGroupLoadBalancerSourceRanges(t *testing.T) {
 			SecurityRules: &[]network.SecurityRule{},
 		},
 	}
-	lbIP := pointer.String("1.1.1.1")
+	lbIPs := &[]string{"1.1.1.1"}
 	expectedSg := network.SecurityGroup{
 		Name: pointer.String("nsg"),
 		SecurityGroupPropertiesFormat: &network.SecurityGroupPropertiesFormat{
@@ -4402,7 +4495,7 @@ func TestReconcileSecurityGroupLoadBalancerSourceRanges(t *testing.T) {
 	mockSGClient := az.SecurityGroupsClient.(*mocksecuritygroupclient.MockInterface)
 	mockSGClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, gomock.Any(), gomock.Any()).Return(existingSg, nil)
 	mockSGClient.EXPECT().CreateOrUpdate(gomock.Any(), az.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
-	sg, err := az.reconcileSecurityGroup("testCluster", &service, lbIP, nil, true)
+	sg, err := az.reconcileSecurityGroup("testCluster", &service, lbIPs, nil, true)
 	assert.NoError(t, err)
 	assert.Equal(t, expectedSg, *sg)
 }
@@ -5142,7 +5235,7 @@ func compareStrings(s0, s1 []string) bool {
 	return ss0.Equal(ss1)
 }
 
-func TestEnsurePublicIPExists(t *testing.T) {
+func TestEnsurePublicIPExistsCommon(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
@@ -5486,7 +5579,8 @@ func TestEnsurePublicIPExists(t *testing.T) {
 				{
 					Name: pointer.String("pip1"),
 					PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{
-						IPAddress: pointer.String("1.2.3.4"),
+						PublicIPAddressVersion: network.IPv4,
+						IPAddress:              pointer.String("1.2.3.4"),
 					},
 					Tags: map[string]*string{"a": pointer.String("b")},
 				},
@@ -6399,7 +6493,7 @@ func TestRemoveFrontendIPConfigurationFromLoadBalancerDelete(t *testing.T) {
 		mockPLSClient := cloud.PrivateLinkServiceClient.(*mockprivatelinkserviceclient.MockInterface)
 		mockPLSClient.EXPECT().List(gomock.Any(), "rg").Return(expectedPLS, nil).MaxTimes(1)
 		existingLBs := []network.LoadBalancer{{Name: pointer.String("lb")}}
-		err := cloud.removeFrontendIPConfigurationFromLoadBalancer(&lb, existingLBs, fip, "testCluster", &service)
+		err := cloud.removeFrontendIPConfigurationFromLoadBalancer(&lb, existingLBs, []*network.FrontendIPConfiguration{fip}, "testCluster", &service)
 		assert.NoError(t, err)
 	})
 }
@@ -6430,7 +6524,7 @@ func TestRemoveFrontendIPConfigurationFromLoadBalancerUpdate(t *testing.T) {
 		expectedPLS := make([]network.PrivateLinkService, 0)
 		mockPLSClient := cloud.PrivateLinkServiceClient.(*mockprivatelinkserviceclient.MockInterface)
 		mockPLSClient.EXPECT().List(gomock.Any(), "rg").Return(expectedPLS, nil).MaxTimes(1)
-		err := cloud.removeFrontendIPConfigurationFromLoadBalancer(&lb, []network.LoadBalancer{}, fip, "testCluster", &service)
+		err := cloud.removeFrontendIPConfigurationFromLoadBalancer(&lb, []network.LoadBalancer{}, []*network.FrontendIPConfiguration{fip}, "testCluster", &service)
 		assert.NoError(t, err)
 	})
 }
@@ -6626,8 +6720,8 @@ func TestReconcileZonesForFrontendIPConfigs(t *testing.T) {
 			isDualStack := isServiceDualStack(&service)
 			defaultLBFrontendIPConfigName := cloud.getDefaultFrontendIPConfigName(&service)
 			lbFrontendIPConfigNames := map[bool]string{
-				false: getResourceByIPFamily(defaultLBFrontendIPConfigName, isDualStack, false),
-				true:  getResourceByIPFamily(defaultLBFrontendIPConfigName, isDualStack, true),
+				consts.IPVersionIPv4: getResourceByIPFamily(defaultLBFrontendIPConfigName, isDualStack, consts.IPVersionIPv4),
+				consts.IPVersionIPv6: getResourceByIPFamily(defaultLBFrontendIPConfigName, isDualStack, consts.IPVersionIPv6),
 			}
 			_, _, dirty, err := cloud.reconcileFrontendIPConfigs("testCluster", &service, &lb, tc.status, true, lbFrontendIPConfigNames)
 			if tc.expectedErr == nil {
diff --git a/pkg/provider/azure_standard_test.go b/pkg/provider/azure_standard_test.go
index e384a91eb..29c8db003 100644
--- a/pkg/provider/azure_standard_test.go
+++ b/pkg/provider/azure_standard_test.go
@@ -1052,7 +1052,7 @@ func TestGetBackendPoolNames(t *testing.T) {
 			name:              "GetBackendPoolNames should return 2 backend pool names",
 			service:           getTestService("test1", v1.ProtocolTCP, nil, true, 80),
 			clusterName:       "azure",
-			expectedPoolNames: map[bool]string{false: "azure", true: "azure-IPv6"},
+			expectedPoolNames: map[bool]string{consts.IPVersionIPv4: "azure", consts.IPVersionIPv6: "azure-IPv6"},
 		},
 	}
 	for _, test := range testcases {
diff --git a/pkg/provider/azure_test.go b/pkg/provider/azure_test.go
index e90fba36a..211680d33 100644
--- a/pkg/provider/azure_test.go
+++ b/pkg/provider/azure_test.go
@@ -21,6 +21,7 @@ import (
 	"errors"
 	"fmt"
 	"math"
+	"net"
 	"net/http"
 	"os"
 	"strings"
@@ -40,7 +41,6 @@ import (
 	cloudprovider "k8s.io/cloud-provider"
 	cloudproviderapi "k8s.io/cloud-provider/api"
 	servicehelpers "k8s.io/cloud-provider/service/helpers"
-	utilnet "k8s.io/utils/net"
 	"k8s.io/utils/pointer"
 
 	"sigs.k8s.io/cloud-provider-azure/pkg/azureclients/interfaceclient/mockinterfaceclient"
@@ -58,13 +58,22 @@ import (
 
 const (
 	testClusterName = "testCluster"
-	testIP1         = "192.168.77.88"
-	testIP2         = "192.168.33.44"
-	testIP3         = "192.168.99.11"
-	testRuleName    = "shared-TCP-80-Internet"
-	testRuleName2   = "shared-TCP-4444-Internet"
-	testRuleName3   = "shared-TCP-8888-Internet"
-	testRuleName4   = "TCP-80-Internet"
+)
+
+var (
+	testIPs = []map[bool]string{
+		{false: "192.168.77.88", true: "fe::1"},
+		{false: "192.168.33.44", true: "fe::2"},
+		{false: "192.168.99.11", true: "fe::3"},
+		{false: "192.168.22.33", true: "fe::4"},
+	}
+
+	testRuleNames = []map[bool]string{
+		{false: "shared-TCP-80-Internet", true: "shared-TCP-80-Internet-IPv6"},
+		{false: "shared-TCP-4444-Internet", true: "shared-TCP-4444-Internet-IPv6"},
+		{false: "shared-TCP-8888-Internet", true: "shared-TCP-8888-Internet-IPv6"},
+		{false: "TCP-80-Internet", true: "TCP-80-Internet-IPv6"},
+	}
 )
 
 // Test flipServiceInternalAnnotation
@@ -126,23 +135,43 @@ func TestAddPort(t *testing.T) {
 	validateLoadBalancer(t, lb, svc)
 }
 
-func TestLoadBalancerInternalServiceModeSelection(t *testing.T) {
-	testLoadBalancerServiceDefaultModeSelection(t, true)
-	testLoadBalancerServiceAutoModeSelection(t, true)
-	testLoadBalancerServicesSpecifiedSelection(t, true)
-	testLoadBalancerMaxRulesServices(t, true)
-	testLoadBalancerServiceAutoModeDeleteSelection(t, true)
-}
+func TestLoadBalancerSelection(t *testing.T) {
+	testcases := []struct {
+		desc       string
+		isInternal bool
+	}{
+		{
+			desc:       "internal",
+			isInternal: true,
+		},
+		{
+			desc:       "external",
+			isInternal: false,
+		},
+	}
 
-func TestLoadBalancerExternalServiceModeSelection(t *testing.T) {
-	testLoadBalancerServiceDefaultModeSelection(t, false)
-	testLoadBalancerServiceAutoModeSelection(t, false)
-	testLoadBalancerServicesSpecifiedSelection(t, false)
-	testLoadBalancerMaxRulesServices(t, false)
-	testLoadBalancerServiceAutoModeDeleteSelection(t, false)
+	for _, tc := range testcases {
+		t.Run(tc.desc, func(t *testing.T) {
+			t.Run("defaultModeSelection", func(t *testing.T) {
+				testLoadBalancerServiceDefaultModeSelection(t, tc.isInternal)
+			})
+			t.Run("autoModeSelection", func(t *testing.T) {
+				testLoadBalancerServiceAutoModeSelection(t, tc.isInternal)
+			})
+			t.Run("specifiedSelection", func(t *testing.T) {
+				testLoadBalancerServicesSpecifiedSelection(t, tc.isInternal)
+			})
+			t.Run("maxRulesServices", func(t *testing.T) {
+				testLoadBalancerMaxRulesServices(t, tc.isInternal)
+			})
+			t.Run("autoModeDeleteSelection", func(t *testing.T) {
+				testLoadBalancerServiceAutoModeDeleteSelection(t, tc.isInternal)
+			})
+		})
+	}
 }
 
-func setMockEnvDualStack(az *Cloud, ctrl *gomock.Controller, expectedInterfaces []network.Interface, expectedVirtualMachines []compute.VirtualMachine, serviceCount int) {
+func setMockEnvDualStack(az *Cloud, ctrl *gomock.Controller, expectedInterfaces []network.Interface, expectedVirtualMachines []compute.VirtualMachine, serviceCount int, services ...v1.Service) {
 	mockInterfacesClient := mockinterfaceclient.NewMockInterface(ctrl)
 	az.InterfacesClient = mockInterfacesClient
 	for i := range expectedInterfaces {
@@ -159,7 +188,7 @@ func setMockEnvDualStack(az *Cloud, ctrl *gomock.Controller, expectedInterfaces
 
 	setMockPublicIPs(az, ctrl, serviceCount, true, true)
 
-	sg := getTestSecurityGroup(az)
+	sg := getTestSecurityGroupDualStack(az, services...)
 	setMockSecurityGroup(az, ctrl, sg)
 }
 
@@ -178,13 +207,7 @@ func setMockEnv(az *Cloud, ctrl *gomock.Controller, expectedInterfaces []network
 		mockVirtualMachinesClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, fmt.Sprintf("vm-%d", i), gomock.Any()).Return(expectedVirtualMachines[i], nil).AnyTimes()
 	}
 
-	if len(services) > 0 {
-		v4Enabled, v6Enabled := getIPFamiliesEnabled(&services[0])
-		setMockPublicIPs(az, ctrl, serviceCount, v4Enabled, v6Enabled)
-	} else {
-		// Default is IPv4 only.
-		setMockPublicIPs(az, ctrl, serviceCount, true, false)
-	}
+	setMockPublicIPs(az, ctrl, serviceCount, true, false)
 
 	sg := getTestSecurityGroup(az, services...)
 	setMockSecurityGroup(az, ctrl, sg)
@@ -433,6 +456,7 @@ func setMockLBs(az *Cloud, ctrl *gomock.Controller, expectedLBs *[]network.LoadB
 				FrontendIPConfigurationPropertiesFormat: &network.FrontendIPConfigurationPropertiesFormat{
 					PrivateIPAllocationMethod: "Dynamic",
 					PublicIPAddress:           &network.PublicIPAddress{ID: pointer.String(fmt.Sprintf("testCluster-a%s%d", fullServiceName, serviceIndex))},
+					PrivateIPAddressVersion:   network.IPv4,
 				},
 			},
 		}
@@ -452,6 +476,7 @@ func setMockLBs(az *Cloud, ctrl *gomock.Controller, expectedLBs *[]network.LoadB
 			FrontendIPConfigurationPropertiesFormat: &network.FrontendIPConfigurationPropertiesFormat{
 				PrivateIPAllocationMethod: "Dynamic",
 				PublicIPAddress:           &network.PublicIPAddress{ID: pointer.String(fmt.Sprintf("testCluster-a%s%d", fullServiceName, serviceIndex))},
+				PrivateIPAddressVersion:   network.IPv4,
 			},
 		}
 		if isInternal {
@@ -579,26 +604,20 @@ func testLoadBalancerServiceAutoModeSelection(t *testing.T, isInternal bool) {
 		az.PrivateLinkServiceClient = mockPLSClient
 
 		lbStatus, err := az.EnsureLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes)
-		if err != nil {
-			t.Errorf("Unexpected error: %q", err)
-		}
-		if lbStatus == nil {
-			t.Errorf("Unexpected error: %s", svcName)
-		}
+		assert.Nil(t, err)
+		assert.NotNil(t, lbStatus, svc.Name)
 
 		// expected is MIN(index, availabilitySetCount)
 		expectedNumOfLB := int(math.Min(float64(index), float64(availabilitySetCount)))
 		ctx, cancel := getContextWithCancel()
 		defer cancel()
-		result, _ := az.LoadBalancerClient.List(ctx, az.Config.ResourceGroup)
-		lbCount := len(result)
-		if lbCount != expectedNumOfLB {
-			t.Errorf("Unexpected number of LB's: Expected (%d) Found (%d)", expectedNumOfLB, lbCount)
-		}
+		lbs, _ := az.LoadBalancerClient.List(ctx, az.Config.ResourceGroup)
+		lbCount := len(lbs)
+		assert.Equal(t, expectedNumOfLB, lbCount)
 
 		maxRules := 0
 		minRules := serviceCount
-		for _, lb := range result {
+		for _, lb := range lbs {
 			ruleCount := len(*lb.LoadBalancingRules)
 			if ruleCount < minRules {
 				minRules = ruleCount
@@ -891,19 +910,20 @@ func TestReconcileSecurityGroupFromAnyDestinationAddressPrefixToLoadBalancerIP(t
 	defer ctrl.Finish()
 
 	az := GetTestCloud(ctrl)
-	svc1 := getTestService("serviceea", v1.ProtocolTCP, nil, false, 80)
+	svc1 := getTestServiceDualStack("serviceea", v1.ProtocolTCP, nil, 80)
 	setServiceLoadBalancerIP(&svc1, "192.168.0.0")
+	setServiceLoadBalancerIP(&svc1, "fdf8:f535:82e4::53")
 
-	sg := getTestSecurityGroup(az)
+	sg := getTestSecurityGroupDualStack(az)
 	setMockSecurityGroup(az, ctrl, sg)
 
 	// Simulate a pre-Kubernetes 1.8 NSG, where we do not specify the destination address prefix
-	_, err := az.reconcileSecurityGroup(testClusterName, &svc1, pointer.String(""), nil, true)
+	_, err := az.reconcileSecurityGroup(testClusterName, &svc1, &[]string{""}, nil, true)
 	assert.Nil(t, err)
-	isIPv6 := utilnet.IsIPv6String(svc1.Spec.ClusterIP)
-	sg, err = az.reconcileSecurityGroup(testClusterName, &svc1, pointer.String(getServiceLoadBalancerIP(&svc1, isIPv6)), nil, true)
+	lbIPs := []string{getServiceLoadBalancerIP(&svc1, false), getServiceLoadBalancerIP(&svc1, true)}
+	sg, err = az.reconcileSecurityGroup(testClusterName, &svc1, &lbIPs, nil, true)
 	assert.Nil(t, err)
-	validateSecurityGroup(t, sg, svc1)
+	validateSecurityGroupDualStack(t, az, sg, svc1)
 }
 
 func TestReconcileSecurityGroupDynamicLoadBalancerIP(t *testing.T) {
@@ -911,16 +931,16 @@ func TestReconcileSecurityGroupDynamicLoadBalancerIP(t *testing.T) {
 	defer ctrl.Finish()
 
 	az := GetTestCloud(ctrl)
-	svc1 := getTestService("servicea", v1.ProtocolTCP, nil, false, 80)
+	svc1 := getTestServiceDualStack("servicea", v1.ProtocolTCP, nil, 80)
 	setServiceLoadBalancerIP(&svc1, "")
 
-	sg := getTestSecurityGroup(az)
+	sg := getTestSecurityGroupDualStack(az)
 	setMockSecurityGroup(az, ctrl, sg)
 
-	dynamicallyAssignedIP := "192.168.0.0"
-	sg, err := az.reconcileSecurityGroup(testClusterName, &svc1, pointer.String(dynamicallyAssignedIP), nil, true)
+	dynamicallyAssignedIPs := []string{"192.168.0.0", "fdf8:f535:82e4::53"}
+	sg, err := az.reconcileSecurityGroup(testClusterName, &svc1, &dynamicallyAssignedIPs, nil, true)
 	assert.Nil(t, err)
-	validateSecurityGroup(t, sg, svc1)
+	validateSecurityGroupDualStack(t, az, sg, svc1)
 }
 
 // Test addition of services on an internal LB using both default and explicit subnets.
@@ -1376,22 +1396,24 @@ func TestReconcileSecurityGroupNewServiceAddsPort(t *testing.T) {
 	defer ctrl.Finish()
 
 	az := GetTestCloud(ctrl)
-	getTestSecurityGroup(az)
-	svc1 := getTestService("service1", v1.ProtocolTCP, nil, false, 80)
+	getTestSecurityGroupDualStack(az)
+	svc1 := getTestServiceDualStack("service1", v1.ProtocolTCP, nil, 80)
 	clusterResources, expectedInterfaces, expectedVirtualMachines := getClusterResources(az, 1, 1)
-	setMockEnv(az, ctrl, expectedInterfaces, expectedVirtualMachines, 1)
+	setMockEnvDualStack(az, ctrl, expectedInterfaces, expectedVirtualMachines, 1)
 	expectedLBs := make([]network.LoadBalancer, 0)
-	setMockLBs(az, ctrl, &expectedLBs, "service", 1, 1, false)
+	setMockLBsDualStack(az, ctrl, &expectedLBs, "service", 1, 1, false)
 	mockLBBackendPool := az.LoadBalancerBackendPool.(*MockBackendPool)
 	mockLBBackendPool.EXPECT().ReconcileBackendPools(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, false, false, nil).AnyTimes()
 	mockLBBackendPool.EXPECT().EnsureHostsInPool(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
 	lb, _ := az.reconcileLoadBalancer(testClusterName, &svc1, clusterResources.nodes, true)
-	lbStatus, _, _ := az.getServiceLoadBalancerStatus(&svc1, lb)
+	lbStatus, _, _, _ := az.getServiceLoadBalancerStatus(&svc1, lb)
 
-	sg, err := az.reconcileSecurityGroup(testClusterName, &svc1, &lbStatus.Ingress[0].IP, nil, true /* wantLb */)
-	assert.Nil(t, err)
+	assert.Equal(t, 2, len(lbStatus.Ingress))
+	lbIPs := []string{lbStatus.Ingress[0].IP, lbStatus.Ingress[1].IP}
+	sg, err := az.reconcileSecurityGroup(testClusterName, &svc1, &lbIPs, nil, true /* wantLb */)
+	assert.NoError(t, err)
 
-	validateSecurityGroup(t, sg, svc1)
+	validateSecurityGroupDualStack(t, az, sg, svc1)
 }
 
 func TestReconcileSecurityGroupNewInternalServiceAddsPort(t *testing.T) {
@@ -1413,11 +1435,10 @@ func TestReconcileSecurityGroupNewInternalServiceAddsPort(t *testing.T) {
 	mockLBBackendPool.EXPECT().EnsureHostsInPool(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
 
 	lb, _ := az.reconcileLoadBalancer(testClusterName, &svc1, clusterResources.nodes, true)
-	lbStatus, _, _ := az.getServiceLoadBalancerStatus(&svc1, lb)
-	sg, err := az.reconcileSecurityGroup(testClusterName, &svc1, &lbStatus.Ingress[0].IP, nil, true /* wantLb */)
+	lbStatus, _, _, _ := az.getServiceLoadBalancerStatus(&svc1, lb)
+	sg, err := az.reconcileSecurityGroup(testClusterName, &svc1, &[]string{lbStatus.Ingress[0].IP}, nil, true /* wantLb */)
 	assert.Nil(t, err)
-
-	validateSecurityGroup(t, sg, svc1)
+	validateSecurityGroup(t, az, sg, svc1)
 }
 
 func TestReconcileSecurityGroupRemoveService(t *testing.T) {
@@ -1425,16 +1446,16 @@ func TestReconcileSecurityGroupRemoveService(t *testing.T) {
 	defer ctrl.Finish()
 
 	az := GetTestCloud(ctrl)
-	service1 := getTestService("service1", v1.ProtocolTCP, nil, false, 81)
-	service2 := getTestService("service2", v1.ProtocolTCP, nil, false, 82)
+	service1 := getTestServiceDualStack("service1", v1.ProtocolTCP, nil, 81)
+	service2 := getTestServiceDualStack("service2", v1.ProtocolTCP, nil, 82)
 	clusterResources, expectedInterfaces, expectedVirtualMachines := getClusterResources(az, 1, 1)
-	setMockEnv(az, ctrl, expectedInterfaces, expectedVirtualMachines, 2, service1, service2)
+	setMockEnvDualStack(az, ctrl, expectedInterfaces, expectedVirtualMachines, 2, service1, service2)
 
 	expectedLBs := make([]network.LoadBalancer, 0)
-	setMockLBs(az, ctrl, &expectedLBs, "service", 1, 1, true)
+	setMockLBsDualStack(az, ctrl, &expectedLBs, "service", 1, 1, true)
 	mockLBClient := az.LoadBalancerClient.(*mockloadbalancerclient.MockInterface)
 	mockLBClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "testCluster", gomock.Any()).Return(
-		getTestLoadBalancer(pointer.String(testClusterName),
+		getTestLoadBalancerDualStack(pointer.String(testClusterName),
 			pointer.String(az.ResourceGroup),
 			pointer.String(testClusterName),
 			pointer.String("aservice1"),
@@ -1448,15 +1469,18 @@ func TestReconcileSecurityGroupRemoveService(t *testing.T) {
 	lb, _ := az.reconcileLoadBalancer(testClusterName, &service1, clusterResources.nodes, true)
 	_, _ = az.reconcileLoadBalancer(testClusterName, &service2, clusterResources.nodes, true)
 
-	lbStatus, _, _ := az.getServiceLoadBalancerStatus(&service1, lb)
+	lbStatus, _, _, err := az.getServiceLoadBalancerStatus(&service1, lb)
+	assert.NoError(t, err)
+	assert.NotNil(t, lbStatus)
 
-	sg := getTestSecurityGroup(az, service1, service2)
-	validateSecurityGroup(t, sg, service1, service2)
+	sg := getTestSecurityGroupDualStack(az, service1, service2)
+	validateSecurityGroupDualStack(t, az, sg, service1, service2)
 
-	sg, err := az.reconcileSecurityGroup(testClusterName, &service1, &lbStatus.Ingress[0].IP, nil, false /* wantLb */)
+	assert.Equal(t, 2, len(lbStatus.Ingress))
+	sg, err = az.reconcileSecurityGroup(testClusterName, &service1, &[]string{lbStatus.Ingress[0].IP, lbStatus.Ingress[1].IP}, nil, false /* wantLb */)
 	assert.Nil(t, err)
 
-	validateSecurityGroup(t, sg, service2)
+	validateSecurityGroupDualStack(t, az, sg, service2)
 }
 
 func TestReconcileSecurityGroupRemoveServiceRemovesPort(t *testing.T) {
@@ -1464,33 +1488,34 @@ func TestReconcileSecurityGroupRemoveServiceRemovesPort(t *testing.T) {
 	defer ctrl.Finish()
 
 	az := GetTestCloud(ctrl)
-	svc := getTestService("service1", v1.ProtocolTCP, nil, false, 80, 443)
+	svc := getTestServiceDualStack("service1", v1.ProtocolTCP, nil, 80, 443)
 	clusterResources, expectedInterfaces, expectedVirtualMachines := getClusterResources(az, 1, 1)
-	setMockEnv(az, ctrl, expectedInterfaces, expectedVirtualMachines, 1)
+	setMockEnvDualStack(az, ctrl, expectedInterfaces, expectedVirtualMachines, 1)
 
 	mockLBBackendPool := az.LoadBalancerBackendPool.(*MockBackendPool)
 	mockLBBackendPool.EXPECT().ReconcileBackendPools(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, false, false, nil).AnyTimes()
 	mockLBBackendPool.EXPECT().EnsureHostsInPool(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
 
-	getTestSecurityGroup(az, svc)
-	svcUpdated := getTestService("service1", v1.ProtocolTCP, nil, false, 80)
+	getTestSecurityGroupDualStack(az, svc)
+	svcUpdated := getTestServiceDualStack("service1", v1.ProtocolTCP, nil, 80)
 	expectedLBs := make([]network.LoadBalancer, 0)
-	setMockLBs(az, ctrl, &expectedLBs, "service", 1, 1, true)
+	setMockLBsDualStack(az, ctrl, &expectedLBs, "service", 1, 1, true)
 	mockLBClient := az.LoadBalancerClient.(*mockloadbalancerclient.MockInterface)
 	mockLBClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "testCluster", gomock.Any()).Return(
-		getTestLoadBalancer(pointer.String(testClusterName),
+		getTestLoadBalancerDualStack(pointer.String(testClusterName),
 			pointer.String(az.ResourceGroup),
 			pointer.String(testClusterName),
 			pointer.String("aservice1"),
 			svc,
 			"Standard"), nil).AnyTimes()
 	lb, _ := az.reconcileLoadBalancer(testClusterName, &svc, clusterResources.nodes, true)
-	lbStatus, _, _ := az.getServiceLoadBalancerStatus(&svc, lb)
+	lbStatus, _, _, _ := az.getServiceLoadBalancerStatus(&svc, lb)
 
-	sg, err := az.reconcileSecurityGroup(testClusterName, &svcUpdated, &lbStatus.Ingress[0].IP, nil, true /* wantLb */)
+	assert.Equal(t, 2, len(lbStatus.Ingress))
+	sg, err := az.reconcileSecurityGroup(testClusterName, &svcUpdated, &[]string{lbStatus.Ingress[0].IP, lbStatus.Ingress[1].IP}, nil, true /* wantLb */)
 	assert.Nil(t, err)
 
-	validateSecurityGroup(t, sg, svcUpdated)
+	validateSecurityGroupDualStack(t, az, sg, svcUpdated)
 }
 
 func TestReconcileSecurityWithSourceRanges(t *testing.T) {
@@ -1498,28 +1523,31 @@ func TestReconcileSecurityWithSourceRanges(t *testing.T) {
 	defer ctrl.Finish()
 
 	az := GetTestCloud(ctrl)
-	svc := getTestService("service1", v1.ProtocolTCP, nil, false, 80, 443)
+	svc := getTestServiceDualStack("service1", v1.ProtocolTCP, nil, 80, 443)
 	svc.Spec.LoadBalancerSourceRanges = []string{
 		"192.168.0.0/24",
 		"10.0.0.0/32",
+		"fe00::/64",
+		"ee00::/64",
 	}
 	clusterResources, expectedInterfaces, expectedVirtualMachines := getClusterResources(az, 1, 1)
-	setMockEnv(az, ctrl, expectedInterfaces, expectedVirtualMachines, 1)
+	setMockEnvDualStack(az, ctrl, expectedInterfaces, expectedVirtualMachines, 1)
 
 	mockLBBackendPool := az.LoadBalancerBackendPool.(*MockBackendPool)
 	mockLBBackendPool.EXPECT().ReconcileBackendPools(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, false, false, nil).AnyTimes()
 	mockLBBackendPool.EXPECT().EnsureHostsInPool(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
 
-	getTestSecurityGroup(az, svc)
+	getTestSecurityGroupDualStack(az, svc)
 	expectedLBs := make([]network.LoadBalancer, 0)
-	setMockLBs(az, ctrl, &expectedLBs, "service", 1, 1, false)
+	setMockLBsDualStack(az, ctrl, &expectedLBs, "service", 1, 1, false)
 	lb, _ := az.reconcileLoadBalancer(testClusterName, &svc, clusterResources.nodes, true)
-	lbStatus, _, _ := az.getServiceLoadBalancerStatus(&svc, lb)
+	lbStatus, _, _, _ := az.getServiceLoadBalancerStatus(&svc, lb)
 
-	sg, err := az.reconcileSecurityGroup(testClusterName, &svc, &lbStatus.Ingress[0].IP, nil, true /* wantLb */)
+	assert.Equal(t, 2, len(lbStatus.Ingress))
+	sg, err := az.reconcileSecurityGroup(testClusterName, &svc, &[]string{lbStatus.Ingress[0].IP, lbStatus.Ingress[1].IP}, nil, true /* wantLb */)
 	assert.Nil(t, err)
 
-	validateSecurityGroup(t, sg, svc)
+	validateSecurityGroupDualStack(t, az, sg, svc)
 }
 
 func TestReconcileSecurityGroupEtagMismatch(t *testing.T) {
@@ -1527,14 +1555,14 @@ func TestReconcileSecurityGroupEtagMismatch(t *testing.T) {
 	defer ctrl.Finish()
 	az := GetTestCloud(ctrl)
 
-	sg := getTestSecurityGroup(az)
+	sg := getTestSecurityGroupDualStack(az)
 	cachedSG := *sg
 	cachedSG.Etag = pointer.String("1111111-0000-0000-0000-000000000000")
 	az.nsgCache.Set(pointer.StringDeref(sg.Name, ""), &cachedSG)
 
-	svc1 := getTestService("service1", v1.ProtocolTCP, nil, false, 80)
+	svc1 := getTestServiceDualStack("service1", v1.ProtocolTCP, nil, 80)
 	clusterResources, expectedInterfaces, expectedVirtualMachines := getClusterResources(az, 1, 1)
-	setMockEnv(az, ctrl, expectedInterfaces, expectedVirtualMachines, 1)
+	setMockEnvDualStack(az, ctrl, expectedInterfaces, expectedVirtualMachines, 1)
 	mockSGsClient := mocksecuritygroupclient.NewMockInterface(ctrl)
 	az.SecurityGroupsClient = mockSGsClient
 	mockSGsClient.EXPECT().Get(gomock.Any(), az.SecurityGroupResourceGroup, az.SecurityGroupName, gomock.Any()).Return(cachedSG, nil).AnyTimes()
@@ -1545,7 +1573,7 @@ func TestReconcileSecurityGroupEtagMismatch(t *testing.T) {
 	mockSGsClient.EXPECT().CreateOrUpdate(gomock.Any(), az.SecurityGroupResourceGroup, az.SecurityGroupName, gomock.Any(), gomock.Any()).Return(expectedError).AnyTimes()
 
 	expectedLBs := make([]network.LoadBalancer, 0)
-	setMockLBs(az, ctrl, &expectedLBs, "service", 1, 1, true)
+	setMockLBsDualStack(az, ctrl, &expectedLBs, "service", 1, 1, true)
 	mockLBClient := az.LoadBalancerClient.(*mockloadbalancerclient.MockInterface)
 	mockLBClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "testCluster", gomock.Any()).Return(
 		getTestLoadBalancer(pointer.String(testClusterName),
@@ -1560,9 +1588,9 @@ func TestReconcileSecurityGroupEtagMismatch(t *testing.T) {
 	mockLBBackendPool.EXPECT().EnsureHostsInPool(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
 
 	lb, _ := az.reconcileLoadBalancer(testClusterName, &svc1, clusterResources.nodes, true)
-	lbStatus, _, _ := az.getServiceLoadBalancerStatus(&svc1, lb)
+	lbStatus, _, _, _ := az.getServiceLoadBalancerStatus(&svc1, lb)
 
-	newSG, err := az.reconcileSecurityGroup(testClusterName, &svc1, &lbStatus.Ingress[0].IP, nil, true /* wantLb */)
+	newSG, err := az.reconcileSecurityGroup(testClusterName, &svc1, &[]string{lbStatus.Ingress[0].IP}, nil, true /* wantLb */)
 	assert.Nil(t, newSG)
 	assert.Error(t, err)
 
@@ -1885,22 +1913,31 @@ func getServiceSourceRanges(service *v1.Service) []string {
 	return service.Spec.LoadBalancerSourceRanges
 }
 
-func getTestSecurityGroup(az *Cloud, services ...v1.Service) *network.SecurityGroup {
+func getTestSecurityGroupCommon(az *Cloud, v4Enabled, v6Enabled bool, services ...v1.Service) *network.SecurityGroup {
 	rules := []network.SecurityRule{}
-
 	for i, service := range services {
 		for _, port := range service.Spec.Ports {
-			sources := getServiceSourceRanges(&services[i])
-			for _, src := range sources {
-				isIPv6 := utilnet.IsIPv6String(services[i].Spec.ClusterIP)
-				ruleName := az.getSecurityRuleName(&services[i], port, src, isIPv6)
-				rules = append(rules, network.SecurityRule{
+			getRule := func(svc *v1.Service, port v1.ServicePort, src string, isIPv6 bool) network.SecurityRule {
+				ruleName := az.getSecurityRuleName(svc, port, src, isIPv6)
+				return network.SecurityRule{
 					Name: pointer.String(ruleName),
 					SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
 						SourceAddressPrefix:  pointer.String(src),
 						DestinationPortRange: pointer.String(fmt.Sprintf("%d", port.Port)),
 					},
-				})
+				}
+			}
+
+			sources := getServiceSourceRanges(&services[i])
+			for _, src := range sources {
+				if v4Enabled {
+					rule := getRule(&services[i], port, src, false)
+					rules = append(rules, rule)
+				}
+				if v6Enabled {
+					rule := getRule(&services[i], port, src, true)
+					rules = append(rules, rule)
+				}
 			}
 		}
 	}
@@ -1916,6 +1953,14 @@ func getTestSecurityGroup(az *Cloud, services ...v1.Service) *network.SecurityGr
 	return &sg
 }
 
+func getTestSecurityGroupDualStack(az *Cloud, services ...v1.Service) *network.SecurityGroup {
+	return getTestSecurityGroupCommon(az, true, true, services...)
+}
+
+func getTestSecurityGroup(az *Cloud, services ...v1.Service) *network.SecurityGroup {
+	return getTestSecurityGroupCommon(az, true, false, services...)
+}
+
 func validateLoadBalancer(t *testing.T, loadBalancer *network.LoadBalancer, services ...v1.Service) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
@@ -1930,10 +1975,11 @@ func validateLoadBalancer(t *testing.T, loadBalancer *network.LoadBalancer, serv
 		svcIPFamilyCount = len(services[0].Spec.IPFamilies)
 	}
 	for i, svc := range services {
+		isInternal := requiresInternalLoadBalancer(&services[i])
 		if len(svc.Spec.Ports) > 0 {
 			expectedFrontendIPCount += svcIPFamilyCount
 			expectedSubnetName := ""
-			if requiresInternalLoadBalancer(&services[i]) {
+			if isInternal {
 				expectedSubnetName = svc.Annotations[consts.ServiceAnnotationLoadBalancerInternalSubnet]
 				if expectedSubnetName == "" {
 					expectedSubnetName = az.SubnetName
@@ -1962,10 +2008,17 @@ func validateLoadBalancer(t *testing.T, loadBalancer *network.LoadBalancer, serv
 				foundRule := false
 				for _, actualRule := range *loadBalancer.LoadBalancingRules {
 					if strings.EqualFold(*actualRule.Name, wantedRuleName) &&
-						*actualRule.FrontendPort == wantedRule.Port &&
-						*actualRule.BackendPort == wantedRule.Port {
-						foundRule = true
-						break
+						*actualRule.FrontendPort == wantedRule.Port {
+						if isInternal {
+							if (!isIPv6 && *actualRule.BackendPort == wantedRule.Port) ||
+								(isIPv6 && *actualRule.BackendPort == wantedRule.NodePort) {
+								foundRule = true
+								break
+							}
+						} else if *actualRule.BackendPort == wantedRule.Port {
+							foundRule = true
+							break
+						}
 					}
 				}
 				if !foundRule {
@@ -2099,17 +2152,11 @@ func validatePublicIP(t *testing.T, publicIP *network.PublicIPAddress, service *
 	}
 
 	serviceName := getServiceName(service)
-	if serviceName != *(publicIP.Tags[consts.ServiceTagKey]) {
-		t.Errorf("Expected publicIP resource has matching tags[%s]", consts.ServiceTagKey)
-	}
-
-	if publicIP.Tags[consts.ClusterNameKey] == nil {
-		t.Fatalf("Expected publicIP resource does not have tags[%s]", consts.ClusterNameKey)
-	}
-
-	if *(publicIP.Tags[consts.ClusterNameKey]) != testClusterName {
-		t.Errorf("Expected publicIP resource has matching tags[%s]", consts.ClusterNameKey)
-	}
+	assert.Equalf(t, serviceName, pointer.StringDeref(publicIP.Tags[consts.ServiceTagKey], ""),
+		"Expected publicIP resource has matching tags[%s]", consts.ServiceTagKey)
+	assert.NotNilf(t, publicIP.Tags[consts.ClusterNameKey], "Expected publicIP resource does not have tags[%s]", consts.ClusterNameKey)
+	assert.Equalf(t, testClusterName, pointer.StringDeref(publicIP.Tags[consts.ClusterNameKey], ""),
+		"Expected publicIP resource has matching tags[%s]", consts.ClusterNameKey)
 
 	// We cannot use Service LoadBalancerIP to compare with
 	// Public IP's IPAddress
@@ -2168,84 +2215,112 @@ func securityRuleMatches(serviceSourceRange string, servicePort v1.ServicePort,
 	return nil
 }
 
-func validateSecurityGroup(t *testing.T, securityGroup *network.SecurityGroup, services ...v1.Service) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-
-	az := GetTestCloud(ctrl)
-	seenRules := make(map[string]string)
-	for i, svc := range services {
+func validateSecurityGroupCommon(t *testing.T, az *Cloud, securityGroup *network.SecurityGroup, v4Enabled, v6Enabled bool, services ...v1.Service) {
+	expectedRules := make(map[string]bool)
+	for _, svc := range services {
 		svc := svc
 		for _, wantedRule := range svc.Spec.Ports {
-			sources := getServiceSourceRanges(&services[i])
+			sources := getServiceSourceRanges(&svc)
 			for _, source := range sources {
-				isIPv6 := utilnet.IsIPv6String(services[i].Spec.ClusterIP)
-				wantedRuleName := az.getSecurityRuleName(&services[i], wantedRule, source, isIPv6)
-				seenRules[wantedRuleName] = wantedRuleName
-				foundRule := false
-				for _, actualRule := range *securityGroup.SecurityRules {
-					if strings.EqualFold(*actualRule.Name, wantedRuleName) {
-						err := securityRuleMatches(source, wantedRule, getServiceLoadBalancerIP(&svc, isIPv6), actualRule)
-						if err != nil {
-							t.Errorf("Found matching security rule %q but properties were incorrect: %v", wantedRuleName, err)
+				checkRule := func(svc *v1.Service, wantedRule v1.ServicePort, source string, isIPv6 bool) {
+					sourceIP, _, err := net.ParseCIDR(source)
+					// err == nil means source is a valid CIDR.
+					if err == nil && sourceIP.To4() == nil != isIPv6 {
+						return
+					}
+					wantedRuleName := az.getSecurityRuleName(svc, wantedRule, source, isIPv6)
+					expectedRules[wantedRuleName] = true
+
+					foundRule := false
+					for _, actualRule := range *securityGroup.SecurityRules {
+						if strings.EqualFold(*actualRule.Name, wantedRuleName) {
+							if err := securityRuleMatches(source, wantedRule, getServiceLoadBalancerIP(svc, isIPv6), actualRule); err != nil {
+								t.Errorf("Found matching security rule %q but properties were incorrect: %v", wantedRuleName, err)
+							}
+							foundRule = true
+							break
 						}
-						foundRule = true
-						break
+					}
+					if !foundRule {
+						t.Errorf("Expected security group rule but didn't find it: %q", wantedRuleName)
 					}
 				}
-				if !foundRule {
-					t.Errorf("Expected security group rule but didn't find it: %q", wantedRuleName)
+
+				if v4Enabled {
+					checkRule(&svc, wantedRule, source, false)
+				}
+				if v6Enabled {
+					checkRule(&svc, wantedRule, source, true)
 				}
 			}
 		}
 	}
 
-	lenRules := len(*securityGroup.SecurityRules)
-	expectedRuleCount := len(seenRules)
-	if lenRules != expectedRuleCount {
-		t.Errorf("Expected the loadbalancer to have %d rules. Found %d.\n", expectedRuleCount, lenRules)
+	actualRules := []string{}
+	for _, rule := range *securityGroup.SecurityRules {
+		actualRules = append(actualRules, *rule.Name)
 	}
+	lenRules := len(*securityGroup.SecurityRules)
+	expectedRuleCount := len(expectedRules)
+	assert.Equalf(t, expectedRuleCount, lenRules, "Expected rules: %v, actual ones: %v", expectedRules, actualRules)
 }
 
-func TestSecurityRulePriorityPicksNextAvailablePriority(t *testing.T) {
-	rules := []network.SecurityRule{}
+func validateSecurityGroupDualStack(t *testing.T, az *Cloud, securityGroup *network.SecurityGroup, services ...v1.Service) {
+	validateSecurityGroupCommon(t, az, securityGroup, true, true, services...)
+}
 
-	var expectedPriority int32 = consts.LoadBalancerMinimumPriority + 50
+func validateSecurityGroup(t *testing.T, az *Cloud, securityGroup *network.SecurityGroup, services ...v1.Service) {
+	validateSecurityGroupCommon(t, az, securityGroup, true, false, services...)
+}
 
-	var i int32
-	for i = consts.LoadBalancerMinimumPriority; i < expectedPriority; i++ {
-		rules = append(rules, network.SecurityRule{
+func TestGetNextAvailablePriority(t *testing.T) {
+	rules50, rulesTooMany := []network.SecurityRule{}, []network.SecurityRule{}
+	for i := int32(consts.LoadBalancerMinimumPriority); i < consts.LoadBalancerMinimumPriority+50; i++ {
+		rules50 = append(rules50, network.SecurityRule{
 			SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
 				Priority: pointer.Int32(i),
 			},
 		})
 	}
-
-	priority, err := getNextAvailablePriority(rules)
-	if err != nil {
-		t.Errorf("Unexpectected error: %q", err)
-	}
-
-	if priority != expectedPriority {
-		t.Errorf("Expected priority %d. Got priority %d.", expectedPriority, priority)
-	}
-}
-
-func TestSecurityRulePriorityFailsIfExhausted(t *testing.T) {
-	rules := []network.SecurityRule{}
-
-	var i int32
-	for i = consts.LoadBalancerMinimumPriority; i < consts.LoadBalancerMaximumPriority; i++ {
-		rules = append(rules, network.SecurityRule{
+	for i := int32(consts.LoadBalancerMinimumPriority); i < consts.LoadBalancerMaximumPriority; i++ {
+		rulesTooMany = append(rulesTooMany, network.SecurityRule{
 			SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
 				Priority: pointer.Int32(i),
 			},
 		})
 	}
 
-	_, err := getNextAvailablePriority(rules)
-	if err == nil {
-		t.Error("Expect an error. There are no priority levels left.")
+	testcases := []struct {
+		desc             string
+		rules            []network.SecurityRule
+		lastPriority     int32
+		expectErr        bool
+		expectedPriority int32
+	}{
+		{
+			desc:             "50 rules",
+			rules:            rules50,
+			lastPriority:     consts.LoadBalancerMinimumPriority - 1,
+			expectedPriority: consts.LoadBalancerMinimumPriority + 50,
+		},
+		{
+			desc:         "too many rules",
+			rules:        rulesTooMany,
+			lastPriority: consts.LoadBalancerMinimumPriority - 1,
+			expectErr:    true,
+		},
+	}
+
+	for _, tc := range testcases {
+		t.Run(tc.desc, func(t *testing.T) {
+			priority, err := getNextAvailablePriority(tc.rules)
+			if tc.expectErr {
+				assert.NotNil(t, err)
+			} else {
+				assert.Nil(t, err)
+				assert.Equal(t, tc.expectedPriority, priority)
+			}
+		})
 	}
 }
 
@@ -2638,40 +2713,28 @@ func TestIfServiceSpecifiesSharedRuleAndRuleDoesNotExistItIsCreated(t *testing.T
 	defer ctrl.Finish()
 
 	az := GetTestCloud(ctrl)
-	svc := getTestService("servicea", v1.ProtocolTCP, nil, false, 80)
-	setServiceLoadBalancerIP(&svc, testIP1)
+	svc := getTestServiceDualStack("servicea", v1.ProtocolTCP, nil, 80)
+	setServiceLoadBalancerIP(&svc, testIPs[0][false])
+	setServiceLoadBalancerIP(&svc, testIPs[0][true])
 	svc.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	sg := getTestSecurityGroup(az)
+	sg := getTestSecurityGroupDualStack(az)
 	setMockSecurityGroup(az, ctrl, sg)
 
-	isIPv6 := utilnet.IsIPv6String(svc.Spec.ClusterIP)
-	sg, err := az.reconcileSecurityGroup(testClusterName, &svc, pointer.String(getServiceLoadBalancerIP(&svc, isIPv6)), nil, true)
+	sg, err := az.reconcileSecurityGroup(testClusterName, &svc, &[]string{getServiceLoadBalancerIP(&svc, false), getServiceLoadBalancerIP(&svc, true)}, nil, true)
 	assert.Nil(t, err)
 
-	validateSecurityGroup(t, sg, svc)
+	validateSecurityGroupDualStack(t, az, sg, svc)
 
-	expectedRuleName := testRuleName
-	_, securityRule, ruleFound := findSecurityRuleByName(*sg.SecurityRules, expectedRuleName)
-	if !ruleFound {
-		t.Fatalf("Expected security rule %q but it was not present", expectedRuleName)
-	}
-
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 80}, testIP1, securityRule)
-	if err != nil {
-		t.Errorf("Shared rule was not updated with new service IP: %v", err)
-	}
-
-	if securityRule.Priority == nil {
-		t.Errorf("Shared rule %s had no priority", expectedRuleName)
-	}
-
-	if securityRule.Access != network.SecurityRuleAccessAllow {
-		t.Errorf("Shared rule %s did not have Allow access", expectedRuleName)
-	}
-
-	if securityRule.Direction != network.SecurityRuleDirectionInbound {
-		t.Errorf("Shared rule %s did not have Inbound direction", expectedRuleName)
+	for isIPv6, expectedRuleName := range testRuleNames[0] {
+		t.Run(fmt.Sprintf("expectedRuleName=%s isIPv6=%t", expectedRuleName, isIPv6), func(t *testing.T) {
+			_, securityRule, ruleFound := findSecurityRuleByName(*sg.SecurityRules, expectedRuleName)
+			assert.True(t, ruleFound)
+			assert.Nil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 80}, testIPs[0][isIPv6], securityRule))
+			assert.NotNil(t, securityRule.Priority)
+			assert.Equal(t, network.SecurityRuleAccessAllow, securityRule.Access)
+			assert.Equal(t, network.SecurityRuleDirectionInbound, securityRule.Direction)
+		})
 	}
 }
 
@@ -2680,53 +2743,45 @@ func TestIfServiceSpecifiesSharedRuleAndRuleExistsThenTheServicesPortAndAddressA
 	defer ctrl.Finish()
 
 	az := GetTestCloud(ctrl)
-	svc := getTestService("servicesr", v1.ProtocolTCP, nil, false, 80)
-	setServiceLoadBalancerIP(&svc, testIP1)
+	svc := getTestServiceDualStack("servicesr", v1.ProtocolTCP, nil, 80)
+	setServiceLoadBalancerIP(&svc, testIPs[0][false])
+	setServiceLoadBalancerIP(&svc, testIPs[0][true])
 	svc.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	expectedRuleName := testRuleName
-
-	sg := getTestSecurityGroup(az)
-	sg.SecurityRules = &[]network.SecurityRule{
-		{
-			Name: &expectedRuleName,
+	sg := getTestSecurityGroupDualStack(az)
+	rules := []network.SecurityRule{}
+	for _, isIPv6 := range []bool{false, true} {
+		ruleName := testRuleNames[0][isIPv6]
+		rule := network.SecurityRule{
+			Name: &ruleName,
 			SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
 				Protocol:                 network.SecurityRuleProtocolTCP,
 				SourcePortRange:          pointer.String("*"),
 				SourceAddressPrefix:      pointer.String("Internet"),
 				DestinationPortRange:     pointer.String("80"),
-				DestinationAddressPrefix: pointer.String(testIP2),
+				DestinationAddressPrefix: pointer.String(testIPs[1][isIPv6]),
 				Access:                   network.SecurityRuleAccessAllow,
 				Direction:                network.SecurityRuleDirectionInbound,
 			},
-		},
+		}
+		rules = append(rules, rule)
 	}
+	sg.SecurityRules = &rules
 	setMockSecurityGroup(az, ctrl, sg)
 
-	isIPv6 := utilnet.IsIPv6String(svc.Spec.ClusterIP)
-	sg, err := az.reconcileSecurityGroup(testClusterName, &svc, pointer.String(getServiceLoadBalancerIP(&svc, isIPv6)), nil, true)
+	sg, err := az.reconcileSecurityGroup(testClusterName, &svc, &[]string{getServiceLoadBalancerIP(&svc, false), getServiceLoadBalancerIP(&svc, true)}, nil, true)
 	assert.Nil(t, err)
 
-	validateSecurityGroup(t, sg, svc)
-
-	_, securityRule, ruleFound := findSecurityRuleByName(*sg.SecurityRules, expectedRuleName)
-	if !ruleFound {
-		t.Fatalf("Expected security rule %q but it was not present", expectedRuleName)
-	}
+	validateSecurityGroupDualStack(t, az, sg, svc)
 
-	expectedDestinationIPCount := 2
-	if len(*securityRule.DestinationAddressPrefixes) != expectedDestinationIPCount {
-		t.Errorf("Shared rule should have had %d destination IP addresses but had %d", expectedDestinationIPCount, len(*securityRule.DestinationAddressPrefixes))
-	}
-
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 80}, testIP2, securityRule)
-	if err != nil {
-		t.Errorf("Shared rule no longer matched other service IP: %v", err)
-	}
-
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 80}, testIP1, securityRule)
-	if err != nil {
-		t.Errorf("Shared rule was not updated with new service IP: %v", err)
+	for isIPv6, expectedRuleName := range testRuleNames[0] {
+		t.Run(fmt.Sprintf("expectedRuleName=%s isIPv6=%t", expectedRuleName, isIPv6), func(t *testing.T) {
+			_, securityRule, ruleFound := findSecurityRuleByName(*sg.SecurityRules, expectedRuleName)
+			assert.True(t, ruleFound)
+			assert.Equal(t, 2, len(*securityRule.DestinationAddressPrefixes))
+			assert.Nil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 80}, testIPs[1][isIPv6], securityRule))
+			assert.Nil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 80}, testIPs[0][isIPv6], securityRule))
+		})
 	}
 }
 
@@ -2735,70 +2790,45 @@ func TestIfServicesSpecifySharedRuleButDifferentPortsThenSeparateRulesAreCreated
 	defer ctrl.Finish()
 	az := GetTestCloud(ctrl)
 
-	svc1 := getTestService("servicesr1", v1.ProtocolTCP, nil, false, 4444)
-	setServiceLoadBalancerIP(&svc1, testIP1)
+	svc1 := getTestServiceDualStack("servicesr1", v1.ProtocolTCP, nil, 4444)
+	setServiceLoadBalancerIP(&svc1, testIPs[0][false])
+	setServiceLoadBalancerIP(&svc1, testIPs[0][true])
 	svc1.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	svc2 := getTestService("servicesr2", v1.ProtocolTCP, nil, false, 8888)
-	setServiceLoadBalancerIP(&svc2, testIP2)
+	svc2 := getTestServiceDualStack("servicesr2", v1.ProtocolTCP, nil, 8888)
+	setServiceLoadBalancerIP(&svc2, testIPs[1][false])
+	setServiceLoadBalancerIP(&svc2, testIPs[1][true])
 	svc2.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	sg := getTestSecurityGroup(az)
-
+	sg := getTestSecurityGroupDualStack(az)
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 := utilnet.IsIPv6String(svc1.Spec.ClusterIP)
-	sg, err := az.reconcileSecurityGroup(testClusterName, &svc1, pointer.String(getServiceLoadBalancerIP(&svc1, isIPv6)), nil, true)
-	if err != nil {
-		t.Errorf("Unexpected error adding svc1: %q", err)
-	}
+	sg, err := az.reconcileSecurityGroup(testClusterName, &svc1, &[]string{getServiceLoadBalancerIP(&svc1, false), getServiceLoadBalancerIP(&svc1, true)}, nil, true)
+	assert.Nil(t, err)
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 = utilnet.IsIPv6String(svc2.Spec.ClusterIP)
-	sg, err = az.reconcileSecurityGroup(testClusterName, &svc2, pointer.String(getServiceLoadBalancerIP(&svc2, isIPv6)), nil, true)
-	if err != nil {
-		t.Errorf("Unexpected error adding svc2: %q", err)
-	}
-
-	validateSecurityGroup(t, sg, svc1, svc2)
-
-	_, securityRule1, rule1Found := findSecurityRuleByName(*sg.SecurityRules, testRuleName2)
-	if !rule1Found {
-		t.Fatalf("Expected security rule %q but it was not present", testRuleName2)
-	}
-
-	_, securityRule2, rule2Found := findSecurityRuleByName(*sg.SecurityRules, testRuleName3)
-	if !rule2Found {
-		t.Fatalf("Expected security rule %q but it was not present", testRuleName3)
-	}
-
-	expectedDestinationIPCount1 := 1
-	if len(*securityRule1.DestinationAddressPrefixes) != expectedDestinationIPCount1 {
-		t.Errorf("Shared rule %s should have had %d destination IP addresses but had %d", testRuleName2, expectedDestinationIPCount1, len(*securityRule1.DestinationAddressPrefixes))
-	}
+	sg, err = az.reconcileSecurityGroup(testClusterName, &svc2, &[]string{getServiceLoadBalancerIP(&svc2, false), getServiceLoadBalancerIP(&svc2, true)}, nil, true)
+	assert.Nil(t, err)
 
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIP1, securityRule1)
-	if err != nil {
-		t.Errorf("Shared rule %s did not match service IP: %v", testRuleName2, err)
-	}
+	validateSecurityGroupDualStack(t, az, sg, svc1, svc2)
 
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 8888}, testIP2, securityRule1)
-	if err == nil {
-		t.Errorf("Shared rule %s matched wrong service's port and IP", testRuleName2)
-	}
-
-	expectedDestinationIPCount2 := 1
-	if len(*securityRule2.DestinationAddressPrefixes) != expectedDestinationIPCount2 {
-		t.Errorf("Shared rule %s should have had %d destination IP addresses but had %d", testRuleName3, expectedDestinationIPCount2, len(*securityRule2.DestinationAddressPrefixes))
-	}
-
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 8888}, testIP2, securityRule2)
-	if err != nil {
-		t.Errorf("Shared rule %s did not match service IP: %v", testRuleName3, err)
+	for isIPv6, ruleName := range testRuleNames[1] {
+		t.Run(fmt.Sprintf("ruleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			_, securityRule, ruleFound := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.True(t, ruleFound)
+			assert.Equal(t, 1, len(*securityRule.DestinationAddressPrefixes))
+			assert.Nil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIPs[0][isIPv6], securityRule))
+			assert.NotNil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 8888}, testIPs[1][isIPv6], securityRule))
+		})
 	}
 
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIP1, securityRule2)
-	if err == nil {
-		t.Errorf("Shared rule %s matched wrong service's port and IP", testRuleName3)
+	for isIPv6, ruleName := range testRuleNames[2] {
+		t.Run(fmt.Sprintf("ruleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			_, securityRule, ruleFound := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.True(t, ruleFound)
+			assert.Equal(t, 1, len(*securityRule.DestinationAddressPrefixes))
+			assert.NotNil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIPs[0][isIPv6], securityRule))
+			assert.Nil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 8888}, testIPs[1][isIPv6], securityRule))
+		})
 	}
 }
 
@@ -2807,71 +2837,55 @@ func TestIfServicesSpecifySharedRuleButDifferentProtocolsThenSeparateRulesAreCre
 	defer ctrl.Finish()
 	az := GetTestCloud(ctrl)
 
-	svc1 := getTestService("servicesr1", v1.ProtocolTCP, nil, false, 4444)
-
-	setServiceLoadBalancerIP(&svc1, testIP1)
+	svc1 := getTestServiceDualStack("servicesr1", v1.ProtocolTCP, nil, 4444)
+	setServiceLoadBalancerIP(&svc1, testIPs[0][false])
+	setServiceLoadBalancerIP(&svc1, testIPs[0][true])
 	svc1.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	svc2 := getTestService("servicesr2", v1.ProtocolUDP, nil, false, 4444)
-	setServiceLoadBalancerIP(&svc2, testIP1)
+	svc2 := getTestServiceDualStack("servicesr2", v1.ProtocolUDP, nil, 4444)
+	setServiceLoadBalancerIP(&svc2, testIPs[0][false])
+	setServiceLoadBalancerIP(&svc2, testIPs[0][true])
 	svc2.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	testRuleName3 := "shared-UDP-4444-Internet"
+	ruleName2 := map[bool]string{
+		false: "shared-UDP-4444-Internet",
+		true:  "shared-UDP-4444-Internet-IPv6",
+	}
 
-	sg := getTestSecurityGroup(az)
+	sg := getTestSecurityGroupDualStack(az)
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 := utilnet.IsIPv6String(svc1.Spec.ClusterIP)
-	sg, err := az.reconcileSecurityGroup(testClusterName, &svc1, pointer.String(getServiceLoadBalancerIP(&svc1, isIPv6)), nil, true)
+	sg, err := az.reconcileSecurityGroup(testClusterName, &svc1, &[]string{getServiceLoadBalancerIP(&svc1, false), getServiceLoadBalancerIP(&svc1, true)}, nil, true)
 	if err != nil {
 		t.Errorf("Unexpected error adding svc1: %q", err)
 	}
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 = utilnet.IsIPv6String(svc2.Spec.ClusterIP)
-	sg, err = az.reconcileSecurityGroup(testClusterName, &svc2, pointer.String(getServiceLoadBalancerIP(&svc2, isIPv6)), nil, true)
+	sg, err = az.reconcileSecurityGroup(testClusterName, &svc2, &[]string{getServiceLoadBalancerIP(&svc2, false), getServiceLoadBalancerIP(&svc2, true)}, nil, true)
 	if err != nil {
 		t.Errorf("Unexpected error adding svc2: %q", err)
 	}
 
-	validateSecurityGroup(t, sg, svc1, svc2)
-
-	_, securityRule1, rule1Found := findSecurityRuleByName(*sg.SecurityRules, testRuleName2)
-	if !rule1Found {
-		t.Fatalf("Expected security rule %q but it was not present", testRuleName2)
-	}
-
-	_, securityRule2, rule2Found := findSecurityRuleByName(*sg.SecurityRules, testRuleName3)
-	if !rule2Found {
-		t.Fatalf("Expected security rule %q but it was not present", testRuleName3)
-	}
-
-	expectedDestinationIPCount1 := 1
-	if len(*securityRule1.DestinationAddressPrefixes) != expectedDestinationIPCount1 {
-		t.Errorf("Shared rule %s should have had %d destination IP addresses but had %d", testRuleName2, expectedDestinationIPCount1, len(*securityRule1.DestinationAddressPrefixes))
-	}
-
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIP1, securityRule1)
-	if err != nil {
-		t.Errorf("Shared rule %s did not match service IP: %v", testRuleName2, err)
-	}
-
-	if securityRule1.Protocol != network.SecurityRuleProtocolTCP {
-		t.Errorf("Shared rule %s should have been %s but was %s", testRuleName2, network.SecurityRuleProtocolTCP, securityRule1.Protocol)
-	}
-
-	expectedDestinationIPCount2 := 1
-	if len(*securityRule2.DestinationAddressPrefixes) != expectedDestinationIPCount2 {
-		t.Errorf("Shared rule %s should have had %d destination IP addresses but had %d", testRuleName3, expectedDestinationIPCount2, len(*securityRule2.DestinationAddressPrefixes))
-	}
+	validateSecurityGroupDualStack(t, az, sg, svc1, svc2)
 
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIP1, securityRule2)
-	if err != nil {
-		t.Errorf("Shared rule %s did not match service IP: %v", testRuleName3, err)
+	for isIPv6, ruleName := range testRuleNames[1] {
+		t.Run(fmt.Sprintf("ruleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			_, securityRule1, rule1Found := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.True(t, rule1Found)
+			assert.Equal(t, 1, len(*securityRule1.DestinationAddressPrefixes))
+			assert.Nil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIPs[0][isIPv6], securityRule1))
+			assert.Equal(t, network.SecurityRuleProtocolTCP, securityRule1.Protocol)
+		})
 	}
 
-	if securityRule2.Protocol != network.SecurityRuleProtocolUDP {
-		t.Errorf("Shared rule %s should have been %s but was %s", testRuleName3, network.SecurityRuleProtocolUDP, securityRule2.Protocol)
+	for isIPv6, ruleName := range ruleName2 {
+		t.Run(fmt.Sprintf("ruleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			_, securityRule2, rule2Found := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.True(t, rule2Found)
+			assert.Equal(t, 1, len(*securityRule2.DestinationAddressPrefixes))
+			assert.Nil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIPs[0][isIPv6], securityRule2))
+			assert.Equal(t, network.SecurityRuleProtocolUDP, securityRule2.Protocol)
+		})
 	}
 }
 
@@ -2880,75 +2894,66 @@ func TestIfServicesSpecifySharedRuleButDifferentSourceAddressesThenSeparateRules
 	defer ctrl.Finish()
 	az := GetTestCloud(ctrl)
 
-	svc1 := getTestService("servicesr1", v1.ProtocolTCP, nil, false, 80)
-	setServiceLoadBalancerIP(&svc1, testIP1)
-	svc1.Spec.LoadBalancerSourceRanges = []string{"192.168.12.0/24"}
+	svc1 := getTestServiceDualStack("servicesr1", v1.ProtocolTCP, nil, 80)
+	setServiceLoadBalancerIP(&svc1, testIPs[0][false])
+	setServiceLoadBalancerIP(&svc1, testIPs[0][true])
+	svc1.Spec.LoadBalancerSourceRanges = []string{"192.168.12.0/24", "fd::1:0/112"}
 	svc1.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	svc2 := getTestService("servicesr2", v1.ProtocolTCP, nil, false, 80)
-	setServiceLoadBalancerIP(&svc2, testIP2)
-	svc2.Spec.LoadBalancerSourceRanges = []string{"192.168.34.0/24"}
+	svc2 := getTestServiceDualStack("servicesr2", v1.ProtocolTCP, nil, 80)
+	setServiceLoadBalancerIP(&svc2, testIPs[1][false])
+	setServiceLoadBalancerIP(&svc2, testIPs[1][true])
+	svc2.Spec.LoadBalancerSourceRanges = []string{"192.168.34.0/24", "fd::2:0/112"}
 	svc2.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	testRuleName2 := "shared-TCP-80-192.168.12.0_24"
-	testRuleName3 := "shared-TCP-80-192.168.34.0_24"
-
-	sg := getTestSecurityGroup(az)
-
-	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 := utilnet.IsIPv6String(svc1.Spec.ClusterIP)
-	sg, err := az.reconcileSecurityGroup(testClusterName, &svc1, pointer.String(getServiceLoadBalancerIP(&svc1, isIPv6)), nil, true)
-	if err != nil {
-		t.Errorf("Unexpected error adding svc1: %q", err)
-	}
-
-	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 = utilnet.IsIPv6String(svc2.Spec.ClusterIP)
-	sg, err = az.reconcileSecurityGroup(testClusterName, &svc2, pointer.String(getServiceLoadBalancerIP(&svc2, isIPv6)), nil, true)
-	if err != nil {
-		t.Errorf("Unexpected error adding svc2: %q", err)
-	}
-
-	validateSecurityGroup(t, sg, svc1, svc2)
-
-	_, securityRule1, rule1Found := findSecurityRuleByName(*sg.SecurityRules, testRuleName2)
-	if !rule1Found {
-		t.Fatalf("Expected security rule %q but it was not present", testRuleName2)
+	ruleName1 := map[bool]string{
+		false: "shared-TCP-80-192.168.12.0_24",
+		true:  "shared-TCP-80-fd..1.0_112-IPv6",
 	}
-
-	_, securityRule2, rule2Found := findSecurityRuleByName(*sg.SecurityRules, testRuleName3)
-	if !rule2Found {
-		t.Fatalf("Expected security rule %q but it was not present", testRuleName3)
+	ruleName2 := map[bool]string{
+		false: "shared-TCP-80-192.168.34.0_24",
+		true:  "shared-TCP-80-fd..2.0_112-IPv6",
 	}
 
-	expectedDestinationIPCount1 := 1
-	if len(*securityRule1.DestinationAddressPrefixes) != expectedDestinationIPCount1 {
-		t.Errorf("Shared rule %s should have had %d destination IP addresses but had %d", testRuleName2, expectedDestinationIPCount1, len(*securityRule1.DestinationAddressPrefixes))
-	}
+	sg := getTestSecurityGroupDualStack(az)
+	var err error
 
-	err = securityRuleMatches(svc1.Spec.LoadBalancerSourceRanges[0], v1.ServicePort{Port: 80}, testIP1, securityRule1)
-	if err != nil {
-		t.Errorf("Shared rule %s did not match service IP: %v", testRuleName2, err)
-	}
+	setMockSecurityGroup(az, ctrl, sg)
+	sg, err = az.reconcileSecurityGroup(testClusterName, &svc1, &[]string{getServiceLoadBalancerIP(&svc1, false), getServiceLoadBalancerIP(&svc1, true)}, nil, true)
+	assert.Nil(t, err)
 
-	err = securityRuleMatches(svc2.Spec.LoadBalancerSourceRanges[0], v1.ServicePort{Port: 80}, testIP2, securityRule1)
-	if err == nil {
-		t.Errorf("Shared rule %s matched wrong service's port and IP", testRuleName2)
-	}
+	setMockSecurityGroup(az, ctrl, sg)
+	sg, err = az.reconcileSecurityGroup(testClusterName, &svc2, &[]string{getServiceLoadBalancerIP(&svc2, false), getServiceLoadBalancerIP(&svc2, true)}, nil, true)
+	assert.Nil(t, err)
 
-	expectedDestinationIPCount2 := 1
-	if len(*securityRule2.DestinationAddressPrefixes) != expectedDestinationIPCount2 {
-		t.Errorf("Shared rule %s should have had %d destination IP addresses but had %d", testRuleName3, expectedDestinationIPCount2, len(*securityRule2.DestinationAddressPrefixes))
-	}
+	validateSecurityGroupDualStack(t, az, sg, svc1, svc2)
 
-	err = securityRuleMatches(svc2.Spec.LoadBalancerSourceRanges[0], v1.ServicePort{Port: 80}, testIP2, securityRule2)
-	if err != nil {
-		t.Errorf("Shared rule %s did not match service IP: %v", testRuleName3, err)
+	for isIPv6, ruleName := range ruleName1 {
+		t.Run(fmt.Sprintf("ruleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			loadBalancerSourceRange := svc1.Spec.LoadBalancerSourceRanges[0]
+			if isIPv6 {
+				loadBalancerSourceRange = svc1.Spec.LoadBalancerSourceRanges[1]
+			}
+			_, securityRule1, rule1Found := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.True(t, rule1Found)
+			assert.Equal(t, 1, len(*securityRule1.DestinationAddressPrefixes))
+			assert.Nil(t, securityRuleMatches(loadBalancerSourceRange, v1.ServicePort{Port: 80}, testIPs[0][isIPv6], securityRule1))
+			assert.NotNil(t, securityRuleMatches(loadBalancerSourceRange, v1.ServicePort{Port: 80}, testIPs[1][isIPv6], securityRule1))
+		})
 	}
 
-	err = securityRuleMatches(svc1.Spec.LoadBalancerSourceRanges[0], v1.ServicePort{Port: 80}, testIP1, securityRule2)
-	if err == nil {
-		t.Errorf("Shared rule %s matched wrong service's port and IP", testRuleName3)
+	for isIPv6, ruleName := range ruleName2 {
+		t.Run(fmt.Sprintf("ruleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			loadBalancerSourceRange := svc2.Spec.LoadBalancerSourceRanges[0]
+			if isIPv6 {
+				loadBalancerSourceRange = svc2.Spec.LoadBalancerSourceRanges[1]
+			}
+			_, securityRule2, rule2Found := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.True(t, rule2Found)
+			assert.Equal(t, 1, len(*securityRule2.DestinationAddressPrefixes))
+			assert.Nil(t, securityRuleMatches(loadBalancerSourceRange, v1.ServicePort{Port: 80}, testIPs[1][isIPv6], securityRule2))
+			assert.NotNil(t, securityRuleMatches(loadBalancerSourceRange, v1.ServicePort{Port: 80}, testIPs[0][isIPv6], securityRule2))
+		})
 	}
 }
 
@@ -2957,104 +2962,61 @@ func TestIfServicesSpecifySharedRuleButSomeAreOnDifferentPortsThenRulesAreSepara
 	defer ctrl.Finish()
 	az := GetTestCloud(ctrl)
 
-	svc1 := getTestService("servicesr1", v1.ProtocolTCP, nil, false, 4444)
-	setServiceLoadBalancerIP(&svc1, testIP1)
+	svc1 := getTestServiceDualStack("servicesr1", v1.ProtocolTCP, nil, 4444)
+	setServiceLoadBalancerIP(&svc1, testIPs[0][false])
+	setServiceLoadBalancerIP(&svc1, testIPs[0][true])
 	svc1.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	svc2 := getTestService("servicesr2", v1.ProtocolTCP, nil, false, 8888)
-	setServiceLoadBalancerIP(&svc2, testIP2)
+	svc2 := getTestServiceDualStack("servicesr2", v1.ProtocolTCP, nil, 8888)
+	setServiceLoadBalancerIP(&svc2, testIPs[1][false])
+	setServiceLoadBalancerIP(&svc2, testIPs[1][true])
 	svc2.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	svc3 := getTestService("servicesr3", v1.ProtocolTCP, nil, false, 4444)
-	setServiceLoadBalancerIP(&svc3, testIP3)
+	svc3 := getTestServiceDualStack("servicesr3", v1.ProtocolTCP, nil, 4444)
+	setServiceLoadBalancerIP(&svc3, testIPs[2][false])
+	setServiceLoadBalancerIP(&svc3, testIPs[2][true])
 	svc3.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	testRuleName23 := testRuleName2
-	sg := getTestSecurityGroup(az)
+	testRuleName23 := testRuleNames[1]
+	sg := getTestSecurityGroupDualStack(az)
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 := utilnet.IsIPv6String(svc1.Spec.ClusterIP)
-	sg, err := az.reconcileSecurityGroup(testClusterName, &svc1, pointer.String(getServiceLoadBalancerIP(&svc1, isIPv6)), nil, true)
-	if err != nil {
-		t.Errorf("Unexpected error adding svc1: %q", err)
-	}
+	sg, err := az.reconcileSecurityGroup(testClusterName, &svc1, &[]string{getServiceLoadBalancerIP(&svc1, false), getServiceLoadBalancerIP(&svc1, true)}, nil, true)
+	assert.Nil(t, err)
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 = utilnet.IsIPv6String(svc2.Spec.ClusterIP)
-	sg, err = az.reconcileSecurityGroup(testClusterName, &svc2, pointer.String(getServiceLoadBalancerIP(&svc2, isIPv6)), nil, true)
-	if err != nil {
-		t.Errorf("Unexpected error adding svc2: %q", err)
-	}
+	sg, err = az.reconcileSecurityGroup(testClusterName, &svc2, &[]string{getServiceLoadBalancerIP(&svc2, false), getServiceLoadBalancerIP(&svc2, true)}, nil, true)
+	assert.Nil(t, err)
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 = utilnet.IsIPv6String(svc3.Spec.ClusterIP)
-	sg, err = az.reconcileSecurityGroup(testClusterName, &svc3, pointer.String(getServiceLoadBalancerIP(&svc3, isIPv6)), nil, true)
-	if err != nil {
-		t.Errorf("Unexpected error adding svc3: %q", err)
-	}
-
-	validateSecurityGroup(t, sg, svc1, svc2, svc3)
-
-	_, securityRule13, rule13Found := findSecurityRuleByName(*sg.SecurityRules, testRuleName23)
-	if !rule13Found {
-		t.Fatalf("Expected security rule %q but it was not present", testRuleName23)
-	}
-
-	_, securityRule2, rule2Found := findSecurityRuleByName(*sg.SecurityRules, testRuleName3)
-	if !rule2Found {
-		t.Fatalf("Expected security rule %q but it was not present", testRuleName3)
-	}
-
-	expectedDestinationIPCount13 := 2
-	if len(*securityRule13.DestinationAddressPrefixes) != expectedDestinationIPCount13 {
-		t.Errorf("Shared rule %s should have had %d destination IP addresses but had %d", testRuleName23, expectedDestinationIPCount13, len(*securityRule13.DestinationAddressPrefixes))
-	}
-
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIP1, securityRule13)
-	if err != nil {
-		t.Errorf("Shared rule %s did not match service IP: %v", testRuleName23, err)
-	}
-
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIP3, securityRule13)
-	if err != nil {
-		t.Errorf("Shared rule %s did not match service IP: %v", testRuleName23, err)
-	}
-
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 8888}, testIP2, securityRule13)
-	if err == nil {
-		t.Errorf("Shared rule %s matched wrong service's port and IP", testRuleName23)
-	}
-
-	if securityRule13.Priority == nil {
-		t.Errorf("Shared rule %s had no priority", testRuleName23)
-	}
-
-	if securityRule13.Access != network.SecurityRuleAccessAllow {
-		t.Errorf("Shared rule %s did not have Allow access", testRuleName23)
-	}
-
-	if securityRule13.Direction != network.SecurityRuleDirectionInbound {
-		t.Errorf("Shared rule %s did not have Inbound direction", testRuleName23)
-	}
-
-	expectedDestinationIPCount2 := 1
-	if len(*securityRule2.DestinationAddressPrefixes) != expectedDestinationIPCount2 {
-		t.Errorf("Shared rule %s should have had %d destination IP addresses but had %d", testRuleName3, expectedDestinationIPCount2, len(*securityRule2.DestinationAddressPrefixes))
-	}
-
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 8888}, testIP2, securityRule2)
-	if err != nil {
-		t.Errorf("Shared rule %s did not match service IP: %v", testRuleName3, err)
-	}
+	sg, err = az.reconcileSecurityGroup(testClusterName, &svc3, &[]string{getServiceLoadBalancerIP(&svc3, false), getServiceLoadBalancerIP(&svc3, true)}, nil, true)
+	assert.Nil(t, err)
 
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIP1, securityRule2)
-	if err == nil {
-		t.Errorf("Shared rule %s matched wrong service's port and IP", testRuleName3)
+	validateSecurityGroupDualStack(t, az, sg, svc1, svc2, svc3)
+
+	for isIPv6, ruleName := range testRuleName23 {
+		t.Run(fmt.Sprintf("ruleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			_, rule, ruleFound := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.True(t, ruleFound)
+			assert.Equal(t, 2, len(*rule.DestinationAddressPrefixes))
+			assert.Nil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIPs[0][isIPv6], rule))
+			assert.Nil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIPs[2][isIPv6], rule))
+			assert.NotNil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 8888}, testIPs[1][isIPv6], rule))
+			assert.NotNil(t, rule.Priority)
+			assert.Equal(t, network.SecurityRuleAccessAllow, rule.Access)
+			assert.Equal(t, network.SecurityRuleDirectionInbound, rule.Direction)
+		})
 	}
 
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIP3, securityRule2)
-	if err == nil {
-		t.Errorf("Shared rule %s matched wrong service's port and IP", testRuleName3)
+	for isIPv6, ruleName := range testRuleNames[2] {
+		t.Run(fmt.Sprintf("ruleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			_, rule, ruleFound := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.True(t, ruleFound)
+			assert.Equal(t, 1, len(*rule.DestinationAddressPrefixes))
+			assert.Nil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 8888}, testIPs[1][isIPv6], rule))
+			assert.NotNil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIPs[0][isIPv6], rule))
+			assert.NotNil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIPs[2][isIPv6], rule))
+		})
 	}
 }
 
@@ -3063,61 +3025,42 @@ func TestIfServiceSpecifiesSharedRuleAndServiceIsDeletedThenTheServicesPortAndAd
 	defer ctrl.Finish()
 	az := GetTestCloud(ctrl)
 
-	svc1 := getTestService("servicesr1", v1.ProtocolTCP, nil, false, 80)
-	setServiceLoadBalancerIP(&svc1, testIP1)
+	svc1 := getTestServiceDualStack("servicesr1", v1.ProtocolTCP, nil, 80)
+	setServiceLoadBalancerIP(&svc1, testIPs[0][false])
+	setServiceLoadBalancerIP(&svc1, testIPs[0][true])
 	svc1.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	svc2 := getTestService("servicesr2", v1.ProtocolTCP, nil, false, 80)
-	setServiceLoadBalancerIP(&svc2, testIP2)
+	svc2 := getTestServiceDualStack("servicesr2", v1.ProtocolTCP, nil, 80)
+	setServiceLoadBalancerIP(&svc2, testIPs[1][false])
+	setServiceLoadBalancerIP(&svc2, testIPs[1][true])
 	svc2.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	expectedRuleName := testRuleName
-
-	sg := getTestSecurityGroup(az)
+	sg := getTestSecurityGroupDualStack(az)
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 := utilnet.IsIPv6String(svc1.Spec.ClusterIP)
-	sg, err := az.reconcileSecurityGroup(testClusterName, &svc1, pointer.String(getServiceLoadBalancerIP(&svc1, isIPv6)), nil, true)
-	if err != nil {
-		t.Errorf("Unexpected error adding svc1: %q", err)
-	}
+	sg, err := az.reconcileSecurityGroup(testClusterName, &svc1, &[]string{getServiceLoadBalancerIP(&svc1, false), getServiceLoadBalancerIP(&svc1, true)}, nil, true)
+	assert.Nil(t, err)
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 = utilnet.IsIPv6String(svc2.Spec.ClusterIP)
-	sg, err = az.reconcileSecurityGroup(testClusterName, &svc2, pointer.String(getServiceLoadBalancerIP(&svc2, isIPv6)), nil, true)
-	if err != nil {
-		t.Errorf("Unexpected error adding svc2: %q", err)
-	}
+	sg, err = az.reconcileSecurityGroup(testClusterName, &svc2, &[]string{getServiceLoadBalancerIP(&svc2, false), getServiceLoadBalancerIP(&svc2, true)}, nil, true)
+	assert.Nil(t, err)
 
-	validateSecurityGroup(t, sg, svc1, svc2)
+	validateSecurityGroupDualStack(t, az, sg, svc1, svc2)
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 = utilnet.IsIPv6String(svc1.Spec.ClusterIP)
-	sg, err = az.reconcileSecurityGroup(testClusterName, &svc1, pointer.String(getServiceLoadBalancerIP(&svc1, isIPv6)), nil, false)
-	if err != nil {
-		t.Errorf("Unexpected error removing svc1: %q", err)
-	}
-
-	validateSecurityGroup(t, sg, svc2)
-
-	_, securityRule, ruleFound := findSecurityRuleByName(*sg.SecurityRules, expectedRuleName)
-	if !ruleFound {
-		t.Fatalf("Expected security rule %q but it was not present", expectedRuleName)
-	}
-
-	expectedDestinationIPCount := 1
-	if len(*securityRule.DestinationAddressPrefixes) != expectedDestinationIPCount {
-		t.Errorf("Shared rule should have had %d destination IP addresses but had %d", expectedDestinationIPCount, len(*securityRule.DestinationAddressPrefixes))
-	}
+	sg, err = az.reconcileSecurityGroup(testClusterName, &svc1, &[]string{getServiceLoadBalancerIP(&svc1, false), getServiceLoadBalancerIP(&svc1, true)}, nil, false)
+	assert.Nil(t, err)
 
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 80}, testIP2, securityRule)
-	if err != nil {
-		t.Errorf("Shared rule no longer matched other service IP: %v", err)
-	}
+	validateSecurityGroupDualStack(t, az, sg, svc2)
 
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 80}, testIP1, securityRule)
-	if err == nil {
-		t.Error("Shared rule was not updated to remove deleted service IP")
+	for isIPv6, ruleName := range testRuleNames[0] {
+		t.Run(fmt.Sprintf("ruleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			_, securityRule, ruleFound := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.True(t, ruleFound)
+			assert.Equal(t, 1, len(*securityRule.DestinationAddressPrefixes))
+			assert.Nil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 80}, testIPs[1][isIPv6], securityRule))
+			assert.NotNil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 80}, testIPs[0][isIPv6], securityRule))
+		})
 	}
 }
 
@@ -3126,113 +3069,67 @@ func TestIfSomeServicesShareARuleAndOneIsDeletedItIsRemovedFromTheRightRule(t *t
 	defer ctrl.Finish()
 	az := GetTestCloud(ctrl)
 
-	svc1 := getTestService("servicesr1", v1.ProtocolTCP, nil, false, 4444)
-	setServiceLoadBalancerIP(&svc1, testIP1)
+	svc1 := getTestServiceDualStack("servicesr1", v1.ProtocolTCP, nil, 4444)
+	setServiceLoadBalancerIP(&svc1, testIPs[0][false])
+	setServiceLoadBalancerIP(&svc1, testIPs[0][true])
 	svc1.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	svc2 := getTestService("servicesr2", v1.ProtocolTCP, nil, false, 8888)
-	setServiceLoadBalancerIP(&svc2, testIP2)
+	svc2 := getTestServiceDualStack("servicesr2", v1.ProtocolTCP, nil, 8888)
+	setServiceLoadBalancerIP(&svc2, testIPs[1][false])
+	setServiceLoadBalancerIP(&svc2, testIPs[1][true])
 	svc2.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	svc3 := getTestService("servicesr3", v1.ProtocolTCP, nil, false, 4444)
-	setServiceLoadBalancerIP(&svc3, testIP3)
+	svc3 := getTestServiceDualStack("servicesr3", v1.ProtocolTCP, nil, 4444)
+	setServiceLoadBalancerIP(&svc3, testIPs[2][false])
+	setServiceLoadBalancerIP(&svc3, testIPs[2][true])
 	svc3.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	testRuleName23 := testRuleName2
-	sg := getTestSecurityGroup(az)
+	sg := getTestSecurityGroupDualStack(az)
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 := utilnet.IsIPv6String(svc1.Spec.ClusterIP)
-	sg, err := az.reconcileSecurityGroup(testClusterName, &svc1, pointer.String(getServiceLoadBalancerIP(&svc1, isIPv6)), nil, true)
-	if err != nil {
-		t.Errorf("Unexpected error adding svc1: %q", err)
-	}
+	sg, err := az.reconcileSecurityGroup(testClusterName, &svc1, &[]string{getServiceLoadBalancerIP(&svc1, false), getServiceLoadBalancerIP(&svc1, true)}, nil, true)
+	assert.Nil(t, err)
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 = utilnet.IsIPv6String(svc2.Spec.ClusterIP)
-	sg, err = az.reconcileSecurityGroup(testClusterName, &svc2, pointer.String(getServiceLoadBalancerIP(&svc2, isIPv6)), nil, true)
-	if err != nil {
-		t.Errorf("Unexpected error adding svc2: %q", err)
-	}
+	sg, err = az.reconcileSecurityGroup(testClusterName, &svc2, &[]string{getServiceLoadBalancerIP(&svc2, false), getServiceLoadBalancerIP(&svc2, true)}, nil, true)
+	assert.Nil(t, err)
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 = utilnet.IsIPv6String(svc3.Spec.ClusterIP)
-	sg, err = az.reconcileSecurityGroup(testClusterName, &svc3, pointer.String(getServiceLoadBalancerIP(&svc3, isIPv6)), nil, true)
-	if err != nil {
-		t.Errorf("Unexpected error adding svc3: %q", err)
-	}
+	sg, err = az.reconcileSecurityGroup(testClusterName, &svc3, &[]string{getServiceLoadBalancerIP(&svc3, false), getServiceLoadBalancerIP(&svc3, true)}, nil, true)
+	assert.Nil(t, err)
 
-	validateSecurityGroup(t, sg, svc1, svc2, svc3)
+	validateSecurityGroupDualStack(t, az, sg, svc1, svc2, svc3)
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 = utilnet.IsIPv6String(svc1.Spec.ClusterIP)
-	sg, err = az.reconcileSecurityGroup(testClusterName, &svc1, pointer.String(getServiceLoadBalancerIP(&svc1, isIPv6)), nil, false)
-	if err != nil {
-		t.Errorf("Unexpected error removing svc1: %q", err)
-	}
-
-	validateSecurityGroup(t, sg, svc2, svc3)
-
-	_, securityRule13, rule13Found := findSecurityRuleByName(*sg.SecurityRules, testRuleName23)
-	if !rule13Found {
-		t.Fatalf("Expected security rule %q but it was not present", testRuleName23)
-	}
-
-	_, securityRule2, rule2Found := findSecurityRuleByName(*sg.SecurityRules, testRuleName3)
-	if !rule2Found {
-		t.Fatalf("Expected security rule %q but it was not present", testRuleName3)
-	}
-
-	expectedDestinationIPCount13 := 1
-	if len(*securityRule13.DestinationAddressPrefixes) != expectedDestinationIPCount13 {
-		t.Errorf("Shared rule %s should have had %d destination IP addresses but had %d", testRuleName23, expectedDestinationIPCount13, len(*securityRule13.DestinationAddressPrefixes))
-	}
-
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIP1, securityRule13)
-	if err == nil {
-		t.Errorf("Shared rule %s should have had svc1 removed but did not", testRuleName23)
-	}
-
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIP3, securityRule13)
-	if err != nil {
-		t.Errorf("Shared rule %s did not match service IP: %v", testRuleName23, err)
-	}
-
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 8888}, testIP2, securityRule13)
-	if err == nil {
-		t.Errorf("Shared rule %s matched wrong service's port and IP", testRuleName23)
-	}
-
-	if securityRule13.Priority == nil {
-		t.Errorf("Shared rule %s had no priority", testRuleName23)
-	}
-
-	if securityRule13.Access != network.SecurityRuleAccessAllow {
-		t.Errorf("Shared rule %s did not have Allow access", testRuleName23)
-	}
+	sg, err = az.reconcileSecurityGroup(testClusterName, &svc1, &[]string{getServiceLoadBalancerIP(&svc1, false), getServiceLoadBalancerIP(&svc1, true)}, nil, false)
+	assert.Nil(t, err)
 
-	if securityRule13.Direction != network.SecurityRuleDirectionInbound {
-		t.Errorf("Shared rule %s did not have Inbound direction", testRuleName23)
+	validateSecurityGroupDualStack(t, az, sg, svc2, svc3)
+
+	for isIPv6, ruleName := range testRuleNames[1] {
+		t.Run(fmt.Sprintf("ruleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			_, rule, ruleFound := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.True(t, ruleFound)
+			assert.Equal(t, 1, len(*rule.DestinationAddressPrefixes))
+			assert.NotNil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIPs[0][isIPv6], rule))
+			assert.Nil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIPs[2][isIPv6], rule))
+			assert.NotNil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 8888}, testIPs[1][isIPv6], rule))
+			assert.NotNil(t, rule.Priority)
+			assert.Equal(t, network.SecurityRuleAccessAllow, rule.Access)
+			assert.Equal(t, network.SecurityRuleDirectionInbound, rule.Direction)
+		})
 	}
 
-	expectedDestinationIPCount2 := 1
-	if len(*securityRule2.DestinationAddressPrefixes) != expectedDestinationIPCount2 {
-		t.Errorf("Shared rule %s should have had %d destination IP addresses but had %d", testRuleName3, expectedDestinationIPCount2, len(*securityRule2.DestinationAddressPrefixes))
-	}
+	for isIPv6, ruleName := range testRuleNames[2] {
+		t.Run(fmt.Sprintf("ruleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			_, rule, ruleFound := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.True(t, ruleFound)
+			assert.Equal(t, 1, len(*rule.DestinationAddressPrefixes))
+			assert.Nil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 8888}, testIPs[1][isIPv6], rule))
 
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 8888}, testIP2, securityRule2)
-	if err != nil {
-		t.Errorf("Shared rule %s did not match service IP: %v", testRuleName3, err)
-	}
-
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIP1, securityRule2)
-	if err == nil {
-		t.Errorf("Shared rule %s matched wrong service's port and IP", testRuleName3)
-	}
-
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIP3, securityRule2)
-	if err == nil {
-		t.Errorf("Shared rule %s matched wrong service's port and IP", testRuleName3)
+			assert.NotNil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIPs[0][isIPv6], rule))
+			assert.NotNil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIPs[2][isIPv6], rule))
+		})
 	}
 }
 
@@ -3241,88 +3138,63 @@ func TestIfServiceSpecifiesSharedRuleAndLastServiceIsDeletedThenRuleIsDeleted(t
 	defer ctrl.Finish()
 	az := GetTestCloud(ctrl)
 
-	svc1 := getTestService("servicesr1", v1.ProtocolTCP, nil, false, 4444)
-	setServiceLoadBalancerIP(&svc1, testIP1)
+	svc1 := getTestServiceDualStack("servicesr1", v1.ProtocolTCP, nil, 4444)
+	setServiceLoadBalancerIP(&svc1, testIPs[0][false])
+	setServiceLoadBalancerIP(&svc1, testIPs[0][true])
 	svc1.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	svc2 := getTestService("servicesr2", v1.ProtocolTCP, nil, false, 8888)
-	setServiceLoadBalancerIP(&svc2, testIP2)
+	svc2 := getTestServiceDualStack("servicesr2", v1.ProtocolTCP, nil, 8888)
+	setServiceLoadBalancerIP(&svc2, testIPs[1][false])
+	setServiceLoadBalancerIP(&svc2, testIPs[1][true])
 	svc2.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	svc3 := getTestService("servicesr3", v1.ProtocolTCP, nil, false, 4444)
-	setServiceLoadBalancerIP(&svc3, testIP3)
+	svc3 := getTestServiceDualStack("servicesr3", v1.ProtocolTCP, nil, 4444)
+	setServiceLoadBalancerIP(&svc3, testIPs[2][false])
+	setServiceLoadBalancerIP(&svc3, testIPs[2][true])
 	svc3.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	testRuleName23 := testRuleName2
-	sg := getTestSecurityGroup(az)
+	sg := getTestSecurityGroupDualStack(az)
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 := utilnet.IsIPv6String(svc1.Spec.ClusterIP)
-	sg, err := az.reconcileSecurityGroup(testClusterName, &svc1, pointer.String(getServiceLoadBalancerIP(&svc1, isIPv6)), nil, true)
-	if err != nil {
-		t.Errorf("Unexpected error adding svc1: %q", err)
-	}
+	sg, err := az.reconcileSecurityGroup(testClusterName, &svc1, &[]string{getServiceLoadBalancerIP(&svc1, false), getServiceLoadBalancerIP(&svc1, true)}, nil, true)
+	assert.Nil(t, err)
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 = utilnet.IsIPv6String(svc2.Spec.ClusterIP)
-	sg, err = az.reconcileSecurityGroup(testClusterName, &svc2, pointer.String(getServiceLoadBalancerIP(&svc2, isIPv6)), nil, true)
-	if err != nil {
-		t.Errorf("Unexpected error adding svc2: %q", err)
-	}
+	sg, err = az.reconcileSecurityGroup(testClusterName, &svc2, &[]string{getServiceLoadBalancerIP(&svc2, false), getServiceLoadBalancerIP(&svc2, true)}, nil, true)
+	assert.Nil(t, err)
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 = utilnet.IsIPv6String(svc3.Spec.ClusterIP)
-	sg, err = az.reconcileSecurityGroup(testClusterName, &svc3, pointer.String(getServiceLoadBalancerIP(&svc3, isIPv6)), nil, true)
-	if err != nil {
-		t.Errorf("Unexpected error adding svc3: %q", err)
-	}
+	sg, err = az.reconcileSecurityGroup(testClusterName, &svc3, &[]string{getServiceLoadBalancerIP(&svc3, false), getServiceLoadBalancerIP(&svc3, true)}, nil, true)
+	assert.Nil(t, err)
 
-	validateSecurityGroup(t, sg, svc1, svc2, svc3)
+	validateSecurityGroupDualStack(t, az, sg, svc1, svc2, svc3)
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 = utilnet.IsIPv6String(svc1.Spec.ClusterIP)
-	sg, err = az.reconcileSecurityGroup(testClusterName, &svc1, pointer.String(getServiceLoadBalancerIP(&svc1, isIPv6)), nil, false)
-	if err != nil {
-		t.Errorf("Unexpected error removing svc1: %q", err)
-	}
+	sg, err = az.reconcileSecurityGroup(testClusterName, &svc1, &[]string{getServiceLoadBalancerIP(&svc1, false), getServiceLoadBalancerIP(&svc1, true)}, nil, false)
+	assert.Nil(t, err)
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 = utilnet.IsIPv6String(svc3.Spec.ClusterIP)
-	sg, err = az.reconcileSecurityGroup(testClusterName, &svc3, pointer.String(getServiceLoadBalancerIP(&svc3, isIPv6)), nil, false)
-	if err != nil {
-		t.Errorf("Unexpected error removing svc3: %q", err)
-	}
-
-	validateSecurityGroup(t, sg, svc2)
-
-	_, _, rule13Found := findSecurityRuleByName(*sg.SecurityRules, testRuleName23)
-	if rule13Found {
-		t.Fatalf("Expected security rule %q to have been deleted but it was still present", testRuleName23)
-	}
-
-	_, securityRule2, rule2Found := findSecurityRuleByName(*sg.SecurityRules, testRuleName3)
-	if !rule2Found {
-		t.Fatalf("Expected security rule %q but it was not present", testRuleName3)
-	}
-
-	expectedDestinationIPCount2 := 1
-	if len(*securityRule2.DestinationAddressPrefixes) != expectedDestinationIPCount2 {
-		t.Errorf("Shared rule %s should have had %d destination IP addresses but had %d", testRuleName3, expectedDestinationIPCount2, len(*securityRule2.DestinationAddressPrefixes))
-	}
+	sg, err = az.reconcileSecurityGroup(testClusterName, &svc3, &[]string{getServiceLoadBalancerIP(&svc3, false), getServiceLoadBalancerIP(&svc3, true)}, nil, false)
+	assert.Nil(t, err)
 
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 8888}, testIP2, securityRule2)
-	if err != nil {
-		t.Errorf("Shared rule %s did not match service IP: %v", testRuleName3, err)
-	}
+	validateSecurityGroupDualStack(t, az, sg, svc2)
 
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIP1, securityRule2)
-	if err == nil {
-		t.Errorf("Shared rule %s matched wrong service's port and IP", testRuleName3)
+	for isIPv6, ruleName := range testRuleNames[1] {
+		t.Run(fmt.Sprintf("ruleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			_, _, ruleFound := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.False(t, ruleFound)
+		})
 	}
 
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIP3, securityRule2)
-	if err == nil {
-		t.Errorf("Shared rule %s matched wrong service's port and IP", testRuleName3)
+	for isIPv6, ruleName := range testRuleNames[2] {
+		t.Run(fmt.Sprintf("ruleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			_, rule, ruleFound := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.True(t, ruleFound)
+			assert.Equal(t, 1, len(*rule.DestinationAddressPrefixes))
+			assert.Nil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 8888}, testIPs[1][isIPv6], rule))
+			assert.NotNil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIPs[0][isIPv6], rule))
+			assert.NotNil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIPs[2][isIPv6], rule))
+		})
 	}
 }
 
@@ -3332,171 +3204,132 @@ func TestCanCombineSharedAndPrivateRulesInSameGroup(t *testing.T) {
 	az := GetTestCloud(ctrl)
 	var err error
 
-	svc1 := getTestService("servicesr1", v1.ProtocolTCP, nil, false, 4444)
-	setServiceLoadBalancerIP(&svc1, testIP1)
+	svc1 := getTestServiceDualStack("servicesr1", v1.ProtocolTCP, nil, 4444)
+	setServiceLoadBalancerIP(&svc1, testIPs[0][false])
+	setServiceLoadBalancerIP(&svc1, testIPs[0][true])
 	svc1.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	svc2 := getTestService("servicesr2", v1.ProtocolTCP, nil, false, 8888)
-	setServiceLoadBalancerIP(&svc2, testIP2)
+	svc2 := getTestServiceDualStack("servicesr2", v1.ProtocolTCP, nil, 8888)
+	setServiceLoadBalancerIP(&svc2, testIPs[1][false])
+	setServiceLoadBalancerIP(&svc2, testIPs[1][true])
 	svc2.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	svc3 := getTestService("servicesr3", v1.ProtocolTCP, nil, false, 4444)
-	setServiceLoadBalancerIP(&svc3, testIP3)
+	svc3 := getTestServiceDualStack("servicesr3", v1.ProtocolTCP, nil, 4444)
+	setServiceLoadBalancerIP(&svc3, testIPs[2][false])
+	setServiceLoadBalancerIP(&svc3, testIPs[2][true])
 	svc3.Annotations[consts.ServiceAnnotationSharedSecurityRule] = consts.TrueAnnotationValue
 
-	svc4 := getTestService("servicesr4", v1.ProtocolTCP, nil, false, 4444)
-	setServiceLoadBalancerIP(&svc4, "192.168.22.33")
+	svc4 := getTestServiceDualStack("servicesr4", v1.ProtocolTCP, nil, 4444)
+	setServiceLoadBalancerIP(&svc4, testIPs[3][false])
+	setServiceLoadBalancerIP(&svc4, testIPs[3][true])
 	svc4.Annotations[consts.ServiceAnnotationSharedSecurityRule] = "false"
 
-	svc5 := getTestService("servicesr5", v1.ProtocolTCP, nil, false, 8888)
-	setServiceLoadBalancerIP(&svc5, "192.168.22.33")
+	svc5 := getTestServiceDualStack("servicesr5", v1.ProtocolTCP, nil, 8888)
+	setServiceLoadBalancerIP(&svc5, testIPs[3][false])
+	setServiceLoadBalancerIP(&svc5, testIPs[3][true])
 	svc5.Annotations[consts.ServiceAnnotationSharedSecurityRule] = "false"
 
 	testServices := []v1.Service{svc1, svc2, svc3, svc4, svc5}
 
-	testRuleName23 := testRuleName2
-	isIPv6 := utilnet.IsIPv6String(svc4.Spec.ClusterIP)
-	expectedRuleName4 := az.getSecurityRuleName(&svc4, v1.ServicePort{Port: 4444, Protocol: v1.ProtocolTCP}, "Internet", isIPv6)
-	isIPv6 = utilnet.IsIPv6String(svc5.Spec.ClusterIP)
-	expectedRuleName5 := az.getSecurityRuleName(&svc5, v1.ServicePort{Port: 8888, Protocol: v1.ProtocolTCP}, "Internet", isIPv6)
+	testRuleName23 := testRuleNames[1]
+	expectedRuleName4 := map[bool]string{
+		false: az.getSecurityRuleName(&svc4, v1.ServicePort{Port: 4444, Protocol: v1.ProtocolTCP}, "Internet", false),
+		true:  az.getSecurityRuleName(&svc4, v1.ServicePort{Port: 4444, Protocol: v1.ProtocolTCP}, "Internet", true),
+	}
+	expectedRuleName5 := map[bool]string{
+		false: az.getSecurityRuleName(&svc5, v1.ServicePort{Port: 8888, Protocol: v1.ProtocolTCP}, "Internet", false),
+		true:  az.getSecurityRuleName(&svc5, v1.ServicePort{Port: 8888, Protocol: v1.ProtocolTCP}, "Internet", true),
+	}
 
-	sg := getTestSecurityGroup(az)
+	sg := getTestSecurityGroupDualStack(az)
 
 	for i, svc := range testServices {
 		svc := svc
 		setMockSecurityGroup(az, ctrl, sg)
-		isIPv6 := utilnet.IsIPv6String(svc.Spec.ClusterIP)
-		sg, err = az.reconcileSecurityGroup(testClusterName, &testServices[i], pointer.String(getServiceLoadBalancerIP(&svc, isIPv6)), nil, true)
-		if err != nil {
-			t.Errorf("Unexpected error adding svc%d: %q", i+1, err)
-		}
-	}
-
-	validateSecurityGroup(t, sg, svc1, svc2, svc3, svc4, svc5)
-
-	expectedRuleCount := 4
-	if len(*sg.SecurityRules) != expectedRuleCount {
-		t.Errorf("Expected security group to have %d rules but it had %d", expectedRuleCount, len(*sg.SecurityRules))
-	}
-
-	_, securityRule13, rule13Found := findSecurityRuleByName(*sg.SecurityRules, testRuleName23)
-	if !rule13Found {
-		t.Fatalf("Expected security rule %q but it was not present", testRuleName23)
-	}
-
-	_, securityRule2, rule2Found := findSecurityRuleByName(*sg.SecurityRules, testRuleName3)
-	if !rule2Found {
-		t.Fatalf("Expected security rule %q but it was not present", testRuleName3)
+		sg, err = az.reconcileSecurityGroup(testClusterName, &testServices[i], &[]string{getServiceLoadBalancerIP(&svc, false), getServiceLoadBalancerIP(&svc, true)}, nil, true)
+		assert.Nil(t, err)
 	}
 
-	_, securityRule4, rule4Found := findSecurityRuleByName(*sg.SecurityRules, expectedRuleName4)
-	if !rule4Found {
-		t.Fatalf("Expected security rule %q but it was not present", expectedRuleName4)
-	}
-
-	_, securityRule5, rule5Found := findSecurityRuleByName(*sg.SecurityRules, expectedRuleName5)
-	if !rule5Found {
-		t.Fatalf("Expected security rule %q but it was not present", expectedRuleName5)
-	}
-
-	expectedDestinationIPCount13 := 2
-	if len(*securityRule13.DestinationAddressPrefixes) != expectedDestinationIPCount13 {
-		t.Errorf("Shared rule %s should have had %d destination IP addresses but had %d", testRuleName23, expectedDestinationIPCount13, len(*securityRule13.DestinationAddressPrefixes))
-	}
-
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIP1, securityRule13)
-	if err != nil {
-		t.Errorf("Shared rule %s did not match service IP: %v", testRuleName23, err)
-	}
-
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIP3, securityRule13)
-	if err != nil {
-		t.Errorf("Shared rule %s did not match service IP: %v", testRuleName23, err)
-	}
-
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, "192.168.22.33", securityRule13)
-	if err == nil {
-		t.Errorf("Shared rule %s matched wrong (unshared) service's port and IP", testRuleName23)
-	}
+	validateSecurityGroupDualStack(t, az, sg, svc1, svc2, svc3, svc4, svc5)
 
-	expectedDestinationIPCount2 := 1
-	if len(*securityRule2.DestinationAddressPrefixes) != expectedDestinationIPCount2 {
-		t.Errorf("Shared rule %s should have had %d destination IP addresses but had %d", testRuleName3, expectedDestinationIPCount2, len(*securityRule2.DestinationAddressPrefixes))
-	}
+	assert.Equal(t, 8, len(*sg.SecurityRules))
 
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 8888}, testIP2, securityRule2)
-	if err != nil {
-		t.Errorf("Shared rule %s did not match service IP: %v", testRuleName3, err)
-	}
-
-	err = securityRuleMatches("Internet", v1.ServicePort{Port: 8888}, "192.168.22.33", securityRule2)
-	if err == nil {
-		t.Errorf("Shared rule %s matched wrong (unshared) service's port and IP", testRuleName3)
-	}
-
-	if securityRule4.DestinationAddressPrefixes != nil {
-		t.Errorf("Expected unshared rule %s to use single destination IP address but used collection", expectedRuleName4)
+	for isIPv6, ruleName := range testRuleName23 {
+		t.Run(fmt.Sprintf("ruleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			_, securityRule13, rule13Found := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.True(t, rule13Found)
+			assert.Equal(t, 2, len(*securityRule13.DestinationAddressPrefixes))
+			assert.Nil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIPs[0][isIPv6], securityRule13))
+			assert.Nil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIPs[2][isIPv6], securityRule13))
+			assert.NotNil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 4444}, testIPs[3][isIPv6], securityRule13))
+		})
 	}
 
-	if securityRule4.DestinationAddressPrefix == nil {
-		t.Errorf("Expected unshared rule %s to have a destination IP address", expectedRuleName4)
-	} else {
-		isIPv6 := utilnet.IsIPv6String(svc4.Spec.ClusterIP)
-		if !strings.EqualFold(*securityRule4.DestinationAddressPrefix, getServiceLoadBalancerIP(&svc4, isIPv6)) {
-			t.Errorf("Expected unshared rule %s to have a destination %s but had %s", expectedRuleName4, getServiceLoadBalancerIP(&svc4, isIPv6), *securityRule4.DestinationAddressPrefix)
-		}
+	for isIPv6, ruleName := range testRuleNames[2] {
+		t.Run(fmt.Sprintf("ruleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			_, securityRule2, rule2Found := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.True(t, rule2Found)
+			assert.Equal(t, 1, len(*securityRule2.DestinationAddressPrefixes))
+			assert.Nil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 8888}, testIPs[1][isIPv6], securityRule2))
+			assert.NotNil(t, securityRuleMatches("Internet", v1.ServicePort{Port: 8888}, testIPs[3][isIPv6], securityRule2))
+		})
 	}
 
-	if securityRule5.DestinationAddressPrefixes != nil {
-		t.Errorf("Expected unshared rule %s to use single destination IP address but used collection", expectedRuleName5)
+	for isIPv6, ruleName := range expectedRuleName4 {
+		t.Run(fmt.Sprintf("expectedRuleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			_, securityRule4, rule4Found := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.True(t, rule4Found)
+			assert.Nil(t, securityRule4.DestinationAddressPrefixes)
+			assert.NotNil(t, securityRule4.DestinationAddressPrefix)
+			assert.Equal(t, getServiceLoadBalancerIP(&svc4, isIPv6), *securityRule4.DestinationAddressPrefix)
+		})
 	}
 
-	if securityRule5.DestinationAddressPrefix == nil {
-		t.Errorf("Expected unshared rule %s to have a destination IP address", expectedRuleName5)
-	} else {
-		isIPv6 := utilnet.IsIPv6String(svc5.Spec.ClusterIP)
-		if !strings.EqualFold(*securityRule5.DestinationAddressPrefix, getServiceLoadBalancerIP(&svc5, isIPv6)) {
-			t.Errorf("Expected unshared rule %s to have a destination %s but had %s", expectedRuleName5, getServiceLoadBalancerIP(&svc5, isIPv6), *securityRule5.DestinationAddressPrefix)
-		}
+	for isIPv6, ruleName := range expectedRuleName5 {
+		t.Run(fmt.Sprintf("expectedRuleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			_, securityRule5, rule5Found := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.True(t, rule5Found)
+			assert.Nil(t, securityRule5.DestinationAddressPrefixes)
+			assert.NotNil(t, securityRule5.DestinationAddressPrefix)
+			assert.Equal(t, getServiceLoadBalancerIP(&svc5, isIPv6), *securityRule5.DestinationAddressPrefix)
+		})
 	}
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 = utilnet.IsIPv6String(svc1.Spec.ClusterIP)
-	sg, err = az.reconcileSecurityGroup(testClusterName, &svc1, pointer.String(getServiceLoadBalancerIP(&svc1, isIPv6)), nil, false)
-	if err != nil {
-		t.Errorf("Unexpected error removing svc1: %q", err)
-	}
+	sg, err = az.reconcileSecurityGroup(testClusterName, &svc1, &[]string{getServiceLoadBalancerIP(&svc1, false), getServiceLoadBalancerIP(&svc1, true)}, nil, false)
+	assert.Nil(t, err)
 
 	setMockSecurityGroup(az, ctrl, sg)
-	isIPv6 = utilnet.IsIPv6String(svc5.Spec.ClusterIP)
-	sg, err = az.reconcileSecurityGroup(testClusterName, &svc5, pointer.String(getServiceLoadBalancerIP(&svc5, isIPv6)), nil, false)
-	if err != nil {
-		t.Errorf("Unexpected error removing svc5: %q", err)
-	}
-
-	_, securityRule13, rule13Found = findSecurityRuleByName(*sg.SecurityRules, testRuleName23)
-	if !rule13Found {
-		t.Fatalf("Expected security rule %q but it was not present", testRuleName23)
-	}
+	sg, err = az.reconcileSecurityGroup(testClusterName, &svc5, &[]string{getServiceLoadBalancerIP(&svc5, false), getServiceLoadBalancerIP(&svc5, true)}, nil, false)
+	assert.Nil(t, err)
 
-	_, _, rule2Found = findSecurityRuleByName(*sg.SecurityRules, testRuleName3)
-	if !rule2Found {
-		t.Fatalf("Expected security rule %q but it was not present", testRuleName3)
+	for isIPv6, ruleName := range testRuleName23 {
+		t.Run(fmt.Sprintf("ruleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			_, securityRule13, rule13Found := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.True(t, rule13Found)
+			assert.Equal(t, 1, len(*securityRule13.DestinationAddressPrefixes))
+		})
 	}
 
-	_, _, rule4Found = findSecurityRuleByName(*sg.SecurityRules, expectedRuleName4)
-	if !rule4Found {
-		t.Fatalf("Expected security rule %q but it was not present", expectedRuleName4)
+	for isIPv6, ruleName := range testRuleNames[2] {
+		t.Run(fmt.Sprintf("ruleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			_, _, rule2Found := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.True(t, rule2Found)
+		})
 	}
 
-	_, _, rule5Found = findSecurityRuleByName(*sg.SecurityRules, expectedRuleName5)
-	if rule5Found {
-		t.Fatalf("Expected security rule %q to have been removed but it was not present", expectedRuleName5)
+	for isIPv6, ruleName := range expectedRuleName4 {
+		t.Run(fmt.Sprintf("expectedRuleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			_, _, rule4Found := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.True(t, rule4Found)
+		})
 	}
 
-	expectedDestinationIPCount13 = 1
-	if len(*securityRule13.DestinationAddressPrefixes) != expectedDestinationIPCount13 {
-		t.Errorf("Shared rule %s should have had %d destination IP addresses but had %d", testRuleName23, expectedDestinationIPCount13, len(*securityRule13.DestinationAddressPrefixes))
+	for isIPv6, ruleName := range expectedRuleName5 {
+		t.Run(fmt.Sprintf("expectedRuleName=%s isIPv6=%t", ruleName, isIPv6), func(t *testing.T) {
+			_, _, rule5Found := findSecurityRuleByName(*sg.SecurityRules, ruleName)
+			assert.False(t, rule5Found)
+		})
 	}
 }
 
@@ -3864,17 +3697,32 @@ func TestInitializeCloudFromConfig(t *testing.T) {
 }
 
 func TestFindSecurityRule(t *testing.T) {
-	sg := network.SecurityRule{
-		Name: pointer.String(testRuleName4),
-		SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
-			Protocol:                   network.SecurityRuleProtocolTCP,
-			SourcePortRange:            pointer.String("*"),
-			SourceAddressPrefix:        pointer.String("Internet"),
-			DestinationPortRange:       pointer.String("80"),
-			DestinationAddressPrefix:   pointer.String(testIP1),
-			DestinationAddressPrefixes: &([]string{}),
-			Access:                     network.SecurityRuleAccessAllow,
-			Direction:                  network.SecurityRuleDirectionInbound,
+	srs := []network.SecurityRule{
+		{
+			Name: pointer.String(testRuleNames[3][false]),
+			SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
+				Protocol:                   network.SecurityRuleProtocolTCP,
+				SourcePortRange:            pointer.String("*"),
+				SourceAddressPrefix:        pointer.String("Internet"),
+				DestinationPortRange:       pointer.String("80"),
+				DestinationAddressPrefix:   pointer.String(testIPs[0][false]),
+				DestinationAddressPrefixes: &([]string{}),
+				Access:                     network.SecurityRuleAccessAllow,
+				Direction:                  network.SecurityRuleDirectionInbound,
+			},
+		},
+		{
+			Name: pointer.String(testRuleNames[3][true]),
+			SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
+				Protocol:                   network.SecurityRuleProtocolTCP,
+				SourcePortRange:            pointer.String("*"),
+				SourceAddressPrefix:        pointer.String("Internet"),
+				DestinationPortRange:       pointer.String("80"),
+				DestinationAddressPrefix:   pointer.String(testIPs[0][true]),
+				DestinationAddressPrefixes: &([]string{}),
+				Access:                     network.SecurityRuleAccessAllow,
+				Direction:                  network.SecurityRuleDirectionInbound,
+			},
 		},
 	}
 	testCases := []struct {
@@ -3897,7 +3745,7 @@ func TestFindSecurityRule(t *testing.T) {
 		{
 			desc: "false should be returned when protocol doesn't match",
 			testRule: network.SecurityRule{
-				Name: pointer.String(testRuleName4),
+				Name: pointer.String(testRuleNames[3][false]),
 				SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
 					Protocol: network.SecurityRuleProtocolUDP,
 				},
@@ -3907,7 +3755,7 @@ func TestFindSecurityRule(t *testing.T) {
 		{
 			desc: "false should be returned when SourcePortRange doesn't match",
 			testRule: network.SecurityRule{
-				Name: pointer.String(testRuleName4),
+				Name: pointer.String(testRuleNames[3][false]),
 				SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
 					Protocol:        network.SecurityRuleProtocolUDP,
 					SourcePortRange: pointer.String("1.2.3.4/32"),
@@ -3918,7 +3766,7 @@ func TestFindSecurityRule(t *testing.T) {
 		{
 			desc: "false should be returned when SourceAddressPrefix doesn't match",
 			testRule: network.SecurityRule{
-				Name: pointer.String(testRuleName4),
+				Name: pointer.String(testRuleNames[3][false]),
 				SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
 					Protocol:            network.SecurityRuleProtocolUDP,
 					SourcePortRange:     pointer.String("*"),
@@ -3930,7 +3778,7 @@ func TestFindSecurityRule(t *testing.T) {
 		{
 			desc: "false should be returned when DestinationPortRange doesn't match",
 			testRule: network.SecurityRule{
-				Name: pointer.String(testRuleName4),
+				Name: pointer.String(testRuleNames[3][false]),
 				SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
 					Protocol:             network.SecurityRuleProtocolUDP,
 					SourcePortRange:      pointer.String("*"),
@@ -3943,13 +3791,13 @@ func TestFindSecurityRule(t *testing.T) {
 		{
 			desc: "false should be returned when DestinationAddressPrefix doesn't match",
 			testRule: network.SecurityRule{
-				Name: pointer.String(testRuleName4),
+				Name: pointer.String(testRuleNames[3][false]),
 				SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
 					Protocol:                 network.SecurityRuleProtocolUDP,
 					SourcePortRange:          pointer.String("*"),
 					SourceAddressPrefix:      pointer.String("Internet"),
 					DestinationPortRange:     pointer.String("80"),
-					DestinationAddressPrefix: pointer.String(testIP2),
+					DestinationAddressPrefix: pointer.String(testIPs[1][false]),
 				},
 			},
 			expected: false,
@@ -3957,13 +3805,13 @@ func TestFindSecurityRule(t *testing.T) {
 		{
 			desc: "false should be returned when Access doesn't match",
 			testRule: network.SecurityRule{
-				Name: pointer.String(testRuleName4),
+				Name: pointer.String(testRuleNames[3][false]),
 				SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
 					Protocol:                 network.SecurityRuleProtocolUDP,
 					SourcePortRange:          pointer.String("*"),
 					SourceAddressPrefix:      pointer.String("Internet"),
 					DestinationPortRange:     pointer.String("80"),
-					DestinationAddressPrefix: pointer.String(testIP1),
+					DestinationAddressPrefix: pointer.String(testIPs[0][false]),
 					Access:                   network.SecurityRuleAccessDeny,
 					// Direction:                network.SecurityRuleDirectionInbound,
 				},
@@ -3973,13 +3821,13 @@ func TestFindSecurityRule(t *testing.T) {
 		{
 			desc: "false should be returned when Direction doesn't match",
 			testRule: network.SecurityRule{
-				Name: pointer.String(testRuleName4),
+				Name: pointer.String(testRuleNames[3][false]),
 				SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
 					Protocol:                 network.SecurityRuleProtocolUDP,
 					SourcePortRange:          pointer.String("*"),
 					SourceAddressPrefix:      pointer.String("Internet"),
 					DestinationPortRange:     pointer.String("80"),
-					DestinationAddressPrefix: pointer.String(testIP1),
+					DestinationAddressPrefix: pointer.String(testIPs[0][false]),
 					Access:                   network.SecurityRuleAccessAllow,
 					Direction:                network.SecurityRuleDirectionOutbound,
 				},
@@ -3989,13 +3837,13 @@ func TestFindSecurityRule(t *testing.T) {
 		{
 			desc: "true should be returned when everything matches but protocol is in different case",
 			testRule: network.SecurityRule{
-				Name: pointer.String(testRuleName4),
+				Name: pointer.String(testRuleNames[3][false]),
 				SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
 					Protocol:                 network.SecurityRuleProtocol("TCP"),
 					SourcePortRange:          pointer.String("*"),
 					SourceAddressPrefix:      pointer.String("Internet"),
 					DestinationPortRange:     pointer.String("80"),
-					DestinationAddressPrefix: pointer.String(testIP1),
+					DestinationAddressPrefix: pointer.String(testIPs[0][false]),
 					Access:                   network.SecurityRuleAccessAllow,
 					Direction:                network.SecurityRuleDirectionInbound,
 				},
@@ -4005,13 +3853,13 @@ func TestFindSecurityRule(t *testing.T) {
 		{
 			desc: "true should be returned when everything matches but DestinationAddressPrefixes is nil",
 			testRule: network.SecurityRule{
-				Name: pointer.String(testRuleName4),
+				Name: pointer.String(testRuleNames[3][false]),
 				SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
 					Protocol:                   network.SecurityRuleProtocolTCP,
 					SourcePortRange:            pointer.String("*"),
 					SourceAddressPrefix:        pointer.String("Internet"),
 					DestinationPortRange:       pointer.String("80"),
-					DestinationAddressPrefix:   pointer.String(testIP1),
+					DestinationAddressPrefix:   pointer.String(testIPs[0][false]),
 					DestinationAddressPrefixes: nil,
 					Access:                     network.SecurityRuleAccessAllow,
 					Direction:                  network.SecurityRuleDirectionInbound,
@@ -4020,15 +3868,22 @@ func TestFindSecurityRule(t *testing.T) {
 			expected: true,
 		},
 		{
-			desc:     "true should be returned when everything matches",
-			testRule: sg,
+			desc:     "true should be returned when everything matches IPv4",
+			testRule: srs[0],
+			expected: true,
+		},
+		{
+			desc:     "true should be returned when everything matches IPv6",
+			testRule: srs[1],
 			expected: true,
 		},
 	}
 
 	for i := range testCases {
-		found := findSecurityRule([]network.SecurityRule{sg}, testCases[i].testRule)
-		assert.Equal(t, testCases[i].expected, found, testCases[i].desc)
+		t.Run(testCases[i].desc, func(t *testing.T) {
+			found := findSecurityRule(srs, testCases[i].testRule)
+			assert.Equal(t, testCases[i].expected, found)
+		})
 	}
 }
 
diff --git a/pkg/provider/azure_utils_test.go b/pkg/provider/azure_utils_test.go
index c81f2c2cd..7a5db3447 100644
--- a/pkg/provider/azure_utils_test.go
+++ b/pkg/provider/azure_utils_test.go
@@ -674,6 +674,18 @@ func TestSetServiceLoadBalancerIP(t *testing.T) {
 		svc         *v1.Service
 		expectedSvc *v1.Service
 	}{
+		{
+			"IPv4",
+			"10.0.0.1",
+			&v1.Service{},
+			&v1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						consts.ServiceAnnotationLoadBalancerIPDualStack[consts.IPVersionIPv4]: "10.0.0.1",
+					},
+				},
+			},
+		},
 		{
 			"IPv6",
 			"2001::1",
@@ -681,11 +693,33 @@ func TestSetServiceLoadBalancerIP(t *testing.T) {
 			&v1.Service{
 				ObjectMeta: metav1.ObjectMeta{
 					Annotations: map[string]string{
-						consts.ServiceAnnotationLoadBalancerIPDualStack[true]: "2001::1",
+						consts.ServiceAnnotationLoadBalancerIPDualStack[consts.IPVersionIPv6]: "2001::1",
 					},
 				},
 			},
 		},
+		{
+			"empty IP",
+			"",
+			&v1.Service{},
+			&v1.Service{
+				ObjectMeta: metav1.ObjectMeta{},
+			},
+		},
+		{
+			"invalid IP",
+			"invalid-ip",
+			&v1.Service{},
+			&v1.Service{
+				ObjectMeta: metav1.ObjectMeta{},
+			},
+		},
+		{
+			"empty Service",
+			"10.0.0.1",
+			nil,
+			nil,
+		},
 	}
 	for _, tc := range testcases {
 		t.Run(tc.desc, func(t *testing.T) {
diff --git a/tests/e2e/network/ensureloadbalancer.go b/tests/e2e/network/ensureloadbalancer.go
index 913ae855c..2ccc2d4fa 100644
--- a/tests/e2e/network/ensureloadbalancer.go
+++ b/tests/e2e/network/ensureloadbalancer.go
@@ -890,7 +890,7 @@ var _ = Describe("EnsureLoadBalancer should not update any resources when servic
 			annotation[consts.ServiceAnnotationPIPNameDualStack[false]] = utils.GetNameWithSuffix(ipNameBase, utils.Suffixes[false])
 			annotation[consts.ServiceAnnotationPIPNameDualStack[true]] = utils.GetNameWithSuffix(ipNameBase, utils.Suffixes[true])
 		} else {
-			annotation[consts.ServiceAnnotationPIPNameDualStack[false]] = ipNameBase
+			annotation[consts.ServiceAnnotationPIPNameDualStack[false]] = utils.GetNameWithSuffix(ipNameBase, utils.Suffixes[tc.IPFamily == utils.IPv6])
 		}
 
 		service := utils.CreateLoadBalancerServiceManifest(testServiceName, annotation, labels, ns.Name, ports)
diff --git a/tests/e2e/network/network_security_group.go b/tests/e2e/network/network_security_group.go
index 5f0b0f55a..2a1946008 100644
--- a/tests/e2e/network/network_security_group.go
+++ b/tests/e2e/network/network_security_group.go
@@ -167,7 +167,8 @@ var _ = Describe("Network security group", Label(utils.TestSuiteLabelNSG), func(
 		Expect(err).NotTo(HaveOccurred())
 
 		ipList := append(ips1, ips2...)
-		Expect(validateSharedSecurityRuleExists(nsgs, ipList, port)).To(BeTrue(), "Security rule for service %s not exists", serviceName)
+		exist := validateSharedSecurityRuleExists(nsgs, ipList, port)
+		Expect(exist).To(BeTrue(), "Security rule for service %s not exists, ipList %q, port %q", serviceName, ipList, port)
 
 		By("Validate automatically adjust or delete the rule, when service is deleted")
 		Expect(utils.DeleteService(cs, ns.Name, serviceName)).NotTo(HaveOccurred())
@@ -308,11 +309,11 @@ var _ = Describe("Network security group", Label(utils.TestSuiteLabelNSG), func(
 		Expect(err).NotTo(HaveOccurred())
 		By("Checking if there are LoadBalancerSourceRanges rules")
 		found = validateLoadBalancerSourceRangesRuleExists(nsgs, internalIPs, allowCIDRs, ipRangesSuffixes)
-		Expect(found).To(BeTrue())
+		Expect(found).To(BeTrue(), "internalIPs %q allowCIDRs %q ipRangesSuffixes %q", internalIPs, allowCIDRs, ipRangesSuffixes)
 
 		By("Checking if there is a deny_all rule")
 		found = validateDenyAllSecurityRuleExists(nsgs, internalIPs)
-		Expect(found).To(BeTrue())
+		Expect(found).To(BeTrue(), "internalIPs %q", internalIPs)
 	})
 
 	It("should support service annotation `service.beta.kubernetes.io/azure-disable-load-balancer-floating-ip`", func() {
@@ -403,13 +404,32 @@ func validateUnsharedSecurityRuleExists(nsgs []aznetwork.SecurityGroup, ips []st
 }
 
 func validateSharedSecurityRuleExists(nsgs []aznetwork.SecurityGroup, ips []string, port string) bool {
+	ipsV4, ipsV6 := []string{}, []string{}
+	for _, ip := range ips {
+		if net.ParseIP(ip).To4() != nil {
+			ipsV4 = append(ipsV4, ip)
+		} else {
+			ipsV6 = append(ipsV6, ip)
+		}
+	}
+	v4Exist := validateSharedSecurityRuleExistsSingleStack(nsgs, ipsV4, port, false)
+	v6Exist := validateSharedSecurityRuleExistsSingleStack(nsgs, ipsV6, port, true)
+	return v4Exist && v6Exist
+}
+
+func validateSharedSecurityRuleExistsSingleStack(nsgs []aznetwork.SecurityGroup, ips []string, port string, isIPv6 bool) bool {
+	if len(ips) == 0 {
+		return true
+	}
+	utils.Logf("validateSharedSecurityRuleExists for isIPv6 %t", isIPv6)
 	for _, nsg := range nsgs {
 		if nsg.SecurityRules == nil {
 			continue
 		}
 		utils.Logf("Checking nsg %q", pointer.StringDeref(nsg.Name, ""))
 		for _, securityRule := range *nsg.SecurityRules {
-			utils.Logf("Checking security rule %q", pointer.StringDeref(securityRule.Name, ""))
+			utils.Logf("Checking security rule %q DestinationPortRange %q DestinationAddressPrefixes %q",
+				pointer.StringDeref(securityRule.Name, ""), pointer.StringDeref(securityRule.DestinationPortRange, ""), *securityRule.DestinationAddressPrefixes)
 			if strings.EqualFold(pointer.StringDeref(securityRule.DestinationPortRange, ""), port) {
 				found := true
 				for _, ip := range ips {
@@ -429,6 +449,25 @@ func validateSharedSecurityRuleExists(nsgs []aznetwork.SecurityGroup, ips []stri
 }
 
 func validateLoadBalancerSourceRangesRuleExists(nsgs []aznetwork.SecurityGroup, ips, sourceAddressPrefixes, ipRangesSuffixes []string) bool {
+	ipsV4, ipsV6 := []string{}, []string{}
+	for _, ip := range ips {
+		if net.ParseIP(ip).To4() != nil {
+			ipsV4 = append(ipsV4, ip)
+		} else {
+			ipsV6 = append(ipsV6, ip)
+		}
+	}
+	isDualStack := len(ipsV4) != 0 && len(ipsV6) != 0
+	v4Exist := validateLoadBalancerSourceRangesRuleExistsSingleStack(nsgs, ipsV4, sourceAddressPrefixes, ipRangesSuffixes, isDualStack, false)
+	v6Exist := validateLoadBalancerSourceRangesRuleExistsSingleStack(nsgs, ipsV6, sourceAddressPrefixes, ipRangesSuffixes, isDualStack, true)
+	return v4Exist && v6Exist
+}
+
+func validateLoadBalancerSourceRangesRuleExistsSingleStack(nsgs []aznetwork.SecurityGroup, ips, sourceAddressPrefixes, ipRangesSuffixes []string, isDualStack, isIPv6 bool) bool {
+	if len(ips) == 0 {
+		return true
+	}
+	utils.Logf("validateLoadBalancerSourceRangesRuleExists for isIPv6 %t", isIPv6)
 	if len(sourceAddressPrefixes) != len(ipRangesSuffixes) {
 		return false
 	}
@@ -441,13 +480,18 @@ func validateLoadBalancerSourceRangesRuleExists(nsgs []aznetwork.SecurityGroup,
 		for _, ip := range ips {
 			utils.Logf("Checking IP %q", ip)
 			for _, securityRule := range *nsg.SecurityRules {
-				utils.Logf("Checking security rule %q", pointer.StringDeref(securityRule.Name, ""))
+				utils.Logf("Checking security rule %q access %q DestinationAddressPrefix %q SourceAddressPrefix %q",
+					pointer.StringDeref(securityRule.Name, ""), securityRule.Access,
+					pointer.StringDeref(securityRule.DestinationAddressPrefix, ""), pointer.StringDeref(securityRule.SourceAddressPrefix, ""))
 				found := false
 				if securityRule.Access == aznetwork.SecurityRuleAccessAllow &&
 					strings.EqualFold(pointer.StringDeref(securityRule.DestinationAddressPrefix, ""), ip) {
 					for i := range sourceAddressPrefixes {
 						sourceAddressPrefix := sourceAddressPrefixes[i]
 						ipRangesSuffix := ipRangesSuffixes[i]
+						if isDualStack {
+							ipRangesSuffix = ipRangesSuffixes[i] + utils.Suffixes[isIPv6]
+						}
 						if strings.HasSuffix(pointer.StringDeref(securityRule.Name, ""), ipRangesSuffix) &&
 							strings.EqualFold(pointer.StringDeref(securityRule.SourceAddressPrefix, ""), sourceAddressPrefix) {
 							utils.Logf("Found")
@@ -471,6 +515,25 @@ func validateLoadBalancerSourceRangesRuleExists(nsgs []aznetwork.SecurityGroup,
 }
 
 func validateDenyAllSecurityRuleExists(nsgs []aznetwork.SecurityGroup, ips []string) bool {
+	ipsV4, ipsV6 := []string{}, []string{}
+	for _, ip := range ips {
+		if net.ParseIP(ip).To4() != nil {
+			ipsV4 = append(ipsV4, ip)
+		} else {
+			ipsV6 = append(ipsV6, ip)
+		}
+	}
+	isDualStack := len(ipsV4) != 0 && len(ipsV6) != 0
+	v4Exist := validateDenyAllSecurityRuleExistsSingleStack(nsgs, ipsV4, isDualStack, false)
+	v6Exist := validateDenyAllSecurityRuleExistsSingleStack(nsgs, ipsV6, isDualStack, true)
+	return v4Exist && v6Exist
+}
+
+func validateDenyAllSecurityRuleExistsSingleStack(nsgs []aznetwork.SecurityGroup, ips []string, isDualStack, isIPv6 bool) bool {
+	if len(ips) == 0 {
+		return true
+	}
+	utils.Logf("validateDenyAllSecurityRuleExists for isIPv6 %t", isIPv6)
 	for _, nsg := range nsgs {
 		if nsg.SecurityRules == nil {
 			continue
@@ -480,10 +543,15 @@ func validateDenyAllSecurityRuleExists(nsgs []aznetwork.SecurityGroup, ips []str
 		for _, ip := range ips {
 			utils.Logf("Checking IP %q", ip)
 			for _, securityRule := range *nsg.SecurityRules {
-				utils.Logf("Checking security rule %q", pointer.StringDeref(securityRule.Name, ""))
+				utils.Logf("Checking security rule %q DestinationAddressPrefix %q SourceAddressPrefix %q",
+					pointer.StringDeref(securityRule.Name, ""), pointer.StringDeref(securityRule.DestinationAddressPrefix, ""), pointer.StringDeref(securityRule.SourceAddressPrefix, ""))
+				denyAllSuffix := "deny_all"
+				if isDualStack {
+					denyAllSuffix = "deny_all" + utils.Suffixes[isIPv6]
+				}
 				if securityRule.Access == aznetwork.SecurityRuleAccessDeny &&
 					strings.EqualFold(pointer.StringDeref(securityRule.DestinationAddressPrefix, ""), ip) &&
-					strings.HasSuffix(pointer.StringDeref(securityRule.Name, ""), "deny_all") &&
+					strings.HasSuffix(pointer.StringDeref(securityRule.Name, ""), denyAllSuffix) &&
 					strings.EqualFold(pointer.StringDeref(securityRule.SourceAddressPrefix, ""), "*") {
 					utils.Logf("Found")
 					count++
diff --git a/tests/e2e/network/node.go b/tests/e2e/network/node.go
index 72c11a8e4..19d8e2a8e 100644
--- a/tests/e2e/network/node.go
+++ b/tests/e2e/network/node.go
@@ -32,6 +32,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/intstr"
 	clientset "k8s.io/client-go/kubernetes"
+	"k8s.io/utils/pointer"
 
 	"sigs.k8s.io/cloud-provider-azure/tests/e2e/utils"
 )
@@ -197,7 +198,7 @@ var _ = Describe("Azure node resources", Label(utils.TestSuiteLabelNode), func()
 
 		utils.Logf("getting all NICs of VMSS VMs")
 		for _, vmssVM := range vmssVMs {
-			utils.Logf("Checking %d VMSS VM %q", vmssVM.Name)
+			utils.Logf("Checking VMSS VM %q", pointer.StringDeref(vmssVM.Name, ""))
 			nodeName, err := utils.GetVMSSVMComputerName(vmssVM)
 			Expect(err).NotTo(HaveOccurred())
 
diff --git a/tests/e2e/network/service_annotations.go b/tests/e2e/network/service_annotations.go
index f0a3242aa..45c8f45ee 100644
--- a/tests/e2e/network/service_annotations.go
+++ b/tests/e2e/network/service_annotations.go
@@ -610,14 +610,9 @@ var _ = Describe("Service with annotation", Label(utils.TestSuiteLabelServiceAnn
 
 		By("Creating a service referring to the first pip")
 		annotation := map[string]string{}
-		// TODO: dual-stack
-		if utils.DualstackSupported {
-			if v4Enabled {
-				annotation[consts.ServiceAnnotationPIPNameDualStack[false]] = pipNames1[false]
-			}
-			if v6Enabled {
-				annotation[consts.ServiceAnnotationPIPNameDualStack[true]] = pipNames1[true]
-			}
+		if tc.IPFamily == utils.DualStack {
+			annotation[consts.ServiceAnnotationPIPNameDualStack[false]] = pipNames1[false]
+			annotation[consts.ServiceAnnotationPIPNameDualStack[true]] = pipNames1[true]
 		} else {
 			annotation[consts.ServiceAnnotationPIPNameDualStack[false]] = pipNames1[tc.IPFamily == utils.IPv6]
 		}
@@ -638,13 +633,9 @@ var _ = Describe("Service with annotation", Label(utils.TestSuiteLabelServiceAnn
 		By("Updating the service to refer to the second service")
 		service, err = cs.CoreV1().Services(ns.Name).Get(context.TODO(), serviceName, metav1.GetOptions{})
 		Expect(err).NotTo(HaveOccurred())
-		if utils.DualstackSupported {
-			if v4Enabled {
-				service.Annotations[consts.ServiceAnnotationPIPNameDualStack[false]] = pipNames2[false]
-			}
-			if v6Enabled {
-				service.Annotations[consts.ServiceAnnotationPIPNameDualStack[true]] = pipNames2[true]
-			}
+		if tc.IPFamily == utils.DualStack {
+			service.Annotations[consts.ServiceAnnotationPIPNameDualStack[false]] = pipNames2[false]
+			service.Annotations[consts.ServiceAnnotationPIPNameDualStack[true]] = pipNames2[true]
 		} else {
 			service.Annotations[consts.ServiceAnnotationPIPNameDualStack[false]] = pipNames2[tc.IPFamily == utils.IPv6]
 		}
@@ -707,8 +698,7 @@ var _ = Describe("Service with annotation", Label(utils.TestSuiteLabelServiceAnn
 		{
 			annotation := map[string]string{}
 			for isIPv6, id := range prefixIDs1 {
-				// TODO: Update after dual-stack implementation finishes
-				if utils.DualstackSupported {
+				if tc.IPFamily == utils.DualStack {
 					annotation[consts.ServiceAnnotationPIPPrefixIDDualStack[isIPv6]] = id
 				} else {
 					annotation[consts.ServiceAnnotationPIPPrefixIDDualStack[false]] = id
@@ -745,7 +735,7 @@ var _ = Describe("Service with annotation", Label(utils.TestSuiteLabelServiceAnn
 			Expect(err).NotTo(HaveOccurred())
 			for isIPv6, id := range prefixIDs2 {
 				// TODO: Update after dual-stack implementation finishes
-				if utils.DualstackSupported {
+				if tc.IPFamily == utils.DualStack {
 					service.Annotations[consts.ServiceAnnotationPIPPrefixIDDualStack[isIPv6]] = id
 				} else {
 					service.Annotations[consts.ServiceAnnotationPIPPrefixIDDualStack[false]] = id
@@ -893,6 +883,7 @@ var _ = Describe("Service with annotation", Label(utils.TestSuiteLabelServiceAnn
 
 		var lb *network.LoadBalancer
 		var targetProbes []*network.Probe
+		// There should be no other Services besides the one in this test or the check below will fail.
 		expectedTargetProbesCount := 1
 		if tc.IPFamily == utils.DualStack {
 			expectedTargetProbesCount = 2
diff --git a/tests/e2e/network/standard_lb.go b/tests/e2e/network/standard_lb.go
index 6edc6e24a..61fae658d 100644
--- a/tests/e2e/network/standard_lb.go
+++ b/tests/e2e/network/standard_lb.go
@@ -109,11 +109,18 @@ var _ = Describe("[StandardLoadBalancer] Standard load balancer", func() {
 		isVMSS := len(vmsses) != 0
 
 		ipcIDs := []string{}
-		for _, backendAddressPool := range *lb.BackendAddressPools {
-			if os.Getenv(utils.AKSTestCCM) != "" && *backendAddressPool.Name == "aksOutboundBackendPool" {
+		for i := range *lb.BackendAddressPools {
+			backendAddressPool := (*lb.BackendAddressPools)[i]
+			if (os.Getenv(utils.AKSTestCCM) != "" && *backendAddressPool.Name == "aksOutboundBackendPool") ||
+				strings.Contains(*backendAddressPool.Name, "outboundBackendPool") {
 				continue
 			}
-			for _, ipc := range *backendAddressPool.BackendIPConfigurations {
+			if backendAddressPool.BackendIPConfigurations == nil {
+				utils.Logf("BackendIPConfigurations is nil for backendAddressPool %q", *backendAddressPool.Name)
+				continue
+			}
+			for j := range *backendAddressPool.BackendIPConfigurations {
+				ipc := (*backendAddressPool.BackendIPConfigurations)[j]
 				if ipc.ID != nil {
 					if utils.IsAutoscalingAKSCluster() && strings.Contains(*ipc.ID, utils.SystemPool) {
 						continue
@@ -141,7 +148,11 @@ var _ = Describe("[StandardLoadBalancer] Standard load balancer", func() {
 				Expect(err).NotTo(HaveOccurred())
 				allVMs = append(allVMs, vms...)
 			}
-			Expect(len(allVMs)).To(Equal(len(ipcIDs)))
+			if tc.IPFamily == utils.DualStack {
+				Expect(len(allVMs) * 2).To(Equal(len(ipcIDs)))
+			} else {
+				Expect(len(allVMs)).To(Equal(len(ipcIDs)))
+			}
 			for _, vm := range allVMs {
 				utils.Logf("Checking VM %q", *vm.ID)
 				found := false
diff --git a/tests/e2e/utils/network_utils.go b/tests/e2e/utils/network_utils.go
index 106541c0a..d7a66dfde 100644
--- a/tests/e2e/utils/network_utils.go
+++ b/tests/e2e/utils/network_utils.go
@@ -46,9 +46,6 @@ var (
 		false: "",
 		true:  "-IPv6",
 	}
-
-	// TODO: After dual-stack implementation finished, update here.
-	DualstackSupported = false
 )
 
 // getVirtualNetworkList returns the list of virtual networks in the cluster resource group.
@@ -660,6 +657,7 @@ func retrieveCIDRs(cmd string, reg string) ([]string, error) {
 func GetClusterServiceIPFamily() (IPFamily, error) {
 	// Only test IPv4 in AKS pipeline
 	if os.Getenv(AKSTestCCM) != "" {
+		Logf("Cluster IP family for AKS pipeline: IPv4")
 		return IPv4, nil
 	}
 
@@ -667,13 +665,15 @@ func GetClusterServiceIPFamily() (IPFamily, error) {
 	if err != nil {
 		return "", err
 	}
+	ipFamily := DualStack
 	if svcCIDRs[0] != "" && svcCIDRs[1] == "" {
-		return IPv4, nil
+		ipFamily = IPv4
 	}
 	if svcCIDRs[0] == "" && svcCIDRs[1] != "" {
-		return IPv6, nil
+		ipFamily = IPv6
 	}
-	return DualStack, nil
+	Logf("Cluster IP family: %s", ipFamily)
+	return ipFamily, nil
 }
 
 func IfIPFamiliesEnabled(ipFamily IPFamily) (v4Enabled bool, v6Enabled bool) {
@@ -687,10 +687,6 @@ func IfIPFamiliesEnabled(ipFamily IPFamily) (v4Enabled bool, v6Enabled bool) {
 }
 
 // GetNameWithSuffix returns resource name with IP family suffix.
-// After dual-stack implementation is finished, this function returns name + suffix for all IP families.
 func GetNameWithSuffix(name, suffix string) string {
-	if DualstackSupported {
-		return name + suffix
-	}
-	return name
+	return name + suffix
 }
diff --git a/tests/e2e/utils/service_utils.go b/tests/e2e/utils/service_utils.go
index a58bd3b29..25a8b4d83 100644
--- a/tests/e2e/utils/service_utils.go
+++ b/tests/e2e/utils/service_utils.go
@@ -50,14 +50,21 @@ func DeleteService(cs clientset.Interface, ns string, serviceName string) error
 			Logf("Service %q does not exist, no need to delete", serviceName)
 			return nil
 		}
+		output, _ := RunKubectl(ns, "describe", "service", serviceName)
+		Logf("Service describe info: %s", output)
 		return err
 	}
-	return wait.PollImmediate(poll, deletionTimeout, func() (bool, error) {
+	err := wait.PollImmediate(poll, deletionTimeout, func() (bool, error) {
 		if _, err := cs.CoreV1().Services(ns).Get(context.TODO(), serviceName, metav1.GetOptions{}); err != nil {
 			return apierrs.IsNotFound(err), nil
 		}
 		return false, nil
 	})
+	if err != nil {
+		output, _ := RunKubectl(ns, "describe", "service", serviceName)
+		Logf("Service describe info: %s", output)
+	}
+	return err
 }
 
 // GetServiceDomainName cat prefix and azure suffix