Repository: kubernetes-sigs/cluster-api-provider-azure
Pull request #1466 (2021-06-23 21:44:23)

Make control plane outbound lb configurable
 <!-- If this is your first PR, welcome! Please make sure you read the [contributing guidelines](../CONTRIBUTING.md). -->

 <!-- Please label this pull request according to what type of issue you are addressing (see ../CONTRIBUTING.md) -->
**What type of PR is this?**
/kind feature
<!--
Add one of the following kinds:
/kind feature
/kind bug
/kind api-change
/kind cleanup
/kind deprecation
/kind design
/kind documentation
/kind failing-test
/kind flake
/kind other
-->

**What this PR does / why we need it**:

- Adds a field `ControlPlaneOutboundLB` in `AzureCluster` to configure control plane outbound load balancer. This is useful for private clusters when we don't want a public control plane lb created by default
- Works very similar to `NodeOutboundLB`.

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes #
Addresses https://github.com/kubernetes-sigs/cluster-api-provider-azure/issues/1159

**Special notes for your reviewer**:


**TODOs**:
<!-- Put an "X" character inside the brackets of each completed task. Some may be optional depending on the PR. -->

- [x] squashed commits
- [x] includes documentation
- [x] adds unit tests

**Release note**:
<!--  Write your release note:
1. Enter your extended release note in the below block. If the PR requires additional action from users switching to the new release, include the string "action required".
2. If no release note is required, just write "NONE".
-->
```release-note
Make control plane outbound lb configurable
```


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

This pull-request has been approved by:
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **justinsb** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @justinsb` in a comment when ready.

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

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

- **[OWNERS](https://github.com/kubernetes-sigs/cluster-api-provider-azure/blob/master/OWNERS)**

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-e2e | 0aa39ce77387516334e66cc57f59cad8d0158b13 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-e2e/1407816839618105344) | `/test pull-cluster-api-provider-azure-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1466). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ashysank). 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/cluster-api-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 -->
CecileRobertMichon: @fiunchinho what's the impact of this PR on #894 if any?
k8s-ci-robot: @shysank: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-e2e | 30a5433c90188ef0692f5c2a23d8de239e922b1c | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-e2e/1407847578623545344) | `/test pull-cluster-api-provider-azure-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1466). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ashysank). 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/cluster-api-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: @shysank: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-e2e | 30a5433c90188ef0692f5c2a23d8de239e922b1c | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-e2e/1407847578623545344) | `/test pull-cluster-api-provider-azure-e2e`
pull-cluster-api-provider-azure-verify | 3841af4952df6ab5cc0b9274a042df2d103afa2e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-verify/1408091835460489217) | `/test pull-cluster-api-provider-azure-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1466). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ashysank). 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/cluster-api-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: @shysank: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-verify | 3841af4952df6ab5cc0b9274a042df2d103afa2e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-verify/1408091835460489217) | `/test pull-cluster-api-provider-azure-verify`
pull-cluster-api-provider-azure-e2e | 3841af4952df6ab5cc0b9274a042df2d103afa2e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-e2e/1408091835422740480) | `/test pull-cluster-api-provider-azure-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1466). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ashysank). 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/cluster-api-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: @shysank: 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: @shysank: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-verify | 3841af4952df6ab5cc0b9274a042df2d103afa2e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-verify/1408091835460489217) | `/test pull-cluster-api-provider-azure-verify`
pull-cluster-api-provider-azure-e2e | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-e2e/1408109839158087680) | `/test pull-cluster-api-provider-azure-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1466). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ashysank). 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/cluster-api-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: @shysank: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-e2e | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-e2e/1408109839158087680) | `/test pull-cluster-api-provider-azure-e2e`
pull-cluster-api-provider-azure-verify | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-verify/1408109839225196544) | `/test pull-cluster-api-provider-azure-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1466). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ashysank). 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/cluster-api-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: @shysank: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-e2e | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-e2e/1408109839158087680) | `/test pull-cluster-api-provider-azure-e2e`
pull-cluster-api-provider-azure-verify | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-verify/1408109839225196544) | `/test pull-cluster-api-provider-azure-verify`
pull-cluster-api-provider-azure-e2e-windows | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-e2e-windows/1408109839195836416) | `/test pull-cluster-api-provider-azure-e2e-windows`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1466). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ashysank). 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/cluster-api-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: @shysank: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-e2e | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-e2e/1408109839158087680) | `/test pull-cluster-api-provider-azure-e2e`
pull-cluster-api-provider-azure-verify | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-verify/1408109839225196544) | `/test pull-cluster-api-provider-azure-verify`
pull-cluster-api-provider-azure-e2e-windows | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-e2e-windows/1408109839195836416) | `/test pull-cluster-api-provider-azure-e2e-windows`
pull-cluster-api-provider-azure-test | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-test/1408109839103561728) | `/test pull-cluster-api-provider-azure-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1466). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ashysank). 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/cluster-api-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: @shysank: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-e2e | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-e2e/1408109839158087680) | `/test pull-cluster-api-provider-azure-e2e`
pull-cluster-api-provider-azure-verify | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-verify/1408109839225196544) | `/test pull-cluster-api-provider-azure-verify`
pull-cluster-api-provider-azure-e2e-windows | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-e2e-windows/1408109839195836416) | `/test pull-cluster-api-provider-azure-e2e-windows`
pull-cluster-api-provider-azure-test | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-test/1408109839103561728) | `/test pull-cluster-api-provider-azure-test`
pull-cluster-api-provider-azure-apidiff | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-apidiff/1408109839250362368) | `/test pull-cluster-api-provider-azure-apidiff`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1466). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ashysank). 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/cluster-api-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: @shysank: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-e2e | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-e2e/1408109839158087680) | `/test pull-cluster-api-provider-azure-e2e`
pull-cluster-api-provider-azure-verify | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-verify/1408109839225196544) | `/test pull-cluster-api-provider-azure-verify`
pull-cluster-api-provider-azure-e2e-windows | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-e2e-windows/1408109839195836416) | `/test pull-cluster-api-provider-azure-e2e-windows`
pull-cluster-api-provider-azure-test | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-test/1408109839103561728) | `/test pull-cluster-api-provider-azure-test`
pull-cluster-api-provider-azure-apidiff | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-apidiff/1408109839250362368) | `/test pull-cluster-api-provider-azure-apidiff`
pull-cluster-api-provider-azure-build | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-build/1408109839124533248) | `/test pull-cluster-api-provider-azure-build`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1466). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ashysank). 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/cluster-api-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: @shysank: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-e2e | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-e2e/1408109839158087680) | `/test pull-cluster-api-provider-azure-e2e`
pull-cluster-api-provider-azure-verify | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-verify/1408109839225196544) | `/test pull-cluster-api-provider-azure-verify`
pull-cluster-api-provider-azure-e2e-windows | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-e2e-windows/1408109839195836416) | `/test pull-cluster-api-provider-azure-e2e-windows`
pull-cluster-api-provider-azure-test | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-test/1408109839103561728) | `/test pull-cluster-api-provider-azure-test`
pull-cluster-api-provider-azure-apidiff | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-apidiff/1408109839250362368) | `/test pull-cluster-api-provider-azure-apidiff`
pull-cluster-api-provider-azure-build | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-build/1408109839124533248) | `/test pull-cluster-api-provider-azure-build`
pull-cluster-api-provider-azure-coverage | e28da11f5112ba6277731a3fa74125f2c7572811 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-coverage/1408109839275528192) | `/test pull-cluster-api-provider-azure-coverage`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1466). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ashysank). 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/cluster-api-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: @shysank: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-e2e | 82efc1970decede8f4d8499099fa82a1830c346c | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-e2e/1408110269346877440) | `/test pull-cluster-api-provider-azure-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1466). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ashysank). 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/cluster-api-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 -->
shysank: /test pull-cluster-api-provider-azure-e2e

k8s-ci-robot: @shysank: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-e2e | 82efc1970decede8f4d8499099fa82a1830c346c | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1466/pull-cluster-api-provider-azure-e2e/1408128727400845312) | `/test pull-cluster-api-provider-azure-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1466). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ashysank). 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/cluster-api-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 -->
fiunchinho: > @fiunchinho what's the impact of this PR on #894 if any?

