Repository: kubernetes-sigs/cluster-api-provider-aws
Pull request #3804 (2022-10-31 10:35:33)

Add ability to use NLBs as control plane load-balancers
<!-- Thanks for this PR! If this is your first PR please read the [contributing guide](../CONTRIBUTING.md) -->
<!-- If this PR is still work-in-progress and is being open for visibility please prefix the title with `WIP:` -->

**What type of PR is this?**

/kind feature

**What this PR does / why we need it**:

<!-- Enter a description of the change and why this change is needed -->

**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 https://github.com/kubernetes-sigs/cluster-api-provider-aws/issues/3088

**Special notes for your reviewer**:

**TODOs**:

- [x] IPv6 - res.Attributes["ipv6.deny_all_igw_traffic"] and the ipType for the NLB
- [x] Hosts re-write
- [ ] SSL
- [x] Bunch of tests
- [x] Update status with an NLB if present
- [x] Documentation
- [x] Samples configuration
- [x] Deregister nodes when they are being deleted and they are part of NLB

**Checklist**:
<!-- Put an "X" character inside the brackets of each completed task. Some may be optional depending on the PR in which case these can be deleted -->

- [ ] squashed commits
- [ ] includes documentation
- [ ] adds unit tests
- [ ] adds or updates e2e tests


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

This pull-request has been approved by:
**Once this PR has been reviewed and has the lgtm label**, please ask for approval from skarlso by writing `/assign @skarlso` in a comment. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

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

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

- **[OWNERS](https://github.com/kubernetes-sigs/cluster-api-provider-aws/blob/main/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":["skarlso"]} -->
k8s-ci-robot: @Skarlso: 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-cluster-api-provider-aws-build | 0bff7b351d19aba08b6289c4d753e87d2b3ed1b1 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-build/1587030513200140288) | true | `/test pull-cluster-api-provider-aws-build`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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: @Skarlso: 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-cluster-api-provider-aws-build | 0bff7b351d19aba08b6289c4d753e87d2b3ed1b1 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-build/1587030513200140288) | true | `/test pull-cluster-api-provider-aws-build`
pull-cluster-api-provider-aws-test | 0bff7b351d19aba08b6289c4d753e87d2b3ed1b1 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-test/1587030513120448512) | true | `/test pull-cluster-api-provider-aws-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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: @Skarlso: 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-cluster-api-provider-aws-build | 0bff7b351d19aba08b6289c4d753e87d2b3ed1b1 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-build/1587030513200140288) | true | `/test pull-cluster-api-provider-aws-build`
pull-cluster-api-provider-aws-test | 0bff7b351d19aba08b6289c4d753e87d2b3ed1b1 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-test/1587030513120448512) | true | `/test pull-cluster-api-provider-aws-test`
pull-cluster-api-provider-aws-verify | 0bff7b351d19aba08b6289c4d753e87d2b3ed1b1 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-verify/1587030513233694720) | true | `/test pull-cluster-api-provider-aws-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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: @Skarlso: 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-cluster-api-provider-aws-test | 0bff7b351d19aba08b6289c4d753e87d2b3ed1b1 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-test/1587030513120448512) | true | `/test pull-cluster-api-provider-aws-test`
pull-cluster-api-provider-aws-verify | 0bff7b351d19aba08b6289c4d753e87d2b3ed1b1 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-verify/1587030513233694720) | true | `/test pull-cluster-api-provider-aws-verify`
pull-cluster-api-provider-aws-build | 2fc126346f67b8aa370e1fd71f0e1d0e112b9be6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-build/1587038580293767168) | true | `/test pull-cluster-api-provider-aws-build`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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: @Skarlso: 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-cluster-api-provider-aws-verify | 0bff7b351d19aba08b6289c4d753e87d2b3ed1b1 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-verify/1587030513233694720) | true | `/test pull-cluster-api-provider-aws-verify`
pull-cluster-api-provider-aws-build | 2fc126346f67b8aa370e1fd71f0e1d0e112b9be6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-build/1587038580293767168) | true | `/test pull-cluster-api-provider-aws-build`
pull-cluster-api-provider-aws-test | 2fc126346f67b8aa370e1fd71f0e1d0e112b9be6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-test/1587038580214075392) | true | `/test pull-cluster-api-provider-aws-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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: @Skarlso: 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-cluster-api-provider-aws-build | 2fc126346f67b8aa370e1fd71f0e1d0e112b9be6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-build/1587038580293767168) | true | `/test pull-cluster-api-provider-aws-build`
pull-cluster-api-provider-aws-test | 2fc126346f67b8aa370e1fd71f0e1d0e112b9be6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-test/1587038580214075392) | true | `/test pull-cluster-api-provider-aws-test`
pull-cluster-api-provider-aws-verify | 2fc126346f67b8aa370e1fd71f0e1d0e112b9be6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-verify/1587038580331515904) | true | `/test pull-cluster-api-provider-aws-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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: @Skarlso: 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-cluster-api-provider-aws-verify | 2fc126346f67b8aa370e1fd71f0e1d0e112b9be6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-verify/1587038580331515904) | true | `/test pull-cluster-api-provider-aws-verify`
pull-cluster-api-provider-aws-test | a6d33c569f2bf586fa6793403d02d2348146f30d | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-test/1587044411525566464) | true | `/test pull-cluster-api-provider-aws-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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: @Skarlso: 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-cluster-api-provider-aws-test | a6d33c569f2bf586fa6793403d02d2348146f30d | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-test/1587044411525566464) | true | `/test pull-cluster-api-provider-aws-test`
pull-cluster-api-provider-aws-verify | a6d33c569f2bf586fa6793403d02d2348146f30d | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-verify/1587044411634618368) | true | `/test pull-cluster-api-provider-aws-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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: @Skarlso: 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-cluster-api-provider-aws-verify | a6d33c569f2bf586fa6793403d02d2348146f30d | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-verify/1587044411634618368) | true | `/test pull-cluster-api-provider-aws-verify`
pull-cluster-api-provider-aws-test | d24cdc7b6a849ef6354d83eb47e20baea3591a81 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-test/1587056619424845824) | true | `/test pull-cluster-api-provider-aws-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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: @Skarlso: 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-cluster-api-provider-aws-test | ac4fba805b03e58678db9bf62923832180e5154b | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-test/1587110319996538880) | true | `/test pull-cluster-api-provider-aws-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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: @Skarlso: 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-cluster-api-provider-aws-test | 1a63dfca3a29f7dce1da096790158a9996890dc2 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-test/1587372243040604160) | true | `/test pull-cluster-api-provider-aws-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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: @Skarlso: 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-cluster-api-provider-aws-test | cff8230d6354b19a20d61ad2271bd43e46228fa1 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-test/1587381164673339392) | true | `/test pull-cluster-api-provider-aws-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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 -->
Skarlso: Hah, so I just learned that you can't attach security groups to network load balancers.
Skarlso: Almost there. :D It can't create the target group which I don't understand at all why because the error is super unhelpful.

```
name="test-aws-cluster" reconcileID=35f899c1-3e15-484e-b030-6870452713c5 cluster="default/test-aws-cluster" group=<
	{
	  HealthCheckEnabled: true,
	  HealthCheckIntervalSeconds: 10000000000,
	  HealthCheckProtocol: "TCP",
	  HealthCheckTimeoutSeconds: 5000000000,
	  HealthyThresholdCount: 5,
	  Name: "apiserver-target",
	  Port: 6443,
	  Protocol: "TCP",
	  VpcId: "vpc-064d130f24068f471"
	}
 > listener={Protocol:TCP Port:6443 TargetGroup:{Name:0xc0011018d0 Port:0xc000b59520 Protocol:TCP VpcID:0xc0011018e0 HealthCheck:0xc0004c0150}}
E1101 20:46:18.337089     986 logger.go:84] "failed to create LB" err=<
	failed to create target group for load balancer: MalformedInput: 
		status code: 400, request id: ced119e5-11d9-4109-a1bf-b98f1554f126
 > controller="awscluster" controllerGroup="infrastructure.cluster.x-k8s.io" controllerKind="AWSCluster" aWSCluster="default/test-aws-cluster" namespace="default" name="test-aws-cluster" reconcileID=35f899c1-3e15-484e-b030-6870452713c5 cluster="default/test-aws-cluster"
E1101 20:46:18.337178     986 logger.go:84] "failed to reconcile load balancer" err=<
	failed to create target group for load balancer: MalformedInput: 
		status code: 400, request id: ced119e5-11d9-4109-a1bf-b98f1554f126
```

It looks okay to me. :D 
Skarlso: IT WORKED! I have a working NLB! :D 

It was the health check, apparently; I'm not sure yet why yet.


Skarlso: <img width="1582" alt="Screenshot 2022-11-02 at 11 05 38" src="https://user-images.githubusercontent.com/182850/199461867-354888b5-30f2-453d-81a2-03ef87554fc1.png">

BOOM!
k8s-ci-robot: @Skarlso: 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-cluster-api-provider-aws-test | 9ac04fca699f02d5884b7f1f6728026ca339635c | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-test/1587845281917964288) | true | `/test pull-cluster-api-provider-aws-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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: @Skarlso: 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-cluster-api-provider-aws-test | 9ac04fca699f02d5884b7f1f6728026ca339635c | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-test/1587845281917964288) | true | `/test pull-cluster-api-provider-aws-test`
pull-cluster-api-provider-aws-build | 1a9945a110cfb8569423e09d2ab2d3168505c51e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-build/1587902209696206848) | true | `/test pull-cluster-api-provider-aws-build`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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: @Skarlso: 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-cluster-api-provider-aws-build | 1a9945a110cfb8569423e09d2ab2d3168505c51e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-build/1587902209696206848) | true | `/test pull-cluster-api-provider-aws-build`
pull-cluster-api-provider-aws-test | 1a9945a110cfb8569423e09d2ab2d3168505c51e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-test/1587902209616515072) | true | `/test pull-cluster-api-provider-aws-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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: @Skarlso: 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-cluster-api-provider-aws-test | 1a9945a110cfb8569423e09d2ab2d3168505c51e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-test/1587902209616515072) | true | `/test pull-cluster-api-provider-aws-test`
pull-cluster-api-provider-aws-build | c1577687451243e1e342d3037e33c5496bb02819 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-build/1587903381391806464) | true | `/test pull-cluster-api-provider-aws-build`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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: @Skarlso: 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-cluster-api-provider-aws-build | c1577687451243e1e342d3037e33c5496bb02819 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-build/1587903381391806464) | true | `/test pull-cluster-api-provider-aws-build`
pull-cluster-api-provider-aws-test | c1577687451243e1e342d3037e33c5496bb02819 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-test/1587903381316308992) | true | `/test pull-cluster-api-provider-aws-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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: @Skarlso: 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-cluster-api-provider-aws-test | 6dfd0f9c2562a661173434a59ad1596fc830acdc | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-test/1587914504757317632) | true | `/test pull-cluster-api-provider-aws-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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: @Skarlso: 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-cluster-api-provider-aws-test | 760efdfaed3685db0e2ee220fd5bb5a0a086347b | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-test/1587916557093179392) | true | `/test pull-cluster-api-provider-aws-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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: @Skarlso: 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-cluster-api-provider-aws-test | 760efdfaed3685db0e2ee220fd5bb5a0a086347b | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-test/1587916557093179392) | true | `/test pull-cluster-api-provider-aws-test`
pull-cluster-api-provider-aws-verify | 760efdfaed3685db0e2ee220fd5bb5a0a086347b | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-verify/1587916557223202816) | true | `/test pull-cluster-api-provider-aws-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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 -->
Skarlso: <img width="1601" alt="Screenshot 2022-11-03 at 13 25 49" src="https://user-images.githubusercontent.com/182850/199719830-781b489a-66b3-4494-9073-9c9863fa27a7.png">

IT'S ALIVE
k8s-ci-robot: @Skarlso: 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-cluster-api-provider-aws-verify | b5d43af43b91efb5b915c16f20339500ffb2e3c7 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-verify/1588572487694684160) | true | `/test pull-cluster-api-provider-aws-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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 -->
Skarlso: I'm considering adding an E2E test, but that might further lengthen this already massive PR. 🤔 
Skarlso: Hello @richardcase and @Ankitasw and @dlipovetsky 👋 :) 

This is now ready for a first-pass review. :D I know it's enormous, but most of it is test code and the doc and some generated code. :) So, hopefully, it won't be too much of a problem.
Ankitasw: @Skarlso I will take a look at this PR today 
k8s-ci-robot: @Skarlso: 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-cluster-api-provider-aws-verify | 46aa4811461e770b8c9669f3f83f993687edbee7 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-verify/1589525624748249088) | true | `/test pull-cluster-api-provider-aws-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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 -->
richardcase: @Skarlso - I will review this today. Could you squash the commits before this merges?
Skarlso: @richardcase Yep, sure. I'll do that. I kind-of wanted to keep the history until the reviews are done in case I need to revert to something. :D 
k8s-ci-robot: @Skarlso: 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: @Skarlso: 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>
Skarlso: @richardcase I squashed the commits to a reasonable 3. And will reduce it to one after the review is done. :)
k8s-ci-robot: @Skarlso: 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-cluster-api-provider-aws-verify | 0cbc844cbdf9b24256a046be633857eb22137551 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-verify/1592953994483863552) | true | `/test pull-cluster-api-provider-aws-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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 -->
Skarlso: /test pull-cluster-api-provider-aws-e2e-eks
k8s-ci-robot: @Skarlso: 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-cluster-api-provider-aws-e2e-eks | 6985bf91bb5fd21257c354d5bdd4378955a7553b | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3804/pull-cluster-api-provider-aws-e2e-eks/1594067871955161088) | false | `/test pull-cluster-api-provider-aws-e2e-eks`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=3804). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ASkarlso). 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-aws/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 -->
Skarlso: /test pull-cluster-api-provider-aws-e2e-eks
Ankitasw: /lgtm
Ankitasw: cc @richardcase for final approval
richardcase: And i should've said, looks great @Skarlso....excellent work. Most of my comments are nits.
johannesfrey: Also, totally forgot so say awesome work @Skarlso :heart: :slightly_smiling_face: 
Skarlso: Thank you, all! :)
k8s-ci-robot: New changes are detected. LGTM label has been removed.
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
**Once this PR has been reviewed and has the lgtm label**, please ask for approval from ankitasw by writing `/assign @ankitasw` in a comment. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

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

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

- **[OWNERS](https://github.com/kubernetes-sigs/cluster-api-provider-aws/blob/main/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":["ankitasw"]} -->

Patch:
diff --git a/api/v1beta1/awscluster_conversion.go b/api/v1beta1/awscluster_conversion.go
index 69650fda8..abd5b940f 100644
--- a/api/v1beta1/awscluster_conversion.go
+++ b/api/v1beta1/awscluster_conversion.go
@@ -41,17 +41,30 @@ func (src *AWSCluster) ConvertTo(dstRaw conversion.Hub) error {
 		}
 		restoreControlPlaneLoadBalancer(restored.Spec.ControlPlaneLoadBalancer, dst.Spec.ControlPlaneLoadBalancer)
 	}
+	restoreControlPlaneLoadBalancerStatus(&restored.Status.Network.APIServerELB, &dst.Status.Network.APIServerELB)
 
 	dst.Spec.S3Bucket = restored.Spec.S3Bucket
 
 	return nil
 }
 
+// restoreControlPlaneLoadBalancerStatus manually restores the control plane loadbalancer status data.
+// Assumes restored and dst are non-nil.
+func restoreControlPlaneLoadBalancerStatus(restored, dst *infrav1.LoadBalancer) {
+	dst.ARN = restored.ARN
+	dst.LoadBalancerType = restored.LoadBalancerType
+	dst.ELBAttributes = restored.ELBAttributes
+	dst.ELBListeners = restored.ELBListeners
+}
+
 // restoreControlPlaneLoadBalancer manually restores the control plane loadbalancer data.
 // Assumes restored and dst are non-nil.
 func restoreControlPlaneLoadBalancer(restored, dst *infrav1.AWSLoadBalancerSpec) {
 	dst.Name = restored.Name
 	dst.HealthCheckProtocol = restored.HealthCheckProtocol
+	dst.LoadBalancerType = restored.LoadBalancerType
+	dst.DisableHostsRewrite = restored.DisableHostsRewrite
+	dst.PreserveClientIP = restored.PreserveClientIP
 }
 
 // ConvertFrom converts the v1beta1 AWSCluster receiver to a v1beta1 AWSCluster.
diff --git a/api/v1beta1/conversion.go b/api/v1beta1/conversion.go
index f722198de..c8c2789c3 100644
--- a/api/v1beta1/conversion.go
+++ b/api/v1beta1/conversion.go
@@ -17,6 +17,8 @@ limitations under the License.
 package v1beta1
 
 import (
+	"unsafe"
+
 	"k8s.io/apimachinery/pkg/conversion"
 	"sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
 )
@@ -32,3 +34,39 @@ func Convert_v1beta1_AWSResourceReference_To_v1beta2_AWSResourceReference(in *AW
 func Convert_v1beta1_AWSMachineSpec_To_v1beta2_AWSMachineSpec(in *AWSMachineSpec, out *v1beta2.AWSMachineSpec, s conversion.Scope) error {
 	return autoConvert_v1beta1_AWSMachineSpec_To_v1beta2_AWSMachineSpec(in, out, s)
 }
+
+func Convert_v1beta2_AWSLoadBalancerSpec_To_v1beta1_AWSLoadBalancerSpec(in *v1beta2.AWSLoadBalancerSpec, out *AWSLoadBalancerSpec, s conversion.Scope) error {
+	return autoConvert_v1beta2_AWSLoadBalancerSpec_To_v1beta1_AWSLoadBalancerSpec(in, out, s)
+}
+
+func Convert_v1beta2_NetworkStatus_To_v1beta1_NetworkStatus(in *v1beta2.NetworkStatus, out *NetworkStatus, s conversion.Scope) error {
+	return autoConvert_v1beta2_NetworkStatus_To_v1beta1_NetworkStatus(in, out, s)
+}
+
+func Convert_v1beta1_ClassicELB_To_v1beta2_LoadBalancer(in *ClassicELB, out *v1beta2.LoadBalancer, s conversion.Scope) error {
+	out.Name = in.Name
+	out.DNSName = in.DNSName
+	out.Scheme = v1beta2.ELBScheme(in.Scheme)
+	out.HealthCheck = (*v1beta2.ClassicELBHealthCheck)(in.HealthCheck)
+	out.AvailabilityZones = in.AvailabilityZones
+	out.ClassicElbAttributes = (v1beta2.ClassicELBAttributes)(in.Attributes)
+	out.ClassicELBListeners = *(*[]v1beta2.ClassicELBListener)(unsafe.Pointer(&in.Listeners))
+	out.SecurityGroupIDs = in.SecurityGroupIDs
+	out.Tags = in.Tags
+	out.SubnetIDs = in.SubnetIDs
+	return nil
+}
+
+func Convert_v1beta2_LoadBalancer_To_v1beta1_ClassicELB(in *v1beta2.LoadBalancer, out *ClassicELB, s conversion.Scope) error {
+	out.Name = in.Name
+	out.DNSName = in.DNSName
+	out.Scheme = ClassicELBScheme(in.Scheme)
+	out.HealthCheck = (*ClassicELBHealthCheck)(in.HealthCheck)
+	out.AvailabilityZones = in.AvailabilityZones
+	out.Attributes = (ClassicELBAttributes)(in.ClassicElbAttributes)
+	out.Listeners = *(*[]ClassicELBListener)(unsafe.Pointer(&in.ClassicELBListeners))
+	out.SecurityGroupIDs = in.SecurityGroupIDs
+	out.Tags = in.Tags
+	out.SubnetIDs = in.SubnetIDs
+	return nil
+}
diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go
index ff415c111..2fb0fe306 100644
--- a/api/v1beta1/zz_generated.conversion.go
+++ b/api/v1beta1/zz_generated.conversion.go
@@ -240,11 +240,6 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
-	if err := s.AddGeneratedConversionFunc((*v1beta2.AWSLoadBalancerSpec)(nil), (*AWSLoadBalancerSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_v1beta2_AWSLoadBalancerSpec_To_v1beta1_AWSLoadBalancerSpec(a.(*v1beta2.AWSLoadBalancerSpec), b.(*AWSLoadBalancerSpec), scope)
-	}); err != nil {
-		return err
-	}
 	if err := s.AddGeneratedConversionFunc((*AWSMachine)(nil), (*v1beta2.AWSMachine)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1beta1_AWSMachine_To_v1beta2_AWSMachine(a.(*AWSMachine), b.(*v1beta2.AWSMachine), scope)
 	}); err != nil {
@@ -395,16 +390,6 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
-	if err := s.AddGeneratedConversionFunc((*ClassicELB)(nil), (*v1beta2.ClassicELB)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_v1beta1_ClassicELB_To_v1beta2_ClassicELB(a.(*ClassicELB), b.(*v1beta2.ClassicELB), scope)
-	}); err != nil {
-		return err
-	}
-	if err := s.AddGeneratedConversionFunc((*v1beta2.ClassicELB)(nil), (*ClassicELB)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_v1beta2_ClassicELB_To_v1beta1_ClassicELB(a.(*v1beta2.ClassicELB), b.(*ClassicELB), scope)
-	}); err != nil {
-		return err
-	}
 	if err := s.AddGeneratedConversionFunc((*ClassicELBAttributes)(nil), (*v1beta2.ClassicELBAttributes)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1beta1_ClassicELBAttributes_To_v1beta2_ClassicELBAttributes(a.(*ClassicELBAttributes), b.(*v1beta2.ClassicELBAttributes), scope)
 	}); err != nil {
@@ -510,11 +495,6 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
-	if err := s.AddGeneratedConversionFunc((*v1beta2.NetworkStatus)(nil), (*NetworkStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_v1beta2_NetworkStatus_To_v1beta1_NetworkStatus(a.(*v1beta2.NetworkStatus), b.(*NetworkStatus), scope)
-	}); err != nil {
-		return err
-	}
 	if err := s.AddGeneratedConversionFunc((*RouteTable)(nil), (*v1beta2.RouteTable)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1beta1_RouteTable_To_v1beta2_RouteTable(a.(*RouteTable), b.(*v1beta2.RouteTable), scope)
 	}); err != nil {
@@ -595,11 +575,31 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddConversionFunc((*ClassicELB)(nil), (*v1beta2.LoadBalancer)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta1_ClassicELB_To_v1beta2_LoadBalancer(a.(*ClassicELB), b.(*v1beta2.LoadBalancer), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddConversionFunc((*v1beta2.AWSClusterSpec)(nil), (*AWSClusterSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1beta2_AWSClusterSpec_To_v1beta1_AWSClusterSpec(a.(*v1beta2.AWSClusterSpec), b.(*AWSClusterSpec), scope)
 	}); err != nil {
 		return err
 	}
+	if err := s.AddConversionFunc((*v1beta2.AWSLoadBalancerSpec)(nil), (*AWSLoadBalancerSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta2_AWSLoadBalancerSpec_To_v1beta1_AWSLoadBalancerSpec(a.(*v1beta2.AWSLoadBalancerSpec), b.(*AWSLoadBalancerSpec), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddConversionFunc((*v1beta2.LoadBalancer)(nil), (*ClassicELB)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta2_LoadBalancer_To_v1beta1_ClassicELB(a.(*v1beta2.LoadBalancer), b.(*ClassicELB), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddConversionFunc((*v1beta2.NetworkStatus)(nil), (*NetworkStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta2_NetworkStatus_To_v1beta1_NetworkStatus(a.(*v1beta2.NetworkStatus), b.(*NetworkStatus), scope)
+	}); err != nil {
+		return err
+	}
 	return nil
 }
 
@@ -881,7 +881,15 @@ func autoConvert_v1beta1_AWSClusterSpec_To_v1beta2_AWSClusterSpec(in *AWSCluster
 	out.SSHKeyName = (*string)(unsafe.Pointer(in.SSHKeyName))
 	out.ControlPlaneEndpoint = in.ControlPlaneEndpoint
 	out.AdditionalTags = *(*v1beta2.Tags)(unsafe.Pointer(&in.AdditionalTags))
-	out.ControlPlaneLoadBalancer = (*v1beta2.AWSLoadBalancerSpec)(unsafe.Pointer(in.ControlPlaneLoadBalancer))
+	if in.ControlPlaneLoadBalancer != nil {
+		in, out := &in.ControlPlaneLoadBalancer, &out.ControlPlaneLoadBalancer
+		*out = new(v1beta2.AWSLoadBalancerSpec)
+		if err := Convert_v1beta1_AWSLoadBalancerSpec_To_v1beta2_AWSLoadBalancerSpec(*in, *out, s); err != nil {
+			return err
+		}
+	} else {
+		out.ControlPlaneLoadBalancer = nil
+	}
 	out.ImageLookupFormat = in.ImageLookupFormat
 	out.ImageLookupOrg = in.ImageLookupOrg
 	out.ImageLookupBaseOS = in.ImageLookupBaseOS
@@ -906,7 +914,15 @@ func autoConvert_v1beta2_AWSClusterSpec_To_v1beta1_AWSClusterSpec(in *v1beta2.AW
 	out.SSHKeyName = (*string)(unsafe.Pointer(in.SSHKeyName))
 	out.ControlPlaneEndpoint = in.ControlPlaneEndpoint
 	out.AdditionalTags = *(*Tags)(unsafe.Pointer(&in.AdditionalTags))
-	out.ControlPlaneLoadBalancer = (*AWSLoadBalancerSpec)(unsafe.Pointer(in.ControlPlaneLoadBalancer))
+	if in.ControlPlaneLoadBalancer != nil {
+		in, out := &in.ControlPlaneLoadBalancer, &out.ControlPlaneLoadBalancer
+		*out = new(AWSLoadBalancerSpec)
+		if err := Convert_v1beta2_AWSLoadBalancerSpec_To_v1beta1_AWSLoadBalancerSpec(*in, *out, s); err != nil {
+			return err
+		}
+	} else {
+		out.ControlPlaneLoadBalancer = nil
+	}
 	out.ImageLookupFormat = in.ImageLookupFormat
 	out.ImageLookupOrg = in.ImageLookupOrg
 	out.ImageLookupBaseOS = in.ImageLookupBaseOS
@@ -1166,10 +1182,10 @@ func Convert_v1beta2_AWSIdentityReference_To_v1beta1_AWSIdentityReference(in *v1
 
 func autoConvert_v1beta1_AWSLoadBalancerSpec_To_v1beta2_AWSLoadBalancerSpec(in *AWSLoadBalancerSpec, out *v1beta2.AWSLoadBalancerSpec, s conversion.Scope) error {
 	out.Name = (*string)(unsafe.Pointer(in.Name))
-	out.Scheme = (*v1beta2.ClassicELBScheme)(unsafe.Pointer(in.Scheme))
+	out.Scheme = (*v1beta2.ELBScheme)(unsafe.Pointer(in.Scheme))
 	out.CrossZoneLoadBalancing = in.CrossZoneLoadBalancing
 	out.Subnets = *(*[]string)(unsafe.Pointer(&in.Subnets))
-	out.HealthCheckProtocol = (*v1beta2.ClassicELBProtocol)(unsafe.Pointer(in.HealthCheckProtocol))
+	out.HealthCheckProtocol = (*v1beta2.ELBProtocol)(unsafe.Pointer(in.HealthCheckProtocol))
 	out.AdditionalSecurityGroups = *(*[]string)(unsafe.Pointer(&in.AdditionalSecurityGroups))
 	return nil
 }
@@ -1186,14 +1202,12 @@ func autoConvert_v1beta2_AWSLoadBalancerSpec_To_v1beta1_AWSLoadBalancerSpec(in *
 	out.Subnets = *(*[]string)(unsafe.Pointer(&in.Subnets))
 	out.HealthCheckProtocol = (*ClassicELBProtocol)(unsafe.Pointer(in.HealthCheckProtocol))
 	out.AdditionalSecurityGroups = *(*[]string)(unsafe.Pointer(&in.AdditionalSecurityGroups))
+	// WARNING: in.LoadBalancerType requires manual conversion: does not exist in peer-type
+	// WARNING: in.DisableHostsRewrite requires manual conversion: does not exist in peer-type
+	// WARNING: in.PreserveClientIP requires manual conversion: does not exist in peer-type
 	return nil
 }
 
-// Convert_v1beta2_AWSLoadBalancerSpec_To_v1beta1_AWSLoadBalancerSpec is an autogenerated conversion function.
-func Convert_v1beta2_AWSLoadBalancerSpec_To_v1beta1_AWSLoadBalancerSpec(in *v1beta2.AWSLoadBalancerSpec, out *AWSLoadBalancerSpec, s conversion.Scope) error {
-	return autoConvert_v1beta2_AWSLoadBalancerSpec_To_v1beta1_AWSLoadBalancerSpec(in, out, s)
-}
-
 func autoConvert_v1beta1_AWSMachine_To_v1beta2_AWSMachine(in *AWSMachine, out *v1beta2.AWSMachine, s conversion.Scope) error {
 	out.ObjectMeta = in.ObjectMeta
 	if err := Convert_v1beta1_AWSMachineSpec_To_v1beta2_AWSMachineSpec(&in.Spec, &out.Spec, s); err != nil {
@@ -1716,48 +1730,6 @@ func Convert_v1beta2_CNISpec_To_v1beta1_CNISpec(in *v1beta2.CNISpec, out *CNISpe
 	return autoConvert_v1beta2_CNISpec_To_v1beta1_CNISpec(in, out, s)
 }
 
-func autoConvert_v1beta1_ClassicELB_To_v1beta2_ClassicELB(in *ClassicELB, out *v1beta2.ClassicELB, s conversion.Scope) error {
-	out.Name = in.Name
-	out.DNSName = in.DNSName
-	out.Scheme = v1beta2.ClassicELBScheme(in.Scheme)
-	out.AvailabilityZones = *(*[]string)(unsafe.Pointer(&in.AvailabilityZones))
-	out.SubnetIDs = *(*[]string)(unsafe.Pointer(&in.SubnetIDs))
-	out.SecurityGroupIDs = *(*[]string)(unsafe.Pointer(&in.SecurityGroupIDs))
-	out.Listeners = *(*[]v1beta2.ClassicELBListener)(unsafe.Pointer(&in.Listeners))
-	out.HealthCheck = (*v1beta2.ClassicELBHealthCheck)(unsafe.Pointer(in.HealthCheck))
-	if err := Convert_v1beta1_ClassicELBAttributes_To_v1beta2_ClassicELBAttributes(&in.Attributes, &out.Attributes, s); err != nil {
-		return err
-	}
-	out.Tags = *(*map[string]string)(unsafe.Pointer(&in.Tags))
-	return nil
-}
-
-// Convert_v1beta1_ClassicELB_To_v1beta2_ClassicELB is an autogenerated conversion function.
-func Convert_v1beta1_ClassicELB_To_v1beta2_ClassicELB(in *ClassicELB, out *v1beta2.ClassicELB, s conversion.Scope) error {
-	return autoConvert_v1beta1_ClassicELB_To_v1beta2_ClassicELB(in, out, s)
-}
-
-func autoConvert_v1beta2_ClassicELB_To_v1beta1_ClassicELB(in *v1beta2.ClassicELB, out *ClassicELB, s conversion.Scope) error {
-	out.Name = in.Name
-	out.DNSName = in.DNSName
-	out.Scheme = ClassicELBScheme(in.Scheme)
-	out.AvailabilityZones = *(*[]string)(unsafe.Pointer(&in.AvailabilityZones))
-	out.SubnetIDs = *(*[]string)(unsafe.Pointer(&in.SubnetIDs))
-	out.SecurityGroupIDs = *(*[]string)(unsafe.Pointer(&in.SecurityGroupIDs))
-	out.Listeners = *(*[]ClassicELBListener)(unsafe.Pointer(&in.Listeners))
-	out.HealthCheck = (*ClassicELBHealthCheck)(unsafe.Pointer(in.HealthCheck))
-	if err := Convert_v1beta2_ClassicELBAttributes_To_v1beta1_ClassicELBAttributes(&in.Attributes, &out.Attributes, s); err != nil {
-		return err
-	}
-	out.Tags = *(*map[string]string)(unsafe.Pointer(&in.Tags))
-	return nil
-}
-
-// Convert_v1beta2_ClassicELB_To_v1beta1_ClassicELB is an autogenerated conversion function.
-func Convert_v1beta2_ClassicELB_To_v1beta1_ClassicELB(in *v1beta2.ClassicELB, out *ClassicELB, s conversion.Scope) error {
-	return autoConvert_v1beta2_ClassicELB_To_v1beta1_ClassicELB(in, out, s)
-}
-
 func autoConvert_v1beta1_ClassicELBAttributes_To_v1beta2_ClassicELBAttributes(in *ClassicELBAttributes, out *v1beta2.ClassicELBAttributes, s conversion.Scope) error {
 	out.IdleTimeout = time.Duration(in.IdleTimeout)
 	out.CrossZoneLoadBalancing = in.CrossZoneLoadBalancing
@@ -1809,9 +1781,9 @@ func Convert_v1beta2_ClassicELBHealthCheck_To_v1beta1_ClassicELBHealthCheck(in *
 }
 
 func autoConvert_v1beta1_ClassicELBListener_To_v1beta2_ClassicELBListener(in *ClassicELBListener, out *v1beta2.ClassicELBListener, s conversion.Scope) error {
-	out.Protocol = v1beta2.ClassicELBProtocol(in.Protocol)
+	out.Protocol = v1beta2.ELBProtocol(in.Protocol)
 	out.Port = in.Port
-	out.InstanceProtocol = v1beta2.ClassicELBProtocol(in.InstanceProtocol)
+	out.InstanceProtocol = v1beta2.ELBProtocol(in.InstanceProtocol)
 	out.InstancePort = in.InstancePort
 	return nil
 }
@@ -2052,7 +2024,7 @@ func Convert_v1beta2_NetworkSpec_To_v1beta1_NetworkSpec(in *v1beta2.NetworkSpec,
 
 func autoConvert_v1beta1_NetworkStatus_To_v1beta2_NetworkStatus(in *NetworkStatus, out *v1beta2.NetworkStatus, s conversion.Scope) error {
 	out.SecurityGroups = *(*map[v1beta2.SecurityGroupRole]v1beta2.SecurityGroup)(unsafe.Pointer(&in.SecurityGroups))
-	if err := Convert_v1beta1_ClassicELB_To_v1beta2_ClassicELB(&in.APIServerELB, &out.APIServerELB, s); err != nil {
+	if err := Convert_v1beta1_ClassicELB_To_v1beta2_LoadBalancer(&in.APIServerELB, &out.APIServerELB, s); err != nil {
 		return err
 	}
 	return nil
@@ -2065,17 +2037,12 @@ func Convert_v1beta1_NetworkStatus_To_v1beta2_NetworkStatus(in *NetworkStatus, o
 
 func autoConvert_v1beta2_NetworkStatus_To_v1beta1_NetworkStatus(in *v1beta2.NetworkStatus, out *NetworkStatus, s conversion.Scope) error {
 	out.SecurityGroups = *(*map[SecurityGroupRole]SecurityGroup)(unsafe.Pointer(&in.SecurityGroups))
-	if err := Convert_v1beta2_ClassicELB_To_v1beta1_ClassicELB(&in.APIServerELB, &out.APIServerELB, s); err != nil {
+	if err := Convert_v1beta2_LoadBalancer_To_v1beta1_ClassicELB(&in.APIServerELB, &out.APIServerELB, s); err != nil {
 		return err
 	}
 	return nil
 }
 
-// Convert_v1beta2_NetworkStatus_To_v1beta1_NetworkStatus is an autogenerated conversion function.
-func Convert_v1beta2_NetworkStatus_To_v1beta1_NetworkStatus(in *v1beta2.NetworkStatus, out *NetworkStatus, s conversion.Scope) error {
-	return autoConvert_v1beta2_NetworkStatus_To_v1beta1_NetworkStatus(in, out, s)
-}
-
 func autoConvert_v1beta1_RouteTable_To_v1beta2_RouteTable(in *RouteTable, out *v1beta2.RouteTable, s conversion.Scope) error {
 	out.ID = in.ID
 	return nil
diff --git a/api/v1beta2/awscluster_types.go b/api/v1beta2/awscluster_types.go
index d95115a71..732a5f143 100644
--- a/api/v1beta2/awscluster_types.go
+++ b/api/v1beta2/awscluster_types.go
@@ -152,6 +152,15 @@ type Bastion struct {
 	AMI string `json:"ami,omitempty"`
 }
 
+type LoadBalancerType string
+
+var (
+	LoadBalancerTypeClassic = LoadBalancerType("classic")
+	LoadBalancerTypeELB     = LoadBalancerType("elb")
+	LoadBalancerTypeALB     = LoadBalancerType("alb")
+	LoadBalancerTypeNLB     = LoadBalancerType("nlb")
+)
+
 // AWSLoadBalancerSpec defines the desired state of an AWS load balancer.
 type AWSLoadBalancerSpec struct {
 	// Name sets the name of the classic ELB load balancer. As per AWS, the name must be unique
@@ -167,7 +176,7 @@ type AWSLoadBalancerSpec struct {
 	// +kubebuilder:default=internet-facing
 	// +kubebuilder:validation:Enum=internet-facing;internal
 	// +optional
-	Scheme *ClassicELBScheme `json:"scheme,omitempty"`
+	Scheme *ELBScheme `json:"scheme,omitempty"`
 
 	// CrossZoneLoadBalancing enables the classic ELB cross availability zone balancing.
 	//
@@ -184,15 +193,27 @@ type AWSLoadBalancerSpec struct {
 	// +optional
 	Subnets []string `json:"subnets,omitempty"`
 
-	// HealthCheckProtocol sets the protocol type for classic ELB health check target
-	// default value is ClassicELBProtocolSSL
+	// HealthCheckProtocol sets the protocol type for ELB health check target
+	// default value is ELBProtocolSSL
 	// +optional
-	HealthCheckProtocol *ClassicELBProtocol `json:"healthCheckProtocol,omitempty"`
+	HealthCheckProtocol *ELBProtocol `json:"healthCheckProtocol,omitempty"`
 
 	// AdditionalSecurityGroups sets the security groups used by the load balancer. Expected to be security group IDs
 	// This is optional - if not provided new security groups will be created for the load balancer
 	// +optional
 	AdditionalSecurityGroups []string `json:"additionalSecurityGroups,omitempty"`
+
+	// LoadBalancerType sets the type for a load balancer. The default type is classic.
+	// +kubebuilder:validation:Enum:=classic;elb;alb;nlb
+	LoadBalancerType LoadBalancerType `json:"loadBalancerType,omitempty"`
+
+	// DisableHostsRewrite disabled the hair pinning issue solution that adds the NLB's address as 127.0.0.1 to the hosts
+	// file of each instance. This is by default, false.
+	DisableHostsRewrite bool `json:"disableHostsRewrite,omitempty"`
+
+	// PreserveClientIP lets the user control if preservation of client ips must be retained or not.
+	// If this is enabled 6443 will be opened to 0.0.0.0/0.
+	PreserveClientIP bool `json:"preserveClientIP,omitempty"`
 }
 
 // AWSClusterStatus defines the observed state of AWSCluster.
diff --git a/api/v1beta2/awscluster_webhook.go b/api/v1beta2/awscluster_webhook.go
index d12799cd3..013b5e280 100644
--- a/api/v1beta2/awscluster_webhook.go
+++ b/api/v1beta2/awscluster_webhook.go
@@ -92,7 +92,7 @@ func (r *AWSCluster) ValidateUpdate(old runtime.Object) error {
 	}
 	if oldC.Spec.ControlPlaneLoadBalancer == nil {
 		// If old scheme was nil, the only value accepted here is the default value: internet-facing
-		if newLoadBalancer.Scheme != nil && newLoadBalancer.Scheme.String() != ClassicELBSchemeInternetFacing.String() {
+		if newLoadBalancer.Scheme != nil && newLoadBalancer.Scheme.String() != ELBSchemeInternetFacing.String() {
 			allErrs = append(allErrs,
 				field.Invalid(field.NewPath("spec", "controlPlaneLoadBalancer", "scheme"),
 					r.Spec.ControlPlaneLoadBalancer.Scheme, "field is immutable, default value was set to internet-facing"),
@@ -117,7 +117,7 @@ func (r *AWSCluster) ValidateUpdate(old runtime.Object) error {
 		}
 	}
 
-	// Block the update for HealthCheckProtocol :
+	// Block the update for Protocol :
 	// - if it was not set in old spec but added in new spec
 	// - if it was set in old spec but changed in new spec
 	if !cmp.Equal(newLoadBalancer.HealthCheckProtocol, existingLoadBalancer.HealthCheckProtocol) {
diff --git a/api/v1beta2/defaults.go b/api/v1beta2/defaults.go
index 02ed38722..f938d5f46 100644
--- a/api/v1beta2/defaults.go
+++ b/api/v1beta2/defaults.go
@@ -13,6 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 */
+
 package v1beta2
 
 import (
@@ -61,7 +62,10 @@ func SetDefaults_AWSClusterSpec(s *AWSClusterSpec) { //nolint:golint,stylecheck
 		}
 	}
 	if s.ControlPlaneLoadBalancer == nil {
-		s.ControlPlaneLoadBalancer = &AWSLoadBalancerSpec{Scheme: &ClassicELBSchemeInternetFacing}
+		s.ControlPlaneLoadBalancer = &AWSLoadBalancerSpec{
+			Scheme:           &ELBSchemeInternetFacing,
+			LoadBalancerType: LoadBalancerTypeClassic,
+		}
 	}
 }
 
diff --git a/api/v1beta2/network_types.go b/api/v1beta2/network_types.go
index 31ffb0f86..bf8743fc6 100644
--- a/api/v1beta2/network_types.go
+++ b/api/v1beta2/network_types.go
@@ -22,55 +22,113 @@ import (
 	"time"
 )
 
+const (
+	// DefaultAPIServerPort defines the API server port when defining a Load Balancer.
+	DefaultAPIServerPort = 6443
+	// DefaultAPIServerPortString defines the API server port as a string for convenience.
+	DefaultAPIServerPortString = "6443"
+)
+
 // NetworkStatus encapsulates AWS networking resources.
 type NetworkStatus struct {
 	// SecurityGroups is a map from the role/kind of the security group to its unique name, if any.
 	SecurityGroups map[SecurityGroupRole]SecurityGroup `json:"securityGroups,omitempty"`
 
-	// APIServerELB is the Kubernetes api server classic load balancer.
-	APIServerELB ClassicELB `json:"apiServerElb,omitempty"`
+	// APIServerELB is the Kubernetes api server load balancer.
+	APIServerELB LoadBalancer `json:"apiServerElb,omitempty"`
 }
 
-// ClassicELBScheme defines the scheme of a classic load balancer.
-type ClassicELBScheme string
+// ELBScheme defines the scheme of a load balancer.
+type ELBScheme string
 
 var (
-	// ClassicELBSchemeInternetFacing defines an internet-facing, publicly
-	// accessible AWS Classic ELB scheme.
-	ClassicELBSchemeInternetFacing = ClassicELBScheme("internet-facing")
+	// ELBSchemeInternetFacing defines an internet-facing, publicly
+	// accessible AWS ELB scheme.
+	ELBSchemeInternetFacing = ELBScheme("internet-facing")
 
-	// ClassicELBSchemeInternal defines an internal-only facing
+	// ELBSchemeInternal defines an internal-only facing
 	// load balancer internal to an ELB.
-	ClassicELBSchemeInternal = ClassicELBScheme("internal")
+	ELBSchemeInternal = ELBScheme("internal")
 )
 
-func (e ClassicELBScheme) String() string {
+func (e ELBScheme) String() string {
 	return string(e)
 }
 
-// ClassicELBProtocol defines listener protocols for a classic load balancer.
-type ClassicELBProtocol string
+// ELBProtocol defines listener protocols for a load balancer.
+type ELBProtocol string
 
-func (e ClassicELBProtocol) String() string {
+func (e ELBProtocol) String() string {
 	return string(e)
 }
 
 var (
-	// ClassicELBProtocolTCP defines the ELB API string representing the TCP protocol.
-	ClassicELBProtocolTCP = ClassicELBProtocol("TCP")
+	// ELBProtocolTCP defines the ELB API string representing the TCP protocol.
+	ELBProtocolTCP = ELBProtocol("TCP")
+	// ELBProtocolSSL defines the ELB API string representing the TLS protocol.
+	ELBProtocolSSL = ELBProtocol("SSL")
+	// ELBProtocolHTTP defines the ELB API string representing the HTTP protocol at L7.
+	ELBProtocolHTTP = ELBProtocol("HTTP")
+	// ELBProtocolHTTPS defines the ELB API string representing the HTTP protocol at L7.
+	ELBProtocolHTTPS = ELBProtocol("HTTPS")
+	// ELBProtocolTLS defines the NLB API string representing the TLS protocol.
+	ELBProtocolTLS = ELBProtocol("TLS")
+	// ELBProtocolUDP defines the NLB API string representing the UPD protocol.
+	ELBProtocolUDP = ELBProtocol("UDP")
+)
 
-	// ClassicELBProtocolSSL defines the ELB API string representing the TLS protocol.
-	ClassicELBProtocolSSL = ClassicELBProtocol("SSL")
+// TargetGroupHealthCheck defines health check settings for the target group.
+type TargetGroupHealthCheck struct {
+	Protocol        *string `json:"protocol,omitempty"`
+	Path            *string `json:"path,omitempty"`
+	Port            *string `json:"port,omitempty"`
+	IntervalSeconds *int64  `json:"intervalSeconds,omitempty"`
+	TimeoutSeconds  *int64  `json:"timeoutSeconds,omitempty"`
+	ThresholdCount  *int64  `json:"thresholdCount,omitempty"`
+}
 
-	// ClassicELBProtocolHTTP defines the ELB API string representing the HTTP protocol at L7.
-	ClassicELBProtocolHTTP = ClassicELBProtocol("HTTP")
+// TargetGroupAttribute defines attribute key values for V2 Load Balancer Attributes.
+type TargetGroupAttribute string
 
-	// ClassicELBProtocolHTTPS defines the ELB API string representing the HTTP protocol at L7.
-	ClassicELBProtocolHTTPS = ClassicELBProtocol("HTTPS")
+var (
+	TargetGroupAttributeEnablePreserveClientIP = "preserve_client_ip.enabled"
 )
 
-// ClassicELB defines an AWS classic load balancer.
-type ClassicELB struct {
+// LoadBalancerAttribute defines a set of attributes for a V2 load balancer
+type LoadBalancerAttribute string
+
+var (
+	LoadBalancerAttributeEnableLoadBalancingCrossZone           = "load_balancing.cross_zone.enabled"
+	LoadBalancerAttributeIdleTimeTimeoutSeconds                 = "idle_timeout.timeout_seconds"
+	LoadBalancerAttributeIdleTimeDefaultTimeoutSecondsInSeconds = "60"
+)
+
+// TargetGroupSpec specifies target group settings for a given listener.
+// This is created first, and the ARN is then passed to the listener.
+type TargetGroupSpec struct {
+	// Name of the TargetGroup. Must be unique over the same group of listeners.
+	Name string `json:"name"`
+	// Port is the exposed port
+	Port int64 `json:"port"`
+	// +kubebuilder:validation:Enum=tcp;tls;upd
+	Protocol ELBProtocol `json:"protocol"`
+	VpcID    string      `json:"vpcId"`
+	// HealthCheck is the elb health check associated with the load balancer.
+	HealthCheck *TargetGroupHealthCheck `json:"targetGroupHealthCheck,omitempty"`
+}
+
+// Listener defines an AWS network load balancer listener.
+type Listener struct {
+	Protocol    ELBProtocol     `json:"protocol"`
+	Port        int64           `json:"port"`
+	TargetGroup TargetGroupSpec `json:"targetGroup"`
+}
+
+// LoadBalancer defines an AWS load balancer.
+type LoadBalancer struct {
+	// ARN of the load balancer. Unlike the ClassicLB, ARN is used mostly
+	// to define and get it.
+	ARN string `json:"arn,omitempty"`
 	// The name of the load balancer. It must be unique within the set of load balancers
 	// defined in the region. It also serves as identifier.
 	// +optional
@@ -80,7 +138,7 @@ type ClassicELB struct {
 	DNSName string `json:"dnsName,omitempty"`
 
 	// Scheme is the load balancer scheme, either internet-facing or private.
-	Scheme ClassicELBScheme `json:"scheme,omitempty"`
+	Scheme ELBScheme `json:"scheme,omitempty"`
 
 	// AvailabilityZones is an array of availability zones in the VPC attached to the load balancer.
 	AvailabilityZones []string `json:"availabilityZones,omitempty"`
@@ -91,26 +149,36 @@ type ClassicELB struct {
 	// SecurityGroupIDs is an array of security groups assigned to the load balancer.
 	SecurityGroupIDs []string `json:"securityGroupIds,omitempty"`
 
-	// Listeners is an array of classic elb listeners associated with the load balancer. There must be at least one.
-	Listeners []ClassicELBListener `json:"listeners,omitempty"`
+	// ClassicELBListeners is an array of classic elb listeners associated with the load balancer. There must be at least one.
+	ClassicELBListeners []ClassicELBListener `json:"listeners,omitempty"`
 
 	// HealthCheck is the classic elb health check associated with the load balancer.
 	HealthCheck *ClassicELBHealthCheck `json:"healthChecks,omitempty"`
 
-	// Attributes defines extra attributes associated with the load balancer.
-	Attributes ClassicELBAttributes `json:"attributes,omitempty"`
+	// ClassicElbAttributes defines extra attributes associated with the load balancer.
+	ClassicElbAttributes ClassicELBAttributes `json:"attributes,omitempty"`
 
 	// Tags is a map of tags associated with the load balancer.
 	Tags map[string]string `json:"tags,omitempty"`
+
+	// ELBListeners is an array of listeners associated with the load balancer. There must be at least one.
+	ELBListeners []Listener `json:"elbListeners,omitempty"`
+
+	// ELBAttributes defines extra attributes associated with v2 load balancers.
+	ELBAttributes map[string]*string `json:"elbAttributes,omitempty"`
+
+	// LoadBalancerType sets the type for a load balancer. The default type is classic.
+	// +kubebuilder:validation:Enum:=classic;elb;alb;nlb
+	LoadBalancerType LoadBalancerType `json:"loadBalancerType"`
 }
 
 // IsUnmanaged returns true if the Classic ELB is unmanaged.
-func (b *ClassicELB) IsUnmanaged(clusterName string) bool {
+func (b *LoadBalancer) IsUnmanaged(clusterName string) bool {
 	return b.Name != "" && !Tags(b.Tags).HasOwned(clusterName)
 }
 
 // IsManaged returns true if Classic ELB is managed.
-func (b *ClassicELB) IsManaged(clusterName string) bool {
+func (b *LoadBalancer) IsManaged(clusterName string) bool {
 	return !b.IsUnmanaged(clusterName)
 }
 
@@ -127,10 +195,10 @@ type ClassicELBAttributes struct {
 
 // ClassicELBListener defines an AWS classic load balancer listener.
 type ClassicELBListener struct {
-	Protocol         ClassicELBProtocol `json:"protocol"`
-	Port             int64              `json:"port"`
-	InstanceProtocol ClassicELBProtocol `json:"instanceProtocol"`
-	InstancePort     int64              `json:"instancePort"`
+	Protocol         ELBProtocol `json:"protocol"`
+	Port             int64       `json:"port"`
+	InstanceProtocol ELBProtocol `json:"instanceProtocol"`
+	InstancePort     int64       `json:"instancePort"`
 }
 
 // ClassicELBHealthCheck defines an AWS classic load balancer health check.
diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go
index 6f8fb1093..199f547c5 100644
--- a/api/v1beta2/zz_generated.deepcopy.go
+++ b/api/v1beta2/zz_generated.deepcopy.go
@@ -556,7 +556,7 @@ func (in *AWSLoadBalancerSpec) DeepCopyInto(out *AWSLoadBalancerSpec) {
 	}
 	if in.Scheme != nil {
 		in, out := &in.Scheme, &out.Scheme
-		*out = new(ClassicELBScheme)
+		*out = new(ELBScheme)
 		**out = **in
 	}
 	if in.Subnets != nil {
@@ -566,7 +566,7 @@ func (in *AWSLoadBalancerSpec) DeepCopyInto(out *AWSLoadBalancerSpec) {
 	}
 	if in.HealthCheckProtocol != nil {
 		in, out := &in.HealthCheckProtocol, &out.HealthCheckProtocol
-		*out = new(ClassicELBProtocol)
+		*out = new(ELBProtocol)
 		**out = **in
 	}
 	if in.AdditionalSecurityGroups != nil {
@@ -1160,54 +1160,6 @@ func (in *CNISpec) DeepCopy() *CNISpec {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ClassicELB) DeepCopyInto(out *ClassicELB) {
-	*out = *in
-	if in.AvailabilityZones != nil {
-		in, out := &in.AvailabilityZones, &out.AvailabilityZones
-		*out = make([]string, len(*in))
-		copy(*out, *in)
-	}
-	if in.SubnetIDs != nil {
-		in, out := &in.SubnetIDs, &out.SubnetIDs
-		*out = make([]string, len(*in))
-		copy(*out, *in)
-	}
-	if in.SecurityGroupIDs != nil {
-		in, out := &in.SecurityGroupIDs, &out.SecurityGroupIDs
-		*out = make([]string, len(*in))
-		copy(*out, *in)
-	}
-	if in.Listeners != nil {
-		in, out := &in.Listeners, &out.Listeners
-		*out = make([]ClassicELBListener, len(*in))
-		copy(*out, *in)
-	}
-	if in.HealthCheck != nil {
-		in, out := &in.HealthCheck, &out.HealthCheck
-		*out = new(ClassicELBHealthCheck)
-		**out = **in
-	}
-	out.Attributes = in.Attributes
-	if in.Tags != nil {
-		in, out := &in.Tags, &out.Tags
-		*out = make(map[string]string, len(*in))
-		for key, val := range *in {
-			(*out)[key] = val
-		}
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClassicELB.
-func (in *ClassicELB) DeepCopy() *ClassicELB {
-	if in == nil {
-		return nil
-	}
-	out := new(ClassicELB)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ClassicELBAttributes) DeepCopyInto(out *ClassicELBAttributes) {
 	*out = *in
@@ -1463,6 +1415,92 @@ func (in *Instance) DeepCopy() *Instance {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Listener) DeepCopyInto(out *Listener) {
+	*out = *in
+	in.TargetGroup.DeepCopyInto(&out.TargetGroup)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Listener.
+func (in *Listener) DeepCopy() *Listener {
+	if in == nil {
+		return nil
+	}
+	out := new(Listener)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LoadBalancer) DeepCopyInto(out *LoadBalancer) {
+	*out = *in
+	if in.AvailabilityZones != nil {
+		in, out := &in.AvailabilityZones, &out.AvailabilityZones
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.SubnetIDs != nil {
+		in, out := &in.SubnetIDs, &out.SubnetIDs
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.SecurityGroupIDs != nil {
+		in, out := &in.SecurityGroupIDs, &out.SecurityGroupIDs
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.ClassicELBListeners != nil {
+		in, out := &in.ClassicELBListeners, &out.ClassicELBListeners
+		*out = make([]ClassicELBListener, len(*in))
+		copy(*out, *in)
+	}
+	if in.HealthCheck != nil {
+		in, out := &in.HealthCheck, &out.HealthCheck
+		*out = new(ClassicELBHealthCheck)
+		**out = **in
+	}
+	out.ClassicElbAttributes = in.ClassicElbAttributes
+	if in.Tags != nil {
+		in, out := &in.Tags, &out.Tags
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+	if in.ELBListeners != nil {
+		in, out := &in.ELBListeners, &out.ELBListeners
+		*out = make([]Listener, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.ELBAttributes != nil {
+		in, out := &in.ELBAttributes, &out.ELBAttributes
+		*out = make(map[string]*string, len(*in))
+		for key, val := range *in {
+			var outVal *string
+			if val == nil {
+				(*out)[key] = nil
+			} else {
+				in, out := &val, &outVal
+				*out = new(string)
+				**out = **in
+			}
+			(*out)[key] = outVal
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancer.
+func (in *LoadBalancer) DeepCopy() *LoadBalancer {
+	if in == nil {
+		return nil
+	}
+	out := new(LoadBalancer)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *NetworkSpec) DeepCopyInto(out *NetworkSpec) {
 	*out = *in
@@ -1679,6 +1717,71 @@ func (in Tags) DeepCopy() Tags {
 	return *out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TargetGroupHealthCheck) DeepCopyInto(out *TargetGroupHealthCheck) {
+	*out = *in
+	if in.Protocol != nil {
+		in, out := &in.Protocol, &out.Protocol
+		*out = new(string)
+		**out = **in
+	}
+	if in.Path != nil {
+		in, out := &in.Path, &out.Path
+		*out = new(string)
+		**out = **in
+	}
+	if in.Port != nil {
+		in, out := &in.Port, &out.Port
+		*out = new(string)
+		**out = **in
+	}
+	if in.IntervalSeconds != nil {
+		in, out := &in.IntervalSeconds, &out.IntervalSeconds
+		*out = new(int64)
+		**out = **in
+	}
+	if in.TimeoutSeconds != nil {
+		in, out := &in.TimeoutSeconds, &out.TimeoutSeconds
+		*out = new(int64)
+		**out = **in
+	}
+	if in.ThresholdCount != nil {
+		in, out := &in.ThresholdCount, &out.ThresholdCount
+		*out = new(int64)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetGroupHealthCheck.
+func (in *TargetGroupHealthCheck) DeepCopy() *TargetGroupHealthCheck {
+	if in == nil {
+		return nil
+	}
+	out := new(TargetGroupHealthCheck)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TargetGroupSpec) DeepCopyInto(out *TargetGroupSpec) {
+	*out = *in
+	if in.HealthCheck != nil {
+		in, out := &in.HealthCheck, &out.HealthCheck
+		*out = new(TargetGroupHealthCheck)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetGroupSpec.
+func (in *TargetGroupSpec) DeepCopy() *TargetGroupSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(TargetGroupSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *VPCSpec) DeepCopyInto(out *VPCSpec) {
 	*out = *in
diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml
index 78648977e..41ce07dc3 100644
--- a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml
+++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml
@@ -1087,12 +1087,15 @@ spec:
                   used by the control plane
                 properties:
                   apiServerElb:
-                    description: APIServerELB is the Kubernetes api server classic
-                      load balancer.
+                    description: APIServerELB is the Kubernetes api server load balancer.
                     properties:
+                      arn:
+                        description: ARN of the load balancer. Unlike the ClassicLB,
+                          ARN is used mostly to define and get it.
+                        type: string
                       attributes:
-                        description: Attributes defines extra attributes associated
-                          with the load balancer.
+                        description: ClassicElbAttributes defines extra attributes
+                          associated with the load balancer.
                         properties:
                           crossZoneLoadBalancing:
                             description: CrossZoneLoadBalancing enables the classic
@@ -1114,6 +1117,81 @@ spec:
                       dnsName:
                         description: DNSName is the dns name of the load balancer.
                         type: string
+                      elbAttributes:
+                        additionalProperties:
+                          type: string
+                        description: ELBAttributes defines extra attributes associated
+                          with v2 load balancers.
+                        type: object
+                      elbListeners:
+                        description: ELBListeners is an array of listeners associated
+                          with the load balancer. There must be at least one.
+                        items:
+                          description: Listener defines an AWS network load balancer
+                            listener.
+                          properties:
+                            port:
+                              format: int64
+                              type: integer
+                            protocol:
+                              description: ELBProtocol defines listener protocols
+                                for a load balancer.
+                              type: string
+                            targetGroup:
+                              description: TargetGroupSpec specifies target group
+                                settings for a given listener. This is created first,
+                                and the ARN is then passed to the listener.
+                              properties:
+                                name:
+                                  description: Name of the TargetGroup. Must be unique
+                                    over the same group of listeners.
+                                  type: string
+                                port:
+                                  description: Port is the exposed port
+                                  format: int64
+                                  type: integer
+                                protocol:
+                                  description: ELBProtocol defines listener protocols
+                                    for a load balancer.
+                                  enum:
+                                  - tcp
+                                  - tls
+                                  - upd
+                                  type: string
+                                targetGroupHealthCheck:
+                                  description: HealthCheck is the elb health check
+                                    associated with the load balancer.
+                                  properties:
+                                    intervalSeconds:
+                                      format: int64
+                                      type: integer
+                                    path:
+                                      type: string
+                                    port:
+                                      type: string
+                                    protocol:
+                                      type: string
+                                    thresholdCount:
+                                      format: int64
+                                      type: integer
+                                    timeoutSeconds:
+                                      format: int64
+                                      type: integer
+                                  type: object
+                                vpcId:
+                                  type: string
+                              required:
+                              - name
+                              - port
+                              - protocol
+                              - vpcId
+                              type: object
+                          required:
+                          - port
+                          - protocol
+                          - targetGroup
+                          type: object
+                        type: array
                       healthChecks:
                         description: HealthCheck is the classic elb health check associated
                           with the load balancer.
@@ -1148,9 +1226,9 @@ spec:
                         - unhealthyThreshold
                         type: object
                       listeners:
-                        description: Listeners is an array of classic elb listeners
-                          associated with the load balancer. There must be at least
-                          one.
+                        description: ClassicELBListeners is an array of classic elb
+                          listeners associated with the load balancer. There must
+                          be at least one.
                         items:
                           description: ClassicELBListener defines an AWS classic load
                             balancer listener.
@@ -1159,15 +1237,15 @@ spec:
                               format: int64
                               type: integer
                             instanceProtocol:
-                              description: ClassicELBProtocol defines listener protocols
-                                for a classic load balancer.
+                              description: ELBProtocol defines listener protocols
+                                for a load balancer.
                               type: string
                             port:
                               format: int64
                               type: integer
                             protocol:
-                              description: ClassicELBProtocol defines listener protocols
-                                for a classic load balancer.
+                              description: ELBProtocol defines listener protocols
+                                for a load balancer.
                               type: string
                           required:
                           - instancePort
@@ -1176,6 +1254,15 @@ spec:
                           - protocol
                           type: object
                         type: array
+                      loadBalancerType:
+                        description: LoadBalancerType sets the type for a load balancer.
+                          The default type is classic.
+                        enum:
+                        - classic
+                        - elb
+                        - alb
+                        - nlb
+                        type: string
                       name:
                         description: The name of the load balancer. It must be unique
                           within the set of load balancers defined in the region.
@@ -1203,6 +1290,8 @@ spec:
                         description: Tags is a map of tags associated with the load
                           balancer.
                         type: object
+                    required:
+                    - loadBalancerType
                     type: object
                   securityGroups:
                     additionalProperties:
@@ -2365,12 +2454,15 @@ spec:
                   used by the control plane
                 properties:
                   apiServerElb:
-                    description: APIServerELB is the Kubernetes api server classic
-                      load balancer.
+                    description: APIServerELB is the Kubernetes api server load balancer.
                     properties:
+                      arn:
+                        description: ARN of the load balancer. Unlike the ClassicLB,
+                          ARN is used mostly to define and get it.
+                        type: string
                       attributes:
-                        description: Attributes defines extra attributes associated
-                          with the load balancer.
+                        description: ClassicElbAttributes defines extra attributes
+                          associated with the load balancer.
                         properties:
                           crossZoneLoadBalancing:
                             description: CrossZoneLoadBalancing enables the classic
@@ -2392,6 +2484,81 @@ spec:
                       dnsName:
                         description: DNSName is the dns name of the load balancer.
                         type: string
+                      elbAttributes:
+                        additionalProperties:
+                          type: string
+                        description: ELBAttributes defines extra attributes associated
+                          with v2 load balancers.
+                        type: object
+                      elbListeners:
+                        description: ELBListeners is an array of listeners associated
+                          with the load balancer. There must be at least one.
+                        items:
+                          description: Listener defines an AWS network load balancer
+                            listener.
+                          properties:
+                            port:
+                              format: int64
+                              type: integer
+                            protocol:
+                              description: ELBProtocol defines listener protocols
+                                for a load balancer.
+                              type: string
+                            targetGroup:
+                              description: TargetGroupSpec specifies target group
+                                settings for a given listener. This is created first,
+                                and the ARN is then passed to the listener.
+                              properties:
+                                name:
+                                  description: Name of the TargetGroup. Must be unique
+                                    over the same group of listeners.
+                                  type: string
+                                port:
+                                  description: Port is the exposed port
+                                  format: int64
+                                  type: integer
+                                protocol:
+                                  description: ELBProtocol defines listener protocols
+                                    for a load balancer.
+                                  enum:
+                                  - tcp
+                                  - tls
+                                  - upd
+                                  type: string
+                                targetGroupHealthCheck:
+                                  description: HealthCheck is the elb health check
+                                    associated with the load balancer.
+                                  properties:
+                                    intervalSeconds:
+                                      format: int64
+                                      type: integer
+                                    path:
+                                      type: string
+                                    port:
+                                      type: string
+                                    protocol:
+                                      type: string
+                                    thresholdCount:
+                                      format: int64
+                                      type: integer
+                                    timeoutSeconds:
+                                      format: int64
+                                      type: integer
+                                  type: object
+                                vpcId:
+                                  type: string
+                              required:
+                              - name
+                              - port
+                              - protocol
+                              - vpcId
+                              type: object
+                          required:
+                          - port
+                          - protocol
+                          - targetGroup
+                          type: object
+                        type: array
                       healthChecks:
                         description: HealthCheck is the classic elb health check associated
                           with the load balancer.
@@ -2426,9 +2593,9 @@ spec:
                         - unhealthyThreshold
                         type: object
                       listeners:
-                        description: Listeners is an array of classic elb listeners
-                          associated with the load balancer. There must be at least
-                          one.
+                        description: ClassicELBListeners is an array of classic elb
+                          listeners associated with the load balancer. There must
+                          be at least one.
                         items:
                           description: ClassicELBListener defines an AWS classic load
                             balancer listener.
@@ -2437,15 +2604,15 @@ spec:
                               format: int64
                               type: integer
                             instanceProtocol:
-                              description: ClassicELBProtocol defines listener protocols
-                                for a classic load balancer.
+                              description: ELBProtocol defines listener protocols
+                                for a load balancer.
                               type: string
                             port:
                               format: int64
                               type: integer
                             protocol:
-                              description: ClassicELBProtocol defines listener protocols
-                                for a classic load balancer.
+                              description: ELBProtocol defines listener protocols
+                                for a load balancer.
                               type: string
                           required:
                           - instancePort
@@ -2454,6 +2621,15 @@ spec:
                           - protocol
                           type: object
                         type: array
+                      loadBalancerType:
+                        description: LoadBalancerType sets the type for a load balancer.
+                          The default type is classic.
+                        enum:
+                        - classic
+                        - elb
+                        - alb
+                        - nlb
+                        type: string
                       name:
                         description: The name of the load balancer. It must be unique
                           within the set of load balancers defined in the region.
@@ -2481,6 +2657,8 @@ spec:
                         description: Tags is a map of tags associated with the load
                           balancer.
                         type: object
+                    required:
+                    - loadBalancerType
                     type: object
                   securityGroups:
                     additionalProperties:
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml
index 125082fc2..004959dea 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml
@@ -984,9 +984,23 @@ spec:
                       registered instances in its Availability Zone only. \n Defaults
                       to false."
                     type: boolean
+                  disableHostsRewrite:
+                    description: DisableHostsRewrite disabled the hair pinning issue
+                      solution that adds the NLB's address as 127.0.0.1 to the hosts
+                      file of each instance. This is by default, false.
+                    type: boolean
                   healthCheckProtocol:
-                    description: HealthCheckProtocol sets the protocol type for classic
-                      ELB health check target default value is ClassicELBProtocolSSL
+                    description: HealthCheckProtocol sets the protocol type for ELB
+                      health check target default value is ELBProtocolSSL
+                    type: string
+                  loadBalancerType:
+                    description: LoadBalancerType sets the type for a load balancer.
+                      The default type is classic.
+                    enum:
+                    - classic
+                    - elb
+                    - alb
+                    - nlb
                     type: string
                   name:
                     description: Name sets the name of the classic ELB load balancer.
@@ -997,6 +1011,11 @@ spec:
                     maxLength: 32
                     pattern: ^[A-Za-z0-9]([A-Za-z0-9]{0,31}|[-A-Za-z0-9]{0,30}[A-Za-z0-9])$
                     type: string
+                  preserveClientIP:
+                    description: PreserveClientIP lets the user control if preservation
+                      of client ips must be retained or not. If this is enabled 6443
+                      will be opened to 0.0.0.0/0.
+                    type: boolean
                   scheme:
                     default: internet-facing
                     description: Scheme sets the scheme of the load balancer (defaults
@@ -1520,12 +1539,15 @@ spec:
                 description: NetworkStatus encapsulates AWS networking resources.
                 properties:
                   apiServerElb:
-                    description: APIServerELB is the Kubernetes api server classic
-                      load balancer.
+                    description: APIServerELB is the Kubernetes api server load balancer.
                     properties:
+                      arn:
+                        description: ARN of the load balancer. Unlike the ClassicLB,
+                          ARN is used mostly to define and get it.
+                        type: string
                       attributes:
-                        description: Attributes defines extra attributes associated
-                          with the load balancer.
+                        description: ClassicElbAttributes defines extra attributes
+                          associated with the load balancer.
                         properties:
                           crossZoneLoadBalancing:
                             description: CrossZoneLoadBalancing enables the classic
@@ -1547,6 +1569,81 @@ spec:
                       dnsName:
                         description: DNSName is the dns name of the load balancer.
                         type: string
+                      elbAttributes:
+                        additionalProperties:
+                          type: string
+                        description: ELBAttributes defines extra attributes associated
+                          with v2 load balancers.
+                        type: object
+                      elbListeners:
+                        description: ELBListeners is an array of listeners associated
+                          with the load balancer. There must be at least one.
+                        items:
+                          description: Listener defines an AWS network load balancer
+                            listener.
+                          properties:
+                            port:
+                              format: int64
+                              type: integer
+                            protocol:
+                              description: ELBProtocol defines listener protocols
+                                for a load balancer.
+                              type: string
+                            targetGroup:
+                              description: TargetGroupSpec specifies target group
+                                settings for a given listener. This is created first,
+                                and the ARN is then passed to the listener.
+                              properties:
+                                name:
+                                  description: Name of the TargetGroup. Must be unique
+                                    over the same group of listeners.
+                                  type: string
+                                port:
+                                  description: Port is the exposed port
+                                  format: int64
+                                  type: integer
+                                protocol:
+                                  description: ELBProtocol defines listener protocols
+                                    for a load balancer.
+                                  enum:
+                                  - tcp
+                                  - tls
+                                  - upd
+                                  type: string
+                                targetGroupHealthCheck:
+                                  description: HealthCheck is the elb health check
+                                    associated with the load balancer.
+                                  properties:
+                                    intervalSeconds:
+                                      format: int64
+                                      type: integer
+                                    path:
+                                      type: string
+                                    port:
+                                      type: string
+                                    protocol:
+                                      type: string
+                                    thresholdCount:
+                                      format: int64
+                                      type: integer
+                                    timeoutSeconds:
+                                      format: int64
+                                      type: integer
+                                  type: object
+                                vpcId:
+                                  type: string
+                              required:
+                              - name
+                              - port
+                              - protocol
+                              - vpcId
+                              type: object
+                          required:
+                          - port
+                          - protocol
+                          - targetGroup
+                          type: object
+                        type: array
                       healthChecks:
                         description: HealthCheck is the classic elb health check associated
                           with the load balancer.
@@ -1581,9 +1678,9 @@ spec:
                         - unhealthyThreshold
                         type: object
                       listeners:
-                        description: Listeners is an array of classic elb listeners
-                          associated with the load balancer. There must be at least
-                          one.
+                        description: ClassicELBListeners is an array of classic elb
+                          listeners associated with the load balancer. There must
+                          be at least one.
                         items:
                           description: ClassicELBListener defines an AWS classic load
                             balancer listener.
@@ -1592,15 +1689,15 @@ spec:
                               format: int64
                               type: integer
                             instanceProtocol:
-                              description: ClassicELBProtocol defines listener protocols
-                                for a classic load balancer.
+                              description: ELBProtocol defines listener protocols
+                                for a load balancer.
                               type: string
                             port:
                               format: int64
                               type: integer
                             protocol:
-                              description: ClassicELBProtocol defines listener protocols
-                                for a classic load balancer.
+                              description: ELBProtocol defines listener protocols
+                                for a load balancer.
                               type: string
                           required:
                           - instancePort
@@ -1609,6 +1706,15 @@ spec:
                           - protocol
                           type: object
                         type: array
+                      loadBalancerType:
+                        description: LoadBalancerType sets the type for a load balancer.
+                          The default type is classic.
+                        enum:
+                        - classic
+                        - elb
+                        - alb
+                        - nlb
+                        type: string
                       name:
                         description: The name of the load balancer. It must be unique
                           within the set of load balancers defined in the region.
@@ -1636,6 +1742,8 @@ spec:
                         description: Tags is a map of tags associated with the load
                           balancer.
                         type: object
+                    required:
+                    - loadBalancerType
                     type: object
                   securityGroups:
                     additionalProperties:
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclustertemplates.yaml
index 931792a03..a1c476f5b 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclustertemplates.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclustertemplates.yaml
@@ -578,10 +578,24 @@ spec:
                               registered instances in its Availability Zone only.
                               \n Defaults to false."
                             type: boolean
+                          disableHostsRewrite:
+                            description: DisableHostsRewrite disabled the hair pinning
+                              issue solution that adds the NLB's address as 127.0.0.1
+                              to the hosts file of each instance. This is by default,
+                              false.
+                            type: boolean
                           healthCheckProtocol:
                             description: HealthCheckProtocol sets the protocol type
-                              for classic ELB health check target default value is
-                              ClassicELBProtocolSSL
+                              for ELB health check target default value is ELBProtocolSSL
+                            type: string
+                          loadBalancerType:
+                            description: LoadBalancerType sets the type for a load
+                              balancer. The default type is classic.
+                            enum:
+                            - classic
+                            - elb
+                            - alb
+                            - nlb
                             type: string
                           name:
                             description: Name sets the name of the classic ELB load
@@ -593,6 +607,11 @@ spec:
                             maxLength: 32
                             pattern: ^[A-Za-z0-9]([A-Za-z0-9]{0,31}|[-A-Za-z0-9]{0,30}[A-Za-z0-9])$
                             type: string
+                          preserveClientIP:
+                            description: PreserveClientIP lets the user control if
+                              preservation of client ips must be retained or not.
+                              If this is enabled 6443 will be opened to 0.0.0.0/0.
+                            type: boolean
                           scheme:
                             default: internet-facing
                             description: Scheme sets the scheme of the load balancer
diff --git a/controllers/awscluster_controller.go b/controllers/awscluster_controller.go
index b68eb8fb0..4738754cc 100644
--- a/controllers/awscluster_controller.go
+++ b/controllers/awscluster_controller.go
@@ -325,10 +325,12 @@ func (r *AWSClusterReconciler) reconcileNormal(clusterScope *scope.ClusterScope)
 		return reconcile.Result{RequeueAfter: 15 * time.Second}, nil
 	}
 
+	clusterScope.Debug("looking up IP address for DNS", "dns", awsCluster.Status.Network.APIServerELB.DNSName)
 	if _, err := net.LookupIP(awsCluster.Status.Network.APIServerELB.DNSName); err != nil {
+		clusterScope.Error(err, "failed to get IP address for dns name", "dns", awsCluster.Status.Network.APIServerELB.DNSName)
 		conditions.MarkFalse(awsCluster, infrav1.LoadBalancerReadyCondition, infrav1.WaitForDNSNameResolveReason, clusterv1.ConditionSeverityInfo, "")
 		clusterScope.Info("Waiting on API server ELB DNS name to resolve")
-		return reconcile.Result{RequeueAfter: 15 * time.Second}, nil //nolint:nilerr
+		return reconcile.Result{RequeueAfter: 15 * time.Second}, nil
 	}
 	conditions.MarkTrue(awsCluster, infrav1.LoadBalancerReadyCondition)
 
diff --git a/controllers/awsmachine_controller.go b/controllers/awsmachine_controller.go
index b293eedbb..61925830c 100644
--- a/controllers/awsmachine_controller.go
+++ b/controllers/awsmachine_controller.go
@@ -476,7 +476,6 @@ func (r *AWSMachineReconciler) reconcileNormal(_ context.Context, machineScope *
 		machineScope.Error(err, "unable to patch object")
 		return ctrl.Result{}, err
 	}
-
 	// Create new instance since providerId is nil and instance could not be found by tags.
 	if instance == nil {
 		// Avoid a flickering condition between InstanceProvisionStarted and InstanceProvisionFailed if there's a persistent failure with createInstance
@@ -511,7 +510,6 @@ func (r *AWSMachineReconciler) reconcileNormal(_ context.Context, machineScope *
 	// Make sure Spec.ProviderID and Spec.InstanceID are always set.
 	machineScope.SetProviderID(instance.ID, instance.AvailabilityZone)
 	machineScope.SetInstanceID(instance.ID)
-
 	// See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-lifecycle.html
 
 	// Sets the AWSMachine status Interruptible, when the SpotMarketOptions is enabled for AWSMachine, Interruptible is set as true.
@@ -598,6 +596,7 @@ func (r *AWSMachineReconciler) reconcileNormal(_ context.Context, machineScope *
 		conditions.MarkTrue(machineScope.AWSMachine, infrav1.SecurityGroupsReadyCondition)
 	}
 
+	machineScope.Debug("done reconciling instance", "instance", instance)
 	return ctrl.Result{}, nil
 }
 
@@ -791,28 +790,25 @@ func (r *AWSMachineReconciler) reconcileLBAttachment(machineScope *scope.Machine
 	// In order to prevent sending request to a "not-ready" control plane machines, it is required to remove the machine
 	// from the ELB as soon as the machine gets deleted or when the machine is in a not running state.
 	if !machineScope.AWSMachine.DeletionTimestamp.IsZero() || !machineScope.InstanceIsRunning() {
-		registered, err := elbsvc.IsInstanceRegisteredWithAPIServerELB(i)
-		if err != nil {
-			r.Recorder.Eventf(machineScope.AWSMachine, corev1.EventTypeWarning, "FailedDetachControlPlaneELB",
-				"Failed to deregister control plane instance %q from load balancer: failed to determine registration status: %v", i.ID, err)
-			return errors.Wrapf(err, "could not deregister control plane instance %q from load balancer - error determining registration status", i.ID)
-		}
-		if !registered {
-			// Already deregistered - nothing more to do
-			return nil
+		if elbScope.ControlPlaneLoadBalancer().LoadBalancerType == infrav1.LoadBalancerTypeClassic {
+			machineScope.Debug("deregistering from classic load balancer")
+			return r.deregisterInstanceFromClassicLB(machineScope, elbsvc, i)
 		}
+		machineScope.Debug("deregistering from v2 load balancer")
+		return r.deregisterInstanceFromV2LB(machineScope, elbsvc, i)
+	}
 
-		if err := elbsvc.DeregisterInstanceFromAPIServerELB(i); err != nil {
-			r.Recorder.Eventf(machineScope.AWSMachine, corev1.EventTypeWarning, "FailedDetachControlPlaneELB",
-				"Failed to deregister control plane instance %q from load balancer: %v", i.ID, err)
-			conditions.MarkFalse(machineScope.AWSMachine, infrav1.ELBAttachedCondition, infrav1.ELBDetachFailedReason, clusterv1.ConditionSeverityError, err.Error())
-			return errors.Wrapf(err, "could not deregister control plane instance %q from load balancer", i.ID)
-		}
-		r.Recorder.Eventf(machineScope.AWSMachine, corev1.EventTypeNormal, "SuccessfulDetachControlPlaneELB",
-			"Control plane instance %q is de-registered from load balancer", i.ID)
-		return nil
+	// This changes the flow because previously it didn't care about this part.
+	if elbScope.ControlPlaneLoadBalancer().LoadBalancerType == infrav1.LoadBalancerTypeClassic {
+		machineScope.Debug("registering to classic load balancer")
+		return r.registerInstanceToClassicLB(machineScope, elbsvc, i)
 	}
 
+	machineScope.Debug("registering to v2 load balancer")
+	return r.registerInstanceToV2LB(machineScope, elbsvc, i)
+}
+
+func (r *AWSMachineReconciler) registerInstanceToClassicLB(machineScope *scope.MachineScope, elbsvc services.ELBInterface, i *infrav1.Instance) error {
 	registered, err := elbsvc.IsInstanceRegisteredWithAPIServerELB(i)
 	if err != nil {
 		r.Recorder.Eventf(machineScope.AWSMachine, corev1.EventTypeWarning, "FailedAttachControlPlaneELB",
@@ -836,6 +832,78 @@ func (r *AWSMachineReconciler) reconcileLBAttachment(machineScope *scope.Machine
 	return nil
 }
 
+func (r *AWSMachineReconciler) registerInstanceToV2LB(machineScope *scope.MachineScope, elbsvc services.ELBInterface, instance *infrav1.Instance) error {
+	_, registered, err := elbsvc.IsInstanceRegisteredWithAPIServerLB(instance)
+	if err != nil {
+		r.Recorder.Eventf(machineScope.AWSMachine, corev1.EventTypeWarning, "FailedAttachControlPlaneELB",
+			"Failed to register control plane instance %q with load balancer: failed to determine registration status: %v", instance.ID, err)
+		return errors.Wrapf(err, "could not register control plane instance %q with load balancer - error determining registration status", instance.ID)
+	}
+	if registered {
+		machineScope.Logger.Debug("Instance is already registered.", "instance", instance.ID)
+		return nil
+	}
+
+	if err := elbsvc.RegisterInstanceWithAPIServerLB(instance); err != nil {
+		r.Recorder.Eventf(machineScope.AWSMachine, corev1.EventTypeWarning, "FailedAttachControlPlaneELB",
+			"Failed to register control plane instance %q with load balancer: %v", instance.ID, err)
+		conditions.MarkFalse(machineScope.AWSMachine, infrav1.ELBAttachedCondition, infrav1.ELBAttachFailedReason, clusterv1.ConditionSeverityError, err.Error())
+		return errors.Wrapf(err, "could not register control plane instance %q with load balancer", instance.ID)
+	}
+	r.Recorder.Eventf(machineScope.AWSMachine, corev1.EventTypeNormal, "SuccessfulAttachControlPlaneELB",
+		"Control plane instance %q is registered with load balancer", instance.ID)
+	conditions.MarkTrue(machineScope.AWSMachine, infrav1.ELBAttachedCondition)
+	return nil
+}
+
+func (r *AWSMachineReconciler) deregisterInstanceFromClassicLB(machineScope *scope.MachineScope, elbsvc services.ELBInterface, instance *infrav1.Instance) error {
+	registered, err := elbsvc.IsInstanceRegisteredWithAPIServerELB(instance)
+	if err != nil {
+		r.Recorder.Eventf(machineScope.AWSMachine, corev1.EventTypeWarning, "FailedDetachControlPlaneELB",
+			"Failed to deregister control plane instance %q from load balancer: failed to determine registration status: %v", instance.ID, err)
+		return errors.Wrapf(err, "could not deregister control plane instance %q from load balancer - error determining registration status", instance.ID)
+	}
+	if !registered {
+		machineScope.Logger.Debug("Instance is already registered.", "instance", instance.ID)
+		return nil
+	}
+
+	if err := elbsvc.DeregisterInstanceFromAPIServerELB(instance); err != nil {
+		r.Recorder.Eventf(machineScope.AWSMachine, corev1.EventTypeWarning, "FailedDetachControlPlaneELB",
+			"Failed to deregister control plane instance %q from load balancer: %v", instance.ID, err)
+		conditions.MarkFalse(machineScope.AWSMachine, infrav1.ELBAttachedCondition, infrav1.ELBDetachFailedReason, clusterv1.ConditionSeverityError, err.Error())
+		return errors.Wrapf(err, "could not deregister control plane instance %q from load balancer", instance.ID)
+	}
+
+	r.Recorder.Eventf(machineScope.AWSMachine, corev1.EventTypeNormal, "SuccessfulDetachControlPlaneELB",
+		"Control plane instance %q is de-registered from load balancer", instance.ID)
+	return nil
+}
+
+func (r *AWSMachineReconciler) deregisterInstanceFromV2LB(machineScope *scope.MachineScope, elbsvc services.ELBInterface, i *infrav1.Instance) error {
+	targetGroupArn, registered, err := elbsvc.IsInstanceRegisteredWithAPIServerLB(i)
+	if err != nil {
+		r.Recorder.Eventf(machineScope.AWSMachine, corev1.EventTypeWarning, "FailedDetachControlPlaneELB",
+			"Failed to deregister control plane instance %q from load balancer: failed to determine registration status: %v", i.ID, err)
+		return errors.Wrapf(err, "could not deregister control plane instance %q from load balancer - error determining registration status", i.ID)
+	}
+	if !registered {
+		// Already deregistered - nothing more to do
+		return nil
+	}
+
+	if err := elbsvc.DeregisterInstanceFromAPIServerLB(targetGroupArn, i); err != nil {
+		r.Recorder.Eventf(machineScope.AWSMachine, corev1.EventTypeWarning, "FailedDetachControlPlaneELB",
+			"Failed to deregister control plane instance %q from load balancer: %v", i.ID, err)
+		conditions.MarkFalse(machineScope.AWSMachine, infrav1.ELBAttachedCondition, infrav1.ELBDetachFailedReason, clusterv1.ConditionSeverityError, err.Error())
+		return errors.Wrapf(err, "could not deregister control plane instance %q from load balancer", i.ID)
+	}
+
+	r.Recorder.Eventf(machineScope.AWSMachine, corev1.EventTypeNormal, "SuccessfulDetachControlPlaneELB",
+		"Control plane instance %q is de-registered from load balancer", i.ID)
+	return nil
+}
+
 // AWSClusterToAWSMachines is a handler.ToRequestsFunc to be used to enqeue requests for reconciliation
 // of AWSMachines.
 func (r *AWSMachineReconciler) AWSClusterToAWSMachines(log logger.Wrapper) handler.MapFunc {
diff --git a/controlplane/eks/api/v1beta1/zz_generated.conversion.go b/controlplane/eks/api/v1beta1/zz_generated.conversion.go
index fbc248bac..f38dfa213 100644
--- a/controlplane/eks/api/v1beta1/zz_generated.conversion.go
+++ b/controlplane/eks/api/v1beta1/zz_generated.conversion.go
@@ -250,11 +250,6 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
-	if err := s.AddConversionFunc((*apiv1beta2.NetworkStatus)(nil), (*apiv1beta1.NetworkStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_v1beta2_NetworkStatus_To_v1beta1_NetworkStatus(a.(*apiv1beta2.NetworkStatus), b.(*apiv1beta1.NetworkStatus), scope)
-	}); err != nil {
-		return err
-	}
 	if err := s.AddConversionFunc((*v1beta2.VpcCni)(nil), (*VpcCni)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1beta2_VpcCni_To_v1beta1_VpcCni(a.(*v1beta2.VpcCni), b.(*VpcCni), scope)
 	}); err != nil {
diff --git a/docs/book/src/topics/network-load-balancer-with-awscluster.md b/docs/book/src/topics/network-load-balancer-with-awscluster.md
new file mode 100644
index 000000000..a309d822d
--- /dev/null
+++ b/docs/book/src/topics/network-load-balancer-with-awscluster.md
@@ -0,0 +1,68 @@
+# Setting up a Network Load Balancer
+
+## Overview
+
+It's possible to set up and use a [Network Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/introduction.html) with `AWSCluster` instead of the
+Classic Load Balancer that is created by default.
+
+## `AWSCluster` setting
+
+To make CAPA create a network load balancer simply set the load balancer type to `network` like this:
+
+```yaml
+---
+apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
+kind: AWSCluster
+metadata:
+  name: "test-aws-cluster"
+spec:
+  region: "eu-central-1"
+  controlPlaneLoadBalancer:
+    loadBalancerType: nlb
+```
+
+This will create the following objects:
+
+- A network load balancer
+- Listeners
+- A target group
+
+It will also take into consideration IPv6 enabled clusters and create an IPv6 aware load balancer.
+
+## Preserve Client IPs
+
+By default, client ip preservation is disabled. This is to avoid [hairpinning](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-troubleshooting.html#loopback-timeout) issues between kubelet and the node
+registration process. To enable client IP preservation, you can set it to enable with the following flag:
+
+```yaml
+---
+apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
+kind: AWSCluster
+metadata:
+  name: "test-aws-cluster"
+spec:
+  region: "eu-central-1"
+  sshKeyName: "capa-key"
+  controlPlaneLoadBalancer:
+    loadBalancerType: nlb
+    preserveClientIP: true
+```
+
+## Security
+
+NLBs cannot use Security Groups. Therefore, the following steps have been taken to increase security for nodes
+communication. NLBs need access to the node in order to send traffic its way. A port has to be opened using an ip
+address range instead of a security group as a _source_. There are two scenarios and CIDRs that can be enabled.
+
+First, if client IP preservation is _disabled_ we only add the VPC's private CIDR range as allowed source for the API
+server's port (usually 6443). This will work because then the NLB will use its dynamically allocated internal IP
+address as source.
+
+Second, if client IP preservation is _enabled_ we MUST set `0.0.0.0/0` as communication source because then the
+incoming IP address will be that of the client's that might not be in the current VPC. This shouldn't be too much of a
+problem, but user's need to be aware of this restriction.
+
+## Extension of the code
+
+Right now, only NLBs and a Classic Load Balancer is supported. However, the code has been written in a way that it
+should be easy to extend with an ALB or a GLB.
diff --git a/pkg/cloud/converters/tags.go b/pkg/cloud/converters/tags.go
index e44e8c606..85c88b341 100644
--- a/pkg/cloud/converters/tags.go
+++ b/pkg/cloud/converters/tags.go
@@ -21,6 +21,7 @@ import (
 	"github.com/aws/aws-sdk-go/service/autoscaling"
 	"github.com/aws/aws-sdk-go/service/ec2"
 	"github.com/aws/aws-sdk-go/service/elb"
+	"github.com/aws/aws-sdk-go/service/elbv2"
 	"github.com/aws/aws-sdk-go/service/secretsmanager"
 	"github.com/aws/aws-sdk-go/service/ssm"
 
@@ -76,6 +77,17 @@ func ELBTagsToMap(src []*elb.Tag) infrav1.Tags {
 	return tags
 }
 
+// V2TagsToMap converts a []*elbv2.Tag into a infrav1.Tags.
+func V2TagsToMap(src []*elbv2.Tag) infrav1.Tags {
+	tags := make(infrav1.Tags, len(src))
+
+	for _, t := range src {
+		tags[*t.Key] = *t.Value
+	}
+
+	return tags
+}
+
 // MapToELBTags converts a infrav1.Tags to a []*elb.Tag.
 func MapToELBTags(src infrav1.Tags) []*elb.Tag {
 	tags := make([]*elb.Tag, 0, len(src))
@@ -92,6 +104,22 @@ func MapToELBTags(src infrav1.Tags) []*elb.Tag {
 	return tags
 }
 
+// MapToV2Tags converts a infrav1.Tags to a []*elbv2.Tag.
+func MapToV2Tags(src infrav1.Tags) []*elbv2.Tag {
+	tags := make([]*elbv2.Tag, 0, len(src))
+
+	for k, v := range src {
+		tag := &elbv2.Tag{
+			Key:   aws.String(k),
+			Value: aws.String(v),
+		}
+
+		tags = append(tags, tag)
+	}
+
+	return tags
+}
+
 // MapToSecretsManagerTags converts a infrav1.Tags to a []*secretsmanager.Tag.
 func MapToSecretsManagerTags(src infrav1.Tags) []*secretsmanager.Tag {
 	tags := make([]*secretsmanager.Tag, 0, len(src))
diff --git a/pkg/cloud/scope/cluster.go b/pkg/cloud/scope/cluster.go
index dd376e015..94b009b56 100644
--- a/pkg/cloud/scope/cluster.go
+++ b/pkg/cloud/scope/cluster.go
@@ -179,11 +179,11 @@ func (s *ClusterScope) ControlPlaneLoadBalancer() *infrav1.AWSLoadBalancerSpec {
 }
 
 // ControlPlaneLoadBalancerScheme returns the Classic ELB scheme (public or internal facing).
-func (s *ClusterScope) ControlPlaneLoadBalancerScheme() infrav1.ClassicELBScheme {
+func (s *ClusterScope) ControlPlaneLoadBalancerScheme() infrav1.ELBScheme {
 	if s.ControlPlaneLoadBalancer() != nil && s.ControlPlaneLoadBalancer().Scheme != nil {
 		return *s.ControlPlaneLoadBalancer().Scheme
 	}
-	return infrav1.ClassicELBSchemeInternetFacing
+	return infrav1.ELBSchemeInternetFacing
 }
 
 func (s *ClusterScope) ControlPlaneLoadBalancerName() *string {
@@ -282,7 +282,7 @@ func (s *ClusterScope) APIServerPort() int32 {
 	if s.Cluster.Spec.ClusterNetwork != nil && s.Cluster.Spec.ClusterNetwork.APIServerPort != nil {
 		return *s.Cluster.Spec.ClusterNetwork.APIServerPort
 	}
-	return 6443
+	return infrav1.DefaultAPIServerPort
 }
 
 // SetFailureDomain sets the infrastructure provider failure domain key to the spec given as input.
diff --git a/pkg/cloud/scope/elb.go b/pkg/cloud/scope/elb.go
index 341cdb05a..0a5c65377 100644
--- a/pkg/cloud/scope/elb.go
+++ b/pkg/cloud/scope/elb.go
@@ -42,7 +42,7 @@ type ELBScope interface {
 	ControlPlaneLoadBalancer() *infrav1.AWSLoadBalancerSpec
 
 	// ControlPlaneLoadBalancerScheme returns the Classic ELB scheme (public or internal facing)
-	ControlPlaneLoadBalancerScheme() infrav1.ClassicELBScheme
+	ControlPlaneLoadBalancerScheme() infrav1.ELBScheme
 
 	// ControlPlaneLoadBalancerName returns the Classic ELB name
 	ControlPlaneLoadBalancerName() *string
diff --git a/pkg/cloud/scope/managedcontrolplane.go b/pkg/cloud/scope/managedcontrolplane.go
index 15b422566..1ce4d74e9 100644
--- a/pkg/cloud/scope/managedcontrolplane.go
+++ b/pkg/cloud/scope/managedcontrolplane.go
@@ -396,3 +396,8 @@ func (s *ManagedControlPlaneScope) ServiceCidrs() *clusterv1.NetworkRanges {
 
 	return nil
 }
+
+// ControlPlaneLoadBalancer returns the AWSLoadBalancerSpec.
+func (s *ManagedControlPlaneScope) ControlPlaneLoadBalancer() *infrav1.AWSLoadBalancerSpec {
+	return nil
+}
diff --git a/pkg/cloud/scope/sg.go b/pkg/cloud/scope/sg.go
index 2baea38e6..5fedb8f87 100644
--- a/pkg/cloud/scope/sg.go
+++ b/pkg/cloud/scope/sg.go
@@ -42,4 +42,7 @@ type SGScope interface {
 
 	// Bastion returns the bastion details for the cluster.
 	Bastion() *infrav1.Bastion
+
+	// ControlPlaneLoadBalancer returns the load balancer settings that are requested.
+	ControlPlaneLoadBalancer() *infrav1.AWSLoadBalancerSpec
 }
diff --git a/pkg/cloud/services/elb/loadbalancer.go b/pkg/cloud/services/elb/loadbalancer.go
index f3aca6d64..81b61f418 100644
--- a/pkg/cloud/services/elb/loadbalancer.go
+++ b/pkg/cloud/services/elb/loadbalancer.go
@@ -26,6 +26,7 @@ import (
 	"github.com/aws/aws-sdk-go/aws/awserr"
 	"github.com/aws/aws-sdk-go/service/ec2"
 	"github.com/aws/aws-sdk-go/service/elb"
+	"github.com/aws/aws-sdk-go/service/elbv2"
 	rgapi "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
 	"github.com/google/go-cmp/cmp"
 	"github.com/pkg/errors"
@@ -54,6 +55,341 @@ const maxELBsDescribeTagsRequest = 20
 func (s *Service) ReconcileLoadbalancers() error {
 	s.scope.Debug("Reconciling load balancers")
 
+	// do a switch and reconcile different load-balancer types
+	switch s.scope.ControlPlaneLoadBalancer().LoadBalancerType {
+	case infrav1.LoadBalancerTypeClassic:
+		return s.reconcileClassicLoadBalancer()
+	case infrav1.LoadBalancerTypeNLB, infrav1.LoadBalancerTypeALB, infrav1.LoadBalancerTypeELB:
+		return s.reconcileV2LB()
+	default:
+		return fmt.Errorf("unknown or unsupported load balancer type: %s", s.scope.ControlPlaneLoadBalancer().LoadBalancerType)
+	}
+}
+
+// reconcileV2LB creates a load balancer. It also takes care of generating unique names across
+// namespaces by appending the namespace to the name.
+func (s *Service) reconcileV2LB() error {
+	name, err := LBName(s.scope)
+	if err != nil {
+		return errors.Wrap(err, "failed to get control plane load balancer name")
+	}
+
+	// Get default api server spec.
+	spec, err := s.getAPIServerLBSpec(name)
+	if err != nil {
+		return err
+	}
+	lb, err := s.describeLB(name)
+	switch {
+	case IsNotFound(err) && s.scope.ControlPlaneEndpoint().IsValid():
+		// if elb is not found and owner cluster ControlPlaneEndpoint is already populated, then we should not recreate the elb.
+		return errors.Wrapf(err, "no loadbalancer exists for the AWSCluster %s, the cluster has become unrecoverable and should be deleted manually", s.scope.InfraClusterName())
+	case IsNotFound(err):
+		lb, err = s.createLB(spec)
+		if err != nil {
+			s.scope.Error(err, "failed to create LB")
+			return err
+		}
+
+		s.scope.Debug("Created new network load balancer for apiserver", "api-server-lb-name", lb.Name)
+	case err != nil:
+		// Failed to describe the classic ELB
+		return err
+	}
+
+	// set up the type for later processing
+	lb.LoadBalancerType = s.scope.ControlPlaneLoadBalancer().LoadBalancerType
+	if lb.IsManaged(s.scope.Name()) {
+		if !cmp.Equal(spec.ELBAttributes, lb.ELBAttributes) {
+			if err := s.configureLBAttributes(lb.ARN, spec.ELBAttributes); err != nil {
+				return err
+			}
+		}
+
+		if err := s.reconcileV2LBTags(lb, spec.Tags); err != nil {
+			return errors.Wrapf(err, "failed to reconcile tags for apiserver load balancer %q", lb.Name)
+		}
+
+		// Reconcile the subnets and availability zones from the spec
+		// and the ones currently attached to the load balancer.
+		if len(lb.SubnetIDs) != len(spec.SubnetIDs) {
+			_, err := s.ELBV2Client.SetSubnets(&elbv2.SetSubnetsInput{
+				LoadBalancerArn: &lb.ARN,
+				Subnets:         aws.StringSlice(spec.SubnetIDs),
+			})
+			if err != nil {
+				return errors.Wrapf(err, "failed to set subnets for apiserver load balancer '%s'", lb.Name)
+			}
+		}
+		if len(lb.AvailabilityZones) != len(spec.AvailabilityZones) {
+			lb.AvailabilityZones = spec.AvailabilityZones
+		}
+
+		// Reconcile the security groups from the spec and the ones currently attached to the load balancer
+		if s.scope.ControlPlaneLoadBalancer().LoadBalancerType != infrav1.LoadBalancerTypeNLB && !sets.NewString(lb.SecurityGroupIDs...).Equal(sets.NewString(spec.SecurityGroupIDs...)) {
+			_, err := s.ELBV2Client.SetSecurityGroups(&elbv2.SetSecurityGroupsInput{
+				LoadBalancerArn: &lb.ARN,
+				SecurityGroups:  aws.StringSlice(spec.SecurityGroupIDs),
+			})
+			if err != nil {
+				return errors.Wrapf(err, "failed to apply security groups to load balancer %q", lb.Name)
+			}
+		}
+	} else {
+		s.scope.Trace("Unmanaged control plane load balancer, skipping load balancer configuration", "api-server-elb", lb)
+	}
+	lb.DeepCopyInto(&s.scope.Network().APIServerELB)
+	return nil
+}
+
+func (s *Service) getAPIServerLBSpec(elbName string) (*infrav1.LoadBalancer, error) {
+	var securityGroupIDs []string
+	controlPlaneLoadBalancer := s.scope.ControlPlaneLoadBalancer()
+	if controlPlaneLoadBalancer != nil && controlPlaneLoadBalancer.LoadBalancerType != infrav1.LoadBalancerTypeNLB {
+		securityGroupIDs = append(securityGroupIDs, controlPlaneLoadBalancer.AdditionalSecurityGroups...)
+		securityGroupIDs = append(securityGroupIDs, s.scope.SecurityGroups()[infrav1.SecurityGroupAPIServerLB].ID)
+	}
+
+	res := &infrav1.LoadBalancer{
+		Name:          elbName,
+		Scheme:        s.scope.ControlPlaneLoadBalancerScheme(),
+		ELBAttributes: make(map[string]*string),
+		ELBListeners: []infrav1.Listener{
+			{
+				Protocol: infrav1.ELBProtocolTCP,
+				Port:     infrav1.DefaultAPIServerPort,
+				TargetGroup: infrav1.TargetGroupSpec{
+					Name:     fmt.Sprintf("apiserver-target-%d", time.Now().Unix()),
+					Port:     infrav1.DefaultAPIServerPort,
+					Protocol: infrav1.ELBProtocolTCP,
+					VpcID:    s.scope.VPC().ID,
+					HealthCheck: &infrav1.TargetGroupHealthCheck{
+						Protocol: aws.String(string(infrav1.ELBProtocolTCP)),
+						Port:     aws.String(infrav1.DefaultAPIServerPortString),
+					},
+				},
+			},
+		},
+		SecurityGroupIDs: securityGroupIDs,
+	}
+
+	if s.scope.ControlPlaneLoadBalancer() != nil && s.scope.ControlPlaneLoadBalancer().LoadBalancerType != infrav1.LoadBalancerTypeNLB {
+		res.ELBAttributes[infrav1.LoadBalancerAttributeIdleTimeTimeoutSeconds] = aws.String(infrav1.LoadBalancerAttributeIdleTimeDefaultTimeoutSecondsInSeconds)
+	}
+
+	if s.scope.ControlPlaneLoadBalancer() != nil {
+		res.ELBAttributes[infrav1.LoadBalancerAttributeEnableLoadBalancingCrossZone] = aws.String(fmt.Sprintf("%t", s.scope.ControlPlaneLoadBalancer().CrossZoneLoadBalancing))
+	}
+
+	res.Tags = infrav1.Build(infrav1.BuildParams{
+		ClusterName: s.scope.Name(),
+		Lifecycle:   infrav1.ResourceLifecycleOwned,
+		Name:        aws.String(elbName),
+		Role:        aws.String(infrav1.APIServerRoleTagValue),
+		Additional:  s.scope.AdditionalTags(),
+	})
+
+	// If subnet IDs have been specified for this load balancer
+	if s.scope.ControlPlaneLoadBalancer() != nil && len(s.scope.ControlPlaneLoadBalancer().Subnets) > 0 {
+		// This set of subnets may not match the subnets specified on the Cluster, so we may not have already discovered them
+		// We need to call out to AWS to describe them just in case
+		input := &ec2.DescribeSubnetsInput{
+			SubnetIds: aws.StringSlice(s.scope.ControlPlaneLoadBalancer().Subnets),
+		}
+		out, err := s.EC2Client.DescribeSubnets(input)
+		if err != nil {
+			return nil, err
+		}
+		for _, sn := range out.Subnets {
+			res.AvailabilityZones = append(res.AvailabilityZones, *sn.AvailabilityZone)
+			res.SubnetIDs = append(res.SubnetIDs, *sn.SubnetId)
+		}
+	} else {
+		// The load balancer APIs require us to only attach one subnet for each AZ.
+		subnets := s.scope.Subnets().FilterPrivate()
+
+		if s.scope.ControlPlaneLoadBalancerScheme() == infrav1.ELBSchemeInternetFacing {
+			subnets = s.scope.Subnets().FilterPublic()
+		}
+
+	subnetLoop:
+		for _, sn := range subnets {
+			for _, az := range res.AvailabilityZones {
+				if sn.AvailabilityZone == az {
+					// If we already attached another subnet in the same AZ, there is no need to
+					// add this subnet to the list of the ELB's subnets.
+					continue subnetLoop
+				}
+			}
+			res.AvailabilityZones = append(res.AvailabilityZones, sn.AvailabilityZone)
+			res.SubnetIDs = append(res.SubnetIDs, sn.ID)
+		}
+	}
+
+	return res, nil
+}
+
+func (s *Service) createLB(spec *infrav1.LoadBalancer) (*infrav1.LoadBalancer, error) {
+	var t *string
+	switch s.scope.ControlPlaneLoadBalancer().LoadBalancerType {
+	case infrav1.LoadBalancerTypeNLB:
+		t = aws.String(elbv2.LoadBalancerTypeEnumNetwork)
+	case infrav1.LoadBalancerTypeALB:
+		t = aws.String(elbv2.LoadBalancerTypeEnumApplication)
+	case infrav1.LoadBalancerTypeELB:
+		t = aws.String(elbv2.LoadBalancerTypeEnumGateway)
+	}
+	input := &elbv2.CreateLoadBalancerInput{
+		Name:    aws.String(spec.Name),
+		Subnets: aws.StringSlice(spec.SubnetIDs),
+		Tags:    converters.MapToV2Tags(spec.Tags),
+		Scheme:  aws.String(string(spec.Scheme)),
+		Type:    t,
+	}
+	if s.scope.ControlPlaneLoadBalancer().LoadBalancerType != infrav1.LoadBalancerTypeNLB {
+		input.SecurityGroups = aws.StringSlice(spec.SecurityGroupIDs)
+	}
+
+	if s.scope.VPC().IsIPv6Enabled() {
+		input.IpAddressType = aws.String("dualstack")
+	}
+
+	out, err := s.ELBV2Client.CreateLoadBalancer(input)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to create load balancer: %v", spec)
+	}
+
+	if len(out.LoadBalancers) == 0 {
+		return nil, errors.New("no new network load balancer was created; the returned list is empty")
+	}
+
+	// TODO(Skarlso): Add options to set up SSL.
+	// https://github.com/kubernetes-sigs/cluster-api-provider-aws/issues/3899
+	for _, ln := range spec.ELBListeners {
+		// create the target group first
+		targetGroupInput := &elbv2.CreateTargetGroupInput{
+			Name:     aws.String(ln.TargetGroup.Name),
+			Port:     aws.Int64(ln.TargetGroup.Port),
+			Protocol: aws.String(ln.TargetGroup.Protocol.String()),
+			VpcId:    aws.String(ln.TargetGroup.VpcID),
+		}
+		if s.scope.VPC().IsIPv6Enabled() {
+			targetGroupInput.IpAddressType = aws.String("ipv6")
+		}
+		if ln.TargetGroup.HealthCheck != nil {
+			targetGroupInput.HealthCheckEnabled = aws.Bool(true)
+			targetGroupInput.HealthCheckProtocol = ln.TargetGroup.HealthCheck.Protocol
+			targetGroupInput.HealthCheckPort = ln.TargetGroup.HealthCheck.Port
+		}
+		s.scope.Debug("creating target group", "group", targetGroupInput, "listener", ln)
+		group, err := s.ELBV2Client.CreateTargetGroup(targetGroupInput)
+		if err != nil {
+			return nil, errors.Wrapf(err, "failed to create target group for load balancer")
+		}
+		if len(group.TargetGroups) == 0 {
+			return nil, errors.New("no target group was created; the returned list is empty")
+		}
+
+		if !s.scope.ControlPlaneLoadBalancer().PreserveClientIP {
+			targetGroupAttributeInput := &elbv2.ModifyTargetGroupAttributesInput{
+				TargetGroupArn: group.TargetGroups[0].TargetGroupArn,
+				Attributes: []*elbv2.TargetGroupAttribute{
+					{
+						Key:   aws.String(infrav1.TargetGroupAttributeEnablePreserveClientIP),
+						Value: aws.String("false"),
+					},
+				},
+			}
+			if _, err := s.ELBV2Client.ModifyTargetGroupAttributes(targetGroupAttributeInput); err != nil {
+				return nil, errors.Wrapf(err, "failed to modify target group attribute")
+			}
+		}
+
+		listenerInput := &elbv2.CreateListenerInput{
+			DefaultActions: []*elbv2.Action{
+				{
+					TargetGroupArn: group.TargetGroups[0].TargetGroupArn,
+					Type:           aws.String(elbv2.ActionTypeEnumForward),
+				},
+			},
+			LoadBalancerArn: out.LoadBalancers[0].LoadBalancerArn,
+			Port:            aws.Int64(ln.Port),
+			Protocol:        aws.String(string(ln.Protocol)),
+			Tags:            converters.MapToV2Tags(spec.Tags),
+		}
+		// Create ClassicELBListeners
+		listener, err := s.ELBV2Client.CreateListener(listenerInput)
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to create listener")
+		}
+		if len(listener.Listeners) == 0 {
+			return nil, errors.New("no listener was created; the returned list is empty")
+		}
+	}
+
+	s.scope.Info("Created network load balancer", "dns-name", *out.LoadBalancers[0].DNSName)
+
+	res := spec.DeepCopy()
+	s.scope.Debug("applying load balancer DNS to result", "dns", *out.LoadBalancers[0].DNSName)
+	res.DNSName = *out.LoadBalancers[0].DNSName
+	return res, nil
+}
+
+func (s *Service) describeLB(name string) (*infrav1.LoadBalancer, error) {
+	input := &elbv2.DescribeLoadBalancersInput{
+		Names: aws.StringSlice([]string{name}),
+	}
+
+	out, err := s.ELBV2Client.DescribeLoadBalancers(input)
+	if err != nil {
+		if aerr, ok := err.(awserr.Error); ok {
+			switch aerr.Code() {
+			case elb.ErrCodeAccessPointNotFoundException:
+				return nil, NewNotFound(fmt.Sprintf("no load balancer found with name: %q", name))
+			case elb.ErrCodeDependencyThrottleException:
+				return nil, errors.Wrap(err, "too many requests made to the ELB service")
+			default:
+				return nil, errors.Wrap(err, "unexpected aws error")
+			}
+		} else {
+			return nil, errors.Wrapf(err, "failed to describe load balancer: %s", name)
+		}
+	}
+
+	if out != nil && len(out.LoadBalancers) == 0 {
+		return nil, NewNotFound(fmt.Sprintf("no load balancer found with name %q", name))
+	}
+
+	if s.scope.VPC().ID != "" && s.scope.VPC().ID != *out.LoadBalancers[0].VpcId {
+		return nil, errors.Errorf(
+			"Load balancer names must be unique within a region: %q load balancer already exists in this region in VPC %q",
+			name, *out.LoadBalancers[0].VpcId)
+	}
+
+	if s.scope.ControlPlaneLoadBalancer() != nil &&
+		s.scope.ControlPlaneLoadBalancer().Scheme != nil &&
+		string(*s.scope.ControlPlaneLoadBalancer().Scheme) != aws.StringValue(out.LoadBalancers[0].Scheme) {
+		return nil, errors.Errorf(
+			"Load balancer names must be unique within a region: %q Load balancer already exists in this region with a different scheme %q",
+			name, *out.LoadBalancers[0].Scheme)
+	}
+
+	outAtt, err := s.ELBV2Client.DescribeLoadBalancerAttributes(&elbv2.DescribeLoadBalancerAttributesInput{
+		LoadBalancerArn: out.LoadBalancers[0].LoadBalancerArn,
+	})
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to describe load balancer %q attributes", name)
+	}
+
+	tags, err := s.describeLBTags(aws.StringValue(out.LoadBalancers[0].LoadBalancerArn))
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to describe load balancer tags")
+	}
+
+	return fromSDKTypeToLB(out.LoadBalancers[0], outAtt.Attributes, tags), nil
+}
+
+func (s *Service) reconcileClassicLoadBalancer() error {
 	// Generate a default control plane load balancer name. The load balancer name cannot be
 	// generated by the defaulting webhook, because it is derived from the cluster name, and that
 	// name is undefined at defaulting time when generateName is used.
@@ -85,8 +421,8 @@ func (s *Service) ReconcileLoadbalancers() error {
 	}
 
 	if apiELB.IsManaged(s.scope.Name()) {
-		if !cmp.Equal(spec.Attributes, apiELB.Attributes) {
-			err := s.configureAttributes(apiELB.Name, spec.Attributes)
+		if !cmp.Equal(spec.ClassicElbAttributes, apiELB.ClassicElbAttributes) {
+			err := s.configureAttributes(apiELB.Name, spec.ClassicElbAttributes)
 			if err != nil {
 				return err
 			}
@@ -223,6 +559,52 @@ func (s *Service) DeleteLoadbalancers() error {
 		return errors.Wrap(err, "failed to delete AWS cloud provider load balancer(s)")
 	}
 
+	if err := s.deleteExistingNLBs(); err != nil {
+		return errors.Wrap(err, "failed to delete AWS cloud provider load balancer(s)")
+	}
+
+	return nil
+}
+
+func (s *Service) deleteExistingNLBs() error {
+	name, err := LBName(s.scope)
+	if err != nil {
+		return errors.Wrap(err, "failed to get control plane load balancer name")
+	}
+	conditions.MarkFalse(s.scope.InfraCluster(), infrav1.LoadBalancerReadyCondition, clusterv1.DeletingReason, clusterv1.ConditionSeverityInfo, "")
+	if err := s.scope.PatchObject(); err != nil {
+		return err
+	}
+
+	lb, err := s.describeLB(name)
+	if IsNotFound(err) {
+		return nil
+	}
+	if err != nil {
+		return err
+	}
+
+	if lb.IsUnmanaged(s.scope.Name()) {
+		s.scope.Debug("Found unmanaged load balancer for apiserver, skipping deletion", "api-server-elb-name", lb.Name)
+		return nil
+	}
+	s.scope.Debug("deleting load balancer", "name", name)
+	if err := s.deleteLB(lb.ARN); err != nil {
+		conditions.MarkFalse(s.scope.InfraCluster(), infrav1.LoadBalancerReadyCondition, "DeletingFailed", clusterv1.ConditionSeverityWarning, err.Error())
+		return err
+	}
+
+	if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (done bool, err error) {
+		_, err = s.describeLB(name)
+		done = IsNotFound(err)
+		return done, nil
+	}); err != nil {
+		return errors.Wrapf(err, "failed to wait for %q load balancer deletion", s.scope.Name())
+	}
+
+	conditions.MarkFalse(s.scope.InfraCluster(), infrav1.LoadBalancerReadyCondition, clusterv1.DeletedReason, clusterv1.ConditionSeverityInfo, "")
+	s.scope.Info("Deleted control plane load balancer", "name", name)
+
 	return nil
 }
 
@@ -254,6 +636,52 @@ func (s *Service) IsInstanceRegisteredWithAPIServerELB(i *infrav1.Instance) (boo
 	return false, nil
 }
 
+// IsInstanceRegisteredWithAPIServerLB returns true if the instance is already registered with the APIServer LB.
+func (s *Service) IsInstanceRegisteredWithAPIServerLB(i *infrav1.Instance) (string, bool, error) {
+	name, err := LBName(s.scope)
+	if err != nil {
+		return "", false, errors.Wrap(err, "failed to get control plane load balancer name")
+	}
+
+	input := &elbv2.DescribeLoadBalancersInput{
+		Names: []*string{aws.String(name)},
+	}
+
+	output, err := s.ELBV2Client.DescribeLoadBalancers(input)
+	if err != nil {
+		return "", false, errors.Wrapf(err, "error describing ELB %q", name)
+	}
+	if len(output.LoadBalancers) != 1 {
+		return "", false, errors.Errorf("expected 1 ELB description for %q, got %d", name, len(output.LoadBalancers))
+	}
+
+	describeTargetGroupInput := &elbv2.DescribeTargetGroupsInput{
+		LoadBalancerArn: output.LoadBalancers[0].LoadBalancerArn,
+	}
+
+	targetGroups, err := s.ELBV2Client.DescribeTargetGroups(describeTargetGroupInput)
+	if err != nil {
+		return "", false, errors.Wrapf(err, "error describing ELB's target groups %q", name)
+	}
+
+	for _, tg := range targetGroups.TargetGroups {
+		healthInput := &elbv2.DescribeTargetHealthInput{
+			TargetGroupArn: tg.TargetGroupArn,
+		}
+		instanceHealth, err := s.ELBV2Client.DescribeTargetHealth(healthInput)
+		if err != nil {
+			return "", false, errors.Wrapf(err, "error describing ELB's target groups health %q", name)
+		}
+		for _, id := range instanceHealth.TargetHealthDescriptions {
+			if aws.StringValue(id.Target.Id) == i.ID {
+				return aws.StringValue(tg.TargetGroupArn), true, nil
+			}
+		}
+	}
+
+	return "", false, nil
+}
+
 // RegisterInstanceWithAPIServerELB registers an instance with a classic ELB.
 func (s *Service) RegisterInstanceWithAPIServerELB(i *infrav1.Instance) error {
 	name, err := ELBName(s.scope)
@@ -302,6 +730,49 @@ func (s *Service) RegisterInstanceWithAPIServerELB(i *infrav1.Instance) error {
 	return err
 }
 
+// RegisterInstanceWithAPIServerLB registers an instance with a LB.
+func (s *Service) RegisterInstanceWithAPIServerLB(instance *infrav1.Instance) error {
+	name, err := LBName(s.scope)
+	if err != nil {
+		return errors.Wrap(err, "failed to get control plane load balancer name")
+	}
+	out, err := s.describeLB(name)
+	if err != nil {
+		return err
+	}
+	s.scope.Debug("found load balancer with name", "name", out.Name)
+	describeTargetGroupInput := &elbv2.DescribeTargetGroupsInput{
+		LoadBalancerArn: aws.String(out.ARN),
+	}
+
+	targetGroups, err := s.ELBV2Client.DescribeTargetGroups(describeTargetGroupInput)
+	if err != nil {
+		return errors.Wrapf(err, "error describing ELB's target groups %q", name)
+	}
+	if len(targetGroups.TargetGroups) == 0 {
+		return errors.New(fmt.Sprintf("no target groups found for load balancer with arn '%s'", out.ARN))
+	}
+	// Since TargetGroups and Listeners don't care, or are not aware, of subnets before registration, we ignore that check.
+	// Also, registering with AZ is not supported using the an InstanceID.
+	s.scope.Debug("found number of target groups", "target-groups", len(targetGroups.TargetGroups))
+	for _, tg := range targetGroups.TargetGroups {
+		input := &elbv2.RegisterTargetsInput{
+			TargetGroupArn: tg.TargetGroupArn,
+			Targets: []*elbv2.TargetDescription{
+				{
+					Id:   aws.String(instance.ID),
+					Port: aws.Int64(int64(s.scope.APIServerPort())),
+				},
+			},
+		}
+		if _, err = s.ELBV2Client.RegisterTargets(input); err != nil {
+			return fmt.Errorf("failed to register instance with target group '%s': %w", aws.StringValue(tg.TargetGroupName), err)
+		}
+	}
+
+	return nil
+}
+
 // getControlPlaneLoadBalancerSubnets retrieves ControlPlaneLoadBalancer subnets information.
 func (s *Service) getControlPlaneLoadBalancerSubnets() (infrav1.Subnets, error) {
 	var subnets infrav1.Subnets
@@ -352,6 +823,32 @@ func (s *Service) DeregisterInstanceFromAPIServerELB(i *infrav1.Instance) error
 	return err
 }
 
+// DeregisterInstanceFromAPIServerLB de-registers an instance from a LB.
+func (s *Service) DeregisterInstanceFromAPIServerLB(targetGroupArn string, i *infrav1.Instance) error {
+	input := &elbv2.DeregisterTargetsInput{
+		TargetGroupArn: aws.String(targetGroupArn),
+		Targets: []*elbv2.TargetDescription{
+			{
+				Id: aws.String(i.ID),
+			},
+		},
+	}
+
+	_, err := s.ELBV2Client.DeregisterTargets(input)
+	if err != nil {
+		if aerr, ok := err.(awserr.Error); ok {
+			switch aerr.Code() {
+			case elb.ErrCodeAccessPointNotFoundException, elb.ErrCodeInvalidEndPointException:
+				// Ignoring LoadBalancerNotFound and InvalidInstance when deregistering
+				return nil
+			default:
+				return err
+			}
+		}
+	}
+	return err
+}
+
 // ELBName returns the user-defined API Server ELB name, or a generated default if the user has not defined the ELB
 // name.
 func ELBName(s scope.ELBScope) (string, error) {
@@ -365,6 +862,19 @@ func ELBName(s scope.ELBScope) (string, error) {
 	return name, nil
 }
 
+// LBName returns the user-defined API Server ELB name, or a generated default if the user has not defined the ELB
+// name.
+func LBName(s scope.ELBScope) (string, error) {
+	if userDefinedName := s.ControlPlaneLoadBalancerName(); userDefinedName != nil {
+		return *userDefinedName, nil
+	}
+	name, err := GenerateELBName(fmt.Sprintf("%s-%s", s.Namespace(), s.Name()))
+	if err != nil {
+		return "", fmt.Errorf("failed to generate name: %w", err)
+	}
+	return name, nil
+}
+
 // GenerateELBName generates a formatted ELB name via either
 // concatenating the cluster name to the "-apiserver" suffix
 // or computing a hash for clusters with names above 32 characters.
@@ -406,7 +916,7 @@ func generateHashedELBName(clusterName string) (string, error) {
 	return fmt.Sprintf("%s-%s", shortName, "k8s"), nil
 }
 
-func (s *Service) getAPIServerClassicELBSpec(elbName string) (*infrav1.ClassicELB, error) {
+func (s *Service) getAPIServerClassicELBSpec(elbName string) (*infrav1.LoadBalancer, error) {
 	securityGroupIDs := []string{}
 	controlPlaneLoadBalancer := s.scope.ControlPlaneLoadBalancer()
 	if controlPlaneLoadBalancer != nil && len(controlPlaneLoadBalancer.AdditionalSecurityGroups) != 0 {
@@ -414,32 +924,32 @@ func (s *Service) getAPIServerClassicELBSpec(elbName string) (*infrav1.ClassicEL
 	}
 	securityGroupIDs = append(securityGroupIDs, s.scope.SecurityGroups()[infrav1.SecurityGroupAPIServerLB].ID)
 
-	res := &infrav1.ClassicELB{
+	res := &infrav1.LoadBalancer{
 		Name:   elbName,
 		Scheme: s.scope.ControlPlaneLoadBalancerScheme(),
-		Listeners: []infrav1.ClassicELBListener{
+		ClassicELBListeners: []infrav1.ClassicELBListener{
 			{
-				Protocol:         infrav1.ClassicELBProtocolTCP,
+				Protocol:         infrav1.ELBProtocolTCP,
 				Port:             int64(s.scope.APIServerPort()),
-				InstanceProtocol: infrav1.ClassicELBProtocolTCP,
-				InstancePort:     6443,
+				InstanceProtocol: infrav1.ELBProtocolTCP,
+				InstancePort:     infrav1.DefaultAPIServerPort,
 			},
 		},
 		HealthCheck: &infrav1.ClassicELBHealthCheck{
-			Target:             fmt.Sprintf("%v:%d", s.getHealthCheckELBProtocol(), 6443),
+			Target:             fmt.Sprintf("%v:%d", s.getHealthCheckELBProtocol(), infrav1.DefaultAPIServerPort),
 			Interval:           10 * time.Second,
 			Timeout:            5 * time.Second,
 			HealthyThreshold:   5,
 			UnhealthyThreshold: 3,
 		},
 		SecurityGroupIDs: securityGroupIDs,
-		Attributes: infrav1.ClassicELBAttributes{
+		ClassicElbAttributes: infrav1.ClassicELBAttributes{
 			IdleTimeout: 10 * time.Minute,
 		},
 	}
 
 	if s.scope.ControlPlaneLoadBalancer() != nil {
-		res.Attributes.CrossZoneLoadBalancing = s.scope.ControlPlaneLoadBalancer().CrossZoneLoadBalancing
+		res.ClassicElbAttributes.CrossZoneLoadBalancing = s.scope.ControlPlaneLoadBalancer().CrossZoneLoadBalancing
 	}
 
 	res.Tags = infrav1.Build(infrav1.BuildParams{
@@ -469,7 +979,7 @@ func (s *Service) getAPIServerClassicELBSpec(elbName string) (*infrav1.ClassicEL
 		// The load balancer APIs require us to only attach one subnet for each AZ.
 		subnets := s.scope.Subnets().FilterPrivate()
 
-		if s.scope.ControlPlaneLoadBalancerScheme() == infrav1.ClassicELBSchemeInternetFacing {
+		if s.scope.ControlPlaneLoadBalancerScheme() == infrav1.ELBSchemeInternetFacing {
 			subnets = s.scope.Subnets().FilterPublic()
 		}
 
@@ -490,7 +1000,7 @@ func (s *Service) getAPIServerClassicELBSpec(elbName string) (*infrav1.ClassicEL
 	return res, nil
 }
 
-func (s *Service) createClassicELB(spec *infrav1.ClassicELB) (*infrav1.ClassicELB, error) {
+func (s *Service) createClassicELB(spec *infrav1.LoadBalancer) (*infrav1.LoadBalancer, error) {
 	input := &elb.CreateLoadBalancerInput{
 		LoadBalancerName: aws.String(spec.Name),
 		Subnets:          aws.StringSlice(spec.SubnetIDs),
@@ -499,7 +1009,7 @@ func (s *Service) createClassicELB(spec *infrav1.ClassicELB) (*infrav1.ClassicEL
 		Tags:             converters.MapToELBTags(spec.Tags),
 	}
 
-	for _, ln := range spec.Listeners {
+	for _, ln := range spec.ClassicELBListeners {
 		input.Listeners = append(input.Listeners, &elb.Listener{
 			Protocol:         aws.String(string(ln.Protocol)),
 			LoadBalancerPort: aws.Int64(ln.Port),
@@ -568,6 +1078,31 @@ func (s *Service) configureAttributes(name string, attributes infrav1.ClassicELB
 	return nil
 }
 
+func (s *Service) configureLBAttributes(arn string, attributes map[string]*string) error {
+	attrs := make([]*elbv2.LoadBalancerAttribute, 0)
+	for k, v := range attributes {
+		attrs = append(attrs, &elbv2.LoadBalancerAttribute{
+			Key:   aws.String(k),
+			Value: v,
+		})
+	}
+	s.scope.Debug("adding attributes to load balancer", "attrs", attrs)
+	modifyInput := &elbv2.ModifyLoadBalancerAttributesInput{
+		Attributes:      attrs,
+		LoadBalancerArn: aws.String(arn),
+	}
+
+	if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) {
+		if _, err := s.ELBV2Client.ModifyLoadBalancerAttributes(modifyInput); err != nil {
+			return false, err
+		}
+		return true, nil
+	}, awserrors.LoadBalancerNotFound); err != nil {
+		return errors.Wrapf(err, "failed to configure attributes for load balancer: %v", arn)
+	}
+	return nil
+}
+
 func (s *Service) deleteClassicELB(name string) error {
 	input := &elb.DeleteLoadBalancerInput{
 		LoadBalancerName: aws.String(name),
@@ -581,6 +1116,58 @@ func (s *Service) deleteClassicELB(name string) error {
 	return nil
 }
 
+func (s *Service) deleteLB(arn string) error {
+	// remove listeners and target groups
+	// Order is important. ClassicELBListeners have to be deleted first.
+	// However, we must first gather the groups because after the listeners are deleted the groups
+	// are no longer associated with the LB, so we can't describe them afterwards.
+	groups, err := s.ELBV2Client.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{
+		LoadBalancerArn: aws.String(arn),
+	})
+	if err != nil {
+		return fmt.Errorf("failed to gather target groups for LB: %w", err)
+	}
+	listeners, err := s.ELBV2Client.DescribeListeners(&elbv2.DescribeListenersInput{
+		LoadBalancerArn: aws.String(arn),
+	})
+	if err != nil {
+		return fmt.Errorf("failed to gather listeners: %w", err)
+	}
+	for _, listener := range listeners.Listeners {
+		s.scope.Debug("deleting listener", "arn", aws.StringValue(listener.ListenerArn))
+		deleteListener := &elbv2.DeleteListenerInput{
+			ListenerArn: listener.ListenerArn,
+		}
+		if _, err := s.ELBV2Client.DeleteListener(deleteListener); err != nil {
+			return fmt.Errorf("failed to delete listener '%s': %w", aws.StringValue(listener.ListenerArn), err)
+		}
+	}
+	s.scope.Info("Successfully deleted all associated ClassicELBListeners")
+
+	for _, group := range groups.TargetGroups {
+		s.scope.Debug("deleting target group", "name", aws.StringValue(group.TargetGroupName))
+		deleteTargetGroup := &elbv2.DeleteTargetGroupInput{
+			TargetGroupArn: group.TargetGroupArn,
+		}
+		if _, err := s.ELBV2Client.DeleteTargetGroup(deleteTargetGroup); err != nil {
+			return fmt.Errorf("failed to delete target group '%s': %w", aws.StringValue(group.TargetGroupName), err)
+		}
+	}
+
+	s.scope.Info("Successfully deleted all associated Target Groups")
+
+	deleteLoadBalancerInput := &elbv2.DeleteLoadBalancerInput{
+		LoadBalancerArn: aws.String(arn),
+	}
+
+	if _, err := s.ELBV2Client.DeleteLoadBalancer(deleteLoadBalancerInput); err != nil {
+		return err
+	}
+
+	s.scope.Info("Deleted AWS cloud provider load balancers")
+	return nil
+}
+
 func (s *Service) listByTag(tag string) ([]string, error) {
 	input := rgapi.GetResourcesInput{
 		ResourceTypeFilters: aws.StringSlice([]string{elbResourceType}),
@@ -678,7 +1265,7 @@ func (s *Service) listAWSCloudProviderOwnedELBs() ([]string, error) {
 	return arns, nil
 }
 
-func (s *Service) describeClassicELB(name string) (*infrav1.ClassicELB, error) {
+func (s *Service) describeClassicELB(name string) (*infrav1.LoadBalancer, error) {
 	input := &elb.DescribeLoadBalancersInput{
 		LoadBalancerNames: aws.StringSlice([]string{name}),
 	}
@@ -747,7 +1334,22 @@ func (s *Service) describeClassicELBTags(name string) ([]*elb.Tag, error) {
 	return output.TagDescriptions[0].Tags, nil
 }
 
-func (s *Service) reconcileELBTags(lb *infrav1.ClassicELB, desiredTags map[string]string) error {
+func (s *Service) describeLBTags(arn string) ([]*elbv2.Tag, error) {
+	output, err := s.ELBV2Client.DescribeTags(&elbv2.DescribeTagsInput{
+		ResourceArns: []*string{aws.String(arn)},
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	if len(output.TagDescriptions) == 0 {
+		return nil, errors.Errorf("no tag information returned for load balancer %q", arn)
+	}
+
+	return output.TagDescriptions[0].Tags, nil
+}
+
+func (s *Service) reconcileELBTags(lb *infrav1.LoadBalancer, desiredTags map[string]string) error {
 	addTagsInput := &elb.AddTagsInput{
 		LoadBalancerNames: []*string{aws.String(lb.Name)},
 	}
@@ -787,29 +1389,94 @@ func (s *Service) reconcileELBTags(lb *infrav1.ClassicELB, desiredTags map[strin
 	return nil
 }
 
-func (s *Service) getHealthCheckELBProtocol() *infrav1.ClassicELBProtocol {
+func (s *Service) reconcileV2LBTags(lb *infrav1.LoadBalancer, desiredTags map[string]string) error {
+	addTagsInput := &elbv2.AddTagsInput{
+		ResourceArns: []*string{aws.String(lb.ARN)},
+	}
+
+	removeTagsInput := &elbv2.RemoveTagsInput{
+		ResourceArns: []*string{aws.String(lb.ARN)},
+	}
+
+	currentTags := infrav1.Tags(lb.Tags)
+
+	for k, v := range desiredTags {
+		if val, ok := currentTags[k]; !ok || val != v {
+			s.scope.Trace("adding tag to load balancer", "elb-name", lb.Name, "key", k, "value", v)
+			addTagsInput.Tags = append(addTagsInput.Tags, &elbv2.Tag{Key: aws.String(k), Value: aws.String(v)})
+		}
+	}
+
+	for k := range currentTags {
+		if _, ok := desiredTags[k]; !ok {
+			s.scope.Trace("removing tag from load balancer", "elb-name", lb.Name, "key", k)
+			removeTagsInput.TagKeys = append(removeTagsInput.TagKeys, aws.String(k))
+		}
+	}
+
+	if len(addTagsInput.Tags) > 0 {
+		if _, err := s.ELBV2Client.AddTags(addTagsInput); err != nil {
+			return err
+		}
+	}
+
+	if len(removeTagsInput.TagKeys) > 0 {
+		if _, err := s.ELBV2Client.RemoveTags(removeTagsInput); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (s *Service) getHealthCheckELBProtocol() *infrav1.ELBProtocol {
 	controlPlaneELB := s.scope.ControlPlaneLoadBalancer()
 	if controlPlaneELB != nil && controlPlaneELB.HealthCheckProtocol != nil {
 		return controlPlaneELB.HealthCheckProtocol
 	}
-	return &infrav1.ClassicELBProtocolSSL
+	return &infrav1.ELBProtocolSSL
 }
 
-func fromSDKTypeToClassicELB(v *elb.LoadBalancerDescription, attrs *elb.LoadBalancerAttributes, tags []*elb.Tag) *infrav1.ClassicELB {
-	res := &infrav1.ClassicELB{
+func fromSDKTypeToClassicELB(v *elb.LoadBalancerDescription, attrs *elb.LoadBalancerAttributes, tags []*elb.Tag) *infrav1.LoadBalancer {
+	res := &infrav1.LoadBalancer{
 		Name:             aws.StringValue(v.LoadBalancerName),
-		Scheme:           infrav1.ClassicELBScheme(*v.Scheme),
+		Scheme:           infrav1.ELBScheme(*v.Scheme),
 		SubnetIDs:        aws.StringValueSlice(v.Subnets),
 		SecurityGroupIDs: aws.StringValueSlice(v.SecurityGroups),
 		DNSName:          aws.StringValue(v.DNSName),
 		Tags:             converters.ELBTagsToMap(tags),
+		LoadBalancerType: infrav1.LoadBalancerTypeClassic,
 	}
 
 	if attrs.ConnectionSettings != nil && attrs.ConnectionSettings.IdleTimeout != nil {
-		res.Attributes.IdleTimeout = time.Duration(*attrs.ConnectionSettings.IdleTimeout) * time.Second
+		res.ClassicElbAttributes.IdleTimeout = time.Duration(*attrs.ConnectionSettings.IdleTimeout) * time.Second
 	}
 
-	res.Attributes.CrossZoneLoadBalancing = aws.BoolValue(attrs.CrossZoneLoadBalancing.Enabled)
+	res.ClassicElbAttributes.CrossZoneLoadBalancing = aws.BoolValue(attrs.CrossZoneLoadBalancing.Enabled)
+
+	return res
+}
+
+func fromSDKTypeToLB(v *elbv2.LoadBalancer, attrs []*elbv2.LoadBalancerAttribute, tags []*elbv2.Tag) *infrav1.LoadBalancer {
+	subnetIds := make([]*string, len(v.AvailabilityZones))
+	for i, az := range v.AvailabilityZones {
+		subnetIds[i] = az.SubnetId
+	}
+	res := &infrav1.LoadBalancer{
+		ARN:       aws.StringValue(v.LoadBalancerArn),
+		Name:      aws.StringValue(v.LoadBalancerName),
+		Scheme:    infrav1.ELBScheme(aws.StringValue(v.Scheme)),
+		SubnetIDs: aws.StringValueSlice(subnetIds),
+		// SecurityGroupIDs: aws.StringValueSlice(v.SecurityGroups),
+		DNSName: aws.StringValue(v.DNSName),
+		Tags:    converters.V2TagsToMap(tags),
+	}
+
+	infraAttrs := make(map[string]*string, len(attrs))
+	for _, a := range attrs {
+		infraAttrs[*a.Key] = a.Value
+	}
+	res.ELBAttributes = infraAttrs
 
 	return res
 }
diff --git a/pkg/cloud/services/elb/service.go b/pkg/cloud/services/elb/service.go
index 96d3f6797..b1b78ca35 100644
--- a/pkg/cloud/services/elb/service.go
+++ b/pkg/cloud/services/elb/service.go
@@ -19,6 +19,7 @@ package elb
 import (
 	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
 	"github.com/aws/aws-sdk-go/service/elb/elbiface"
+	"github.com/aws/aws-sdk-go/service/elbv2/elbv2iface"
 	"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
 
 	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/scope"
@@ -31,6 +32,7 @@ type Service struct {
 	scope                 scope.ELBScope
 	EC2Client             ec2iface.EC2API
 	ELBClient             elbiface.ELBAPI
+	ELBV2Client           elbv2iface.ELBV2API
 	ResourceTaggingClient resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI
 }
 
@@ -40,6 +42,7 @@ func NewService(elbScope scope.ELBScope) *Service {
 		scope:                 elbScope,
 		EC2Client:             scope.NewEC2Client(elbScope, elbScope, elbScope, elbScope.InfraCluster()),
 		ELBClient:             scope.NewELBClient(elbScope, elbScope, elbScope, elbScope.InfraCluster()),
+		ELBV2Client:           scope.NewELBv2Client(elbScope, elbScope, elbScope, elbScope.InfraCluster()),
 		ResourceTaggingClient: scope.NewResourgeTaggingClient(elbScope, elbScope, elbScope, elbScope.InfraCluster()),
 	}
 }
diff --git a/pkg/cloud/services/interfaces.go b/pkg/cloud/services/interfaces.go
index 3e85a8754..db004ad00 100644
--- a/pkg/cloud/services/interfaces.go
+++ b/pkg/cloud/services/interfaces.go
@@ -93,8 +93,11 @@ type ELBInterface interface {
 	DeleteLoadbalancers() error
 	ReconcileLoadbalancers() error
 	IsInstanceRegisteredWithAPIServerELB(i *infrav1.Instance) (bool, error)
+	IsInstanceRegisteredWithAPIServerLB(i *infrav1.Instance) (string, bool, error)
 	DeregisterInstanceFromAPIServerELB(i *infrav1.Instance) error
+	DeregisterInstanceFromAPIServerLB(targetGroupArn string, i *infrav1.Instance) error
 	RegisterInstanceWithAPIServerELB(i *infrav1.Instance) error
+	RegisterInstanceWithAPIServerLB(i *infrav1.Instance) error
 }
 
 // NetworkInterface encapsulates the methods exposed to the cluster
diff --git a/pkg/cloud/services/mock_services/elb_interface_mock.go b/pkg/cloud/services/mock_services/elb_interface_mock.go
index 608fae890..8bacbac67 100644
--- a/pkg/cloud/services/mock_services/elb_interface_mock.go
+++ b/pkg/cloud/services/mock_services/elb_interface_mock.go
@@ -78,6 +78,20 @@ func (mr *MockELBInterfaceMockRecorder) DeregisterInstanceFromAPIServerELB(arg0
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeregisterInstanceFromAPIServerELB", reflect.TypeOf((*MockELBInterface)(nil).DeregisterInstanceFromAPIServerELB), arg0)
 }
 
+// DeregisterInstanceFromAPIServerLB mocks base method.
+func (m *MockELBInterface) DeregisterInstanceFromAPIServerLB(arg0 string, arg1 *v1beta2.Instance) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DeregisterInstanceFromAPIServerLB", arg0, arg1)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// DeregisterInstanceFromAPIServerLB indicates an expected call of DeregisterInstanceFromAPIServerLB.
+func (mr *MockELBInterfaceMockRecorder) DeregisterInstanceFromAPIServerLB(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeregisterInstanceFromAPIServerLB", reflect.TypeOf((*MockELBInterface)(nil).DeregisterInstanceFromAPIServerLB), arg0, arg1)
+}
+
 // IsInstanceRegisteredWithAPIServerELB mocks base method.
 func (m *MockELBInterface) IsInstanceRegisteredWithAPIServerELB(arg0 *v1beta2.Instance) (bool, error) {
 	m.ctrl.T.Helper()
@@ -93,6 +107,22 @@ func (mr *MockELBInterfaceMockRecorder) IsInstanceRegisteredWithAPIServerELB(arg
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsInstanceRegisteredWithAPIServerELB", reflect.TypeOf((*MockELBInterface)(nil).IsInstanceRegisteredWithAPIServerELB), arg0)
 }
 
+// IsInstanceRegisteredWithAPIServerLB mocks base method.
+func (m *MockELBInterface) IsInstanceRegisteredWithAPIServerLB(arg0 *v1beta2.Instance) (string, bool, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "IsInstanceRegisteredWithAPIServerLB", arg0)
+	ret0, _ := ret[0].(string)
+	ret1, _ := ret[1].(bool)
+	ret2, _ := ret[2].(error)
+	return ret0, ret1, ret2
+}
+
+// IsInstanceRegisteredWithAPIServerLB indicates an expected call of IsInstanceRegisteredWithAPIServerLB.
+func (mr *MockELBInterfaceMockRecorder) IsInstanceRegisteredWithAPIServerLB(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsInstanceRegisteredWithAPIServerLB", reflect.TypeOf((*MockELBInterface)(nil).IsInstanceRegisteredWithAPIServerLB), arg0)
+}
+
 // ReconcileLoadbalancers mocks base method.
 func (m *MockELBInterface) ReconcileLoadbalancers() error {
 	m.ctrl.T.Helper()
@@ -120,3 +150,17 @@ func (mr *MockELBInterfaceMockRecorder) RegisterInstanceWithAPIServerELB(arg0 in
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterInstanceWithAPIServerELB", reflect.TypeOf((*MockELBInterface)(nil).RegisterInstanceWithAPIServerELB), arg0)
 }
+
+// RegisterInstanceWithAPIServerLB mocks base method.
+func (m *MockELBInterface) RegisterInstanceWithAPIServerLB(arg0 *v1beta2.Instance) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "RegisterInstanceWithAPIServerLB", arg0)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// RegisterInstanceWithAPIServerLB indicates an expected call of RegisterInstanceWithAPIServerLB.
+func (mr *MockELBInterfaceMockRecorder) RegisterInstanceWithAPIServerLB(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterInstanceWithAPIServerLB", reflect.TypeOf((*MockELBInterface)(nil).RegisterInstanceWithAPIServerLB), arg0)
+}
diff --git a/pkg/cloud/services/securitygroup/securitygroups.go b/pkg/cloud/services/securitygroup/securitygroups.go
index 36a6a7108..4bf458b71 100644
--- a/pkg/cloud/services/securitygroup/securitygroups.go
+++ b/pkg/cloud/services/securitygroup/securitygroups.go
@@ -29,6 +29,7 @@ import (
 	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/awserrors"
 	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/converters"
 	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/filter"
+	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/scope"
 	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/services"
 	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/services/wait"
 	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/tags"
@@ -88,6 +89,7 @@ func (s *Service) ReconcileSecurityGroups() error {
 	// First iteration makes sure that the security group are valid and fully created.
 	for i := range s.roles {
 		role := s.roles[i]
+		// role == SecurityGroupLB
 		sg := s.getDefaultSecurityGroup(role)
 
 		// if an override exists for this role use it
@@ -136,9 +138,9 @@ func (s *Service) ReconcileSecurityGroups() error {
 
 	// Second iteration creates or updates all permissions on the security group to match
 	// the specified ingress rules.
-	for i := range s.scope.SecurityGroups() {
-		sg := s.scope.SecurityGroups()[i]
-		s.scope.Debug("second pass security group reconciliation", "group-id", sg.ID, "name", sg.Name, "role", i)
+	for role := range s.scope.SecurityGroups() {
+		sg := s.scope.SecurityGroups()[role]
+		s.scope.Debug("second pass security group reconciliation", "group-id", sg.ID, "name", sg.Name, "role", role)
 
 		if s.securityGroupIsAnOverride(sg.ID) {
 			// skip rule/tag reconciliation on security groups that are overrides, assuming they're managed by another process
@@ -151,7 +153,7 @@ func (s *Service) ReconcileSecurityGroups() error {
 		}
 		current := sg.IngressRules
 
-		want, err := s.getSecurityGroupIngressRules(i)
+		want, err := s.getSecurityGroupIngressRules(role)
 		if err != nil {
 			return err
 		}
@@ -395,9 +397,8 @@ func (s *Service) authorizeSecurityGroupIngressRules(id string, rules infrav1.In
 	input := &ec2.AuthorizeSecurityGroupIngressInput{GroupId: aws.String(id)}
 	for i := range rules {
 		rule := rules[i]
-		input.IpPermissions = append(input.IpPermissions, ingressRuleToSDKType(&rule))
+		input.IpPermissions = append(input.IpPermissions, ingressRuleToSDKType(s.scope, &rule))
 	}
-
 	if _, err := s.EC2Client.AuthorizeSecurityGroupIngress(input); err != nil {
 		record.Warnf(s.scope.InfraCluster(), "FailedAuthorizeSecurityGroupIngressRules", "Failed to authorize security group ingress rules %v for SecurityGroup %q: %v", rules, id, err)
 		return errors.Wrapf(err, "failed to authorize security group %q ingress rules: %v", id, rules)
@@ -411,7 +412,7 @@ func (s *Service) revokeSecurityGroupIngressRules(id string, rules infrav1.Ingre
 	input := &ec2.RevokeSecurityGroupIngressInput{GroupId: aws.String(id)}
 	for i := range rules {
 		rule := rules[i]
-		input.IpPermissions = append(input.IpPermissions, ingressRuleToSDKType(&rule))
+		input.IpPermissions = append(input.IpPermissions, ingressRuleToSDKType(s.scope, &rule))
 	}
 
 	if _, err := s.EC2Client.RevokeSecurityGroupIngress(input); err != nil {
@@ -492,8 +493,8 @@ func (s *Service) getSecurityGroupIngressRules(role infrav1.SecurityGroupRole) (
 			{
 				Description: "Kubernetes API",
 				Protocol:    infrav1.SecurityGroupProtocolTCP,
-				FromPort:    6443,
-				ToPort:      6443,
+				FromPort:    infrav1.DefaultAPIServerPort,
+				ToPort:      infrav1.DefaultAPIServerPort,
 				SourceSecurityGroupIDs: []string{
 					s.scope.SecurityGroups()[infrav1.SecurityGroupAPIServerLB].ID,
 					s.scope.SecurityGroups()[infrav1.SecurityGroupControlPlane].ID,
@@ -583,6 +584,37 @@ func (s *Service) getSecurityGroupIngressRules(role infrav1.SecurityGroupRole) (
 		return rules, nil
 	case infrav1.SecurityGroupLB:
 		// We hand this group off to the in-cluster cloud provider, so these rules aren't used
+		// Except if the load balancer type is NLB, and we have an AWS Cluster in which case we
+		// need to open port 6443 to the NLB traffic and health check inside the VPC.
+		if s.scope.ControlPlaneLoadBalancer() != nil && s.scope.ControlPlaneLoadBalancer().LoadBalancerType == infrav1.LoadBalancerTypeNLB {
+			var (
+				ipv4CidrBlocks []string
+				ipv6CidrBlocks []string
+			)
+
+			ipv4CidrBlocks = []string{s.scope.VPC().CidrBlock}
+			if s.scope.VPC().IsIPv6Enabled() {
+				ipv6CidrBlocks = []string{s.scope.VPC().IPv6.CidrBlock}
+			}
+			if s.scope.ControlPlaneLoadBalancer().PreserveClientIP {
+				ipv4CidrBlocks = []string{services.AnyIPv4CidrBlock}
+				if s.scope.VPC().IsIPv6Enabled() {
+					ipv6CidrBlocks = []string{services.AnyIPv6CidrBlock}
+				}
+			}
+
+			rules := infrav1.IngressRules{
+				{
+					Description:    "Allow NLB traffic to the control plane instances.",
+					Protocol:       infrav1.SecurityGroupProtocolTCP,
+					FromPort:       int64(s.scope.APIServerPort()),
+					ToPort:         int64(s.scope.APIServerPort()),
+					CidrBlocks:     ipv4CidrBlocks,
+					IPv6CidrBlocks: ipv6CidrBlocks,
+				},
+			}
+			return rules, nil
+		}
 		return infrav1.IngressRules{}, nil
 	}
 
@@ -627,7 +659,7 @@ func (s *Service) isEKSOwned(sg infrav1.SecurityGroup) bool {
 	return ok
 }
 
-func ingressRuleToSDKType(i *infrav1.IngressRule) (res *ec2.IpPermission) {
+func ingressRuleToSDKType(scope scope.SGScope, i *infrav1.IngressRule) (res *ec2.IpPermission) {
 	// AWS seems to ignore the From/To port when set on protocols where it doesn't apply, but
 	// we avoid serializing it out for clarity's sake.
 	// See: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_IpPermission.html
@@ -645,6 +677,9 @@ func ingressRuleToSDKType(i *infrav1.IngressRule) (res *ec2.IpPermission) {
 		res = &ec2.IpPermission{
 			IpProtocol: aws.String(string(i.Protocol)),
 		}
+	default:
+		scope.Error(fmt.Errorf("invalid protocol '%s'", i.Protocol), "invalid protocol for security group", "protocol", i.Protocol)
+		return nil
 	}
 
 	for _, cidr := range i.CidrBlocks {


Test patch:
diff --git a/api/v1beta2/awscluster_webhook_test.go b/api/v1beta2/awscluster_webhook_test.go
index e607d1c06..3a6a1bc93 100644
--- a/api/v1beta2/awscluster_webhook_test.go
+++ b/api/v1beta2/awscluster_webhook_test.go
@@ -42,7 +42,7 @@ func TestAWSClusterDefault(t *testing.T) {
 }
 
 func TestAWSCluster_ValidateCreate(t *testing.T) {
-	unsupportedIncorrectScheme := ClassicELBScheme("any-other-scheme")
+	unsupportedIncorrectScheme := ELBScheme("any-other-scheme")
 
 	tests := []struct {
 		name    string
@@ -52,7 +52,7 @@ func TestAWSCluster_ValidateCreate(t *testing.T) {
 	}{
 		// The SSHKeyName tests were moved to sshkeyname_test.go
 		{
-			name: "Supported schemes are 'internet-facing, internal, or nil', rest will be rejected",
+			name: "Supported schemes are 'internet-facing, Internet-facing, internal, or nil', rest will be rejected",
 			cluster: &AWSCluster{
 				Spec: AWSClusterSpec{
 					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{Scheme: &unsupportedIncorrectScheme},
@@ -351,14 +351,14 @@ func TestAWSCluster_ValidateUpdate(t *testing.T) {
 			oldCluster: &AWSCluster{
 				Spec: AWSClusterSpec{
 					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
-						Scheme: &ClassicELBSchemeInternal,
+						Scheme: &ELBSchemeInternal,
 					},
 				},
 			},
 			newCluster: &AWSCluster{
 				Spec: AWSClusterSpec{
 					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
-						Scheme: &ClassicELBSchemeInternetFacing,
+						Scheme: &ELBSchemeInternetFacing,
 					},
 				},
 			},
@@ -372,7 +372,7 @@ func TestAWSCluster_ValidateUpdate(t *testing.T) {
 			newCluster: &AWSCluster{
 				Spec: AWSClusterSpec{
 					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
-						Scheme: &ClassicELBSchemeInternal,
+						Scheme: &ELBSchemeInternal,
 					},
 				},
 			},
@@ -386,7 +386,7 @@ func TestAWSCluster_ValidateUpdate(t *testing.T) {
 			newCluster: &AWSCluster{
 				Spec: AWSClusterSpec{
 					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
-						Scheme: &ClassicELBSchemeInternetFacing,
+						Scheme: &ELBSchemeInternetFacing,
 					},
 				},
 			},
@@ -526,14 +526,14 @@ func TestAWSCluster_ValidateUpdate(t *testing.T) {
 			oldCluster: &AWSCluster{
 				Spec: AWSClusterSpec{
 					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
-						HealthCheckProtocol: &ClassicELBProtocolTCP,
+						HealthCheckProtocol: &ELBProtocolTCP,
 					},
 				},
 			},
 			newCluster: &AWSCluster{
 				Spec: AWSClusterSpec{
 					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
-						HealthCheckProtocol: &ClassicELBProtocolSSL,
+						HealthCheckProtocol: &ELBProtocolSSL,
 					},
 				},
 			},
@@ -544,14 +544,14 @@ func TestAWSCluster_ValidateUpdate(t *testing.T) {
 			oldCluster: &AWSCluster{
 				Spec: AWSClusterSpec{
 					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
-						HealthCheckProtocol: &ClassicELBProtocolTCP,
+						HealthCheckProtocol: &ELBProtocolTCP,
 					},
 				},
 			},
 			newCluster: &AWSCluster{
 				Spec: AWSClusterSpec{
 					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
-						HealthCheckProtocol: &ClassicELBProtocolTCP,
+						HealthCheckProtocol: &ELBProtocolTCP,
 					},
 				},
 			},
@@ -565,7 +565,7 @@ func TestAWSCluster_ValidateUpdate(t *testing.T) {
 			newCluster: &AWSCluster{
 				Spec: AWSClusterSpec{
 					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
-						HealthCheckProtocol: &ClassicELBProtocolTCP,
+						HealthCheckProtocol: &ELBProtocolTCP,
 					},
 				},
 			},
diff --git a/controllers/awscluster_controller_test.go b/controllers/awscluster_controller_test.go
index 3d6dc808b..4dbcb6525 100644
--- a/controllers/awscluster_controller_test.go
+++ b/controllers/awscluster_controller_test.go
@@ -73,7 +73,7 @@ func TestAWSClusterReconciler_IntegrationTests(t *testing.T) {
 		elbMock := mocks.NewMockELBAPI(mockCtrl)
 		expect := func(m *mocks.MockEC2APIMockRecorder, e *mocks.MockELBAPIMockRecorder) {
 			mockedCreateVPCCalls(m)
-			mockedCreateSGCalls(m)
+			mockedCreateSGCalls(false, m)
 			mockedCreateLBCalls(t, e)
 			mockedDescribeInstanceCall(m)
 		}
@@ -85,6 +85,9 @@ func TestAWSClusterReconciler_IntegrationTests(t *testing.T) {
 		g.Expect(err).To(BeNil())
 
 		awsCluster := getAWSCluster("test", ns.Name)
+		awsCluster.Spec.ControlPlaneLoadBalancer = &infrav1.AWSLoadBalancerSpec{
+			LoadBalancerType: infrav1.LoadBalancerTypeClassic,
+		}
 
 		g.Expect(testEnv.Create(ctx, &awsCluster)).To(Succeed())
 		g.Eventually(func() bool {
@@ -159,6 +162,106 @@ func TestAWSClusterReconciler_IntegrationTests(t *testing.T) {
 			{conditionType: infrav1.SubnetsReadyCondition, status: corev1.ConditionTrue, severity: "", reason: ""},
 		})
 	})
+	t.Run("Should successfully reconcile AWSCluster creation with unmanaged VPC and a network type load balancer", func(t *testing.T) {
+		g := NewWithT(t)
+		mockCtrl = gomock.NewController(t)
+		ec2Mock := mocks.NewMockEC2API(mockCtrl)
+		elbv2Mock := mocks.NewMockELBV2API(mockCtrl)
+
+		setup(t)
+		controllerIdentity := createControllerIdentity(g)
+		ns, err := testEnv.CreateNamespace(ctx, fmt.Sprintf("integ-test-%s", util.RandomString(5)))
+		g.Expect(err).To(BeNil())
+
+		awsCluster := getAWSCluster("test", ns.Name)
+		awsCluster.Spec.ControlPlaneLoadBalancer = &infrav1.AWSLoadBalancerSpec{
+			LoadBalancerType: infrav1.LoadBalancerTypeNLB,
+			// Overwrite here because otherwise it's longer than 32, and we'll get a hashed name.
+			Name: aws.String("test-cluster-apiserver"),
+		}
+
+		expect := func(m *mocks.MockEC2APIMockRecorder, e *mocks.MockELBV2APIMockRecorder) {
+			mockedCreateVPCCalls(m)
+			mockedCreateSGCalls(true, m)
+			mockedCreateLBV2Calls(t, e)
+			mockedDescribeInstanceCall(m)
+		}
+		expect(ec2Mock.EXPECT(), elbv2Mock.EXPECT())
+
+		g.Expect(testEnv.Create(ctx, &awsCluster)).To(Succeed())
+		g.Eventually(func() bool {
+			cluster := &infrav1.AWSCluster{}
+			key := client.ObjectKey{
+				Name:      awsCluster.Name,
+				Namespace: ns.Name,
+			}
+			err := testEnv.Get(ctx, key, cluster)
+			return err == nil
+		}, 10*time.Second).Should(Equal(true))
+
+		defer teardown()
+		defer t.Cleanup(func() {
+			g.Expect(testEnv.Cleanup(ctx, &awsCluster, controllerIdentity, ns)).To(Succeed())
+		})
+
+		cs, err := getClusterScope(awsCluster)
+		cs.Cluster.Namespace = ns.Name
+		g.Expect(err).To(BeNil())
+		networkSvc := network.NewService(cs)
+		networkSvc.EC2Client = ec2Mock
+		reconciler.networkServiceFactory = func(clusterScope scope.ClusterScope) services.NetworkInterface {
+			return networkSvc
+		}
+
+		ec2Svc := ec2Service.NewService(cs)
+		ec2Svc.EC2Client = ec2Mock
+		reconciler.ec2ServiceFactory = func(scope scope.EC2Scope) services.EC2Interface {
+			return ec2Svc
+		}
+		testSecurityGroupRoles := []infrav1.SecurityGroupRole{
+			infrav1.SecurityGroupBastion,
+			infrav1.SecurityGroupAPIServerLB,
+			infrav1.SecurityGroupLB,
+			infrav1.SecurityGroupControlPlane,
+			infrav1.SecurityGroupNode,
+		}
+		sgSvc := securitygroup.NewService(cs, testSecurityGroupRoles)
+		sgSvc.EC2Client = ec2Mock
+
+		reconciler.securityGroupFactory = func(clusterScope scope.ClusterScope) services.SecurityGroupInterface {
+			return sgSvc
+		}
+		elbSvc := elbService.NewService(cs)
+		elbSvc.EC2Client = ec2Mock
+		elbSvc.ELBV2Client = elbv2Mock
+
+		reconciler.elbServiceFactory = func(elbScope scope.ELBScope) services.ELBInterface {
+			return elbSvc
+		}
+		cs.SetSubnets([]infrav1.SubnetSpec{
+			{
+				ID:               "subnet-2",
+				AvailabilityZone: "us-east-1c",
+				IsPublic:         true,
+				CidrBlock:        "10.0.11.0/24",
+			},
+			{
+				ID:               "subnet-1",
+				AvailabilityZone: "us-east-1a",
+				CidrBlock:        "10.0.10.0/24",
+				IsPublic:         false,
+			},
+		})
+		_, err = reconciler.reconcileNormal(cs)
+		g.Expect(err).To(BeNil())
+		g.Expect(cs.VPC().ID).To(Equal("vpc-exists"))
+		expectAWSClusterConditions(g, cs.AWSCluster, []conditionAssertion{
+			{conditionType: infrav1.ClusterSecurityGroupsReadyCondition, status: corev1.ConditionTrue, severity: "", reason: ""},
+			{conditionType: infrav1.BastionHostReadyCondition, status: corev1.ConditionTrue, severity: "", reason: ""},
+			{conditionType: infrav1.VpcReadyCondition, status: corev1.ConditionTrue, severity: "", reason: ""},
+			{conditionType: infrav1.SubnetsReadyCondition, status: corev1.ConditionTrue, severity: "", reason: ""},
+		})
+	})
 	t.Run("Should fail on AWSCluster reconciliation if VPC limit exceeded", func(t *testing.T) {
 		// Assuming the max VPC limit is 2 and when two VPCs are created, the creation of 3rd VPC throws mocked error from EC2 API
 		g := NewWithT(t)
@@ -180,6 +283,9 @@ func TestAWSClusterReconciler_IntegrationTests(t *testing.T) {
 			},
 			Spec: infrav1.AWSClusterSpec{
 				Region: "us-east-1",
+				ControlPlaneLoadBalancer: &infrav1.AWSLoadBalancerSpec{
+					LoadBalancerType: infrav1.LoadBalancerTypeClassic,
+				},
 			},
 		}
 		g.Expect(testEnv.Create(ctx, &awsCluster)).To(Succeed())
@@ -214,14 +320,15 @@ func TestAWSClusterReconciler_IntegrationTests(t *testing.T) {
 		mockCtrl = gomock.NewController(t)
 		ec2Mock := mocks.NewMockEC2API(mockCtrl)
 		elbMock := mocks.NewMockELBAPI(mockCtrl)
-		expect := func(m *mocks.MockEC2APIMockRecorder, e *mocks.MockELBAPIMockRecorder) {
+		elbv2Mock := mocks.NewMockELBV2API(mockCtrl)
+		expect := func(m *mocks.MockEC2APIMockRecorder, ev2 *mocks.MockELBV2APIMockRecorder, e *mocks.MockELBAPIMockRecorder) {
 			mockedDeleteVPCCalls(m)
 			mockedDescribeInstanceCall(m)
-			mockedDeleteLBCalls(e)
+			mockedDeleteLBCalls(true, ev2, e)
 			mockedDeleteInstanceCalls(m)
 			mockedDeleteSGCalls(m)
 		}
-		expect(ec2Mock.EXPECT(), elbMock.EXPECT())
+		expect(ec2Mock.EXPECT(), elbv2Mock.EXPECT(), elbMock.EXPECT())
 
 		setup(t)
 		controllerIdentity := createControllerIdentity(g)
@@ -263,6 +370,7 @@ func TestAWSClusterReconciler_IntegrationTests(t *testing.T) {
 		elbSvc := elbService.NewService(cs)
 		elbSvc.EC2Client = ec2Mock
 		elbSvc.ELBClient = elbMock
+		elbSvc.ELBV2Client = elbv2Mock
 		reconciler.elbServiceFactory = func(elbScope scope.ELBScope) services.ELBInterface {
 			return elbSvc
 		}
@@ -745,7 +853,7 @@ func mockedDeleteVPCCalls(m *mocks.MockEC2APIMockRecorder) {
 	}))
 }
 
-func mockedCreateSGCalls(m *mocks.MockEC2APIMockRecorder) {
+func mockedCreateSGCalls(recordLBV2 bool, m *mocks.MockEC2APIMockRecorder) {
 	m.DescribeSecurityGroups(gomock.Eq(&ec2.DescribeSecurityGroupsInput{
 		Filters: []*ec2.Filter{
 			{
@@ -905,4 +1013,11 @@ func mockedCreateSGCalls(m *mocks.MockEC2APIMockRecorder) {
 	})).
 		Return(&ec2.AuthorizeSecurityGroupIngressOutput{}, nil).
 		After(securityGroupNode).Times(2)
+	if recordLBV2 {
+		m.AuthorizeSecurityGroupIngress(gomock.AssignableToTypeOf(&ec2.AuthorizeSecurityGroupIngressInput{
+			GroupId: aws.String("sg-lb"),
+		})).
+			Return(&ec2.AuthorizeSecurityGroupIngressOutput{}, nil).
+			After(securityGroupNode).Times(1)
+	}
 }
diff --git a/controllers/awsmachine_controller_test.go b/controllers/awsmachine_controller_test.go
index a70cb2cd5..4fd4929bf 100644
--- a/controllers/awsmachine_controller_test.go
+++ b/controllers/awsmachine_controller_test.go
@@ -114,6 +114,9 @@ func TestAWSMachineReconciler_IntegrationTests(t *testing.T) {
 		g.Expect(err).To(BeNil())
 		cs.Cluster = &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"}}
 		cs.AWSCluster.Status.Network.APIServerELB.DNSName = DNSName
+		cs.AWSCluster.Spec.ControlPlaneLoadBalancer = &infrav1.AWSLoadBalancerSpec{
+			LoadBalancerType: infrav1.LoadBalancerTypeClassic,
+		}
 		cs.AWSCluster.Status.Network.SecurityGroups = map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{
 			infrav1.SecurityGroupNode: {
 				ID: "1",
@@ -162,13 +165,14 @@ func TestAWSMachineReconciler_IntegrationTests(t *testing.T) {
 		mockCtrl = gomock.NewController(t)
 		ec2Mock := mocks.NewMockEC2API(mockCtrl)
 		elbMock := mocks.NewMockELBAPI(mockCtrl)
+		elbv2Mock := mocks.NewMockELBV2API(mockCtrl)
 
-		expect := func(m *mocks.MockEC2APIMockRecorder, e *mocks.MockELBAPIMockRecorder) {
+		expect := func(m *mocks.MockEC2APIMockRecorder, ev2 *mocks.MockELBV2APIMockRecorder, e *mocks.MockELBAPIMockRecorder) {
 			mockedDescribeInstanceCalls(m)
-			mockedDeleteLBCalls(e)
+			mockedDeleteLBCalls(false, ev2, e)
 			mockedDeleteInstanceCalls(m)
 		}
-		expect(ec2Mock.EXPECT(), elbMock.EXPECT())
+		expect(ec2Mock.EXPECT(), elbv2Mock.EXPECT(), elbMock.EXPECT())
 
 		ns, err := testEnv.CreateNamespace(ctx, fmt.Sprintf("integ-test-%s", util.RandomString(5)))
 		g.Expect(err).To(BeNil())
@@ -186,6 +190,9 @@ func TestAWSMachineReconciler_IntegrationTests(t *testing.T) {
 		cs, err := getClusterScope(infrav1.AWSCluster{ObjectMeta: metav1.ObjectMeta{Name: "test"}})
 		g.Expect(err).To(BeNil())
 		cs.Cluster = &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"}}
+		cs.AWSCluster.Spec.ControlPlaneLoadBalancer = &infrav1.AWSLoadBalancerSpec{
+			LoadBalancerType: infrav1.LoadBalancerTypeClassic,
+		}
 		ms, err := getMachineScope(cs, awsMachine)
 		g.Expect(err).To(BeNil())
 
@@ -202,6 +209,7 @@ func TestAWSMachineReconciler_IntegrationTests(t *testing.T) {
 		elbSvc := elbService.NewService(cs)
 		elbSvc.EC2Client = ec2Mock
 		elbSvc.ELBClient = elbMock
+		elbSvc.ELBV2Client = elbv2Mock
 		reconciler.elbServiceFactory = func(scope scope.ELBScope) services.ELBInterface {
 			return elbSvc
 		}
@@ -263,6 +271,9 @@ func TestAWSMachineReconciler_IntegrationTests(t *testing.T) {
 		g.Expect(err).To(BeNil())
 		cs.Cluster = &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"}}
 		cs.AWSCluster.Status.Network.APIServerELB.DNSName = DNSName
+		cs.AWSCluster.Spec.ControlPlaneLoadBalancer = &infrav1.AWSLoadBalancerSpec{
+			LoadBalancerType: infrav1.LoadBalancerTypeClassic,
+		}
 		cs.AWSCluster.Status.Network.SecurityGroups = map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{
 			infrav1.SecurityGroupNode: {
 				ID: "1",
@@ -309,10 +320,11 @@ func TestAWSMachineReconciler_IntegrationTests(t *testing.T) {
 		mockCtrl = gomock.NewController(t)
 		ec2Mock := mocks.NewMockEC2API(mockCtrl)
 		elbMock := mocks.NewMockELBAPI(mockCtrl)
+		elbv2Mock := mocks.NewMockELBV2API(mockCtrl)
 
-		expect := func(m *mocks.MockEC2APIMockRecorder, e *mocks.MockELBAPIMockRecorder) {
+		expect := func(m *mocks.MockEC2APIMockRecorder, ev2 *mocks.MockELBV2APIMockRecorder, e *mocks.MockELBAPIMockRecorder) {
 			mockedDescribeInstanceCalls(m)
-			mockedDeleteLBCalls(e)
+			mockedDeleteLBCalls(false, ev2, e)
 			m.TerminateInstances(
 				gomock.Eq(&ec2.TerminateInstancesInput{
 					InstanceIds: aws.StringSlice([]string{"id-1"}),
@@ -320,7 +332,7 @@ func TestAWSMachineReconciler_IntegrationTests(t *testing.T) {
 			).
 				Return(nil, errors.New("Failed to delete instance"))
 		}
-		expect(ec2Mock.EXPECT(), elbMock.EXPECT())
+		expect(ec2Mock.EXPECT(), elbv2Mock.EXPECT(), elbMock.EXPECT())
 
 		ns, err := testEnv.CreateNamespace(ctx, fmt.Sprintf("integ-test-%s", util.RandomString(5)))
 		g.Expect(err).To(BeNil())
@@ -338,6 +350,9 @@ func TestAWSMachineReconciler_IntegrationTests(t *testing.T) {
 		cs, err := getClusterScope(infrav1.AWSCluster{ObjectMeta: metav1.ObjectMeta{Name: "test"}})
 		g.Expect(err).To(BeNil())
 		cs.Cluster = &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"}}
+		cs.AWSCluster.Spec.ControlPlaneLoadBalancer = &infrav1.AWSLoadBalancerSpec{
+			LoadBalancerType: infrav1.LoadBalancerTypeClassic,
+		}
 		ms, err := getMachineScope(cs, awsMachine)
 		g.Expect(err).To(BeNil())
 
@@ -354,6 +369,7 @@ func TestAWSMachineReconciler_IntegrationTests(t *testing.T) {
 		elbSvc := elbService.NewService(cs)
 		elbSvc.EC2Client = ec2Mock
 		elbSvc.ELBClient = elbMock
+		elbSvc.ELBV2Client = elbv2Mock
 		reconciler.elbServiceFactory = func(scope scope.ELBScope) services.ELBInterface {
 			return elbSvc
 		}
diff --git a/controllers/awsmachine_controller_unit_test.go b/controllers/awsmachine_controller_unit_test.go
index 57b08317f..358f5524e 100644
--- a/controllers/awsmachine_controller_unit_test.go
+++ b/controllers/awsmachine_controller_unit_test.go
@@ -139,7 +139,13 @@ func TestAWSMachineReconciler(t *testing.T) {
 			},
 		)
 		g.Expect(err).To(BeNil())
-
+		cs.AWSCluster = &infrav1.AWSCluster{
+			Spec: infrav1.AWSClusterSpec{
+				ControlPlaneLoadBalancer: &infrav1.AWSLoadBalancerSpec{
+					LoadBalancerType: infrav1.LoadBalancerTypeClassic,
+				},
+			},
+		}
 		ms, err = scope.NewMachineScope(
 			scope.MachineScopeParams{
 				Client: client,
diff --git a/controllers/helpers_test.go b/controllers/helpers_test.go
index c58634165..73625445f 100644
--- a/controllers/helpers_test.go
+++ b/controllers/helpers_test.go
@@ -21,6 +21,7 @@ import (
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/service/elb"
+	"github.com/aws/aws-sdk-go/service/elbv2"
 	"github.com/golang/mock/gomock"
 	"github.com/google/go-cmp/cmp"
 	. "github.com/onsi/gomega"
@@ -37,6 +38,7 @@ const DNSName = "www.google.com"
 
 var (
 	lbName          = aws.String("test-cluster-apiserver")
+	lbArn           = aws.String("loadbalancer::arn")
 	describeLBInput = &elb.DescribeLoadBalancersInput{
 		LoadBalancerNames: aws.StringSlice([]string{"test-cluster-apiserver"}),
 	}
@@ -46,13 +48,37 @@ var (
 	describeLBOutput = &elb.DescribeLoadBalancersOutput{
 		LoadBalancerDescriptions: []*elb.LoadBalancerDescription{
 			{
-				Scheme:            aws.String(string(infrav1.ClassicELBSchemeInternetFacing)),
+				Scheme:            aws.String(string(infrav1.ELBSchemeInternetFacing)),
 				Subnets:           []*string{aws.String("subnet-1")},
 				AvailabilityZones: []*string{aws.String("us-east-1a")},
 				VPCId:             aws.String("vpc-exists"),
 			},
 		},
 	}
+	describeLBOutputV2 = &elbv2.DescribeLoadBalancersOutput{
+		LoadBalancers: []*elbv2.LoadBalancer{
+			{
+				Scheme: aws.String(string(infrav1.ELBSchemeInternetFacing)),
+				AvailabilityZones: []*elbv2.AvailabilityZone{
+					{
+						SubnetId: aws.String("subnet-1"),
+						ZoneName: aws.String("us-east-1a"),
+					},
+				},
+				LoadBalancerArn: aws.String(*lbArn),
+				VpcId:           aws.String("vpc-exists"),
+				DNSName:         aws.String("dns"),
+			},
+		},
+	}
+	describeLBAttributesOutputV2 = &elbv2.DescribeLoadBalancerAttributesOutput{
+		Attributes: []*elbv2.LoadBalancerAttribute{
+			{
+				Key:   aws.String("cross-zone"),
+				Value: aws.String("true"),
+			},
+		},
+	}
 	describeLBAttributesOutput = &elb.DescribeLoadBalancerAttributesOutput{
 		LoadBalancerAttributes: &elb.LoadBalancerAttributes{
 			CrossZoneLoadBalancing: &elb.CrossZoneLoadBalancing{
@@ -74,6 +100,20 @@ var (
 			Value: aws.String("apiserver"),
 		},
 	}
+	expectedV2Tags = []*elbv2.Tag{
+		{
+			Key:   aws.String("Name"),
+			Value: lbName,
+		},
+		{
+			Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"),
+			Value: aws.String("owned"),
+		},
+		{
+			Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
+			Value: aws.String("apiserver"),
+		},
+	}
 )
 
 func expectAWSClusterConditions(g *WithT, m *infrav1.AWSCluster, expected []conditionAssertion) {
@@ -96,6 +136,9 @@ func getAWSCluster(name, namespace string) infrav1.AWSCluster {
 		},
 		Spec: infrav1.AWSClusterSpec{
 			Region: "us-east-1",
+			ControlPlaneLoadBalancer: &infrav1.AWSLoadBalancerSpec{
+				LoadBalancerType: infrav1.LoadBalancerTypeClassic,
+			},
 			NetworkSpec: infrav1.NetworkSpec{
 				VPC: infrav1.VPCSpec{
 					ID:        "vpc-exists",
@@ -190,7 +233,79 @@ func mockedCreateLBCalls(t *testing.T, m *mocks.MockELBAPIMockRecorder) {
 	m.RegisterInstancesWithLoadBalancer(gomock.Eq(&elb.RegisterInstancesWithLoadBalancerInput{Instances: []*elb.Instance{{InstanceId: aws.String("two")}}, LoadBalancerName: lbName})).MaxTimes(1)
 }
 
-func mockedDeleteLBCalls(m *mocks.MockELBAPIMockRecorder) {
+func mockedCreateLBV2Calls(t *testing.T, m *mocks.MockELBV2APIMockRecorder) {
+	t.Helper()
+	m.DescribeLoadBalancers(gomock.Eq(&elbv2.DescribeLoadBalancersInput{
+		Names: []*string{lbName},
+	})).
+		Return(describeLBOutputV2, nil).MinTimes(1)
+	m.DescribeLoadBalancerAttributes(gomock.Eq(&elbv2.DescribeLoadBalancerAttributesInput{
+		LoadBalancerArn: lbArn,
+	})).Return(describeLBAttributesOutputV2, nil)
+	m.DescribeTags(&elbv2.DescribeTagsInput{ResourceArns: []*string{lbArn}}).Return(
+		&elbv2.DescribeTagsOutput{
+			TagDescriptions: []*elbv2.TagDescription{
+				{
+					ResourceArn: lbArn,
+					Tags: []*elbv2.Tag{{
+						Key:   aws.String(infrav1.ClusterTagKey("test-cluster-apiserver")),
+						Value: aws.String(string(infrav1.ResourceLifecycleOwned)),
+					}},
+				},
+			},
+		}, nil)
+	m.ModifyLoadBalancerAttributes(gomock.Eq(&elbv2.ModifyLoadBalancerAttributesInput{
+		Attributes: []*elbv2.LoadBalancerAttribute{
+			{
+				Key:   aws.String(infrav1.LoadBalancerAttributeEnableLoadBalancingCrossZone),
+				Value: aws.String("false"),
+			},
+		},
+		LoadBalancerArn: lbArn,
+	})).MaxTimes(1)
+	m.AddTags(gomock.AssignableToTypeOf(&elbv2.AddTagsInput{})).Return(&elbv2.AddTagsOutput{}, nil).Do(
+		func(actual *elbv2.AddTagsInput) {
+			sortTagsByKey := func(tags []*elbv2.Tag) {
+				sort.Slice(tags, func(i, j int) bool {
+					return *(tags[i].Key) < *(tags[j].Key)
+				})
+			}
+
+			sortTagsByKey(actual.Tags)
+			if !cmp.Equal(expectedV2Tags, actual.Tags) {
+				t.Fatalf("Actual AddTagsInput did not match expected, Actual : %v, Expected: %v", actual.Tags, expectedV2Tags)
+			}
+		}).AnyTimes()
+	m.RemoveTags(gomock.Eq(&elbv2.RemoveTagsInput{
+		ResourceArns: []*string{lbArn},
+		TagKeys:      []*string{aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster-apiserver")},
+	})).MaxTimes(1)
+}
+
+func mockedDeleteLBCalls(expectV2Call bool, mv2 *mocks.MockELBV2APIMockRecorder, m *mocks.MockELBAPIMockRecorder) {
+	if expectV2Call {
+		mv2.DescribeLoadBalancers(gomock.Any()).Return(describeLBOutputV2, nil)
+		mv2.DescribeLoadBalancerAttributes(gomock.Any()).
+			Return(describeLBAttributesOutputV2, nil).MaxTimes(1)
+		mv2.DescribeTags(gomock.Any()).Return(
+			&elbv2.DescribeTagsOutput{
+				TagDescriptions: []*elbv2.TagDescription{
+					{
+						Tags: []*elbv2.Tag{
+							{
+								Key:   aws.String("name"),
+								Value: lbName,
+							},
+						},
+					},
+				},
+			}, nil).MaxTimes(1)
+		mv2.DescribeTargetGroups(gomock.Any()).Return(&elbv2.DescribeTargetGroupsOutput{}, nil)
+		mv2.DescribeListeners(gomock.Any()).Return(&elbv2.DescribeListenersOutput{}, nil)
+		mv2.DeleteLoadBalancer(gomock.Eq(&elbv2.DeleteLoadBalancerInput{LoadBalancerArn: lbArn})).
+			Return(&elbv2.DeleteLoadBalancerOutput{}, nil).MaxTimes(1)
+		mv2.DescribeLoadBalancers(gomock.Any()).Return(&elbv2.DescribeLoadBalancersOutput{}, nil)
+	}
 	m.DescribeLoadBalancers(gomock.Eq(describeLBInput)).
 		Return(describeLBOutput, nil)
 	m.DescribeLoadBalancers(gomock.Eq(describeLBInput)).
diff --git a/pkg/cloud/services/ec2/instances_test.go b/pkg/cloud/services/ec2/instances_test.go
index b3217aa81..204f2ba26 100644
--- a/pkg/cloud/services/ec2/instances_test.go
+++ b/pkg/cloud/services/ec2/instances_test.go
@@ -348,7 +348,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -454,7 +454,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -550,7 +550,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -680,7 +680,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -811,7 +811,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -943,7 +943,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -1049,7 +1049,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -1155,7 +1155,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -1231,7 +1231,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -1338,7 +1338,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -1418,7 +1418,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -1485,7 +1485,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -1594,7 +1594,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -1685,7 +1685,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -1796,7 +1796,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -1889,7 +1889,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -1958,7 +1958,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -2059,7 +2059,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -2196,7 +2196,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -2326,7 +2326,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -2435,7 +2435,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -2545,7 +2545,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -2655,7 +2655,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -2762,7 +2762,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
@@ -2869,7 +2869,7 @@ func TestCreateInstance(t *testing.T) {
 								ID: "3",
 							},
 						},
-						APIServerELB: infrav1.ClassicELB{
+						APIServerELB: infrav1.LoadBalancer{
 							DNSName: "test-apiserver.us-east-1.aws",
 						},
 					},
diff --git a/pkg/cloud/services/elb/loadbalancer_test.go b/pkg/cloud/services/elb/loadbalancer_test.go
index f38a27a01..b0d71a89c 100644
--- a/pkg/cloud/services/elb/loadbalancer_test.go
+++ b/pkg/cloud/services/elb/loadbalancer_test.go
@@ -26,6 +26,7 @@ import (
 	"github.com/aws/aws-sdk-go/aws/awserr"
 	"github.com/aws/aws-sdk-go/service/ec2"
 	"github.com/aws/aws-sdk-go/service/elb"
+	"github.com/aws/aws-sdk-go/service/elbv2"
 	rgapi "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
 	"github.com/golang/mock/gomock"
 	. "github.com/onsi/gomega"
@@ -153,15 +154,15 @@ func TestGetAPIServerClassicELBSpecControlPlaneLoadBalancer(t *testing.T) {
 		name   string
 		lb     *infrav1.AWSLoadBalancerSpec
 		mocks  func(m *mocks.MockEC2APIMockRecorder)
-		expect func(t *testing.T, g *WithT, res *infrav1.ClassicELB)
+		expect func(t *testing.T, g *WithT, res *infrav1.LoadBalancer)
 	}{
 		{
 			name:  "nil load balancer config",
 			lb:    nil,
 			mocks: func(m *mocks.MockEC2APIMockRecorder) {},
-			expect: func(t *testing.T, g *WithT, res *infrav1.ClassicELB) {
+			expect: func(t *testing.T, g *WithT, res *infrav1.LoadBalancer) {
 				t.Helper()
-				if res.Attributes.CrossZoneLoadBalancing {
+				if res.ClassicElbAttributes.CrossZoneLoadBalancing {
 					t.Error("Expected load balancer not to have cross-zone load balancing enabled")
 				}
 			},
@@ -172,9 +173,9 @@ func TestGetAPIServerClassicELBSpecControlPlaneLoadBalancer(t *testing.T) {
 				CrossZoneLoadBalancing: true,
 			},
 			mocks: func(m *mocks.MockEC2APIMockRecorder) {},
-			expect: func(t *testing.T, g *WithT, res *infrav1.ClassicELB) {
+			expect: func(t *testing.T, g *WithT, res *infrav1.LoadBalancer) {
 				t.Helper()
-				if !res.Attributes.CrossZoneLoadBalancing {
+				if !res.ClassicElbAttributes.CrossZoneLoadBalancing {
 					t.Error("Expected load balancer to have cross-zone load balancing enabled")
 				}
 			},
@@ -204,7 +205,7 @@ func TestGetAPIServerClassicELBSpecControlPlaneLoadBalancer(t *testing.T) {
 						},
 					}, nil)
 			},
-			expect: func(t *testing.T, g *WithT, res *infrav1.ClassicELB) {
+			expect: func(t *testing.T, g *WithT, res *infrav1.LoadBalancer) {
 				t.Helper()
 				if len(res.SubnetIDs) != 2 {
 					t.Errorf("Expected load balancer to be configured for 2 subnets, got %v", len(res.SubnetIDs))
@@ -220,7 +221,7 @@ func TestGetAPIServerClassicELBSpecControlPlaneLoadBalancer(t *testing.T) {
 				AdditionalSecurityGroups: []string{"sg-00001", "sg-00002"},
 			},
 			mocks: func(m *mocks.MockEC2APIMockRecorder) {},
-			expect: func(t *testing.T, g *WithT, res *infrav1.ClassicELB) {
+			expect: func(t *testing.T, g *WithT, res *infrav1.LoadBalancer) {
 				t.Helper()
 				if len(res.SecurityGroupIDs) != 3 {
 					t.Errorf("Expected load balancer to be configured for 3 security groups, got %v", len(res.SecurityGroupIDs))
@@ -230,12 +231,12 @@ func TestGetAPIServerClassicELBSpecControlPlaneLoadBalancer(t *testing.T) {
 		{
 			name: "Should create load balancer spec if elb health check protocol specified in config",
 			lb: &infrav1.AWSLoadBalancerSpec{
-				HealthCheckProtocol: &infrav1.ClassicELBProtocolTCP,
+				HealthCheckProtocol: &infrav1.ELBProtocolTCP,
 			},
 			mocks: func(m *mocks.MockEC2APIMockRecorder) {},
-			expect: func(t *testing.T, g *WithT, res *infrav1.ClassicELB) {
+			expect: func(t *testing.T, g *WithT, res *infrav1.LoadBalancer) {
 				t.Helper()
-				expectedTarget := fmt.Sprintf("%v:%d", infrav1.ClassicELBProtocolTCP, 6443)
+				expectedTarget := fmt.Sprintf("%v:%d", infrav1.ELBProtocolTCP, infrav1.DefaultAPIServerPort)
 				g.Expect(expectedTarget, res.HealthCheck.Target)
 			},
 		},
@@ -243,9 +244,9 @@ func TestGetAPIServerClassicELBSpecControlPlaneLoadBalancer(t *testing.T) {
 			name:  "Should create load balancer spec with default elb health check protocol",
 			lb:    &infrav1.AWSLoadBalancerSpec{},
 			mocks: func(m *mocks.MockEC2APIMockRecorder) {},
-			expect: func(t *testing.T, g *WithT, res *infrav1.ClassicELB) {
+			expect: func(t *testing.T, g *WithT, res *infrav1.LoadBalancer) {
 				t.Helper()
-				expectedTarget := fmt.Sprintf("%v:%d", infrav1.ClassicELBProtocolTCP, 6443)
+				expectedTarget := fmt.Sprintf("%v:%d", infrav1.ELBProtocolTCP, infrav1.DefaultAPIServerPort)
 				g.Expect(expectedTarget, res.HealthCheck.Target)
 			},
 		},
@@ -297,6 +298,161 @@ func TestGetAPIServerClassicELBSpecControlPlaneLoadBalancer(t *testing.T) {
 	}
 }
 
+func TestGetAPIServerV2ELBSpecControlPlaneLoadBalancer(t *testing.T) {
+	tests := []struct {
+		name   string
+		lb     *infrav1.AWSLoadBalancerSpec
+		mocks  func(m *mocks.MockEC2APIMockRecorder)
+		expect func(t *testing.T, g *WithT, res *infrav1.LoadBalancer)
+	}{
+		{
+			name:  "nil load balancer config",
+			lb:    nil,
+			mocks: func(m *mocks.MockEC2APIMockRecorder) {},
+			expect: func(t *testing.T, g *WithT, res *infrav1.LoadBalancer) {
+				t.Helper()
+				if _, ok := res.ELBAttributes["load_balancing.cross_zone.enabled"]; ok {
+					t.Error("Expected load balancer not to have cross-zone load balancing enabled")
+				}
+			},
+		},
+		{
+			name: "load balancer config with cross zone enabled",
+			lb: &infrav1.AWSLoadBalancerSpec{
+				CrossZoneLoadBalancing: true,
+			},
+			mocks: func(m *mocks.MockEC2APIMockRecorder) {},
+			expect: func(t *testing.T, g *WithT, res *infrav1.LoadBalancer) {
+				t.Helper()
+				if _, ok := res.ELBAttributes["load_balancing.cross_zone.enabled"]; !ok {
+					t.Error("Expected load balancer to have cross-zone load balancing enabled")
+				}
+			},
+		},
+		{
+			name: "load balancer config with subnets specified",
+			lb: &infrav1.AWSLoadBalancerSpec{
+				Subnets: []string{"subnet-1", "subnet-2"},
+			},
+			mocks: func(m *mocks.MockEC2APIMockRecorder) {
+				m.DescribeSubnets(gomock.Eq(&ec2.DescribeSubnetsInput{
+					SubnetIds: []*string{
+						aws.String("subnet-1"),
+						aws.String("subnet-2"),
+					},
+				})).
+					Return(&ec2.DescribeSubnetsOutput{
+						Subnets: []*ec2.Subnet{
+							{
+								SubnetId:         aws.String("subnet-1"),
+								AvailabilityZone: aws.String("us-east-1a"),
+							},
+							{
+								SubnetId:         aws.String("subnet-2"),
+								AvailabilityZone: aws.String("us-east-1b"),
+							},
+						},
+					}, nil)
+			},
+			expect: func(t *testing.T, g *WithT, res *infrav1.LoadBalancer) {
+				t.Helper()
+				if len(res.SubnetIDs) != 2 {
+					t.Errorf("Expected load balancer to be configured for 2 subnets, got %v", len(res.SubnetIDs))
+				}
+				if len(res.AvailabilityZones) != 2 {
+					t.Errorf("Expected load balancer to be configured for 2 availability zones, got %v", len(res.AvailabilityZones))
+				}
+			},
+		},
+		{
+			name: "load balancer config with additional security groups specified",
+			lb: &infrav1.AWSLoadBalancerSpec{
+				AdditionalSecurityGroups: []string{"sg-00001", "sg-00002"},
+				LoadBalancerType:         infrav1.LoadBalancerTypeALB,
+			},
+			mocks: func(m *mocks.MockEC2APIMockRecorder) {},
+			expect: func(t *testing.T, g *WithT, res *infrav1.LoadBalancer) {
+				t.Helper()
+				if len(res.SecurityGroupIDs) != 3 {
+					t.Errorf("Expected load balancer to be configured for 3 security groups, got %v", len(res.SecurityGroupIDs))
+				}
+			},
+		},
+		{
+			name: "load balancer config with additional security groups specified for NLB has no security groups",
+			lb: &infrav1.AWSLoadBalancerSpec{
+				AdditionalSecurityGroups: []string{"sg-00001", "sg-00002"},
+				LoadBalancerType:         infrav1.LoadBalancerTypeNLB,
+			},
+			mocks: func(m *mocks.MockEC2APIMockRecorder) {},
+			expect: func(t *testing.T, g *WithT, res *infrav1.LoadBalancer) {
+				t.Helper()
+				if len(res.SecurityGroupIDs) != 0 {
+					t.Errorf("Expected load balancer to be configured for 0 security groups, got %v", len(res.SecurityGroupIDs))
+				}
+			},
+		},
+		{
+			name: "A base listener is set up for NLB",
+			lb: &infrav1.AWSLoadBalancerSpec{
+				LoadBalancerType: infrav1.LoadBalancerTypeNLB,
+			},
+			mocks: func(m *mocks.MockEC2APIMockRecorder) {},
+			expect: func(t *testing.T, g *WithT, res *infrav1.LoadBalancer) {
+				t.Helper()
+				if len(res.ELBListeners) != 1 {
+					t.Errorf("Expected 1 listener to be configured by default, got %v listener(s)", len(res.SecurityGroupIDs))
+				}
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			g := NewWithT(t)
+			mockCtrl := gomock.NewController(t)
+			defer mockCtrl.Finish()
+			ec2Mock := mocks.NewMockEC2API(mockCtrl)
+
+			scheme := runtime.NewScheme()
+			_ = infrav1.AddToScheme(scheme)
+			client := fake.NewClientBuilder().WithScheme(scheme).Build()
+			clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{
+				Client: client,
+				Cluster: &clusterv1.Cluster{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "foo",
+						Name:      "bar",
+					},
+				},
+				AWSCluster: &infrav1.AWSCluster{
+					ObjectMeta: metav1.ObjectMeta{Name: "test"},
+					Spec: infrav1.AWSClusterSpec{
+						ControlPlaneLoadBalancer: tc.lb,
+					},
+				},
+			})
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			tc.mocks(ec2Mock.EXPECT())
+
+			s := &Service{
+				scope:     clusterScope,
+				EC2Client: ec2Mock,
+			}
+
+			spec, err := s.getAPIServerLBSpec(clusterScope.Name())
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			tc.expect(t, g, spec)
+		})
+	}
+}
+
 func TestRegisterInstanceWithAPIServerELB(t *testing.T) {
 	const (
 		namespace       = "foo"
@@ -340,7 +496,7 @@ func TestRegisterInstanceWithAPIServerELB(t *testing.T) {
 						LoadBalancerDescriptions: []*elb.LoadBalancerDescription{
 							{
 								LoadBalancerName: aws.String(elbName),
-								Scheme:           aws.String(string(infrav1.ClassicELBSchemeInternetFacing)),
+								Scheme:           aws.String(string(infrav1.ELBSchemeInternetFacing)),
 								Subnets:          []*string{aws.String(clusterSubnetID)},
 							},
 						},
@@ -408,7 +564,7 @@ func TestRegisterInstanceWithAPIServerELB(t *testing.T) {
 					Return(&elb.DescribeLoadBalancersOutput{
 						LoadBalancerDescriptions: []*elb.LoadBalancerDescription{
 							{
-								Scheme:            aws.String(string(infrav1.ClassicELBSchemeInternetFacing)),
+								Scheme:            aws.String(string(infrav1.ELBSchemeInternetFacing)),
 								Subnets:           []*string{aws.String(elbSubnetID)},
 								AvailabilityZones: []*string{aws.String(az)},
 							},
@@ -491,7 +647,7 @@ func TestRegisterInstanceWithAPIServerELB(t *testing.T) {
 					Return(&elb.DescribeLoadBalancersOutput{
 						LoadBalancerDescriptions: []*elb.LoadBalancerDescription{
 							{
-								Scheme:            aws.String(string(infrav1.ClassicELBSchemeInternetFacing)),
+								Scheme:            aws.String(string(infrav1.ELBSchemeInternetFacing)),
 								Subnets:           []*string{aws.String(elbSubnetID)},
 								AvailabilityZones: []*string{aws.String(differentAZ)},
 							},
@@ -518,32 +674,829 @@ func TestRegisterInstanceWithAPIServerELB(t *testing.T) {
 								}},
 							},
 						},
-					}, nil)
-			},
-			ec2Mocks: func(m *mocks.MockEC2APIMockRecorder) {
-				m.DescribeSubnets(gomock.Eq(&ec2.DescribeSubnetsInput{
-					SubnetIds: []*string{
-						aws.String(elbSubnetID),
+					}, nil)
+			},
+			ec2Mocks: func(m *mocks.MockEC2APIMockRecorder) {
+				m.DescribeSubnets(gomock.Eq(&ec2.DescribeSubnetsInput{
+					SubnetIds: []*string{
+						aws.String(elbSubnetID),
+					},
+				})).
+					Return(&ec2.DescribeSubnetsOutput{
+						Subnets: []*ec2.Subnet{
+							{
+								SubnetId:         aws.String(elbSubnetID),
+								AvailabilityZone: aws.String(differentAZ),
+							},
+						},
+					}, nil)
+			},
+			check: func(t *testing.T, err error) {
+				t.Helper()
+				expectedErrMsg := "failed to register instance with APIServer ELB \"bar-apiserver\": instance is in availability zone \"us-west-1a\", no public subnets attached to the ELB in the same zone"
+				if err == nil {
+					t.Fatalf("Expected error, but got nil")
+				}
+
+				if !strings.Contains(err.Error(), expectedErrMsg) {
+					t.Fatalf("Expected error: %s\nInstead got: %s", expectedErrMsg, err.Error())
+				}
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			mockCtrl := gomock.NewController(t)
+			defer mockCtrl.Finish()
+			elbAPIMocks := mocks.NewMockELBAPI(mockCtrl)
+			ec2Mock := mocks.NewMockEC2API(mockCtrl)
+
+			scheme, err := setupScheme()
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			client := fake.NewClientBuilder().WithScheme(scheme).Build()
+			clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{
+				Client: client,
+				Cluster: &clusterv1.Cluster{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: namespace,
+						Name:      clusterName,
+					},
+				},
+				AWSCluster: tc.awsCluster,
+			})
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			instance := &infrav1.Instance{
+				ID:       instanceID,
+				SubnetID: clusterSubnetID,
+			}
+
+			tc.elbAPIMocks(elbAPIMocks.EXPECT())
+			tc.ec2Mocks(ec2Mock.EXPECT())
+
+			s := &Service{
+				scope:     clusterScope,
+				EC2Client: ec2Mock,
+				ELBClient: elbAPIMocks,
+			}
+
+			err = s.RegisterInstanceWithAPIServerELB(instance)
+			tc.check(t, err)
+		})
+	}
+}
+
+func TestRegisterInstanceWithAPIServerNLB(t *testing.T) {
+	const (
+		namespace       = "foo"
+		clusterName     = "bar"
+		clusterSubnetID = "subnet-1"
+		elbName         = "bar-apiserver"
+		elbArn          = "arn::apiserver"
+		elbSubnetID     = "elb-subnet"
+		instanceID      = "test-instance"
+		az              = "us-west-1a"
+		differentAZ     = "us-east-2c"
+	)
+
+	tests := []struct {
+		name          string
+		awsCluster    *infrav1.AWSCluster
+		elbV2APIMocks func(m *mocks.MockELBV2APIMockRecorder)
+		ec2Mocks      func(m *mocks.MockEC2APIMockRecorder)
+		check         func(t *testing.T, err error)
+	}{
+		{
+			name: "no load balancer subnets specified",
+			awsCluster: &infrav1.AWSCluster{
+				ObjectMeta: metav1.ObjectMeta{Name: clusterName},
+				Spec: infrav1.AWSClusterSpec{
+					ControlPlaneLoadBalancer: &infrav1.AWSLoadBalancerSpec{
+						Name:             aws.String(elbName),
+						LoadBalancerType: infrav1.LoadBalancerTypeNLB,
+					},
+					NetworkSpec: infrav1.NetworkSpec{
+						Subnets: infrav1.Subnets{{
+							ID:               clusterSubnetID,
+							AvailabilityZone: az,
+						}},
+					},
+				},
+			},
+			elbV2APIMocks: func(m *mocks.MockELBV2APIMockRecorder) {
+				m.DescribeLoadBalancers(gomock.Eq(&elbv2.DescribeLoadBalancersInput{
+					Names: aws.StringSlice([]string{elbName}),
+				})).
+					Return(&elbv2.DescribeLoadBalancersOutput{
+						LoadBalancers: []*elbv2.LoadBalancer{
+							{
+								LoadBalancerArn:  aws.String(elbArn),
+								LoadBalancerName: aws.String(elbName),
+								Scheme:           aws.String(string(infrav1.ELBSchemeInternetFacing)),
+								AvailabilityZones: []*elbv2.AvailabilityZone{
+									{
+										SubnetId: aws.String(clusterSubnetID),
+									},
+								},
+							},
+						},
+					}, nil)
+				m.DescribeLoadBalancerAttributes(gomock.Eq(&elbv2.DescribeLoadBalancerAttributesInput{
+					LoadBalancerArn: aws.String(elbArn),
+				})).
+					Return(&elbv2.DescribeLoadBalancerAttributesOutput{
+						Attributes: []*elbv2.LoadBalancerAttribute{
+							{
+								Key:   aws.String("load_balancing.cross_zone.enabled"),
+								Value: aws.String("true"),
+							},
+						},
+					}, nil)
+				m.DescribeTags(&elbv2.DescribeTagsInput{ResourceArns: []*string{aws.String(elbArn)}}).Return(
+					&elbv2.DescribeTagsOutput{
+						TagDescriptions: []*elbv2.TagDescription{
+							{
+								ResourceArn: aws.String(elbArn),
+								Tags: []*elbv2.Tag{{
+									Key:   aws.String(infrav1.ClusterTagKey(clusterName)),
+									Value: aws.String(string(infrav1.ResourceLifecycleOwned)),
+								}},
+							},
+						},
+					}, nil)
+				m.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{
+					LoadBalancerArn: aws.String(elbArn),
+				}).Return(&elbv2.DescribeTargetGroupsOutput{
+					TargetGroups: []*elbv2.TargetGroup{
+						{
+							HealthCheckEnabled:  aws.Bool(true),
+							HealthCheckPort:     aws.String("infrav1.DefaultAPIServerPort"),
+							HealthCheckProtocol: aws.String("TCP"),
+							LoadBalancerArns:    aws.StringSlice([]string{elbArn}),
+							Port:                aws.Int64(infrav1.DefaultAPIServerPort),
+							Protocol:            aws.String("TCP"),
+							TargetGroupArn:      aws.String("target-group::arn"),
+							TargetGroupName:     aws.String("something-generated"),
+							VpcId:               aws.String("vpc-id"),
+						},
+					},
+				}, nil)
+				m.RegisterTargets(gomock.Eq(&elbv2.RegisterTargetsInput{
+					TargetGroupArn: aws.String("target-group::arn"),
+					Targets: []*elbv2.TargetDescription{
+						{
+							Id:   aws.String(instanceID),
+							Port: aws.Int64(infrav1.DefaultAPIServerPort),
+						},
+					},
+				})).Return(&elbv2.RegisterTargetsOutput{}, nil)
+			},
+			ec2Mocks: func(m *mocks.MockEC2APIMockRecorder) {},
+			check: func(t *testing.T, err error) {
+				t.Helper()
+				if err != nil {
+					t.Fatalf("did not expect error: %v", err)
+				}
+			},
+		},
+		{
+			name: "there are no target groups to register the instance into",
+			awsCluster: &infrav1.AWSCluster{
+				ObjectMeta: metav1.ObjectMeta{Name: clusterName},
+				Spec: infrav1.AWSClusterSpec{
+					NetworkSpec: infrav1.NetworkSpec{
+						Subnets: infrav1.Subnets{{
+							ID:               clusterSubnetID,
+							AvailabilityZone: az,
+						}},
+					},
+					ControlPlaneLoadBalancer: &infrav1.AWSLoadBalancerSpec{
+						Name:             aws.String(elbName),
+						Subnets:          []string{elbSubnetID},
+						LoadBalancerType: infrav1.LoadBalancerTypeNLB,
+					},
+				},
+			},
+			elbV2APIMocks: func(m *mocks.MockELBV2APIMockRecorder) {
+				m.DescribeLoadBalancers(gomock.Eq(&elbv2.DescribeLoadBalancersInput{
+					Names: aws.StringSlice([]string{elbName}),
+				})).
+					Return(&elbv2.DescribeLoadBalancersOutput{
+						LoadBalancers: []*elbv2.LoadBalancer{
+							{
+								LoadBalancerArn:  aws.String(elbArn),
+								LoadBalancerName: aws.String(elbName),
+								Scheme:           aws.String(string(infrav1.ELBSchemeInternetFacing)),
+								AvailabilityZones: []*elbv2.AvailabilityZone{
+									{
+										SubnetId: aws.String(clusterSubnetID),
+									},
+								},
+							},
+						},
+					}, nil)
+				m.DescribeLoadBalancerAttributes(gomock.Eq(&elbv2.DescribeLoadBalancerAttributesInput{
+					LoadBalancerArn: aws.String(elbArn),
+				})).
+					Return(&elbv2.DescribeLoadBalancerAttributesOutput{
+						Attributes: []*elbv2.LoadBalancerAttribute{
+							{
+								Key:   aws.String("load_balancing.cross_zone.enabled"),
+								Value: aws.String("true"),
+							},
+						},
+					}, nil)
+				m.DescribeTags(&elbv2.DescribeTagsInput{ResourceArns: []*string{aws.String(elbArn)}}).Return(
+					&elbv2.DescribeTagsOutput{
+						TagDescriptions: []*elbv2.TagDescription{
+							{
+								ResourceArn: aws.String(elbArn),
+								Tags: []*elbv2.Tag{{
+									Key:   aws.String(infrav1.ClusterTagKey(clusterName)),
+									Value: aws.String(string(infrav1.ResourceLifecycleOwned)),
+								}},
+							},
+						},
+					}, nil)
+				m.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{
+					LoadBalancerArn: aws.String(elbArn),
+				}).Return(&elbv2.DescribeTargetGroupsOutput{}, nil)
+			},
+			ec2Mocks: func(m *mocks.MockEC2APIMockRecorder) {},
+			check: func(t *testing.T, err error) {
+				t.Helper()
+				expectedErrMsg := fmt.Sprintf("no target groups found for load balancer with arn '%s'", elbArn)
+				if err == nil {
+					t.Fatalf("Expected error, but got nil")
+				}
+
+				if !strings.Contains(err.Error(), expectedErrMsg) {
+					t.Fatalf("Expected error: %s\nInstead got: %s", expectedErrMsg, err.Error())
+				}
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			mockCtrl := gomock.NewController(t)
+			defer mockCtrl.Finish()
+			elbV2APIMocks := mocks.NewMockELBV2API(mockCtrl)
+			ec2Mock := mocks.NewMockEC2API(mockCtrl)
+
+			scheme, err := setupScheme()
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			client := fake.NewClientBuilder().WithScheme(scheme).Build()
+			clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{
+				Client: client,
+				Cluster: &clusterv1.Cluster{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: namespace,
+						Name:      clusterName,
+					},
+				},
+				AWSCluster: tc.awsCluster,
+			})
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			instance := &infrav1.Instance{
+				ID:       instanceID,
+				SubnetID: clusterSubnetID,
+			}
+
+			tc.elbV2APIMocks(elbV2APIMocks.EXPECT())
+			tc.ec2Mocks(ec2Mock.EXPECT())
+
+			s := &Service{
+				scope:       clusterScope,
+				EC2Client:   ec2Mock,
+				ELBV2Client: elbV2APIMocks,
+			}
+
+			err = s.RegisterInstanceWithAPIServerLB(instance)
+			tc.check(t, err)
+		})
+	}
+}
+
+func TestCreateNLB(t *testing.T) {
+	const (
+		namespace       = "foo"
+		clusterName     = "bar"
+		clusterSubnetID = "subnet-1"
+		elbName         = "bar-apiserver"
+		elbArn          = "arn::apiserver"
+		vpcID           = "vpc-id"
+		dns             = "asdf:9999/asdf"
+	)
+
+	tests := []struct {
+		name          string
+		elbV2APIMocks func(m *mocks.MockELBV2APIMockRecorder)
+		check         func(t *testing.T, lb *infrav1.LoadBalancer, err error)
+		awsCluster    func(acl infrav1.AWSCluster) infrav1.AWSCluster
+		spec          func(spec infrav1.LoadBalancer) infrav1.LoadBalancer
+	}{
+		{
+			name: "main create flow",
+			spec: func(spec infrav1.LoadBalancer) infrav1.LoadBalancer {
+				return spec
+			},
+			awsCluster: func(acl infrav1.AWSCluster) infrav1.AWSCluster {
+				return acl
+			},
+			elbV2APIMocks: func(m *mocks.MockELBV2APIMockRecorder) {
+				m.CreateLoadBalancer(gomock.Eq(&elbv2.CreateLoadBalancerInput{
+					Name:    aws.String(elbName),
+					Scheme:  aws.String("internet-facing"),
+					Type:    aws.String("network"),
+					Subnets: aws.StringSlice([]string{clusterSubnetID}),
+					Tags: []*elbv2.Tag{
+						{
+							Key:   aws.String("test"),
+							Value: aws.String("tag"),
+						},
+					},
+				})).Return(&elbv2.CreateLoadBalancerOutput{
+					LoadBalancers: []*elbv2.LoadBalancer{
+						{
+							LoadBalancerArn:  aws.String(elbArn),
+							LoadBalancerName: aws.String(elbName),
+							Scheme:           aws.String(string(infrav1.ELBSchemeInternetFacing)),
+							DNSName:          aws.String(dns),
+						},
+					},
+				}, nil)
+				m.CreateTargetGroup(gomock.Eq(&elbv2.CreateTargetGroupInput{
+					HealthCheckEnabled:  aws.Bool(true),
+					HealthCheckPort:     aws.String("infrav1.DefaultAPIServerPort"),
+					HealthCheckProtocol: aws.String("tcp"),
+					Name:                aws.String("name"),
+					Port:                aws.Int64(infrav1.DefaultAPIServerPort),
+					Protocol:            aws.String("TCP"),
+					VpcId:               aws.String(vpcID),
+				})).Return(&elbv2.CreateTargetGroupOutput{
+					TargetGroups: []*elbv2.TargetGroup{
+						{
+							TargetGroupArn:  aws.String("target-group::arn"),
+							TargetGroupName: aws.String("name"),
+							VpcId:           aws.String(vpcID),
+						},
+					},
+				}, nil)
+				m.ModifyTargetGroupAttributes(gomock.Eq(&elbv2.ModifyTargetGroupAttributesInput{
+					TargetGroupArn: aws.String("target-group::arn"),
+					Attributes: []*elbv2.TargetGroupAttribute{
+						{
+							Key:   aws.String(infrav1.TargetGroupAttributeEnablePreserveClientIP),
+							Value: aws.String("false"),
+						},
+					},
+				})).Return(nil, nil)
+				m.CreateListener(gomock.Eq(&elbv2.CreateListenerInput{
+					DefaultActions: []*elbv2.Action{
+						{
+							TargetGroupArn: aws.String("target-group::arn"),
+							Type:           aws.String(elbv2.ActionTypeEnumForward),
+						},
+					},
+					LoadBalancerArn: aws.String(elbArn),
+					Port:            aws.Int64(infrav1.DefaultAPIServerPort),
+					Protocol:        aws.String("TCP"),
+					Tags: []*elbv2.Tag{
+						{
+							Key:   aws.String("test"),
+							Value: aws.String("tag"),
+						},
+					},
+				})).Return(&elbv2.CreateListenerOutput{
+					Listeners: []*elbv2.Listener{
+						{
+							ListenerArn: aws.String("listener::arn"),
+						},
+					},
+				}, nil)
+			},
+			check: func(t *testing.T, lb *infrav1.LoadBalancer, err error) {
+				t.Helper()
+				if err != nil {
+					t.Fatalf("did not expect error: %v", err)
+				}
+				if lb.DNSName != dns {
+					t.Fatalf("DNSName did not equal expected value; was: '%s'", lb.DNSName)
+				}
+			},
+		},
+		{
+			name: "created with ipv6 vpc",
+			spec: func(spec infrav1.LoadBalancer) infrav1.LoadBalancer {
+				return spec
+			},
+			awsCluster: func(acl infrav1.AWSCluster) infrav1.AWSCluster {
+				acl.Spec.NetworkSpec.VPC.IPv6 = &infrav1.IPv6{
+					CidrBlock: "2022:1234::/64",
+					PoolID:    "pool-id",
+				}
+				return acl
+			},
+			elbV2APIMocks: func(m *mocks.MockELBV2APIMockRecorder) {
+				m.CreateLoadBalancer(gomock.Eq(&elbv2.CreateLoadBalancerInput{
+					Name:          aws.String(elbName),
+					IpAddressType: aws.String("dualstack"),
+					Scheme:        aws.String("internet-facing"),
+					Type:          aws.String("network"),
+					Subnets:       aws.StringSlice([]string{clusterSubnetID}),
+					Tags: []*elbv2.Tag{
+						{
+							Key:   aws.String("test"),
+							Value: aws.String("tag"),
+						},
+					},
+				})).Return(&elbv2.CreateLoadBalancerOutput{
+					LoadBalancers: []*elbv2.LoadBalancer{
+						{
+							LoadBalancerArn:  aws.String(elbArn),
+							LoadBalancerName: aws.String(elbName),
+							Scheme:           aws.String(string(infrav1.ELBSchemeInternetFacing)),
+							DNSName:          aws.String(dns),
+						},
+					},
+				}, nil)
+				m.CreateTargetGroup(gomock.Eq(&elbv2.CreateTargetGroupInput{
+					HealthCheckEnabled:  aws.Bool(true),
+					HealthCheckPort:     aws.String("infrav1.DefaultAPIServerPort"),
+					HealthCheckProtocol: aws.String("tcp"),
+					Name:                aws.String("name"),
+					Port:                aws.Int64(infrav1.DefaultAPIServerPort),
+					Protocol:            aws.String("TCP"),
+					VpcId:               aws.String(vpcID),
+					IpAddressType:       aws.String("ipv6"),
+				})).Return(&elbv2.CreateTargetGroupOutput{
+					TargetGroups: []*elbv2.TargetGroup{
+						{
+							TargetGroupArn:  aws.String("target-group::arn"),
+							TargetGroupName: aws.String("name"),
+							VpcId:           aws.String(vpcID),
+						},
+					},
+				}, nil)
+				m.ModifyTargetGroupAttributes(gomock.Eq(&elbv2.ModifyTargetGroupAttributesInput{
+					TargetGroupArn: aws.String("target-group::arn"),
+					Attributes: []*elbv2.TargetGroupAttribute{
+						{
+							Key:   aws.String(infrav1.TargetGroupAttributeEnablePreserveClientIP),
+							Value: aws.String("false"),
+						},
+					},
+				})).Return(nil, nil)
+				m.CreateListener(gomock.Eq(&elbv2.CreateListenerInput{
+					DefaultActions: []*elbv2.Action{
+						{
+							TargetGroupArn: aws.String("target-group::arn"),
+							Type:           aws.String(elbv2.ActionTypeEnumForward),
+						},
+					},
+					LoadBalancerArn: aws.String(elbArn),
+					Port:            aws.Int64(infrav1.DefaultAPIServerPort),
+					Protocol:        aws.String("TCP"),
+					Tags: []*elbv2.Tag{
+						{
+							Key:   aws.String("test"),
+							Value: aws.String("tag"),
+						},
+					},
+				})).Return(&elbv2.CreateListenerOutput{
+					Listeners: []*elbv2.Listener{
+						{
+							ListenerArn: aws.String("listener::arn"),
+						},
+					},
+				}, nil)
+			},
+			check: func(t *testing.T, lb *infrav1.LoadBalancer, err error) {
+				t.Helper()
+				if err != nil {
+					t.Fatalf("did not expect error: %v", err)
+				}
+				if lb.DNSName != dns {
+					t.Fatalf("DNSName did not equal expected value; was: '%s'", lb.DNSName)
+				}
+			},
+		},
+		{
+			name: "creating a load balancer fails",
+			spec: func(spec infrav1.LoadBalancer) infrav1.LoadBalancer {
+				return spec
+			},
+			awsCluster: func(acl infrav1.AWSCluster) infrav1.AWSCluster {
+				return acl
+			},
+			elbV2APIMocks: func(m *mocks.MockELBV2APIMockRecorder) {
+				m.CreateLoadBalancer(gomock.Eq(&elbv2.CreateLoadBalancerInput{
+					Name:    aws.String(elbName),
+					Scheme:  aws.String("internet-facing"),
+					Type:    aws.String("network"),
+					Subnets: aws.StringSlice([]string{clusterSubnetID}),
+					Tags: []*elbv2.Tag{
+						{
+							Key:   aws.String("test"),
+							Value: aws.String("tag"),
+						},
+					},
+				})).Return(nil, errors.New("nope"))
+			},
+			check: func(t *testing.T, lb *infrav1.LoadBalancer, err error) {
+				t.Helper()
+				if err == nil {
+					t.Fatal("expected error, got nothing")
+				}
+				if !strings.Contains(err.Error(), "nope") {
+					t.Fatalf("expected error to contain 'nope' was instead: %s", err)
+				}
+			},
+		},
+		{
+			name: "no health check",
+			spec: func(spec infrav1.LoadBalancer) infrav1.LoadBalancer {
+				spec.ELBListeners = []infrav1.Listener{
+					{
+						Protocol: "TCP",
+						Port:     infrav1.DefaultAPIServerPort,
+						TargetGroup: infrav1.TargetGroupSpec{
+							Name:     "name",
+							Port:     infrav1.DefaultAPIServerPort,
+							Protocol: "TCP",
+							VpcID:    vpcID,
+						},
+					},
+				}
+				return spec
+			},
+			awsCluster: func(acl infrav1.AWSCluster) infrav1.AWSCluster {
+				return acl
+			},
+			elbV2APIMocks: func(m *mocks.MockELBV2APIMockRecorder) {
+				m.CreateLoadBalancer(gomock.Eq(&elbv2.CreateLoadBalancerInput{
+					Name:    aws.String(elbName),
+					Scheme:  aws.String("internet-facing"),
+					Type:    aws.String("network"),
+					Subnets: aws.StringSlice([]string{clusterSubnetID}),
+					Tags: []*elbv2.Tag{
+						{
+							Key:   aws.String("test"),
+							Value: aws.String("tag"),
+						},
+					},
+				})).Return(&elbv2.CreateLoadBalancerOutput{
+					LoadBalancers: []*elbv2.LoadBalancer{
+						{
+							LoadBalancerArn:  aws.String(elbArn),
+							LoadBalancerName: aws.String(elbName),
+							Scheme:           aws.String(string(infrav1.ELBSchemeInternetFacing)),
+							DNSName:          aws.String(dns),
+						},
+					},
+				}, nil)
+				m.CreateTargetGroup(gomock.Eq(&elbv2.CreateTargetGroupInput{
+					Name:     aws.String("name"),
+					Port:     aws.Int64(infrav1.DefaultAPIServerPort),
+					Protocol: aws.String("TCP"),
+					VpcId:    aws.String(vpcID),
+				})).Return(&elbv2.CreateTargetGroupOutput{
+					TargetGroups: []*elbv2.TargetGroup{
+						{
+							TargetGroupArn:  aws.String("target-group::arn"),
+							TargetGroupName: aws.String("name"),
+							VpcId:           aws.String(vpcID),
+						},
+					},
+				}, nil)
+				m.ModifyTargetGroupAttributes(gomock.Eq(&elbv2.ModifyTargetGroupAttributesInput{
+					TargetGroupArn: aws.String("target-group::arn"),
+					Attributes: []*elbv2.TargetGroupAttribute{
+						{
+							Key:   aws.String(infrav1.TargetGroupAttributeEnablePreserveClientIP),
+							Value: aws.String("false"),
+						},
+					},
+				})).Return(nil, nil)
+				m.CreateListener(gomock.Eq(&elbv2.CreateListenerInput{
+					DefaultActions: []*elbv2.Action{
+						{
+							TargetGroupArn: aws.String("target-group::arn"),
+							Type:           aws.String(elbv2.ActionTypeEnumForward),
+						},
+					},
+					LoadBalancerArn: aws.String(elbArn),
+					Port:            aws.Int64(infrav1.DefaultAPIServerPort),
+					Protocol:        aws.String("TCP"),
+					Tags: []*elbv2.Tag{
+						{
+							Key:   aws.String("test"),
+							Value: aws.String("tag"),
+						},
+					},
+				})).Return(&elbv2.CreateListenerOutput{
+					Listeners: []*elbv2.Listener{
+						{
+							ListenerArn: aws.String("listener::arn"),
+						},
+					},
+				}, nil)
+			},
+			check: func(t *testing.T, lb *infrav1.LoadBalancer, err error) {
+				t.Helper()
+				if err != nil {
+					t.Fatalf("did not expect error: %v", err)
+				}
+				if lb.DNSName != dns {
+					t.Fatalf("DNSName did not equal expected value; was: '%s'", lb.DNSName)
+				}
+			},
+		},
+		{
+			name: "PreserveClientIP is enabled",
+			spec: func(spec infrav1.LoadBalancer) infrav1.LoadBalancer {
+				return spec
+			},
+			awsCluster: func(acl infrav1.AWSCluster) infrav1.AWSCluster {
+				acl.Spec.ControlPlaneLoadBalancer.PreserveClientIP = true
+				return acl
+			},
+			elbV2APIMocks: func(m *mocks.MockELBV2APIMockRecorder) {
+				m.CreateLoadBalancer(gomock.Eq(&elbv2.CreateLoadBalancerInput{
+					Name:    aws.String(elbName),
+					Scheme:  aws.String("internet-facing"),
+					Type:    aws.String("network"),
+					Subnets: aws.StringSlice([]string{clusterSubnetID}),
+					Tags: []*elbv2.Tag{
+						{
+							Key:   aws.String("test"),
+							Value: aws.String("tag"),
+						},
+					},
+				})).Return(&elbv2.CreateLoadBalancerOutput{
+					LoadBalancers: []*elbv2.LoadBalancer{
+						{
+							LoadBalancerArn:  aws.String(elbArn),
+							LoadBalancerName: aws.String(elbName),
+							Scheme:           aws.String(string(infrav1.ELBSchemeInternetFacing)),
+							DNSName:          aws.String(dns),
+						},
+					},
+				}, nil)
+				m.CreateTargetGroup(gomock.Eq(&elbv2.CreateTargetGroupInput{
+					HealthCheckEnabled:  aws.Bool(true),
+					HealthCheckPort:     aws.String("infrav1.DefaultAPIServerPort"),
+					HealthCheckProtocol: aws.String("tcp"),
+					Name:                aws.String("name"),
+					Port:                aws.Int64(infrav1.DefaultAPIServerPort),
+					Protocol:            aws.String("TCP"),
+					VpcId:               aws.String(vpcID),
+				})).Return(&elbv2.CreateTargetGroupOutput{
+					TargetGroups: []*elbv2.TargetGroup{
+						{
+							TargetGroupArn:  aws.String("target-group::arn"),
+							TargetGroupName: aws.String("name"),
+							VpcId:           aws.String(vpcID),
+						},
+					},
+				}, nil)
+				m.CreateListener(gomock.Eq(&elbv2.CreateListenerInput{
+					DefaultActions: []*elbv2.Action{
+						{
+							TargetGroupArn: aws.String("target-group::arn"),
+							Type:           aws.String(elbv2.ActionTypeEnumForward),
+						},
+					},
+					LoadBalancerArn: aws.String(elbArn),
+					Port:            aws.Int64(infrav1.DefaultAPIServerPort),
+					Protocol:        aws.String("TCP"),
+					Tags: []*elbv2.Tag{
+						{
+							Key:   aws.String("test"),
+							Value: aws.String("tag"),
+						},
+					},
+				})).Return(&elbv2.CreateListenerOutput{
+					Listeners: []*elbv2.Listener{
+						{
+							ListenerArn: aws.String("listener::arn"),
+						},
+					},
+				}, nil)
+			},
+			check: func(t *testing.T, lb *infrav1.LoadBalancer, err error) {
+				t.Helper()
+				if err != nil {
+					t.Fatalf("did not expect error: %v", err)
+				}
+				if lb.DNSName != dns {
+					t.Fatalf("DNSName did not equal expected value; was: '%s'", lb.DNSName)
+				}
+			},
+		},
+		{
+			name: "load balancer is not an NLB scope security groups will be added",
+			spec: func(spec infrav1.LoadBalancer) infrav1.LoadBalancer {
+				spec.SecurityGroupIDs = []string{"sg-id"}
+				return spec
+			},
+			awsCluster: func(acl infrav1.AWSCluster) infrav1.AWSCluster {
+				acl.Spec.ControlPlaneLoadBalancer.LoadBalancerType = infrav1.LoadBalancerTypeALB
+				return acl
+			},
+			elbV2APIMocks: func(m *mocks.MockELBV2APIMockRecorder) {
+				m.CreateLoadBalancer(gomock.Eq(&elbv2.CreateLoadBalancerInput{
+					Name:    aws.String(elbName),
+					Scheme:  aws.String("internet-facing"),
+					Type:    aws.String("application"),
+					Subnets: aws.StringSlice([]string{clusterSubnetID}),
+					Tags: []*elbv2.Tag{
+						{
+							Key:   aws.String("test"),
+							Value: aws.String("tag"),
+						},
+					},
+					SecurityGroups: aws.StringSlice([]string{"sg-id"}),
+				})).Return(&elbv2.CreateLoadBalancerOutput{
+					LoadBalancers: []*elbv2.LoadBalancer{
+						{
+							LoadBalancerArn:  aws.String(elbArn),
+							LoadBalancerName: aws.String(elbName),
+							Scheme:           aws.String(string(infrav1.ELBSchemeInternetFacing)),
+							DNSName:          aws.String(dns),
+						},
+					},
+				}, nil)
+				m.CreateTargetGroup(gomock.Eq(&elbv2.CreateTargetGroupInput{
+					HealthCheckEnabled:  aws.Bool(true),
+					HealthCheckPort:     aws.String("infrav1.DefaultAPIServerPort"),
+					HealthCheckProtocol: aws.String("tcp"),
+					Name:                aws.String("name"),
+					Port:                aws.Int64(infrav1.DefaultAPIServerPort),
+					Protocol:            aws.String("TCP"),
+					VpcId:               aws.String(vpcID),
+				})).Return(&elbv2.CreateTargetGroupOutput{
+					TargetGroups: []*elbv2.TargetGroup{
+						{
+							TargetGroupArn:  aws.String("target-group::arn"),
+							TargetGroupName: aws.String("name"),
+							VpcId:           aws.String(vpcID),
+						},
+					},
+				}, nil)
+				m.ModifyTargetGroupAttributes(gomock.Eq(&elbv2.ModifyTargetGroupAttributesInput{
+					TargetGroupArn: aws.String("target-group::arn"),
+					Attributes: []*elbv2.TargetGroupAttribute{
+						{
+							Key:   aws.String(infrav1.TargetGroupAttributeEnablePreserveClientIP),
+							Value: aws.String("false"),
+						},
 					},
-				})).
-					Return(&ec2.DescribeSubnetsOutput{
-						Subnets: []*ec2.Subnet{
-							{
-								SubnetId:         aws.String(elbSubnetID),
-								AvailabilityZone: aws.String(differentAZ),
-							},
+				})).Return(nil, nil)
+				m.CreateListener(gomock.Eq(&elbv2.CreateListenerInput{
+					DefaultActions: []*elbv2.Action{
+						{
+							TargetGroupArn: aws.String("target-group::arn"),
+							Type:           aws.String(elbv2.ActionTypeEnumForward),
 						},
-					}, nil)
+					},
+					LoadBalancerArn: aws.String(elbArn),
+					Port:            aws.Int64(infrav1.DefaultAPIServerPort),
+					Protocol:        aws.String("TCP"),
+					Tags: []*elbv2.Tag{
+						{
+							Key:   aws.String("test"),
+							Value: aws.String("tag"),
+						},
+					},
+				})).Return(&elbv2.CreateListenerOutput{
+					Listeners: []*elbv2.Listener{
+						{
+							ListenerArn: aws.String("listener::arn"),
+						},
+					},
+				}, nil)
 			},
-			check: func(t *testing.T, err error) {
+			check: func(t *testing.T, lb *infrav1.LoadBalancer, err error) {
 				t.Helper()
-				expectedErrMsg := "failed to register instance with APIServer ELB \"bar-apiserver\": instance is in availability zone \"us-west-1a\", no public subnets attached to the ELB in the same zone"
-				if err == nil {
-					t.Fatalf("Expected error, but got nil")
+				if err != nil {
+					t.Fatalf("did not expect error: %v", err)
 				}
-
-				if !strings.Contains(err.Error(), expectedErrMsg) {
-					t.Fatalf("Expected error: %s\nInstead got: %s", expectedErrMsg, err.Error())
+				if lb.DNSName != dns {
+					t.Fatalf("DNSName did not equal expected value; was: '%s'", lb.DNSName)
 				}
 			},
 		},
@@ -553,15 +1506,28 @@ func TestRegisterInstanceWithAPIServerELB(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			mockCtrl := gomock.NewController(t)
 			defer mockCtrl.Finish()
-			elbAPIMocks := mocks.NewMockELBAPI(mockCtrl)
-			ec2Mock := mocks.NewMockEC2API(mockCtrl)
+			elbV2APIMocks := mocks.NewMockELBV2API(mockCtrl)
 
 			scheme, err := setupScheme()
 			if err != nil {
 				t.Fatal(err)
 			}
-
+			awsCluster := &infrav1.AWSCluster{
+				ObjectMeta: metav1.ObjectMeta{Name: clusterName},
+				Spec: infrav1.AWSClusterSpec{
+					ControlPlaneLoadBalancer: &infrav1.AWSLoadBalancerSpec{
+						Name:             aws.String(elbName),
+						LoadBalancerType: infrav1.LoadBalancerTypeNLB,
+					},
+					NetworkSpec: infrav1.NetworkSpec{
+						VPC: infrav1.VPCSpec{
+							ID: vpcID,
+						},
+					},
+				},
+			}
 			client := fake.NewClientBuilder().WithScheme(scheme).Build()
+			cluster := tc.awsCluster(*awsCluster)
 			clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{
 				Client: client,
 				Cluster: &clusterv1.Cluster{
@@ -570,28 +1536,49 @@ func TestRegisterInstanceWithAPIServerELB(t *testing.T) {
 						Name:      clusterName,
 					},
 				},
-				AWSCluster: tc.awsCluster,
+				AWSCluster: &cluster,
 			})
 			if err != nil {
 				t.Fatal(err)
 			}
 
-			instance := &infrav1.Instance{
-				ID:       instanceID,
-				SubnetID: clusterSubnetID,
-			}
-
-			tc.elbAPIMocks(elbAPIMocks.EXPECT())
-			tc.ec2Mocks(ec2Mock.EXPECT())
+			tc.elbV2APIMocks(elbV2APIMocks.EXPECT())
 
 			s := &Service{
-				scope:     clusterScope,
-				EC2Client: ec2Mock,
-				ELBClient: elbAPIMocks,
+				scope:       clusterScope,
+				ELBV2Client: elbV2APIMocks,
 			}
 
-			err = s.RegisterInstanceWithAPIServerELB(instance)
-			tc.check(t, err)
+			loadBalancerSpec := &infrav1.LoadBalancer{
+				ARN:    elbArn,
+				Name:   elbName,
+				Scheme: infrav1.ELBSchemeInternetFacing,
+				Tags: map[string]string{
+					"test": "tag",
+				},
+				ELBListeners: []infrav1.Listener{
+					{
+						Protocol: "TCP",
+						Port:     infrav1.DefaultAPIServerPort,
+						TargetGroup: infrav1.TargetGroupSpec{
+							Name:     "name",
+							Port:     infrav1.DefaultAPIServerPort,
+							Protocol: "TCP",
+							VpcID:    vpcID,
+							HealthCheck: &infrav1.TargetGroupHealthCheck{
+								Protocol: aws.String("tcp"),
+								Port:     aws.String("infrav1.DefaultAPIServerPort"),
+							},
+						},
+					},
+				},
+				LoadBalancerType: infrav1.LoadBalancerTypeNLB,
+				SubnetIDs:        []string{clusterSubnetID},
+			}
+
+			spec := tc.spec(*loadBalancerSpec)
+			lb, err := s.createLB(&spec)
+			tc.check(t, lb, err)
 		})
 	}
 }
@@ -619,7 +1606,7 @@ func TestDeleteAPIServerELB(t *testing.T) {
 						LoadBalancerDescriptions: []*elb.LoadBalancerDescription{
 							{
 								LoadBalancerName: aws.String(elbName),
-								Scheme:           aws.String(string(infrav1.ClassicELBSchemeInternetFacing)),
+								Scheme:           aws.String(string(infrav1.ELBSchemeInternetFacing)),
 							},
 						},
 					},
@@ -658,7 +1645,7 @@ func TestDeleteAPIServerELB(t *testing.T) {
 						LoadBalancerDescriptions: []*elb.LoadBalancerDescription{
 							{
 								LoadBalancerName: aws.String(elbName),
-								Scheme:           aws.String(string(infrav1.ClassicELBSchemeInternetFacing)),
+								Scheme:           aws.String(string(infrav1.ELBSchemeInternetFacing)),
 							},
 						},
 					},
@@ -759,6 +1746,195 @@ func TestDeleteAPIServerELB(t *testing.T) {
 	}
 }
 
+func TestDeleteNLB(t *testing.T) {
+	clusterName := "bar"
+	elbName := "bar-apiserver"
+	elbArn := "apiserver::arn"
+	tests := []struct {
+		name         string
+		elbv2ApiMock func(m *mocks.MockELBV2APIMockRecorder)
+	}{
+		{
+			name: "if control plane NLB is not found, do nothing",
+			elbv2ApiMock: func(m *mocks.MockELBV2APIMockRecorder) {
+				m.DescribeLoadBalancers(gomock.Eq(&elbv2.DescribeLoadBalancersInput{
+					Names: aws.StringSlice([]string{elbName}),
+				})).Return(nil, awserr.New(elb.ErrCodeAccessPointNotFoundException, "", nil))
+			},
+		},
+		{
+			name: "if control plane NLB is found, and it is not managed, do nothing",
+			elbv2ApiMock: func(m *mocks.MockELBV2APIMockRecorder) {
+				m.DescribeLoadBalancers(&elbv2.DescribeLoadBalancersInput{Names: []*string{aws.String(elbName)}}).Return(
+					&elbv2.DescribeLoadBalancersOutput{
+						LoadBalancers: []*elbv2.LoadBalancer{
+							{
+								LoadBalancerArn:  aws.String(elbArn),
+								LoadBalancerName: aws.String(elbName),
+								Scheme:           aws.String(string(infrav1.ELBSchemeInternetFacing)),
+							},
+						},
+					},
+					nil,
+				)
+
+				m.DescribeLoadBalancerAttributes(&elbv2.DescribeLoadBalancerAttributesInput{LoadBalancerArn: aws.String(elbArn)}).Return(
+					&elbv2.DescribeLoadBalancerAttributesOutput{
+						Attributes: []*elbv2.LoadBalancerAttribute{
+							{
+								Key:   aws.String("load_balancing.cross_zone.enabled"),
+								Value: aws.String("false"),
+							},
+						},
+					},
+					nil,
+				)
+
+				m.DescribeTags(&elbv2.DescribeTagsInput{ResourceArns: []*string{aws.String(elbArn)}}).Return(
+					&elbv2.DescribeTagsOutput{
+						TagDescriptions: []*elbv2.TagDescription{
+							{
+								ResourceArn: aws.String(elbArn),
+								Tags:        []*elbv2.Tag{},
+							},
+						},
+					},
+					nil,
+				)
+			},
+		},
+		{
+			name: "if control plane ELB is found, and it is managed, delete the ELB",
+			elbv2ApiMock: func(m *mocks.MockELBV2APIMockRecorder) {
+				m.DescribeLoadBalancers(&elbv2.DescribeLoadBalancersInput{Names: []*string{aws.String(elbName)}}).Return(
+					&elbv2.DescribeLoadBalancersOutput{
+						LoadBalancers: []*elbv2.LoadBalancer{
+							{
+								LoadBalancerArn:  aws.String(elbArn),
+								LoadBalancerName: aws.String(elbName),
+								Scheme:           aws.String(string(infrav1.ELBSchemeInternetFacing)),
+							},
+						},
+					},
+					nil,
+				)
+
+				m.DescribeLoadBalancerAttributes(&elbv2.DescribeLoadBalancerAttributesInput{LoadBalancerArn: aws.String(elbArn)}).Return(
+					&elbv2.DescribeLoadBalancerAttributesOutput{
+						Attributes: []*elbv2.LoadBalancerAttribute{
+							{
+								Key:   aws.String("load_balancing.cross_zone.enabled"),
+								Value: aws.String("false"),
+							},
+						},
+					},
+					nil,
+				)
+
+				m.DescribeTags(&elbv2.DescribeTagsInput{ResourceArns: []*string{aws.String(elbArn)}}).Return(
+					&elbv2.DescribeTagsOutput{
+						TagDescriptions: []*elbv2.TagDescription{
+							{
+								ResourceArn: aws.String(elbArn),
+								Tags: []*elbv2.Tag{{
+									Key:   aws.String(infrav1.ClusterTagKey(clusterName)),
+									Value: aws.String(string(infrav1.ResourceLifecycleOwned)),
+								}},
+							},
+						},
+					},
+					nil,
+				)
+
+				// delete listeners
+				m.DescribeListeners(&elbv2.DescribeListenersInput{LoadBalancerArn: aws.String(elbArn)}).Return(&elbv2.DescribeListenersOutput{
+					Listeners: []*elbv2.Listener{
+						{
+							ListenerArn: aws.String("listener::arn"),
+						},
+					},
+				}, nil)
+				m.DeleteListener(&elbv2.DeleteListenerInput{ListenerArn: aws.String("listener::arn")}).Return(&elbv2.DeleteListenerOutput{}, nil)
+				// delete target groups
+				m.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{LoadBalancerArn: aws.String(elbArn)}).Return(&elbv2.DescribeTargetGroupsOutput{
+					TargetGroups: []*elbv2.TargetGroup{
+						{
+							TargetGroupArn: aws.String("target-group::arn"),
+						},
+					},
+				}, nil)
+				m.DeleteTargetGroup(&elbv2.DeleteTargetGroupInput{TargetGroupArn: aws.String("target-group::arn")}).Return(&elbv2.DeleteTargetGroupOutput{}, nil)
+				// delete the load balancer
+
+				m.DeleteLoadBalancer(&elbv2.DeleteLoadBalancerInput{LoadBalancerArn: aws.String(elbArn)}).Return(
+					&elbv2.DeleteLoadBalancerOutput{}, nil)
+
+				m.DescribeLoadBalancers(&elbv2.DescribeLoadBalancersInput{Names: []*string{aws.String(elbName)}}).Return(
+					&elbv2.DescribeLoadBalancersOutput{
+						LoadBalancers: []*elbv2.LoadBalancer{},
+					},
+					nil,
+				)
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			mockCtrl := gomock.NewController(t)
+			defer mockCtrl.Finish()
+			rgapiMock := mocks.NewMockResourceGroupsTaggingAPIAPI(mockCtrl)
+			elbv2ApiMock := mocks.NewMockELBV2API(mockCtrl)
+
+			scheme, err := setupScheme()
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			awsCluster := &infrav1.AWSCluster{
+				ObjectMeta: metav1.ObjectMeta{Name: "test"},
+				Spec: infrav1.AWSClusterSpec{
+					ControlPlaneLoadBalancer: &infrav1.AWSLoadBalancerSpec{
+						Name:             aws.String(elbName),
+						LoadBalancerType: infrav1.LoadBalancerTypeNLB,
+					},
+				},
+			}
+
+			client := fake.NewClientBuilder().WithScheme(scheme).Build()
+			ctx := context.TODO()
+			client.Create(ctx, awsCluster)
+
+			clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{
+				Cluster: &clusterv1.Cluster{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "foo",
+						Name:      clusterName,
+					},
+				},
+				AWSCluster: awsCluster,
+				Client:     client,
+			})
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			tc.elbv2ApiMock(elbv2ApiMock.EXPECT())
+
+			s := &Service{
+				scope:                 clusterScope,
+				ResourceTaggingClient: rgapiMock,
+				ELBV2Client:           elbv2ApiMock,
+			}
+
+			err = s.deleteExistingNLBs()
+			if err != nil {
+				t.Fatal(err)
+			}
+		})
+	}
+}
+
 func TestDeleteAWSCloudProviderELBs(t *testing.T) {
 	clusterName := "bar"
 	tests := []struct {
@@ -941,7 +2117,7 @@ func TestDescribeLoadbalancers(t *testing.T) {
 			DescribeElbAPIMocks: func(m *mocks.MockELBAPIMockRecorder) {
 				m.DescribeLoadBalancers(gomock.Eq(&elb.DescribeLoadBalancersInput{
 					LoadBalancerNames: aws.StringSlice([]string{"bar-apiserver"}),
-				})).Return(&elb.DescribeLoadBalancersOutput{LoadBalancerDescriptions: []*elb.LoadBalancerDescription{{Scheme: pointer.StringPtr(string(infrav1.ClassicELBSchemeInternal))}}}, nil)
+				})).Return(&elb.DescribeLoadBalancersOutput{LoadBalancerDescriptions: []*elb.LoadBalancerDescription{{Scheme: pointer.StringPtr(string(infrav1.ELBSchemeInternal))}}}, nil)
 			},
 		},
 	}
@@ -960,7 +2136,7 @@ func TestDescribeLoadbalancers(t *testing.T) {
 			awsCluster := &infrav1.AWSCluster{
 				ObjectMeta: metav1.ObjectMeta{Name: "test"},
 				Spec: infrav1.AWSClusterSpec{ControlPlaneLoadBalancer: &infrav1.AWSLoadBalancerSpec{
-					Scheme: &infrav1.ClassicELBSchemeInternetFacing,
+					Scheme: &infrav1.ELBSchemeInternetFacing,
 				}},
 			}
 
@@ -999,6 +2175,82 @@ func TestDescribeLoadbalancers(t *testing.T) {
 	}
 }
 
+func TestDescribeV2Loadbalancers(t *testing.T) {
+	clusterName := "bar"
+	tests := []struct {
+		name                  string
+		lbName                string
+		rgAPIMocks            func(m *mocks.MockResourceGroupsTaggingAPIAPIMockRecorder)
+		DescribeElbV2APIMocks func(m *mocks.MockELBV2APIMockRecorder)
+	}{
+		{
+			name:   "Error if existing loadbalancer with same name doesn't have same scheme",
+			lbName: "bar-apiserver",
+			rgAPIMocks: func(m *mocks.MockResourceGroupsTaggingAPIAPIMockRecorder) {
+				m.GetResourcesPages(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
+			},
+			DescribeElbV2APIMocks: func(m *mocks.MockELBV2APIMockRecorder) {
+				m.DescribeLoadBalancers(gomock.Eq(&elbv2.DescribeLoadBalancersInput{
+					Names: aws.StringSlice([]string{"bar-apiserver"}),
+				})).Return(&elbv2.DescribeLoadBalancersOutput{LoadBalancers: []*elbv2.LoadBalancer{{Scheme: pointer.StringPtr(string(infrav1.ELBSchemeInternal))}}}, nil)
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			mockCtrl := gomock.NewController(t)
+			defer mockCtrl.Finish()
+			rgapiMock := mocks.NewMockResourceGroupsTaggingAPIAPI(mockCtrl)
+			elbV2ApiMock := mocks.NewMockELBV2API(mockCtrl)
+
+			scheme, err := setupScheme()
+			if err != nil {
+				t.Fatal(err)
+			}
+			awsCluster := &infrav1.AWSCluster{
+				ObjectMeta: metav1.ObjectMeta{Name: "test"},
+				Spec: infrav1.AWSClusterSpec{ControlPlaneLoadBalancer: &infrav1.AWSLoadBalancerSpec{
+					Scheme:           &infrav1.ELBSchemeInternetFacing,
+					LoadBalancerType: infrav1.LoadBalancerTypeNLB,
+				}},
+			}
+
+			client := fake.NewClientBuilder().WithScheme(scheme).Build()
+			ctx := context.TODO()
+			client.Create(ctx, awsCluster)
+
+			clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{
+				Cluster: &clusterv1.Cluster{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "foo",
+						Name:      clusterName,
+					},
+				},
+				AWSCluster: awsCluster,
+				Client:     client,
+			})
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			tc.rgAPIMocks(rgapiMock.EXPECT())
+			tc.DescribeElbV2APIMocks(elbV2ApiMock.EXPECT())
+
+			s := &Service{
+				scope:                 clusterScope,
+				ResourceTaggingClient: rgapiMock,
+				ELBV2Client:           elbV2ApiMock,
+			}
+
+			_, err = s.describeLB(tc.lbName)
+			if err == nil {
+				t.Fatal(err)
+			}
+		})
+	}
+}
+
 func TestChunkELBs(t *testing.T) {
 	base := "loadbalancer"
 	var names []string
diff --git a/pkg/cloud/services/securitygroup/securitygroups_test.go b/pkg/cloud/services/securitygroup/securitygroups_test.go
index 9e89afdd2..26ae6311e 100644
--- a/pkg/cloud/services/securitygroup/securitygroups_test.go
+++ b/pkg/cloud/services/securitygroup/securitygroups_test.go
@@ -54,13 +54,17 @@ func TestReconcileSecurityGroups(t *testing.T) {
 	defer mockCtrl.Finish()
 
 	testCases := []struct {
-		name   string
-		input  *infrav1.NetworkSpec
-		expect func(m *mocks.MockEC2APIMockRecorder)
-		err    error
+		name       string
+		input      *infrav1.NetworkSpec
+		expect     func(m *mocks.MockEC2APIMockRecorder)
+		err        error
+		awsCluster func(acl infrav1.AWSCluster) infrav1.AWSCluster
 	}{
 		{
 			name: "no existing",
+			awsCluster: func(acl infrav1.AWSCluster) infrav1.AWSCluster {
+				return acl
+			},
 			input: &infrav1.NetworkSpec{
 				VPC: infrav1.VPCSpec{
 					ID:                "vpc-securitygroups",
@@ -246,8 +250,210 @@ func TestReconcileSecurityGroups(t *testing.T) {
 					After(securityGroupNode)
 			},
 		},
+		{
+			name: "NLB is defined with preserve client IP disabled",
+			awsCluster: func(acl infrav1.AWSCluster) infrav1.AWSCluster {
+				acl.Spec.ControlPlaneLoadBalancer = &infrav1.AWSLoadBalancerSpec{
+					LoadBalancerType: infrav1.LoadBalancerTypeNLB,
+				}
+				return acl
+			},
+			input: &infrav1.NetworkSpec{
+				VPC: infrav1.VPCSpec{
+					ID:                "vpc-securitygroups",
+					InternetGatewayID: aws.String("igw-01"),
+					Tags: infrav1.Tags{
+						infrav1.ClusterTagKey("test-cluster"): "owned",
+					},
+					CidrBlock: "10.0.0.0/16",
+				},
+				Subnets: infrav1.Subnets{
+					infrav1.SubnetSpec{
+						ID:               "subnet-securitygroups-private",
+						IsPublic:         false,
+						AvailabilityZone: "us-east-1a",
+					},
+					infrav1.SubnetSpec{
+						ID:               "subnet-securitygroups-public",
+						IsPublic:         true,
+						NatGatewayID:     aws.String("nat-01"),
+						AvailabilityZone: "us-east-1a",
+					},
+				},
+			},
+			expect: func(m *mocks.MockEC2APIMockRecorder) {
+				m.DescribeSecurityGroups(gomock.AssignableToTypeOf(&ec2.DescribeSecurityGroupsInput{})).
+					Return(&ec2.DescribeSecurityGroupsOutput{}, nil)
+
+				securityGroupBastion := m.CreateSecurityGroup(gomock.Eq(&ec2.CreateSecurityGroupInput{
+					VpcId:       aws.String("vpc-securitygroups"),
+					GroupName:   aws.String("test-cluster-bastion"),
+					Description: aws.String("Kubernetes cluster test-cluster: bastion"),
+					TagSpecifications: []*ec2.TagSpecification{
+						{
+							ResourceType: aws.String("security-group"),
+							Tags: []*ec2.Tag{
+								{
+									Key:   aws.String("Name"),
+									Value: aws.String("test-cluster-bastion"),
+								},
+								{
+									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"),
+									Value: aws.String("owned"),
+								},
+								{
+									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
+									Value: aws.String("bastion"),
+								},
+							},
+						},
+					},
+				})).
+					Return(&ec2.CreateSecurityGroupOutput{GroupId: aws.String("sg-bastion")}, nil)
+
+				m.AuthorizeSecurityGroupIngress(gomock.AssignableToTypeOf(&ec2.AuthorizeSecurityGroupIngressInput{
+					GroupId: aws.String("sg-bastion"),
+				})).
+					Return(&ec2.AuthorizeSecurityGroupIngressOutput{}, nil).
+					After(securityGroupBastion)
+
+				securityGroupAPIServerLb := m.CreateSecurityGroup(gomock.Eq(&ec2.CreateSecurityGroupInput{
+					VpcId:       aws.String("vpc-securitygroups"),
+					GroupName:   aws.String("test-cluster-apiserver-lb"),
+					Description: aws.String("Kubernetes cluster test-cluster: apiserver-lb"),
+					TagSpecifications: []*ec2.TagSpecification{
+						{
+							ResourceType: aws.String("security-group"),
+							Tags: []*ec2.Tag{
+								{
+									Key:   aws.String("Name"),
+									Value: aws.String("test-cluster-apiserver-lb"),
+								},
+								{
+									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"),
+									Value: aws.String("owned"),
+								},
+								{
+									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
+									Value: aws.String("apiserver-lb"),
+								},
+							},
+						},
+					},
+				})).
+					Return(&ec2.CreateSecurityGroupOutput{GroupId: aws.String("sg-apiserver-lb")}, nil)
+
+				m.AuthorizeSecurityGroupIngress(gomock.AssignableToTypeOf(&ec2.AuthorizeSecurityGroupIngressInput{
+					GroupId: aws.String("sg-apiserver-lb"),
+				})).
+					Return(&ec2.AuthorizeSecurityGroupIngressOutput{}, nil).
+					After(securityGroupAPIServerLb)
+
+				lbSecurityGroup := m.CreateSecurityGroup(gomock.Eq(&ec2.CreateSecurityGroupInput{
+					VpcId:       aws.String("vpc-securitygroups"),
+					GroupName:   aws.String("test-cluster-lb"),
+					Description: aws.String("Kubernetes cluster test-cluster: lb"),
+					TagSpecifications: []*ec2.TagSpecification{
+						{
+							ResourceType: aws.String("security-group"),
+							Tags: []*ec2.Tag{
+								{
+									Key:   aws.String("Name"),
+									Value: aws.String("test-cluster-lb"),
+								},
+								{
+									Key:   aws.String("kubernetes.io/cluster/test-cluster"),
+									Value: aws.String("owned"),
+								},
+								{
+									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"),
+									Value: aws.String("owned"),
+								},
+								{
+									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
+									Value: aws.String("lb"),
+								},
+							},
+						},
+					},
+				})).Return(&ec2.CreateSecurityGroupOutput{GroupId: aws.String("sg-lb")}, nil)
+
+				m.AuthorizeSecurityGroupIngress(gomock.AssignableToTypeOf(&ec2.AuthorizeSecurityGroupIngressInput{
+					GroupId: aws.String("sg-lb"),
+				})).
+					Return(&ec2.AuthorizeSecurityGroupIngressOutput{}, nil).
+					After(lbSecurityGroup)
+
+				securityGroupControl := m.CreateSecurityGroup(gomock.Eq(&ec2.CreateSecurityGroupInput{
+					VpcId:       aws.String("vpc-securitygroups"),
+					GroupName:   aws.String("test-cluster-controlplane"),
+					Description: aws.String("Kubernetes cluster test-cluster: controlplane"),
+					TagSpecifications: []*ec2.TagSpecification{
+						{
+							ResourceType: aws.String("security-group"),
+							Tags: []*ec2.Tag{
+								{
+									Key:   aws.String("Name"),
+									Value: aws.String("test-cluster-controlplane"),
+								},
+								{
+									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"),
+									Value: aws.String("owned"),
+								},
+								{
+									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
+									Value: aws.String("controlplane"),
+								},
+							},
+						},
+					},
+				})).
+					Return(&ec2.CreateSecurityGroupOutput{GroupId: aws.String("sg-control")}, nil)
+
+				m.AuthorizeSecurityGroupIngress(gomock.AssignableToTypeOf(&ec2.AuthorizeSecurityGroupIngressInput{
+					GroupId: aws.String("sg-control"),
+				})).
+					Return(&ec2.AuthorizeSecurityGroupIngressOutput{}, nil).
+					After(securityGroupControl)
+
+				securityGroupNode := m.CreateSecurityGroup(gomock.Eq(&ec2.CreateSecurityGroupInput{
+					VpcId:       aws.String("vpc-securitygroups"),
+					GroupName:   aws.String("test-cluster-node"),
+					Description: aws.String("Kubernetes cluster test-cluster: node"),
+					TagSpecifications: []*ec2.TagSpecification{
+						{
+							ResourceType: aws.String("security-group"),
+							Tags: []*ec2.Tag{
+								{
+									Key:   aws.String("Name"),
+									Value: aws.String("test-cluster-node"),
+								},
+								{
+									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"),
+									Value: aws.String("owned"),
+								},
+								{
+									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
+									Value: aws.String("node"),
+								},
+							},
+						},
+					},
+				})).
+					Return(&ec2.CreateSecurityGroupOutput{GroupId: aws.String("sg-node")}, nil)
+
+				m.AuthorizeSecurityGroupIngress(gomock.AssignableToTypeOf(&ec2.AuthorizeSecurityGroupIngressInput{
+					GroupId: aws.String("sg-node"),
+				})).
+					Return(&ec2.AuthorizeSecurityGroupIngressOutput{}, nil).
+					After(securityGroupNode)
+			},
+		},
 		{
 			name: "all overrides defined, do not tag",
+			awsCluster: func(acl infrav1.AWSCluster) infrav1.AWSCluster {
+				return acl
+			},
 			input: &infrav1.NetworkSpec{
 				VPC: infrav1.VPCSpec{
 					ID:                "vpc-securitygroups",
@@ -289,6 +495,9 @@ func TestReconcileSecurityGroups(t *testing.T) {
 		},
 		{
 			name: "managed vpc with overrides, returns error",
+			awsCluster: func(acl infrav1.AWSCluster) infrav1.AWSCluster {
+				return acl
+			},
 			input: &infrav1.NetworkSpec{
 				VPC: infrav1.VPCSpec{
 					ID:                "vpc-securitygroups",
@@ -341,17 +550,19 @@ func TestReconcileSecurityGroups(t *testing.T) {
 			scheme := runtime.NewScheme()
 			_ = infrav1.AddToScheme(scheme)
 			client := fake.NewClientBuilder().WithScheme(scheme).Build()
+			cluster := &infrav1.AWSCluster{
+				ObjectMeta: metav1.ObjectMeta{Name: "test"},
+				Spec: infrav1.AWSClusterSpec{
+					NetworkSpec: *tc.input,
+				},
+			}
+			awsCluster := tc.awsCluster(*cluster)
 			cs, err := scope.NewClusterScope(scope.ClusterScopeParams{
 				Client: client,
 				Cluster: &clusterv1.Cluster{
 					ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"},
 				},
-				AWSCluster: &infrav1.AWSCluster{
-					ObjectMeta: metav1.ObjectMeta{Name: "test"},
-					Spec: infrav1.AWSClusterSpec{
-						NetworkSpec: *tc.input,
-					},
-				},
+				AWSCluster: &awsCluster,
 			})
 			if err != nil {
 				t.Fatalf("Failed to create test context: %v", err)
diff --git a/test/e2e/shared/aws_helpers.go b/test/e2e/shared/aws_helpers.go
index 2ecf94ef5..a0eedc3f2 100644
--- a/test/e2e/shared/aws_helpers.go
+++ b/test/e2e/shared/aws_helpers.go
@@ -34,20 +34,12 @@ import (
 	infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
 )
 
-type LoadBalancerType string
-
-var (
-	LoadBalancerTypeELB = LoadBalancerType("elb")
-	LoadBalancerTypeALB = LoadBalancerType("alb")
-	LoadBalancerTypeNLB = LoadBalancerType("nlb")
-)
-
 type WaitForLoadBalancerToExistForServiceInput struct {
 	AWSSession       client.ConfigProvider
 	ServiceName      string
 	ServiceNamespace string
 	ClusterName      string
-	Type             LoadBalancerType
+	Type             infrav1.LoadBalancerType
 }
 
 func WaitForLoadBalancerToExistForService(input WaitForLoadBalancerToExistForServiceInput, intervals ...interface{}) {
@@ -79,7 +71,7 @@ type GetLoadBalancerARNsInput struct {
 	ServiceName      string
 	ServiceNamespace string
 	ClusterName      string
-	Type             LoadBalancerType
+	Type             infrav1.LoadBalancerType
 }
 
 func GetLoadBalancerARNs(input GetLoadBalancerARNsInput) ([]string, error) {
@@ -114,15 +106,15 @@ func GetLoadBalancerARNs(input GetLoadBalancerARNsInput) ([]string, error) {
 		}
 
 		switch input.Type {
-		case LoadBalancerTypeALB:
+		case infrav1.LoadBalancerTypeALB:
 			if strings.HasPrefix(parsedArn.Resource, "loadbalancer/app/") {
 				matchingARNs = append(matchingARNs, resARN)
 			}
-		case LoadBalancerTypeNLB:
+		case infrav1.LoadBalancerTypeNLB:
 			if strings.HasPrefix(parsedArn.Resource, "loadbalancer/net/") {
 				matchingARNs = append(matchingARNs, resARN)
 			}
-		case LoadBalancerTypeELB:
+		case infrav1.LoadBalancerTypeELB:
 			if strings.HasPrefix(parsedArn.Resource, "loadbalancer/") {
 				matchingARNs = append(matchingARNs, resARN)
 			}
diff --git a/test/e2e/suites/gc_managed/gc_managed_test.go b/test/e2e/suites/gc_managed/gc_managed_test.go
index 317661b49..2ac3bdbb8 100644
--- a/test/e2e/suites/gc_managed/gc_managed_test.go
+++ b/test/e2e/suites/gc_managed/gc_managed_test.go
@@ -30,6 +30,7 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/utils/pointer"
 
+	infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
 	ekscontrolplanev1 "sigs.k8s.io/cluster-api-provider-aws/v2/controlplane/eks/api/v1beta2"
 	"sigs.k8s.io/cluster-api-provider-aws/v2/test/e2e/shared"
 	ms "sigs.k8s.io/cluster-api-provider-aws/v2/test/e2e/suites/managed"
@@ -118,14 +119,14 @@ var _ = ginkgo.Describe("[managed] [gc] EKS Cluster external resource GC tests",
 			ServiceName:      "podinfo-nlb",
 			ServiceNamespace: "default",
 			ClusterName:      cp.Spec.EKSClusterName,
-			Type:             shared.LoadBalancerTypeNLB,
+			Type:             infrav1.LoadBalancerTypeNLB,
 		}, e2eCtx.E2EConfig.GetIntervals("", "wait-loadbalancer-ready")...)
 		shared.WaitForLoadBalancerToExistForService(shared.WaitForLoadBalancerToExistForServiceInput{
 			AWSSession:       e2eCtx.BootstrapUserAWSSession,
 			ServiceName:      "podinfo-elb",
 			ServiceNamespace: "default",
 			ClusterName:      cp.Spec.EKSClusterName,
-			Type:             shared.LoadBalancerTypeELB,
+			Type:             infrav1.LoadBalancerTypeELB,
 		}, e2eCtx.E2EConfig.GetIntervals("", "wait-loadbalancer-ready")...)
 
 		shared.Byf("Deleting workload/tenant cluster %s", clusterName)
@@ -144,7 +145,7 @@ var _ = ginkgo.Describe("[managed] [gc] EKS Cluster external resource GC tests",
 			ServiceName:      "podinfo-nlb",
 			ServiceNamespace: "default",
 			ClusterName:      cp.Spec.EKSClusterName,
-			Type:             shared.LoadBalancerTypeNLB,
+			Type:             infrav1.LoadBalancerTypeNLB,
 		})
 		Expect(err).NotTo(HaveOccurred())
 		Expect(arns).To(HaveLen(0), "there are %d service load balancers (nlb) still", len(arns))
@@ -153,7 +154,7 @@ var _ = ginkgo.Describe("[managed] [gc] EKS Cluster external resource GC tests",
 			ServiceName:      "podinfo-elb",
 			ServiceNamespace: "default",
 			ClusterName:      cp.Spec.EKSClusterName,
-			Type:             shared.LoadBalancerTypeELB,
+			Type:             infrav1.LoadBalancerTypeELB,
 		})
 		Expect(err).NotTo(HaveOccurred())
 		Expect(arns).To(HaveLen(0), "there are %d service load balancers (elb) still", len(arns))
diff --git a/test/e2e/suites/gc_unmanaged/gc_unmanaged_test.go b/test/e2e/suites/gc_unmanaged/gc_unmanaged_test.go
index 59aa0ee2e..a993e98f7 100644
--- a/test/e2e/suites/gc_unmanaged/gc_unmanaged_test.go
+++ b/test/e2e/suites/gc_unmanaged/gc_unmanaged_test.go
@@ -31,6 +31,7 @@ import (
 	. "github.com/onsi/gomega"
 	"k8s.io/utils/pointer"
 
+	infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
 	"sigs.k8s.io/cluster-api-provider-aws/v2/test/e2e/shared"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
 	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
@@ -99,14 +100,14 @@ var _ = ginkgo.Context("[unmanaged] [gc]", func() {
 			ServiceName:      "podinfo-nlb",
 			ServiceNamespace: "default",
 			ClusterName:      clusterName,
-			Type:             shared.LoadBalancerTypeNLB,
+			Type:             infrav1.LoadBalancerTypeNLB,
 		}, e2eCtx.E2EConfig.GetIntervals("", "wait-loadbalancer-ready")...)
 		shared.WaitForLoadBalancerToExistForService(shared.WaitForLoadBalancerToExistForServiceInput{
 			AWSSession:       e2eCtx.BootstrapUserAWSSession,
 			ServiceName:      "podinfo-elb",
 			ServiceNamespace: "default",
 			ClusterName:      clusterName,
-			Type:             shared.LoadBalancerTypeELB,
+			Type:             infrav1.LoadBalancerTypeELB,
 		}, e2eCtx.E2EConfig.GetIntervals("", "wait-loadbalancer-ready")...)
 
 		shared.Byf("Deleting workload/tenant cluster %s", clusterName)
@@ -125,7 +126,7 @@ var _ = ginkgo.Context("[unmanaged] [gc]", func() {
 			ServiceName:      "podinfo-nlb",
 			ServiceNamespace: "default",
 			ClusterName:      clusterName,
-			Type:             shared.LoadBalancerTypeNLB,
+			Type:             infrav1.LoadBalancerTypeNLB,
 		})
 		Expect(err).NotTo(HaveOccurred())
 		Expect(arns).To(HaveLen(0), "there are %d service load balancers (nlb) still", len(arns))
@@ -134,7 +135,7 @@ var _ = ginkgo.Context("[unmanaged] [gc]", func() {
 			ServiceName:      "podinfo-elb",
 			ServiceNamespace: "default",
 			ClusterName:      clusterName,
-			Type:             shared.LoadBalancerTypeELB,
+			Type:             infrav1.LoadBalancerTypeELB,
 		})
 		Expect(err).NotTo(HaveOccurred())
 		Expect(arns).To(HaveLen(0), "there are %d service load balancers (elb) still", len(arns))