I don't think it has any impact, as we only reconcile nat gateways for worker nodes (we don't for the control plane).
CecileRobertMichon: @shysank PR needs rebase

is this enough to close #1159 ?
k8s-ci-robot: @shysank: 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: @shysank: 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>

Patch:
diff --git a/api/v1alpha3/azurecluster_conversion.go b/api/v1alpha3/azurecluster_conversion.go
index 88eac918b..555c47595 100644
--- a/api/v1alpha3/azurecluster_conversion.go
+++ b/api/v1alpha3/azurecluster_conversion.go
@@ -54,6 +54,7 @@ func (src *AzureCluster) ConvertTo(dstRaw conversion.Hub) error { // nolint
 	dst.Spec.NetworkSpec.APIServerLB.FrontendIPsCount = restored.Spec.NetworkSpec.APIServerLB.FrontendIPsCount
 	dst.Spec.NetworkSpec.APIServerLB.IdleTimeoutInMinutes = restored.Spec.NetworkSpec.APIServerLB.IdleTimeoutInMinutes
 	dst.Spec.NetworkSpec.NodeOutboundLB = restored.Spec.NetworkSpec.NodeOutboundLB
+	dst.Spec.NetworkSpec.ControlPlaneOutboundLB = restored.Spec.NetworkSpec.ControlPlaneOutboundLB
 	dst.Spec.CloudProviderConfigOverrides = restored.Spec.CloudProviderConfigOverrides
 	dst.Spec.BastionSpec = restored.Spec.BastionSpec
 
diff --git a/api/v1alpha3/zz_generated.conversion.go b/api/v1alpha3/zz_generated.conversion.go
index 5618156a4..73bd5a44d 100644
--- a/api/v1alpha3/zz_generated.conversion.go
+++ b/api/v1alpha3/zz_generated.conversion.go
@@ -1325,6 +1325,7 @@ func autoConvert_v1alpha4_NetworkSpec_To_v1alpha3_NetworkSpec(in *v1alpha4.Netwo
 		return err
 	}
 	// WARNING: in.NodeOutboundLB requires manual conversion: does not exist in peer-type
+	// WARNING: in.ControlPlaneOutboundLB requires manual conversion: does not exist in peer-type
 	// WARNING: in.PrivateDNSZoneName requires manual conversion: does not exist in peer-type
 	return nil
 }
diff --git a/api/v1alpha4/azurecluster_default.go b/api/v1alpha4/azurecluster_default.go
index fef73eb7c..8969e80e4 100644
--- a/api/v1alpha4/azurecluster_default.go
+++ b/api/v1alpha4/azurecluster_default.go
@@ -53,6 +53,7 @@ func (c *AzureCluster) setNetworkSpecDefaults() {
 	c.setSubnetDefaults()
 	c.setAPIServerLBDefaults()
 	c.setNodeOutboundLBDefaults()
+	c.setControlPlaneOutboundLBDefaults()
 }
 
 func (c *AzureCluster) setResourceGroupDefault() {
@@ -196,6 +197,42 @@ func (c *AzureCluster) setNodeOutboundLBDefaults() {
 		lb.FrontendIPsCount = pointer.Int32Ptr(1)
 	}
 
+	c.setOutboundLBFrontendIPs(lb, generateNodeOutboundIPName)
+}
+
+func (c *AzureCluster) setControlPlaneOutboundLBDefaults() {
+	// public clusters don't need control plane outbound lb
+	if c.Spec.NetworkSpec.APIServerLB.Type == Public {
+		return
+	}
+
+	// private clusters can disable control plane outbound lb by setting it to nil.
+	if c.Spec.NetworkSpec.ControlPlaneOutboundLB == nil {
+		return
+	}
+
+	lb := c.Spec.NetworkSpec.ControlPlaneOutboundLB
+	lb.Type = Public
+	lb.SKU = SKUStandard
+
+	if lb.Name == "" {
+		lb.Name = generateControlPlaneOutboundLBName(c.ObjectMeta.Name)
+	}
+
+	if lb.IdleTimeoutInMinutes == nil {
+		lb.IdleTimeoutInMinutes = pointer.Int32Ptr(DefaultOutboundRuleIdleTimeoutInMinutes)
+	}
+
+	if lb.FrontendIPsCount == nil {
+		lb.FrontendIPsCount = pointer.Int32Ptr(1)
+	}
+
+	c.setOutboundLBFrontendIPs(lb, generateControlPlaneOutboundIPName)
+}
+
+// setOutboundLBFrontendIPs sets the frontend ips for the given load balancer.
+// The name of the frontend ip is generated using generatePublicIPName function.
+func (c *AzureCluster) setOutboundLBFrontendIPs(lb *LoadBalancerSpec, generatePublicIPName func(string) string) {
 	switch *lb.FrontendIPsCount {
 	case 0:
 		lb.FrontendIPs = []FrontendIP{}
@@ -204,7 +241,7 @@ func (c *AzureCluster) setNodeOutboundLBDefaults() {
 			{
 				Name: generateFrontendIPConfigName(lb.Name),
 				PublicIP: &PublicIPSpec{
-					Name: generateNodeOutboundIPName(c.ObjectMeta.Name),
+					Name: generatePublicIPName(c.ObjectMeta.Name),
 				},
 			},
 		}
@@ -214,7 +251,7 @@ func (c *AzureCluster) setNodeOutboundLBDefaults() {
 			lb.FrontendIPs[i] = FrontendIP{
 				Name: withIndex(generateFrontendIPConfigName(lb.Name), i+1),
 				PublicIP: &PublicIPSpec{
-					Name: withIndex(generateNodeOutboundIPName(c.ObjectMeta.Name), i+1),
+					Name: withIndex(generatePublicIPName(c.ObjectMeta.Name), i+1),
 				},
 			}
 		}
@@ -294,6 +331,11 @@ func generatePublicLBName(clusterName string) string {
 	return fmt.Sprintf("%s-%s", clusterName, "public-lb")
 }
 
+// generateControlPlaneOutboundLBName generates the name of the control plane outbound LB.
+func generateControlPlaneOutboundLBName(clusterName string) string {
+	return fmt.Sprintf("%s-outbound-lb", clusterName)
+}
+
 // generatePublicIPName generates a public IP name, based on the cluster name and a hash.
 func generatePublicIPName(clusterName string) string {
 	return fmt.Sprintf("pip-%s-apiserver", clusterName)
@@ -309,6 +351,11 @@ func generateNodeOutboundIPName(clusterName string) string {
 	return fmt.Sprintf("pip-%s-node-outbound", clusterName)
 }
 
+// generateControlPlaneOutboundIPName generates a public IP name, based on the cluster name.
+func generateControlPlaneOutboundIPName(clusterName string) string {
+	return fmt.Sprintf("pip-%s-controlplane-outbound", clusterName)
+}
+
 // withIndex appends the index as suffix to a generated name.
 func withIndex(name string, n int) string {
 	return fmt.Sprintf("%s-%d", name, n)
diff --git a/api/v1alpha4/azurecluster_validation.go b/api/v1alpha4/azurecluster_validation.go
index 697feac48..3fc2cbf2e 100644
--- a/api/v1alpha4/azurecluster_validation.go
+++ b/api/v1alpha4/azurecluster_validation.go
@@ -137,6 +137,8 @@ func validateNetworkSpec(networkSpec NetworkSpec, old NetworkSpec, fldPath *fiel
 
 	allErrs = append(allErrs, validateNodeOutboundLB(networkSpec.NodeOutboundLB, old.NodeOutboundLB, networkSpec.APIServerLB, nodeSubnet, fldPath.Child("nodeOutboundLB"))...)
 
+	allErrs = append(allErrs, validateControlPlaneOutboundLB(networkSpec.ControlPlaneOutboundLB, networkSpec.APIServerLB, fldPath.Child("controlPlaneOutboundLB"))...)
+
 	allErrs = append(allErrs, validatePrivateDNSZoneName(networkSpec, fldPath)...)
 
 	if len(allErrs) == 0 {
@@ -420,6 +422,34 @@ func validateNodeOutboundLB(lb *LoadBalancerSpec, old *LoadBalancerSpec, apiserv
 	return allErrs
 }
 
+func validateControlPlaneOutboundLB(lb *LoadBalancerSpec, apiserverLB LoadBalancerSpec, fldPath *field.Path) field.ErrorList {
+	var allErrs field.ErrorList
+
+	switch apiserverLB.Type {
+	case Public:
+		if lb != nil {
+			allErrs = append(allErrs, field.Forbidden(fldPath, "Control plane outbound load balancer cannot be set for public clusters."))
+		}
+	case Internal:
+		// Control plane outbound lb can be nil when it's disabled for private clusters.
+		if lb == nil {
+			return nil
+		}
+
+		if lb.FrontendIPsCount != nil && *lb.FrontendIPsCount > MaxLoadBalancerOutboundIPs {
+			allErrs = append(allErrs, field.Invalid(fldPath.Child("frontendIPsCount"), *lb.FrontendIPsCount,
+				fmt.Sprintf("Max front end ips allowed is %d", MaxLoadBalancerOutboundIPs)))
+		}
+
+		if lb.IdleTimeoutInMinutes != nil && (*lb.IdleTimeoutInMinutes < MinLBIdleTimeoutInMinutes || *lb.IdleTimeoutInMinutes > MaxLBIdleTimeoutInMinutes) {
+			allErrs = append(allErrs, field.Invalid(fldPath.Child("idleTimeoutInMinutes"), *lb.IdleTimeoutInMinutes,
+				fmt.Sprintf("Control plane outbound idle timeout should be between %d and %d minutes", MinLBIdleTimeoutInMinutes, MaxLoadBalancerOutboundIPs)))
+		}
+	}
+
+	return allErrs
+}
+
 // validatePrivateDNSZoneName validate the PrivateDNSZoneName.
 func validatePrivateDNSZoneName(networkSpec NetworkSpec, fldPath *field.Path) field.ErrorList {
 	var allErrs field.ErrorList
diff --git a/api/v1alpha4/azurecluster_webhook.go b/api/v1alpha4/azurecluster_webhook.go
index b8e088e51..586028e8a 100644
--- a/api/v1alpha4/azurecluster_webhook.go
+++ b/api/v1alpha4/azurecluster_webhook.go
@@ -106,6 +106,13 @@ func (c *AzureCluster) ValidateUpdate(oldRaw runtime.Object) error {
 		)
 	}
 
+	if !reflect.DeepEqual(c.Spec.NetworkSpec.ControlPlaneOutboundLB, old.Spec.NetworkSpec.ControlPlaneOutboundLB) {
+		allErrs = append(allErrs,
+			field.Invalid(field.NewPath("spec", "networkSpec", "controlPlaneOutboundLB"),
+				c.Spec.NetworkSpec.ControlPlaneOutboundLB, "field is immutable"),
+		)
+	}
+
 	if len(allErrs) == 0 {
 		return c.validateCluster(old)
 	}
diff --git a/api/v1alpha4/types.go b/api/v1alpha4/types.go
index e758258a2..340669414 100644
--- a/api/v1alpha4/types.go
+++ b/api/v1alpha4/types.go
@@ -64,6 +64,11 @@ type NetworkSpec struct {
 	// +optional
 	NodeOutboundLB *LoadBalancerSpec `json:"nodeOutboundLB,omitempty"`
 
+	// ControlPlaneOutboundLB is the configuration for the control-plane outbound load balancer.
+	// This is different from APIServerLB, and is used only in private clusters (optionally) for enabling outbound traffic.
+	// +optional
+	ControlPlaneOutboundLB *LoadBalancerSpec `json:"controlPlaneOutboundLB,omitempty"`
+
 	// PrivateDNSZoneName defines the zone name for the Azure Private DNS.
 	// +optional
 	PrivateDNSZoneName string `json:"privateDNSZoneName,omitempty"`
diff --git a/api/v1alpha4/zz_generated.deepcopy.go b/api/v1alpha4/zz_generated.deepcopy.go
index 89c36db1b..d7899ada4 100644
--- a/api/v1alpha4/zz_generated.deepcopy.go
+++ b/api/v1alpha4/zz_generated.deepcopy.go
@@ -916,6 +916,11 @@ func (in *NetworkSpec) DeepCopyInto(out *NetworkSpec) {
 		*out = new(LoadBalancerSpec)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.ControlPlaneOutboundLB != nil {
+		in, out := &in.ControlPlaneOutboundLB, &out.ControlPlaneOutboundLB
+		*out = new(LoadBalancerSpec)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkSpec.
diff --git a/azure/scope/cluster.go b/azure/scope/cluster.go
index d4c541763..7589fab10 100644
--- a/azure/scope/cluster.go
+++ b/azure/scope/cluster.go
@@ -118,39 +118,26 @@ func (s *ClusterScope) PublicIPSpecs() []azure.PublicIPSpec {
 	var publicIPSpecs []azure.PublicIPSpec
 
 	// Public IP specs for control plane lb
-	var controlPlaneOutboundIP azure.PublicIPSpec
+	var controlPlaneOutboundIPSpecs []azure.PublicIPSpec
 	if s.IsAPIServerPrivate() {
-		controlPlaneOutboundIP = azure.PublicIPSpec{
-			Name: azure.GenerateControlPlaneOutboundIPName(s.ClusterName()),
+		// Public IP specs for control plane outbound lb
+		if s.ControlPlaneOutboundLB() != nil {
+			controlPlaneOutboundIPSpecs = s.getOutboundLBPublicIPSpecs(s.ControlPlaneOutboundLB(), azure.GenerateControlPlaneOutboundIPName)
 		}
 	} else {
-		controlPlaneOutboundIP = azure.PublicIPSpec{
+		controlPlaneOutboundIPSpecs = []azure.PublicIPSpec{{
 			Name:    s.APIServerPublicIP().Name,
 			DNSName: s.APIServerPublicIP().DNSName,
 			IsIPv6:  false, // currently azure requires a ipv4 lb rule to enable ipv6
-		}
+		}}
 	}
-	publicIPSpecs = append(publicIPSpecs, controlPlaneOutboundIP)
+	publicIPSpecs = append(publicIPSpecs, controlPlaneOutboundIPSpecs...)
 
 	// Public IP specs for node outbound lb
-	var nodeOutboundIPSpecs []azure.PublicIPSpec
 	if s.NodeOutboundLB() != nil {
-		loadBalancerNodeOutboundIPs := s.NodeOutboundLB().FrontendIPsCount
-		if loadBalancerNodeOutboundIPs == nil || *loadBalancerNodeOutboundIPs == 0 {
-			// do nothing
-		} else if *loadBalancerNodeOutboundIPs == 1 {
-			nodeOutboundIPSpecs = append(nodeOutboundIPSpecs, azure.PublicIPSpec{
-				Name: azure.GenerateNodeOutboundIPName(s.ClusterName()),
-			})
-		} else {
-			for i := 0; i < int(*loadBalancerNodeOutboundIPs); i++ {
-				publicIPSpecs = append(publicIPSpecs, azure.PublicIPSpec{
-					Name: azure.WithIndex(azure.GenerateNodeOutboundIPName(s.ClusterName()), i+1),
-				})
-			}
-		}
+		nodeOutboundIPSpecs := s.getOutboundLBPublicIPSpecs(s.NodeOutboundLB(), azure.GenerateNodeOutboundIPName)
+		publicIPSpecs = append(publicIPSpecs, nodeOutboundIPSpecs...)
 	}
-	publicIPSpecs = append(publicIPSpecs, nodeOutboundIPSpecs...)
 
 	// Public IP specs for node nat gateway
 	var nodeNatGatewayIPSpecs []azure.PublicIPSpec
@@ -179,7 +166,7 @@ func (s *ClusterScope) PublicIPSpecs() []azure.PublicIPSpec {
 func (s *ClusterScope) LBSpecs() []azure.LBSpec {
 	specs := []azure.LBSpec{
 		{
-			// Control Plane LB
+			// API Server LB
 			Name:                 s.APIServerLB().Name,
 			SubnetName:           s.ControlPlaneSubnet().Name,
 			FrontendIPConfigs:    s.APIServerLB().FrontendIPs,
@@ -192,7 +179,7 @@ func (s *ClusterScope) LBSpecs() []azure.LBSpec {
 		},
 	}
 
-	// Public Node outbound LB
+	// Node outbound LB
 	if s.NodeOutboundLB() != nil {
 		specs = append(specs, azure.LBSpec{
 			Name:                 s.NodeOutboundLBName(),
@@ -205,30 +192,19 @@ func (s *ClusterScope) LBSpecs() []azure.LBSpec {
 		})
 	}
 
-	// When cluster is not private it already has a public LB attached for the API server so it doesn't need an
-	// additional LB to get outbound traffic on control plane nodes.
-	if !s.IsAPIServerPrivate() {
-		return specs
+	// Control Plane Outbound LB
+	if s.ControlPlaneOutboundLB() != nil {
+		specs = append(specs, azure.LBSpec{
+			Name:                 s.ControlPlaneOutboundLB().Name,
+			FrontendIPConfigs:    s.ControlPlaneOutboundLB().FrontendIPs,
+			Type:                 s.ControlPlaneOutboundLB().Type,
+			SKU:                  s.ControlPlaneOutboundLB().SKU,
+			BackendPoolName:      s.OutboundPoolName(azure.GenerateControlPlaneOutboundLBName(s.ClusterName())),
+			IdleTimeoutInMinutes: s.NodeOutboundLB().IdleTimeoutInMinutes,
+			Role:                 infrav1.ControlPlaneOutboundRole,
+		})
 	}
 
-	specs = append(specs, azure.LBSpec{
-		// Public Control Plane outbound LB
-		Name: azure.GenerateControlPlaneOutboundLBName(s.ClusterName()),
-		FrontendIPConfigs: []infrav1.FrontendIP{
-			{
-				Name: azure.GenerateFrontendIPConfigName(azure.GenerateControlPlaneOutboundLBName(s.ClusterName())),
-				PublicIP: &infrav1.PublicIPSpec{
-					Name: azure.GenerateControlPlaneOutboundIPName(s.ClusterName()),
-				},
-			},
-		},
-		Type:                 infrav1.Public,
-		SKU:                  infrav1.SKUStandard,
-		BackendPoolName:      s.OutboundPoolName(azure.GenerateControlPlaneOutboundLBName(s.ClusterName())),
-		Role:                 infrav1.ControlPlaneOutboundRole,
-		IdleTimeoutInMinutes: to.Int32Ptr(4),
-	})
-
 	return specs
 }
 
@@ -450,6 +426,11 @@ func (s *ClusterScope) NodeOutboundLB() *infrav1.LoadBalancerSpec {
 	return s.AzureCluster.Spec.NetworkSpec.NodeOutboundLB
 }
 
+// ControlPlaneOutboundLB returns the cluster control plane outbound load balancer.
+func (s *ClusterScope) ControlPlaneOutboundLB() *infrav1.LoadBalancerSpec {
+	return s.AzureCluster.Spec.NetworkSpec.ControlPlaneOutboundLB
+}
+
 // APIServerLBName returns the API Server LB name.
 func (s *ClusterScope) APIServerLBName() string {
 	return s.APIServerLB().Name
@@ -684,3 +665,24 @@ func (s *ClusterScope) SetDNSName() {
 		s.APIServerPublicIP().DNSName = s.GenerateFQDN(s.APIServerPublicIP().Name)
 	}
 }
+
+// getOutboundLBPublicIPSpecs returns the public ip specs for a LoadBalancerSpec based on the number of frontend ips configured.
+func (s *ClusterScope) getOutboundLBPublicIPSpecs(outboundLB *infrav1.LoadBalancerSpec, generateOutboundIPName func(string) string) []azure.PublicIPSpec {
+	var outboundIPSpecs []azure.PublicIPSpec
+	loadBalancerNodeOutboundIPs := outboundLB.FrontendIPsCount
+	if loadBalancerNodeOutboundIPs == nil || *loadBalancerNodeOutboundIPs == 0 {
+		// do nothing
+	} else if *loadBalancerNodeOutboundIPs == 1 {
+		outboundIPSpecs = append(outboundIPSpecs, azure.PublicIPSpec{
+			Name: generateOutboundIPName(s.ClusterName()),
+		})
+	} else {
+		for i := 0; i < int(*loadBalancerNodeOutboundIPs); i++ {
+			outboundIPSpecs = append(outboundIPSpecs, azure.PublicIPSpec{
+				Name: azure.WithIndex(generateOutboundIPName(s.ClusterName()), i+1),
+			})
+		}
+	}
+
+	return outboundIPSpecs
+}
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml
index d3042f6b8..8ff27b9ec 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml
@@ -828,6 +828,58 @@ spec:
                         description: LBType defines an Azure load balancer Type.
                         type: string
                     type: object
+                  controlPlaneOutboundLB:
+                    description: ControlPlaneOutboundLB is the configuration for the
+                      control-plane outbound load balancer. This is different from
+                      APIServerLB, and is used only in private clusters (optionally)
+                      for enabling outbound traffic.
+                    properties:
+                      frontendIPs:
+                        items:
+                          description: FrontendIP defines a load balancer frontend
+                            IP configuration.
+                          properties:
+                            name:
+                              minLength: 1
+                              type: string
+                            privateIP:
+                              type: string
+                            publicIP:
+                              description: PublicIPSpec defines the inputs to create
+                                an Azure public IP address.
+                              properties:
+                                dnsName:
+                                  type: string
+                                name:
+                                  type: string
+                              required:
+                              - name
+                              type: object
+                          required:
+                          - name
+                          type: object
+                        type: array
+                      frontendIPsCount:
+                        description: FrontendIPsCount specifies the number of frontend
+                          IP addresses for the load balancer.
+                        format: int32
+                        type: integer
+                      id:
+                        type: string
+                      idleTimeoutInMinutes:
+                        description: IdleTimeoutInMinutes specifies the timeout for
+                          the TCP idle connection.
+                        format: int32
+                        type: integer
+                      name:
+                        type: string
+                      sku:
+                        description: SKU defines an Azure load balancer SKU.
+                        type: string
+                      type:
+                        description: LBType defines an Azure load balancer Type.
+                        type: string
+                    type: object
                   nodeOutboundLB:
                     description: NodeOutboundLB is the configuration for the node
                       outbound load balancer.
diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md
index 631f0c178..9f6f2219e 100644
--- a/docs/book/src/SUMMARY.md
+++ b/docs/book/src/SUMMARY.md
@@ -8,6 +8,7 @@
     - [AAD Integration](./topics/aad-integration.md)
     - [API Server Endpoint](./topics/api-server-endpoint.md)
     - [Cloud Provider Config](./topics/cloud-provider-config.md)
+    - [Control Plane Outbound Load Balancer](./topics/control-plane-outbound-lb.md)
     - [Custom Private DNS Zone Name](./topics/custom-dns.md)
     - [Custom Images](./topics/custom-images.md)
     - [Data Disks](./topics/data-disks.md)
diff --git a/docs/book/src/topics/control-plane-outbound-lb.md b/docs/book/src/topics/control-plane-outbound-lb.md
new file mode 100644
index 000000000..773fbc804
--- /dev/null
+++ b/docs/book/src/topics/control-plane-outbound-lb.md
@@ -0,0 +1,38 @@
+# Control Plane Outbound Load Balancer
+
+This document describes how to configure your clusters' control plane outbound load balancer.
+
+### Public Clusters
+
+For public clusters ie. clusters with api server load balancer type set to `Public`, CAPZ automatically does not support adding a control plane outbound load balancer.
+This is because the api server load balancer already allows for outbound traffic in public clusters.
+
+### Private Clusters
+
+For private clusters ie. clusters with api server load balancer type set to `Internal`, CAPZ does not create a control plane outbound load balancer by default. 
+To create a control plane outbound load balancer, include the `controlPlaneOutboundLB` section with the desired settings. 
+
+Here is an example of configuring a control plane outbound load balancer with 1 front end ip for a private cluster:
+
+```yaml
+apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4
+kind: AzureCluster
+metadata:
+  name: my-private-cluster
+  namespace: default
+spec:
+  location: eastus
+  networkSpec:
+    apiServerLB:
+      type: Internal
+    controlPlaneOutboundLB:
+      frontendIPsCount: 1
+```
+
+<aside class="note warning">
+
+<h1> Warning </h1>
+
+The field `controlPlaneOutboundLB` cannot be modified after cluster creation. Trying to do so will result in a validation error.
+
+</aside>
diff --git a/templates/cluster-template-private.yaml b/templates/cluster-template-private.yaml
index 1bcce785c..a0ee8dbaa 100644
--- a/templates/cluster-template-private.yaml
+++ b/templates/cluster-template-private.yaml
@@ -32,6 +32,8 @@ spec:
     apiServerLB:
       name: ${CLUSTER_NAME}-internal-lb
       type: Internal
+    controlPlaneOutboundLB:
+      frontendIPsCount: 1
     nodeOutboundLB:
       frontendIPsCount: 1
     vnet:
diff --git a/templates/flavors/private/patches/private-lb.yaml b/templates/flavors/private/patches/private-lb.yaml
index 4cdc85a50..0baf26fc8 100644
--- a/templates/flavors/private/patches/private-lb.yaml
+++ b/templates/flavors/private/patches/private-lb.yaml
@@ -9,4 +9,5 @@ spec:
       type: Internal
     nodeOutboundLB:
       frontendIPsCount: 1
-
+    controlPlaneOutboundLB:
+      frontendIPsCount: 1


Test patch:
diff --git a/api/v1alpha4/azurecluster_default_test.go b/api/v1alpha4/azurecluster_default_test.go
index 42dbe6992..baba2164d 100644
--- a/api/v1alpha4/azurecluster_default_test.go
+++ b/api/v1alpha4/azurecluster_default_test.go
@@ -916,6 +916,128 @@ func TestNodeOutboundLBDefaults(t *testing.T) {
 	}
 }
 
+func TestControlPlaneOutboundLBDefaults(t *testing.T) {
+	cases := []struct {
+		name    string
+		cluster *AzureCluster
+		output  *AzureCluster
+	}{
+		{
+			name: "no cp lb for public clusters",
+			cluster: &AzureCluster{
+				ObjectMeta: v1.ObjectMeta{
+					Name: "cluster-test",
+				},
+				Spec: AzureClusterSpec{
+					NetworkSpec: NetworkSpec{
+						APIServerLB: LoadBalancerSpec{Type: Public},
+					},
+				},
+			},
+			output: &AzureCluster{
+				ObjectMeta: v1.ObjectMeta{
+					Name: "cluster-test",
+				},
+				Spec: AzureClusterSpec{
+					NetworkSpec: NetworkSpec{
+						APIServerLB: LoadBalancerSpec{
+							Type: Public,
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "no cp lb for private clusters",
+			cluster: &AzureCluster{
+				ObjectMeta: v1.ObjectMeta{
+					Name: "cluster-test",
+				},
+				Spec: AzureClusterSpec{
+					NetworkSpec: NetworkSpec{
+						APIServerLB: LoadBalancerSpec{Type: Internal},
+					},
+				},
+			},
+			output: &AzureCluster{
+				ObjectMeta: v1.ObjectMeta{
+					Name: "cluster-test",
+				},
+				Spec: AzureClusterSpec{
+					NetworkSpec: NetworkSpec{
+						APIServerLB: LoadBalancerSpec{
+							Type: Internal,
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "frontendIPsCount > 1",
+			cluster: &AzureCluster{
+				ObjectMeta: v1.ObjectMeta{
+					Name: "cluster-test",
+				},
+				Spec: AzureClusterSpec{
+					NetworkSpec: NetworkSpec{
+						APIServerLB: LoadBalancerSpec{Type: Internal},
+						ControlPlaneOutboundLB: &LoadBalancerSpec{
+							FrontendIPsCount:     to.Int32Ptr(2),
+							IdleTimeoutInMinutes: to.Int32Ptr(15),
+						},
+					},
+				},
+			},
+			output: &AzureCluster{
+				ObjectMeta: v1.ObjectMeta{
+					Name: "cluster-test",
+				},
+				Spec: AzureClusterSpec{
+					NetworkSpec: NetworkSpec{
+						APIServerLB: LoadBalancerSpec{
+							Type: Internal,
+						},
+						ControlPlaneOutboundLB: &LoadBalancerSpec{
+							Name: "cluster-test-outbound-lb",
+							SKU:  SKUStandard,
+							FrontendIPs: []FrontendIP{
+								{
+									Name: "cluster-test-outbound-lb-frontEnd-1",
+									PublicIP: &PublicIPSpec{
+										Name: "pip-cluster-test-controlplane-outbound-1",
+									},
+								},
+								{
+									Name: "cluster-test-outbound-lb-frontEnd-2",
+									PublicIP: &PublicIPSpec{
+										Name: "pip-cluster-test-controlplane-outbound-2",
+									},
+								},
+							},
+							Type:                 Public,
+							FrontendIPsCount:     to.Int32Ptr(2),
+							IdleTimeoutInMinutes: to.Int32Ptr(15),
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, c := range cases {
+		tc := c
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			tc.cluster.setControlPlaneOutboundLBDefaults()
+			if !reflect.DeepEqual(tc.cluster, tc.output) {
+				expected, _ := json.MarshalIndent(tc.output, "", "\t")
+				actual, _ := json.MarshalIndent(tc.cluster, "", "\t")
+				t.Errorf("Expected %s, got %s", string(expected), string(actual))
+			}
+		})
+	}
+}
+
 func TestBastionDefault(t *testing.T) {
 	cases := map[string]struct {
 		cluster *AzureCluster
diff --git a/api/v1alpha4/azurecluster_validation_test.go b/api/v1alpha4/azurecluster_validation_test.go
index 74eb4fe7d..ba7edaffc 100644
--- a/api/v1alpha4/azurecluster_validation_test.go
+++ b/api/v1alpha4/azurecluster_validation_test.go
@@ -1118,6 +1118,78 @@ func TestValidateNodeOutboundLB(t *testing.T) {
 	}
 }
 
+func TestValidateControlPlaneNodeOutboundLB(t *testing.T) {
+	g := NewWithT(t)
+
+	testcases := []struct {
+		name        string
+		lb          *LoadBalancerSpec
+		old         *LoadBalancerSpec
+		apiServerLB LoadBalancerSpec
+		wantErr     bool
+		expectedErr field.Error
+	}{
+		{
+			name:        "cp outbound lb cannot be set for public clusters",
+			lb:          &LoadBalancerSpec{Name: "foo"},
+			apiServerLB: LoadBalancerSpec{Type: Public},
+			wantErr:     true,
+			expectedErr: field.Error{
+				Type:     "FieldValueForbidden",
+				Field:    "controlPlaneOutboundLB",
+				BadValue: LoadBalancerSpec{Name: "foo"},
+				Detail:   "Control plane outbound load balancer cannot be set for public clusters.",
+			},
+		},
+		{
+			name:        "cp outbound lb can be set for private clusters",
+			lb:          &LoadBalancerSpec{Name: "foo"},
+			apiServerLB: LoadBalancerSpec{Type: Internal},
+			wantErr:     false,
+		},
+		{
+			name:        "cp outbound lb can be nil for private clusters",
+			lb:          nil,
+			apiServerLB: LoadBalancerSpec{Type: Internal},
+			wantErr:     false,
+		},
+		{
+			name: "frontend ips count exceeds max value",
+			lb: &LoadBalancerSpec{
+				FrontendIPsCount: pointer.Int32Ptr(100),
+			},
+			apiServerLB: LoadBalancerSpec{Type: Internal},
+			wantErr:     true,
+			expectedErr: field.Error{
+				Type:     "FieldValueInvalid",
+				Field:    "controlPlaneOutboundLB.frontendIPsCount",
+				BadValue: 100,
+				Detail:   "Max front end ips allowed is 16",
+			},
+		},
+	}
+
+	for _, test := range testcases {
+		test := test
+		t.Run(test.name, func(t *testing.T) {
+			t.Parallel()
+			err := validateControlPlaneOutboundLB(test.lb, test.apiServerLB, field.NewPath("controlPlaneOutboundLB"))
+			if test.wantErr {
+				g.Expect(err).NotTo(HaveLen(0))
+				found := false
+				for _, actual := range err {
+					if actual.Error() == test.expectedErr.Error() {
+						found = true
+					}
+				}
+				g.Expect(found).To(BeTrue())
+			} else {
+				g.Expect(err).To(HaveLen(0))
+			}
+		})
+	}
+}
+
 func TestValidateCloudProviderConfigOverrides(t *testing.T) {
 	g := NewWithT(t)
 
diff --git a/api/v1alpha4/azurecluster_webhook_test.go b/api/v1alpha4/azurecluster_webhook_test.go
index acd299748..da7875212 100644
--- a/api/v1alpha4/azurecluster_webhook_test.go
+++ b/api/v1alpha4/azurecluster_webhook_test.go
@@ -214,6 +214,38 @@ func TestAzureCluster_ValidateUpdate(t *testing.T) {
 			},
 			wantErr: true,
 		},
+		{
+			name: "azurecluster azureEnvironment is immutable",
+			oldCluster: &AzureCluster{
+				Spec: AzureClusterSpec{
+					AzureEnvironment: "AzureGermanCloud",
+				},
+			},
+			cluster: &AzureCluster{
+				Spec: AzureClusterSpec{
+					AzureEnvironment: "AzureChinaCloud",
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "control plane outbound lb is immutable",
+			oldCluster: &AzureCluster{
+				Spec: AzureClusterSpec{
+					NetworkSpec: NetworkSpec{
+						ControlPlaneOutboundLB: &LoadBalancerSpec{Name: "cp-lb"},
+					},
+				},
+			},
+			cluster: &AzureCluster{
+				Spec: AzureClusterSpec{
+					NetworkSpec: NetworkSpec{
+						ControlPlaneOutboundLB: &LoadBalancerSpec{Name: "cp-lb-new"},
+					},
+				},
+			},
+			wantErr: true,
+		},
 	}
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
diff --git a/templates/test/ci/cluster-template-prow-private.yaml b/templates/test/ci/cluster-template-prow-private.yaml
index 52da33e11..444da59d7 100644
--- a/templates/test/ci/cluster-template-prow-private.yaml
+++ b/templates/test/ci/cluster-template-prow-private.yaml
@@ -39,6 +39,8 @@ spec:
         privateIP: ${AZURE_INTERNAL_LB_IP}
       name: ${CLUSTER_NAME}-internal-lb
       type: Internal
+    controlPlaneOutboundLB:
+      frontendIPsCount: 1
     nodeOutboundLB:
       frontendIPsCount: 1
     subnets: