Repository: kubernetes-sigs/cluster-api-provider-aws

Issue:
Migrate gc code to AWS SDK v2
./cmd/clusterawsadm/cmd/gc
./cmd/clusterawsadm/cmd/gc/gc.go
./cmd/clusterawsadm/gc/gc.go
./cmd/clusterawsadm/gc/gc_test.go
./test/e2e/suites/gc_managed/gc_managed_test.go
./test/e2e/suites/gc_managed/gc_managed_suite_test.go
./test/e2e/suites/gc_unmanaged/gc_unmanaged_test.go
./test/e2e/suites/gc_unmanaged/gc_unmanaged_suite_test.go
./pkg/cloud/services/gc

Discussion:
@damdo: See examples on how to migrate here: https://github.com/kubernetes-sigs/cluster-api-provider-aws/pull/5389@Danil-Grigorev: I have a try on this in #5518 

/assign

Solution patch:
diff --git a/api/v1beta2/network_types.go b/api/v1beta2/network_types.go
index 9974cbddb..00a775a54 100644
--- a/api/v1beta2/network_types.go
+++ b/api/v1beta2/network_types.go
@@ -207,6 +207,14 @@ type TargetGroupAttribute string
 var (
 	// TargetGroupAttributeEnablePreserveClientIP defines the attribute key for enabling preserve client IP.
 	TargetGroupAttributeEnablePreserveClientIP = "preserve_client_ip.enabled"
+
+	// TargetGroupAttributeEnableConnectionTermination defines the attribute key for terminating
+	// established connections to unhealthy targets.
+	TargetGroupAttributeEnableConnectionTermination = "target_health_state.unhealthy.connection_termination.enabled"
+
+	// TargetGroupAttributeUnhealthyDrainingIntervalSeconds defines the attribute key for the
+	// unhealthy target connection draining interval.
+	TargetGroupAttributeUnhealthyDrainingIntervalSeconds = "target_health_state.unhealthy.draining_interval_seconds"
 )
 
 // LoadBalancerAttribute defines a set of attributes for a V2 load balancer.
diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml
index 18c5547f5..802b8b23a 100644
--- a/config/manager/manager.yaml
+++ b/config/manager/manager.yaml
@@ -19,7 +19,7 @@ spec:
       containers:
         - args:
             - "--leader-elect"
-            - "--feature-gates=EKS=${CAPA_EKS:=true},EKSEnableIAM=${CAPA_EKS_IAM:=false},EKSAllowAddRoles=${CAPA_EKS_ADD_ROLES:=false},EKSFargate=${EXP_EKS_FARGATE:=false},MachinePool=${EXP_MACHINE_POOL:=false},EventBridgeInstanceState=${EVENT_BRIDGE_INSTANCE_STATE:=false},AutoControllerIdentityCreator=${AUTO_CONTROLLER_IDENTITY_CREATOR:=true},BootstrapFormatIgnition=${EXP_BOOTSTRAP_FORMAT_IGNITION:=false},ExternalResourceGC=${EXTERNAL_RESOURCE_GC:=true},AlternativeGCStrategy=${ALTERNATIVE_GC_STRATEGY:=false},TagUnmanagedNetworkResources=${TAG_UNMANAGED_NETWORK_RESOURCES:=true},ROSA=${EXP_ROSA:=false}"
+            - "--feature-gates=EKS=${CAPA_EKS:=true},EKSEnableIAM=${CAPA_EKS_IAM:=false},EKSAllowAddRoles=${CAPA_EKS_ADD_ROLES:=false},EKSFargate=${EXP_EKS_FARGATE:=false},MachinePool=${EXP_MACHINE_POOL:=false},MachinePoolMachines=${EXP_MACHINE_POOL_MACHINES:=false},EventBridgeInstanceState=${EVENT_BRIDGE_INSTANCE_STATE:=false},AutoControllerIdentityCreator=${AUTO_CONTROLLER_IDENTITY_CREATOR:=true},BootstrapFormatIgnition=${EXP_BOOTSTRAP_FORMAT_IGNITION:=false},ExternalResourceGC=${EXTERNAL_RESOURCE_GC:=true},AlternativeGCStrategy=${ALTERNATIVE_GC_STRATEGY:=false},TagUnmanagedNetworkResources=${TAG_UNMANAGED_NETWORK_RESOURCES:=true},ROSA=${EXP_ROSA:=false}"
             - "--v=${CAPA_LOGLEVEL:=0}"
             - "--diagnostics-address=${CAPA_DIAGNOSTICS_ADDRESS:=:8443}"
             - "--insecure-diagnostics=${CAPA_INSECURE_DIAGNOSTICS:=false}"
diff --git a/docs/book/src/development/tilt-setup.md b/docs/book/src/development/tilt-setup.md
index 97989588a..41737e262 100644
--- a/docs/book/src/development/tilt-setup.md
+++ b/docs/book/src/development/tilt-setup.md
@@ -52,6 +52,7 @@ Next, create a `tilt-settings.json` file and place it in your local copy of `clu
   "kustomize_substitutions": {
     "EXP_CLUSTER_RESOURCE_SET": "true",
     "EXP_MACHINE_POOL": "true",
+    "EXP_MACHINE_POOL_MACHINES": "true",
     "EVENT_BRIDGE_INSTANCE_STATE": "true",
     "AWS_B64ENCODED_CREDENTIALS": "W2RlZmFZSZnRg==",
     "EXP_EKS_FARGATE": "false",
diff --git a/docs/book/src/topics/machinepools.md b/docs/book/src/topics/machinepools.md
index e48ff5405..82dc33d8e 100644
--- a/docs/book/src/topics/machinepools.md
+++ b/docs/book/src/topics/machinepools.md
@@ -148,3 +148,7 @@ When using GitOps, make sure to ignore differences in `spec.replicas` on Machine
       jsonPointers:
         - /spec/replicas
 ```
+
+## Machine pool machines
+
+With the feature gate `MachinePoolMachines=true`, you can enable creation of `Machine`/`AWSMachine` objects for nodes created by a `AWSMachinePool`. This is experimental and will be used to introduce features such as per-node health checks.
diff --git a/docs/book/src/topics/reference/reference.md b/docs/book/src/topics/reference/reference.md
index 05467b957..af2ce4a9d 100644
--- a/docs/book/src/topics/reference/reference.md
+++ b/docs/book/src/topics/reference/reference.md
@@ -9,10 +9,11 @@
 | EKSAllowAddRoles              | CAPA_EKS_ADD_ROLES                | false   |
 | EKSFargate                    | EXP_EKS_FARGATE                   | false   |
 | MachinePool                   | EXP_MACHINE_POOL                  | false   |
+| MachinePoolMachines           | EXP_MACHINE_POOL_MACHINES         | false   |
 | EventBridgeInstanceState      | EVENT_BRIDGE_INSTANCE_STATE       | false   |
 | AutoControllerIdentityCreator | AUTO_CONTROLLER_IDENTITY_CREATOR  | true    |
 | BootstrapFormatIgnition       | EXP_BOOTSTRAP_FORMAT_IGNITION     | false   |
 | ExternalResourceGC            | EXP_EXTERNAL_RESOURCE_GC          | false   |
 | AlternativeGCStrategy         | EXP_ALTERNATIVE_GC_STRATEGY       | false   |
 | TagUnmanagedNetworkResources  | TAG_UNMANAGED_NETWORK_RESOURCES   | true    |
-| ROSA                          | EXP_ROSA                          | false   |
\ No newline at end of file
+| ROSA                          | EXP_ROSA                          | false   |
diff --git a/go.mod b/go.mod
index 188afcece..f4faa4a69 100644
--- a/go.mod
+++ b/go.mod
@@ -7,17 +7,19 @@ require (
 	github.com/apparentlymart/go-cidr v1.1.0
 	github.com/aws/amazon-vpc-cni-k8s v1.15.5
 	github.com/aws/aws-lambda-go v1.41.0
-	github.com/aws/aws-sdk-go v1.55.5
+	github.com/aws/aws-sdk-go v1.55.7
 	github.com/aws/aws-sdk-go-v2 v1.36.5
 	github.com/aws/aws-sdk-go-v2/config v1.27.11
 	github.com/aws/aws-sdk-go-v2/credentials v1.17.11
 	github.com/aws/aws-sdk-go-v2/service/autoscaling v1.52.4
+	github.com/aws/aws-sdk-go-v2/service/ec2 v1.159.0
 	github.com/aws/aws-sdk-go-v2/service/eks v1.64.0
 	github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.29.6
-	github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.34.0
+	github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.45.2
 	github.com/aws/aws-sdk-go-v2/service/iam v1.32.0
 	github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6
 	github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1
+	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.6
 	github.com/aws/aws-sdk-go-v2/service/ssm v1.59.1
 	github.com/aws/aws-sdk-go-v2/service/sts v1.28.6
 	github.com/aws/smithy-go v1.22.4
@@ -89,14 +91,12 @@ require (
 	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect
 	github.com/aws/aws-sdk-go-v2/service/cloudformation v1.50.0 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ec2 v1.159.0
 	github.com/aws/aws-sdk-go-v2/service/eventbridge v1.39.3
 	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 // indirect
 	github.com/aws/aws-sdk-go-v2/service/organizations v1.27.3 // indirect
-	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.6
 	github.com/aws/aws-sdk-go-v2/service/servicequotas v1.21.4 // indirect
 	github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8
 	github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 // indirect
diff --git a/go.sum b/go.sum
index f140c2129..ea5310f7d 100644
--- a/go.sum
+++ b/go.sum
@@ -46,8 +46,8 @@ github.com/aws/amazon-vpc-cni-k8s v1.15.5 h1:/mqTXB4HoGYg4CiU4Gco9iEvZ+V/309Na4H
 github.com/aws/amazon-vpc-cni-k8s v1.15.5/go.mod h1:jV4wNtmgT2Ra1/oZU99DPOFsCUKnf0mYfIyzDyAUVAY=
 github.com/aws/aws-lambda-go v1.41.0 h1:l/5fyVb6Ud9uYd411xdHZzSf2n86TakxzpvIoz7l+3Y=
 github.com/aws/aws-lambda-go v1.41.0/go.mod h1:jwFe2KmMsHmffA1X2R09hH6lFzJQxzI8qK17ewzbQMM=
-github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
-github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
+github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
+github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
 github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0=
 github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to=
@@ -76,8 +76,8 @@ github.com/aws/aws-sdk-go-v2/service/eks v1.64.0 h1:EYeOThTRysemFtC6J6h6b7dNg3jN
 github.com/aws/aws-sdk-go-v2/service/eks v1.64.0/go.mod h1:v1xXy6ea0PHtWkjFUvAUh6B/5wv7UF909Nru0dOIJDk=
 github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.29.6 h1:9grU/+HRwLXJV8XUjEPThJj/H+0oHkeNBFpSSfZekeg=
 github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.29.6/go.mod h1:N4fs285CsnBHlAkzBpQapefR/noggTyF09fWs72EzB4=
-github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.34.0 h1:8rDRtPOu3ax8jEctw7G926JQlnFdhZZA4KJzQ+4ks3Q=
-github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.34.0/go.mod h1:L5bVuO4PeXuDuMYZfL3IW69E6mz6PDCYpp6IKDlcLMA=
+github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.45.2 h1:vX70Z4lNSr7XsioU0uJq5yvxgI50sB66MvD+V/3buS4=
+github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.45.2/go.mod h1:xnCC3vFBfOKpU6PcsCKL2ktgBTZfOwTGxj6V8/X3IS4=
 github.com/aws/aws-sdk-go-v2/service/eventbridge v1.39.3 h1:T6L7fsONflMeXuvsT8qZ247hA8ShBB0jF9yUEhW4JqI=
 github.com/aws/aws-sdk-go-v2/service/eventbridge v1.39.3/go.mod h1:sIrUII6Z+hAVAgcpmsc2e9HvEr++m/v8aBPT7s4ZYUk=
 github.com/aws/aws-sdk-go-v2/service/iam v1.32.0 h1:ZNlfPdw849gBo/lvLFbEEvpTJMij0LXqiNWZ+lIamlU=
diff --git a/pkg/cloud/convertersv2/tags.go b/pkg/cloud/convertersv2/tags.go
new file mode 100644
index 000000000..69d2ae3f6
--- /dev/null
+++ b/pkg/cloud/convertersv2/tags.go
@@ -0,0 +1,202 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package convertersv2 provides conversion functions for AWS SDK V2 types to CAPA types.
+package convertersv2
+
+import (
+	"sort"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	autoscalingtypes "github.com/aws/aws-sdk-go-v2/service/autoscaling/types"
+	v2ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
+	elbv1types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types"
+	elbv2types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types"
+	iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
+	secretsmanagertypes "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
+	ssmtypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
+
+	infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
+)
+
+// TagsToMap converts a []v2ec2types.Tag into a infrav1.Tags.
+func TagsToMap(src []v2ec2types.Tag) infrav1.Tags {
+	tags := make(infrav1.Tags, len(src))
+
+	for _, t := range src {
+		tags[*t.Key] = *t.Value
+	}
+
+	return tags
+}
+
+// MapPtrToMap converts a [string]*string into a infrav1.Tags.
+func MapPtrToMap(src map[string]*string) infrav1.Tags {
+	tags := make(infrav1.Tags, len(src))
+
+	for k, v := range src {
+		tags[k] = *v
+	}
+
+	return tags
+}
+
+// MapToTags converts a infrav1.Tags to a []v2ec2types.Tag.
+func MapToTags(src infrav1.Tags) []v2ec2types.Tag {
+	tags := make([]v2ec2types.Tag, 0, len(src))
+
+	for k, v := range src {
+		tag := v2ec2types.Tag{
+			Key:   aws.String(k),
+			Value: aws.String(v),
+		}
+
+		tags = append(tags, tag)
+	}
+
+	// Sort so that unit tests can expect a stable order
+	sort.Slice(tags, func(i, j int) bool { return *tags[i].Key < *tags[j].Key })
+
+	return tags
+}
+
+// ELBTagsToMap converts a []elbv1types.Tag into a infrav1.Tags.
+func ELBTagsToMap(src []elbv1types.Tag) infrav1.Tags {
+	tags := make(infrav1.Tags, len(src))
+
+	for _, t := range src {
+		tags[*t.Key] = *t.Value
+	}
+
+	return tags
+}
+
+// V2TagsToMap converts a []elbv2types.Tag into a infrav1.Tags.
+func V2TagsToMap(src []elbv2types.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 []elbv1types.Tag.
+func MapToELBTags(src infrav1.Tags) []elbv1types.Tag {
+	tags := make([]elbv1types.Tag, 0, len(src))
+
+	for k, v := range src {
+		tag := elbv1types.Tag{
+			Key:   aws.String(k),
+			Value: aws.String(v),
+		}
+
+		tags = append(tags, tag)
+	}
+
+	// Sort so that unit tests can expect a stable order
+	sort.Slice(tags, func(i, j int) bool { return *tags[i].Key < *tags[j].Key })
+
+	return tags
+}
+
+// MapToV2Tags converts a infrav1.Tags to a []elbv2types.Tag.
+func MapToV2Tags(src infrav1.Tags) []elbv2types.Tag {
+	tags := make([]elbv2types.Tag, 0, len(src))
+
+	for k, v := range src {
+		tag := elbv2types.Tag{
+			Key:   aws.String(k),
+			Value: aws.String(v),
+		}
+
+		tags = append(tags, tag)
+	}
+
+	// Sort so that unit tests can expect a stable order
+	sort.Slice(tags, func(i, j int) bool { return *tags[i].Key < *tags[j].Key })
+
+	return tags
+}
+
+// MapToSecretsManagerTags converts a infrav1.Tags to a []secretsmanagertypes.Tag.
+func MapToSecretsManagerTags(src infrav1.Tags) []secretsmanagertypes.Tag {
+	tags := make([]secretsmanagertypes.Tag, 0, len(src))
+
+	for k, v := range src {
+		tag := secretsmanagertypes.Tag{
+			Key:   aws.String(k),
+			Value: aws.String(v),
+		}
+
+		tags = append(tags, tag)
+	}
+
+	// Sort so that unit tests can expect a stable order
+	sort.Slice(tags, func(i, j int) bool { return *tags[i].Key < *tags[j].Key })
+
+	return tags
+}
+
+// MapToSSMTags converts a infrav1.Tags to a []ssm.Tag.
+func MapToSSMTags(src infrav1.Tags) []ssmtypes.Tag {
+	tags := make([]ssmtypes.Tag, 0, len(src))
+
+	for k, v := range src {
+		tag := ssmtypes.Tag{
+			Key:   aws.String(k),
+			Value: aws.String(v),
+		}
+
+		tags = append(tags, tag)
+	}
+
+	// Sort so that unit tests can expect a stable order
+	sort.Slice(tags, func(i, j int) bool { return *tags[i].Key < *tags[j].Key })
+
+	return tags
+}
+
+// MapToIAMTags converts a infrav1.Tags to a []iamtypes.Tag.
+func MapToIAMTags(src infrav1.Tags) []iamtypes.Tag {
+	tags := make([]iamtypes.Tag, 0, len(src))
+
+	for k, v := range src {
+		tag := iamtypes.Tag{
+			Key:   aws.String(k),
+			Value: aws.String(v),
+		}
+
+		tags = append(tags, tag)
+	}
+
+	// Sort so that unit tests can expect a stable order
+	sort.Slice(tags, func(i, j int) bool { return *tags[i].Key < *tags[j].Key })
+
+	return tags
+}
+
+// ASGTagsToMap converts a []autoscalingtypes.TagDescription into a infrav1.Tags.
+func ASGTagsToMap(src []autoscalingtypes.TagDescription) infrav1.Tags {
+	tags := make(infrav1.Tags, len(src))
+
+	for _, t := range src {
+		tags[*t.Key] = *t.Value
+	}
+
+	return tags
+}
diff --git a/pkg/cloud/endpointsv2/endpoints.go b/pkg/cloud/endpointsv2/endpoints.go
index 7f3cab14a..4548e1512 100644
--- a/pkg/cloud/endpointsv2/endpoints.go
+++ b/pkg/cloud/endpointsv2/endpoints.go
@@ -23,6 +23,7 @@ import (
 	"net/url"
 	"strings"
 
+	"github.com/aws/aws-sdk-go-v2/service/ec2"
 	"github.com/aws/aws-sdk-go-v2/service/eks"
 	elb "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing"
 	elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
@@ -171,6 +172,28 @@ func (s *ELBV2EndpointResolver) ResolveEndpoint(ctx context.Context, params elbv
 	return elbv2.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params)
 }
 
+// EC2EndpointResolver implements EndpointResolverV2 interface for EC2.
+type EC2EndpointResolver struct {
+	*MultiServiceEndpointResolver
+}
+
+// ResolveEndpoint for ELBV2.
+func (s *EC2EndpointResolver) ResolveEndpoint(ctx context.Context, params ec2.EndpointParameters) (smithyendpoints.Endpoint, error) {
+	// If custom endpoint not found, return default endpoint for the service
+	log := logger.FromContext(ctx)
+	endpoint, ok := s.endpoints[ec2.ServiceID]
+
+	if !ok {
+		log.Debug("Custom endpoint not found, using default endpoint")
+		return ec2.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params)
+	}
+
+	log.Debug("Custom endpoint found, using custom endpoint", "endpoint", endpoint.URL)
+	params.Endpoint = &endpoint.URL
+	params.Region = &endpoint.SigningRegion
+	return ec2.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params)
+}
+
 // RGAPIEndpointResolver implements EndpointResolverV2 interface for RGAPI.
 type RGAPIEndpointResolver struct {
 	*MultiServiceEndpointResolver
diff --git a/pkg/cloud/filterv2/ec2.go b/pkg/cloud/filterv2/ec2.go
new file mode 100644
index 000000000..ef87eabd4
--- /dev/null
+++ b/pkg/cloud/filterv2/ec2.go
@@ -0,0 +1,176 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package filterv2 contains the ec2 sdk related filters.
+package filterv2
+
+import (
+	"fmt"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/ec2/types"
+
+	infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
+)
+
+const (
+	filterNameTagKey        = "tag-key"
+	filterNameVpcID         = "vpc-id"
+	filterNameState         = "state"
+	filterNameVpcAttachment = "attachment.vpc-id"
+	filterAvailabilityZone  = "availability-zone"
+	filterNameIPAMPoolID    = "ipam-pool-id"
+)
+
+// EC2 exposes the ec2 sdk related filters.
+var EC2 = new(ec2Filters)
+
+type ec2Filters struct{}
+
+// Cluster returns a filter based on the cluster name.
+func (ec2Filters) Cluster(clusterName string) *types.Filter {
+	return &types.Filter{
+		Name:   aws.String(filterNameTagKey),
+		Values: []string{infrav1.ClusterTagKey(clusterName)},
+	}
+}
+
+// Name returns a filter based on the resource name.
+func (ec2Filters) Name(name string) *types.Filter {
+	return &types.Filter{
+		Name:   aws.String("tag:Name"),
+		Values: []string{name},
+	}
+}
+
+// ClusterOwned returns a filter using the Cluster API per-cluster tag where
+// the resource is owned.
+func (ec2Filters) ClusterOwned(clusterName string) *types.Filter {
+	return &types.Filter{
+		Name:   aws.String(fmt.Sprintf("tag:%s", infrav1.ClusterTagKey(clusterName))),
+		Values: []string{string(infrav1.ResourceLifecycleOwned)},
+	}
+}
+
+// ClusterShared returns a filter using the Cluster API per-cluster tag where
+// the resource is shared.
+func (ec2Filters) ClusterShared(clusterName string) *types.Filter {
+	return &types.Filter{
+		Name:   aws.String(fmt.Sprintf("tag:%s", infrav1.ClusterTagKey(clusterName))),
+		Values: []string{string(infrav1.ResourceLifecycleShared)},
+	}
+}
+
+// ProviderRole returns a filter using cluster-api-provider-aws role tag.
+func (ec2Filters) ProviderRole(role string) *types.Filter {
+	return &types.Filter{
+		Name:   aws.String(fmt.Sprintf("tag:%s", infrav1.NameAWSClusterAPIRole)),
+		Values: []string{role},
+	}
+}
+
+// ProviderOwned returns a filter using the cloud provider tag where the resource is owned.
+func (ec2Filters) ProviderOwned(clusterName string) *types.Filter {
+	return &types.Filter{
+		Name:   aws.String(fmt.Sprintf("tag:%s", infrav1.ClusterAWSCloudProviderTagKey(clusterName))),
+		Values: []string{string(infrav1.ResourceLifecycleOwned)},
+	}
+}
+
+// IPAM returns a filter based on the id of the IPAM Pool.
+func (ec2Filters) IPAM(ipamPoolID string) *types.Filter {
+	return &types.Filter{
+		Name:   aws.String(filterNameIPAMPoolID),
+		Values: []string{ipamPoolID},
+	}
+}
+
+// VPC returns a filter based on the id of the VPC.
+func (ec2Filters) VPC(vpcID string) *types.Filter {
+	return &types.Filter{
+		Name:   aws.String(filterNameVpcID),
+		Values: []string{vpcID},
+	}
+}
+
+// VPCAttachment returns a filter based on the vpc id attached to the resource.
+func (ec2Filters) VPCAttachment(vpcID string) *types.Filter {
+	return &types.Filter{
+		Name:   aws.String(filterNameVpcAttachment),
+		Values: []string{vpcID},
+	}
+}
+
+// Available returns a filter based on the state being available.
+func (ec2Filters) Available() *types.Filter {
+	return &types.Filter{
+		Name:   aws.String(filterNameState),
+		Values: []string{"available"},
+	}
+}
+
+// NATGatewayStates returns a filter based on the list of states passed in.
+func (ec2Filters) NATGatewayStates(states ...string) *types.Filter {
+	return &types.Filter{
+		Name:   aws.String("state"),
+		Values: states,
+	}
+}
+
+// InstanceStates returns a filter based on the list of states passed in.
+func (ec2Filters) InstanceStates(states ...string) *types.Filter {
+	return &types.Filter{
+		Name:   aws.String("instance-state-name"),
+		Values: states,
+	}
+}
+
+// VPCStates returns a filter based on the list of states passed in.
+func (ec2Filters) VPCStates(states ...string) *types.Filter {
+	return &types.Filter{
+		Name:   aws.String("state"),
+		Values: states,
+	}
+}
+
+// SubnetStates returns a filter based on the list of states passed in.
+func (ec2Filters) SubnetStates(states ...string) *types.Filter {
+	return &types.Filter{
+		Name:   aws.String("state"),
+		Values: states,
+	}
+}
+
+func (ec2Filters) AvailabilityZone(zone string) *types.Filter {
+	return &types.Filter{
+		Name:   aws.String(filterAvailabilityZone),
+		Values: []string{zone},
+	}
+}
+
+func (ec2Filters) IgnoreLocalZones() *types.Filter {
+	return &types.Filter{
+		Name:   aws.String("opt-in-status"),
+		Values: []string{"opt-in-not-required"},
+	}
+}
+
+func (ec2Filters) SecurityGroupName(name string) *types.Filter {
+	return &types.Filter{
+		Name:   aws.String("group-name"),
+		Values: []string{name},
+	}
+}
diff --git a/pkg/cloud/scope/clients.go b/pkg/cloud/scope/clients.go
index 345ef62f4..42acd40cb 100644
--- a/pkg/cloud/scope/clients.go
+++ b/pkg/cloud/scope/clients.go
@@ -17,7 +17,10 @@ limitations under the License.
 package scope
 
 import (
+	"context"
+
 	"github.com/aws/aws-sdk-go-v2/service/autoscaling"
+	ec2v2 "github.com/aws/aws-sdk-go-v2/service/ec2"
 	"github.com/aws/aws-sdk-go-v2/service/eks"
 	elb "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing"
 	elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
@@ -49,6 +52,13 @@ import (
 	"sigs.k8s.io/cluster-api-provider-aws/v2/version"
 )
 
+// EC2API is a compatibility layer for the v1 ec2iface.EC2API interface.
+// It is used to provide a consistent interface for the DeleteSecurityGroup method.
+type EC2API interface {
+	DeleteSecurityGroup(context.Context, *ec2v2.DeleteSecurityGroupInput, ...func(*ec2v2.Options)) (*ec2v2.DeleteSecurityGroupOutput, error)
+	ec2v2.DescribeSecurityGroupsAPIClient
+}
+
 // NewASGClient creates a new ASG API client for a given session.
 func NewASGClient(scopeUser cloud.ScopeUsage, session cloud.Session, logger logger.Wrapper, target runtime.Object) *autoscaling.Client {
 	cfg := session.SessionV2()
@@ -312,6 +322,24 @@ func NewS3Client(scopeUser cloud.ScopeUsage, session cloud.Session, logger logge
 	return s3.NewFromConfig(cfg, s3Opts...)
 }
 
+// NewEC2ClientV2 creates a new EC2 API client for a given session using AWS SDK v2.
+func NewEC2ClientV2(scopeUser cloud.ScopeUsage, session cloud.Session, logger logger.Wrapper, target runtime.Object) EC2API {
+	cfg := session.SessionV2()
+	multiSvcEndpointResolver := endpointsv2.NewMultiServiceEndpointResolver()
+	ec2EndpointResolver := &endpointsv2.EC2EndpointResolver{
+		MultiServiceEndpointResolver: multiSvcEndpointResolver,
+	}
+	ec2Opts := []func(*ec2v2.Options){
+		func(o *ec2v2.Options) {
+			o.Logger = logger.GetAWSLogger()
+			o.ClientLogMode = awslogs.GetAWSLogLevelV2(logger.GetLogger())
+			o.EndpointResolverV2 = ec2EndpointResolver
+		},
+		ec2v2.WithAPIOptions(awsmetricsv2.WithMiddlewares(scopeUser.ControllerName(), target), awsmetricsv2.WithCAPAUserAgentMiddleware()),
+	}
+	return ec2v2.NewFromConfig(cfg, ec2Opts...)
+}
+
 func recordAWSPermissionsIssue(target runtime.Object) func(r *request.Request) {
 	return func(r *request.Request) {
 		if awsErr, ok := r.Error.(awserr.Error); ok {
@@ -332,9 +360,11 @@ func getUserAgentHandler() request.NamedHandler {
 
 // AWSClients contains all the aws clients used by the scopes.
 type AWSClients struct {
-	ASG             autoscaling.Client
-	EC2             ec2iface.EC2API
-	ELB             elb.Client
+	ELB             *elb.Client
 	SecretsManager  secretsmanageriface.SecretsManagerAPI
-	ResourceTagging rgapi.Client
+	ResourceTagging *rgapi.Client
+	ASG             *autoscaling.Client
+	EC2             ec2iface.EC2API
+	EC2V2           *ec2v2.Client
+	ELBV2           *elbv2.Client
 }
diff --git a/pkg/cloud/services/elb/loadbalancer.go b/pkg/cloud/services/elb/loadbalancer.go
index 25aefa9b4..6887692dd 100644
--- a/pkg/cloud/services/elb/loadbalancer.go
+++ b/pkg/cloud/services/elb/loadbalancer.go
@@ -1687,16 +1687,32 @@ func (s *Service) reconcileTargetGroupsAndListeners(ctx context.Context, lbARN s
 			}
 			createdTargetGroups = append(createdTargetGroups, group)
 
+			targetGroupAttributeInput := &elbv2.ModifyTargetGroupAttributesInput{TargetGroupArn: group.TargetGroupArn}
+
+			if lbSpec.LoadBalancerType == infrav1.LoadBalancerTypeNLB {
+				targetGroupAttributeInput.Attributes = append(targetGroupAttributeInput.Attributes,
+					elbv2types.TargetGroupAttribute{
+						Key:   aws.String(infrav1.TargetGroupAttributeEnableConnectionTermination),
+						Value: aws.String("false"),
+					},
+					elbv2types.TargetGroupAttribute{
+						Key:   aws.String(infrav1.TargetGroupAttributeUnhealthyDrainingIntervalSeconds),
+						Value: aws.String("300"),
+					},
+				)
+			}
+
 			if !lbSpec.PreserveClientIP {
-				targetGroupAttributeInput := &elbv2.ModifyTargetGroupAttributesInput{
-					TargetGroupArn: group.TargetGroupArn,
-					Attributes: []elbv2types.TargetGroupAttribute{
-						{
-							Key:   aws.String(infrav1.TargetGroupAttributeEnablePreserveClientIP),
-							Value: aws.String("false"),
-						},
+				targetGroupAttributeInput.Attributes = append(targetGroupAttributeInput.Attributes,
+					elbv2types.TargetGroupAttribute{
+						Key:   aws.String(infrav1.TargetGroupAttributeEnablePreserveClientIP),
+						Value: aws.String("false"),
 					},
-				}
+				)
+			}
+
+			if len(targetGroupAttributeInput.Attributes) > 0 {
+				s.scope.Debug("configuring target group attributes", "attributes", targetGroupAttributeInput)
 				if _, err := s.ELBV2Client.ModifyTargetGroupAttributes(ctx, targetGroupAttributeInput); err != nil {
 					return nil, nil, errors.Wrapf(err, "failed to modify target group attribute")
 				}
diff --git a/pkg/cloud/services/gc/ec2.go b/pkg/cloud/services/gc/ec2.go
index 823163ddd..f9d4727db 100644
--- a/pkg/cloud/services/gc/ec2.go
+++ b/pkg/cloud/services/gc/ec2.go
@@ -21,11 +21,12 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/service/ec2"
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/ec2"
+	"github.com/aws/aws-sdk-go-v2/service/ec2/types"
 
-	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/converters"
-	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/filter"
+	converters "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/convertersv2"
+	filter "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/filterv2"
 )
 
 func (s *Service) deleteSecurityGroups(ctx context.Context, resources []*AWSResource) error {
@@ -46,7 +47,7 @@ func (s *Service) deleteSecurityGroups(ctx context.Context, resources []*AWSReso
 }
 
 func (s *Service) isSecurityGroupToDelete(resource *AWSResource) bool {
-	if !s.isMatchingResource(resource, ec2.ServiceName, "security-group") {
+	if !s.isMatchingResource(resource, strings.ToLower(ec2.ServiceID), "security-group") {
 		return false
 	}
 	if eksClusterName := resource.Tags[eksClusterNameTag]; eksClusterName != "" {
@@ -64,7 +65,7 @@ func (s *Service) deleteSecurityGroup(ctx context.Context, securityGroupID strin
 	}
 
 	s.scope.Debug("Deleting security group", "group_id", securityGroupID)
-	if _, err := s.ec2Client.DeleteSecurityGroupWithContext(ctx, &input); err != nil {
+	if _, err := s.ec2Client.DeleteSecurityGroup(ctx, &input); err != nil {
 		return fmt.Errorf("deleting security group: %w", err)
 	}
 
@@ -72,16 +73,22 @@ func (s *Service) deleteSecurityGroup(ctx context.Context, securityGroupID strin
 }
 
 // getProviderOwnedSecurityGroups gets cloud provider created security groups of ELBs for this cluster, filtering by tag: kubernetes.io/cluster/<cluster-name>:owned and VPC Id.
-func (s *Service) getProviderOwnedSecurityGroups(_ context.Context) ([]*AWSResource, error) {
+func (s *Service) getProviderOwnedSecurityGroups(ctx context.Context) ([]*AWSResource, error) {
 	input := &ec2.DescribeSecurityGroupsInput{
-		Filters: []*ec2.Filter{
-			filter.EC2.ProviderOwned(s.scope.KubernetesClusterName()),
+		Filters: []types.Filter{
+			*filter.EC2.ProviderOwned(s.scope.KubernetesClusterName()),
 		},
 	}
 
 	var resources []*AWSResource
-	err := s.ec2Client.DescribeSecurityGroupsPagesWithContext(context.TODO(), input, func(out *ec2.DescribeSecurityGroupsOutput, last bool) bool {
-		for _, group := range out.SecurityGroups {
+	paginator := ec2.NewDescribeSecurityGroupsPaginator(s.ec2Client, input)
+
+	for paginator.HasMorePages() {
+		page, err := paginator.NextPage(ctx)
+		if err != nil {
+			return nil, fmt.Errorf("failed to get next page of security groups: %w", err)
+		}
+		for _, group := range page.SecurityGroups {
 			arn := composeFakeArn(sgService, sgResourcePrefix+*group.GroupId)
 			resource, err := composeAWSResource(arn, converters.TagsToMap(group.Tags))
 			if err != nil {
@@ -90,10 +97,6 @@ func (s *Service) getProviderOwnedSecurityGroups(_ context.Context) ([]*AWSResou
 			}
 			resources = append(resources, resource)
 		}
-		return true
-	})
-	if err != nil {
-		return nil, fmt.Errorf("describe security groups error: %w", err)
 	}
 
 	return resources, nil
diff --git a/pkg/cloud/services/gc/loadbalancer.go b/pkg/cloud/services/gc/loadbalancer.go
index 2d9e7d64c..31b5aa6f4 100644
--- a/pkg/cloud/services/gc/loadbalancer.go
+++ b/pkg/cloud/services/gc/loadbalancer.go
@@ -26,7 +26,7 @@ import (
 	elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
 
 	infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
-	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/converters"
+	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/convertersv2"
 )
 
 func (s *Service) deleteLoadBalancers(ctx context.Context, resources []*AWSResource) error {
@@ -222,7 +222,7 @@ func (s *Service) filterProviderOwnedLB(ctx context.Context, names []string) ([]
 				serviceTag := infrav1.ClusterAWSCloudProviderTagKey(s.scope.KubernetesClusterName())
 				if *tag.Key == serviceTag && *tag.Value == string(infrav1.ResourceLifecycleOwned) {
 					arn := composeFakeArn(elbService, elbResourcePrefix+*tagDesc.LoadBalancerName)
-					resource, err := composeAWSResource(arn, converters.ELBTagsToMap(tagDesc.Tags))
+					resource, err := composeAWSResource(arn, convertersv2.ELBTagsToMap(tagDesc.Tags))
 					if err != nil {
 						return nil, fmt.Errorf("error compose aws elb resource %s: %w", arn, err)
 					}
@@ -250,7 +250,7 @@ func (s *Service) filterProviderOwnedLBV2(ctx context.Context, arns []string) ([
 			for _, tag := range tagDesc.Tags {
 				serviceTag := infrav1.ClusterAWSCloudProviderTagKey(s.scope.KubernetesClusterName())
 				if *tag.Key == serviceTag && *tag.Value == string(infrav1.ResourceLifecycleOwned) {
-					resource, err := composeAWSResource(*tagDesc.ResourceArn, converters.V2TagsToMap(tagDesc.Tags))
+					resource, err := composeAWSResource(*tagDesc.ResourceArn, convertersv2.V2TagsToMap(tagDesc.Tags))
 					if err != nil {
 						return nil, fmt.Errorf("error compose aws elbv2 resource %s: %w", *tagDesc.ResourceArn, err)
 					}
diff --git a/pkg/cloud/services/gc/options.go b/pkg/cloud/services/gc/options.go
index 75beb67e3..1b4b7b606 100644
--- a/pkg/cloud/services/gc/options.go
+++ b/pkg/cloud/services/gc/options.go
@@ -17,8 +17,7 @@ limitations under the License.
 package gc
 
 import (
-	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
-
+	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/scope"
 	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/services/elb"
 )
 
@@ -47,7 +46,7 @@ func withResourceTaggingClient(client elb.ResourceGroupsTaggingAPIAPI) ServiceOp
 }
 
 // withEC2Client is an option for specifying a AWS EC2 Client.
-func withEC2Client(client ec2iface.EC2API) ServiceOption {
+func withEC2Client(client scope.EC2API) ServiceOption {
 	return func(s *Service) {
 		s.ec2Client = client
 	}
diff --git a/pkg/cloud/services/gc/service.go b/pkg/cloud/services/gc/service.go
index 2eda2f695..2aa98e709 100644
--- a/pkg/cloud/services/gc/service.go
+++ b/pkg/cloud/services/gc/service.go
@@ -21,7 +21,6 @@ import (
 	"context"
 
 	"github.com/aws/aws-sdk-go-v2/aws/arn"
-	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
 
 	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud"
 	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/scope"
@@ -34,7 +33,7 @@ type Service struct {
 	elbClient             elb.ELBAPI
 	elbv2Client           elb.ELBV2API
 	resourceTaggingClient elb.ResourceGroupsTaggingAPIAPI
-	ec2Client             ec2iface.EC2API
+	ec2Client             scope.EC2API
 	cleanupFuncs          ResourceCleanupFuncs
 	collectFuncs          ResourceCollectFuncs
 }
@@ -52,9 +51,9 @@ func NewService(clusterScope cloud.ClusterScoper, opts ...ServiceOption) *Servic
 		resourceTaggingClient: &elb.ResourceGroupsTaggingAPIClient{
 			Client: scope.NewResourgeTaggingClient(clusterScope, clusterScope, clusterScope, clusterScope.InfraCluster()),
 		},
-		ec2Client:    scope.NewEC2Client(clusterScope, clusterScope, clusterScope, clusterScope.InfraCluster()),
 		cleanupFuncs: ResourceCleanupFuncs{},
 		collectFuncs: ResourceCollectFuncs{},
+		ec2Client:    scope.NewEC2ClientV2(clusterScope, clusterScope, clusterScope, clusterScope.InfraCluster()),
 	}
 	addDefaultCleanupFuncs(svc)
 
diff --git a/pkg/cloud/services/network/network.go b/pkg/cloud/services/network/network.go
index 960528185..35aa421be 100644
--- a/pkg/cloud/services/network/network.go
+++ b/pkg/cloud/services/network/network.go
@@ -42,48 +42,56 @@ func (s *Service) ReconcileNetwork() (err error) {
 		conditions.MarkFalse(s.scope.InfraCluster(), infrav1.SecondaryCidrsReadyCondition, infrav1.SecondaryCidrReconciliationFailedReason, infrautilconditions.ErrorConditionAfterInit(s.scope.ClusterObj()), "%s", err.Error())
 		return err
 	}
+	conditions.MarkTrue(s.scope.InfraCluster(), infrav1.SecondaryCidrsReadyCondition)
 
 	// Subnets.
 	if err := s.reconcileSubnets(); err != nil {
 		conditions.MarkFalse(s.scope.InfraCluster(), infrav1.SubnetsReadyCondition, infrav1.SubnetsReconciliationFailedReason, infrautilconditions.ErrorConditionAfterInit(s.scope.ClusterObj()), "%s", err.Error())
 		return err
 	}
+	conditions.MarkTrue(s.scope.InfraCluster(), infrav1.SubnetsReadyCondition)
 
 	// Internet Gateways.
 	if err := s.reconcileInternetGateways(); err != nil {
 		conditions.MarkFalse(s.scope.InfraCluster(), infrav1.InternetGatewayReadyCondition, infrav1.InternetGatewayFailedReason, infrautilconditions.ErrorConditionAfterInit(s.scope.ClusterObj()), "%s", err.Error())
 		return err
 	}
+	conditions.MarkTrue(s.scope.InfraCluster(), infrav1.InternetGatewayReadyCondition)
 
 	// Carrier Gateway.
 	if err := s.reconcileCarrierGateway(); err != nil {
 		conditions.MarkFalse(s.scope.InfraCluster(), infrav1.CarrierGatewayReadyCondition, infrav1.CarrierGatewayFailedReason, infrautilconditions.ErrorConditionAfterInit(s.scope.ClusterObj()), "%s", err.Error())
 		return err
 	}
+	conditions.MarkTrue(s.scope.InfraCluster(), infrav1.CarrierGatewayReadyCondition)
 
 	// Egress Only Internet Gateways.
 	if err := s.reconcileEgressOnlyInternetGateways(); err != nil {
 		conditions.MarkFalse(s.scope.InfraCluster(), infrav1.EgressOnlyInternetGatewayReadyCondition, infrav1.EgressOnlyInternetGatewayFailedReason, infrautilconditions.ErrorConditionAfterInit(s.scope.ClusterObj()), "%s", err.Error())
 		return err
 	}
+	conditions.MarkTrue(s.scope.InfraCluster(), infrav1.EgressOnlyInternetGatewayReadyCondition)
 
 	// NAT Gateways.
 	if err := s.reconcileNatGateways(); err != nil {
 		conditions.MarkFalse(s.scope.InfraCluster(), infrav1.NatGatewaysReadyCondition, infrav1.NatGatewaysReconciliationFailedReason, infrautilconditions.ErrorConditionAfterInit(s.scope.ClusterObj()), "%s", err.Error())
 		return err
 	}
+	conditions.MarkTrue(s.scope.InfraCluster(), infrav1.NatGatewaysReadyCondition)
 
 	// Routing tables.
 	if err := s.reconcileRouteTables(); err != nil {
 		conditions.MarkFalse(s.scope.InfraCluster(), infrav1.RouteTablesReadyCondition, infrav1.RouteTableReconciliationFailedReason, infrautilconditions.ErrorConditionAfterInit(s.scope.ClusterObj()), "%s", err.Error())
 		return err
 	}
+	conditions.MarkTrue(s.scope.InfraCluster(), infrav1.RouteTablesReadyCondition)
 
 	// VPC Endpoints.
 	if err := s.reconcileVPCEndpoints(); err != nil {
 		conditions.MarkFalse(s.scope.InfraCluster(), infrav1.VpcEndpointsReadyCondition, infrav1.VpcEndpointsReconciliationFailedReason, infrautilconditions.ErrorConditionAfterInit(s.scope.ClusterObj()), "%s", err.Error())
 		return err
 	}
+	conditions.MarkTrue(s.scope.InfraCluster(), infrav1.VpcEndpointsReadyCondition)
 
 	s.scope.Debug("Reconcile network completed successfully")
 	return nil



Test patch:
diff --git a/controllers/helpers_test.go b/controllers/helpers_test.go
index d68669c40..05f103cfb 100644
--- a/controllers/helpers_test.go
+++ b/controllers/helpers_test.go
@@ -393,6 +393,14 @@ func mockedModifyTargetGroupAttributes(t *testing.T, m *mocks.MockELBV2APIMockRe
 	m.ModifyTargetGroupAttributes(gomock.Any(), gomock.Eq(&elbv2.ModifyTargetGroupAttributesInput{
 		TargetGroupArn: tgArn,
 		Attributes: []elbv2types.TargetGroupAttribute{
+			{
+				Key:   aws.String(infrav1.TargetGroupAttributeEnableConnectionTermination),
+				Value: aws.String("false"),
+			},
+			{
+				Key:   aws.String(infrav1.TargetGroupAttributeUnhealthyDrainingIntervalSeconds),
+				Value: aws.String("300"),
+			},
 			{
 				Key:   aws.String(infrav1.TargetGroupAttributeEnablePreserveClientIP),
 				Value: aws.String("false"),
diff --git a/pkg/cloud/services/elb/loadbalancer_test.go b/pkg/cloud/services/elb/loadbalancer_test.go
index 805167b3b..5bf8baae0 100644
--- a/pkg/cloud/services/elb/loadbalancer_test.go
+++ b/pkg/cloud/services/elb/loadbalancer_test.go
@@ -1543,6 +1543,14 @@ func TestReconcileTargetGroupsAndListeners(t *testing.T) {
 				m.ModifyTargetGroupAttributes(gomock.Any(), gomock.Eq(&elbv2.ModifyTargetGroupAttributesInput{
 					TargetGroupArn: aws.String(tgArn),
 					Attributes: []elbv2types.TargetGroupAttribute{
+						{
+							Key:   aws.String(infrav1.TargetGroupAttributeEnableConnectionTermination),
+							Value: aws.String("false"),
+						},
+						{
+							Key:   aws.String(infrav1.TargetGroupAttributeUnhealthyDrainingIntervalSeconds),
+							Value: aws.String("300"),
+						},
 						{
 							Key:   aws.String(infrav1.TargetGroupAttributeEnablePreserveClientIP),
 							Value: aws.String("false"),
@@ -1661,6 +1669,14 @@ func TestReconcileTargetGroupsAndListeners(t *testing.T) {
 				m.ModifyTargetGroupAttributes(gomock.Any(), &elbv2.ModifyTargetGroupAttributesInput{
 					TargetGroupArn: aws.String(tgArn),
 					Attributes: []elbv2types.TargetGroupAttribute{
+						{
+							Key:   aws.String(infrav1.TargetGroupAttributeEnableConnectionTermination),
+							Value: aws.String("false"),
+						},
+						{
+							Key:   aws.String(infrav1.TargetGroupAttributeUnhealthyDrainingIntervalSeconds),
+							Value: aws.String("300"),
+						},
 						{
 							Key:   aws.String(infrav1.TargetGroupAttributeEnablePreserveClientIP),
 							Value: aws.String("false"),
@@ -1767,6 +1783,14 @@ func TestReconcileTargetGroupsAndListeners(t *testing.T) {
 				m.ModifyTargetGroupAttributes(gomock.Any(), &elbv2.ModifyTargetGroupAttributesInput{
 					TargetGroupArn: aws.String(tgArn),
 					Attributes: []elbv2types.TargetGroupAttribute{
+						{
+							Key:   aws.String(infrav1.TargetGroupAttributeEnableConnectionTermination),
+							Value: aws.String("false"),
+						},
+						{
+							Key:   aws.String(infrav1.TargetGroupAttributeUnhealthyDrainingIntervalSeconds),
+							Value: aws.String("300"),
+						},
 						{
 							Key:   aws.String(infrav1.TargetGroupAttributeEnablePreserveClientIP),
 							Value: aws.String("false"),
@@ -1860,6 +1884,19 @@ func TestReconcileTargetGroupsAndListeners(t *testing.T) {
 						},
 					},
 				}, nil)
+				m.ModifyTargetGroupAttributes(gomock.Any(), gomock.Eq(&elbv2.ModifyTargetGroupAttributesInput{
+					TargetGroupArn: aws.String(tgArn),
+					Attributes: []elbv2types.TargetGroupAttribute{
+						{
+							Key:   aws.String(infrav1.TargetGroupAttributeEnableConnectionTermination),
+							Value: aws.String("false"),
+						},
+						{
+							Key:   aws.String(infrav1.TargetGroupAttributeUnhealthyDrainingIntervalSeconds),
+							Value: aws.String("300"),
+						},
+					},
+				})).Return(nil, nil)
 				m.DescribeListeners(gomock.Any(), &elbv2.DescribeListenersInput{
 					LoadBalancerArn: aws.String(elbArn),
 				}).Return(&elbv2.DescribeListenersOutput{
@@ -2000,6 +2037,14 @@ func TestReconcileTargetGroupsAndListeners(t *testing.T) {
 				m.ModifyTargetGroupAttributes(gomock.Any(), &elbv2.ModifyTargetGroupAttributesInput{
 					TargetGroupArn: aws.String(tgArn),
 					Attributes: []elbv2types.TargetGroupAttribute{
+						{
+							Key:   aws.String(infrav1.TargetGroupAttributeEnableConnectionTermination),
+							Value: aws.String("false"),
+						},
+						{
+							Key:   aws.String(infrav1.TargetGroupAttributeUnhealthyDrainingIntervalSeconds),
+							Value: aws.String("300"),
+						},
 						{
 							Key:   aws.String(infrav1.TargetGroupAttributeEnablePreserveClientIP),
 							Value: aws.String("false"),
@@ -2115,6 +2160,14 @@ func TestReconcileTargetGroupsAndListeners(t *testing.T) {
 				m.ModifyTargetGroupAttributes(gomock.Any(), &elbv2.ModifyTargetGroupAttributesInput{
 					TargetGroupArn: aws.String(tgArn),
 					Attributes: []elbv2types.TargetGroupAttribute{
+						{
+							Key:   aws.String(infrav1.TargetGroupAttributeEnableConnectionTermination),
+							Value: aws.String("false"),
+						},
+						{
+							Key:   aws.String(infrav1.TargetGroupAttributeUnhealthyDrainingIntervalSeconds),
+							Value: aws.String("300"),
+						},
 						{
 							Key:   aws.String(infrav1.TargetGroupAttributeEnablePreserveClientIP),
 							Value: aws.String("false"),
@@ -2403,6 +2456,14 @@ func TestReconcileV2LB(t *testing.T) {
 				m.ModifyTargetGroupAttributes(gomock.Any(), gomock.Eq(&elbv2.ModifyTargetGroupAttributesInput{
 					TargetGroupArn: aws.String(tgArn),
 					Attributes: []elbv2types.TargetGroupAttribute{
+						{
+							Key:   aws.String(infrav1.TargetGroupAttributeEnableConnectionTermination),
+							Value: aws.String("false"),
+						},
+						{
+							Key:   aws.String(infrav1.TargetGroupAttributeUnhealthyDrainingIntervalSeconds),
+							Value: aws.String("300"),
+						},
 						{
 							Key:   aws.String(infrav1.TargetGroupAttributeEnablePreserveClientIP),
 							Value: aws.String("false"),
diff --git a/pkg/cloud/services/gc/cleanup_test.go b/pkg/cloud/services/gc/cleanup_test.go
index 48f9ff7c4..461303adf 100644
--- a/pkg/cloud/services/gc/cleanup_test.go
+++ b/pkg/cloud/services/gc/cleanup_test.go
@@ -21,12 +21,12 @@ import (
 	"testing"
 
 	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/ec2"
 	elb "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing"
 	elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
 	rgapi "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
 	rgapitypes "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
 	"github.com/aws/aws-sdk-go/aws/request"
-	"github.com/aws/aws-sdk-go/service/ec2"
 	"github.com/golang/mock/gomock"
 	. "github.com/onsi/gomega"
 	corev1 "k8s.io/api/core/v1"
@@ -40,6 +40,7 @@ import (
 	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud"
 	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/scope"
 	"sigs.k8s.io/cluster-api-provider-aws/v2/test/mocks"
+	mocksv2 "sigs.k8s.io/cluster-api-provider-aws/v2/test/mocks/v2"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
 )
 
@@ -50,7 +51,7 @@ func TestReconcileDelete(t *testing.T) {
 		elbMocks     func(m *mocks.MockELBAPIMockRecorder)
 		elbv2Mocks   func(m *mocks.MockELBV2APIMockRecorder)
 		rgAPIMocks   func(m *mocks.MockResourceGroupsTaggingAPIAPIMockRecorder)
-		ec2Mocks     func(m *mocks.MockEC2APIMockRecorder)
+		ec2Mocks     func(m *mocksv2.MockEC2APIMockRecorder)
 		expectErr    bool
 	}{
 		{
@@ -59,7 +60,7 @@ func TestReconcileDelete(t *testing.T) {
 			rgAPIMocks:   func(m *mocks.MockResourceGroupsTaggingAPIAPIMockRecorder) {},
 			elbMocks:     func(m *mocks.MockELBAPIMockRecorder) {},
 			elbv2Mocks:   func(m *mocks.MockELBV2APIMockRecorder) {},
-			ec2Mocks:     func(m *mocks.MockEC2APIMockRecorder) {},
+			ec2Mocks:     func(m *mocksv2.MockEC2APIMockRecorder) {},
 			expectErr:    false,
 		},
 		{
@@ -81,7 +82,7 @@ func TestReconcileDelete(t *testing.T) {
 			},
 			elbMocks:   func(m *mocks.MockELBAPIMockRecorder) {},
 			elbv2Mocks: func(m *mocks.MockELBV2APIMockRecorder) {},
-			ec2Mocks:   func(m *mocks.MockEC2APIMockRecorder) {},
+			ec2Mocks:   func(m *mocksv2.MockEC2APIMockRecorder) {},
 			expectErr:  false,
 		},
 		{
@@ -103,7 +104,7 @@ func TestReconcileDelete(t *testing.T) {
 			},
 			elbMocks:   func(m *mocks.MockELBAPIMockRecorder) {},
 			elbv2Mocks: func(m *mocks.MockELBV2APIMockRecorder) {},
-			ec2Mocks:   func(m *mocks.MockEC2APIMockRecorder) {},
+			ec2Mocks:   func(m *mocksv2.MockEC2APIMockRecorder) {},
 			expectErr:  false,
 		},
 		{
@@ -125,7 +126,7 @@ func TestReconcileDelete(t *testing.T) {
 			},
 			elbMocks:   func(m *mocks.MockELBAPIMockRecorder) {},
 			elbv2Mocks: func(m *mocks.MockELBV2APIMockRecorder) {},
-			ec2Mocks:   func(m *mocks.MockEC2APIMockRecorder) {},
+			ec2Mocks:   func(m *mocksv2.MockEC2APIMockRecorder) {},
 			expectErr:  false,
 		},
 		{
@@ -157,7 +158,7 @@ func TestReconcileDelete(t *testing.T) {
 			},
 			elbMocks:   func(m *mocks.MockELBAPIMockRecorder) {},
 			elbv2Mocks: func(m *mocks.MockELBV2APIMockRecorder) {},
-			ec2Mocks:   func(m *mocks.MockEC2APIMockRecorder) {},
+			ec2Mocks:   func(m *mocksv2.MockEC2APIMockRecorder) {},
 			expectErr:  false,
 		},
 		{
@@ -189,7 +190,7 @@ func TestReconcileDelete(t *testing.T) {
 			},
 			elbMocks:   func(m *mocks.MockELBAPIMockRecorder) {},
 			elbv2Mocks: func(m *mocks.MockELBV2APIMockRecorder) {},
-			ec2Mocks:   func(m *mocks.MockEC2APIMockRecorder) {},
+			ec2Mocks:   func(m *mocksv2.MockEC2APIMockRecorder) {},
 			expectErr:  false,
 		},
 		{
@@ -229,7 +230,7 @@ func TestReconcileDelete(t *testing.T) {
 				}).Return(&elb.DeleteLoadBalancerOutput{}, nil)
 			},
 			elbv2Mocks: func(m *mocks.MockELBV2APIMockRecorder) {},
-			ec2Mocks:   func(m *mocks.MockEC2APIMockRecorder) {},
+			ec2Mocks:   func(m *mocksv2.MockEC2APIMockRecorder) {},
 			expectErr:  false,
 		},
 		{
@@ -269,7 +270,7 @@ func TestReconcileDelete(t *testing.T) {
 				}).Return(&elb.DeleteLoadBalancerOutput{}, nil)
 			},
 			elbv2Mocks: func(m *mocks.MockELBV2APIMockRecorder) {},
-			ec2Mocks:   func(m *mocks.MockEC2APIMockRecorder) {},
+			ec2Mocks:   func(m *mocksv2.MockEC2APIMockRecorder) {},
 			expectErr:  false,
 		},
 		{
@@ -309,7 +310,7 @@ func TestReconcileDelete(t *testing.T) {
 					LoadBalancerArn: aws.String("arn:aws:elasticloadbalancing:eu-west-2:1234567890:loadbalancer/net/aec24434cd2ce4630bd14a955413ee37"),
 				}).Return(&elbv2.DeleteLoadBalancerOutput{}, nil)
 			},
-			ec2Mocks:  func(m *mocks.MockEC2APIMockRecorder) {},
+			ec2Mocks:  func(m *mocksv2.MockEC2APIMockRecorder) {},
 			expectErr: false,
 		},
 		{
@@ -349,7 +350,7 @@ func TestReconcileDelete(t *testing.T) {
 					LoadBalancerArn: aws.String("arn:aws:elasticloadbalancing:eu-west-2:1234567890:loadbalancer/net/aec24434cd2ce4630bd14a955413ee37"),
 				}).Return(&elbv2.DeleteLoadBalancerOutput{}, nil)
 			},
-			ec2Mocks:  func(m *mocks.MockEC2APIMockRecorder) {},
+			ec2Mocks:  func(m *mocksv2.MockEC2APIMockRecorder) {},
 			expectErr: false,
 		},
 		{
@@ -389,7 +390,47 @@ func TestReconcileDelete(t *testing.T) {
 					LoadBalancerArn: aws.String("arn:aws:elasticloadbalancing:eu-west-2:1234567890:loadbalancer/app/aec24434cd2ce4630bd14a955413ee37"),
 				}).Return(&elbv2.DeleteLoadBalancerOutput{}, nil)
 			},
-			ec2Mocks:  func(m *mocks.MockEC2APIMockRecorder) {},
+			ec2Mocks:  func(m *mocksv2.MockEC2APIMockRecorder) {},
+			expectErr: false,
+		},
+		{
+			name:         "eks with ALB Service load balancer",
+			clusterScope: createManageScope(t, "", ""),
+			rgAPIMocks: func(m *mocks.MockResourceGroupsTaggingAPIAPIMockRecorder) {
+				m.GetResources(gomock.Any(), &rgapi.GetResourcesInput{
+					TagFilters: []rgapitypes.TagFilter{
+						{
+							Key:    aws.String("kubernetes.io/cluster/eks-test-cluster"),
+							Values: []string{"owned"},
+						},
+					},
+				}).DoAndReturn(func(awsCtx context.Context, input *rgapi.GetResourcesInput, opts ...request.Option) (*rgapi.GetResourcesOutput, error) {
+					return &rgapi.GetResourcesOutput{
+						ResourceTagMappingList: []rgapitypes.ResourceTagMapping{
+							{
+								ResourceARN: aws.String("arn:aws:elasticloadbalancing:eu-west-2:1234567890:loadbalancer/app/aec24434cd2ce4630bd14a955413ee37"),
+								Tags: []rgapitypes.Tag{
+									{
+										Key:   aws.String("kubernetes.io/cluster/eks-test-cluster"),
+										Value: aws.String("owned"),
+									},
+									{
+										Key:   aws.String(serviceNameTag),
+										Value: aws.String("default/svc1"),
+									},
+								},
+							},
+						},
+					}, nil
+				})
+			},
+			elbMocks: func(m *mocks.MockELBAPIMockRecorder) {},
+			elbv2Mocks: func(m *mocks.MockELBV2APIMockRecorder) {
+				m.DeleteLoadBalancer(gomock.Any(), &elbv2.DeleteLoadBalancerInput{
+					LoadBalancerArn: aws.String("arn:aws:elasticloadbalancing:eu-west-2:1234567890:loadbalancer/app/aec24434cd2ce4630bd14a955413ee37"),
+				}).Return(&elbv2.DeleteLoadBalancerOutput{}, nil)
+			},
+			ec2Mocks:  func(m *mocksv2.MockEC2APIMockRecorder) {},
 			expectErr: false,
 		},
 		{
@@ -429,7 +470,7 @@ func TestReconcileDelete(t *testing.T) {
 					LoadBalancerArn: aws.String("arn:aws:elasticloadbalancing:eu-west-2:1234567890:loadbalancer/app/aec24434cd2ce4630bd14a955413ee37"),
 				}).Return(&elbv2.DeleteLoadBalancerOutput{}, nil)
 			},
-			ec2Mocks:  func(m *mocks.MockEC2APIMockRecorder) {},
+			ec2Mocks:  func(m *mocksv2.MockEC2APIMockRecorder) {},
 			expectErr: false,
 		},
 		{
@@ -497,12 +538,12 @@ func TestReconcileDelete(t *testing.T) {
 			elbv2Mocks: func(m *mocks.MockELBV2APIMockRecorder) {
 				m.DeleteTargetGroup(gomock.Any(), &elbv2.DeleteTargetGroupInput{
 					TargetGroupArn: aws.String("arn:aws:elasticloadbalancing:eu-west-2:1234567890:targetgroup/k8s-default-podinfo-2c868b281a/e979fe9bd6825433"),
-				})
+				}).Return(&elbv2.DeleteTargetGroupOutput{}, nil)
 			},
-			ec2Mocks: func(m *mocks.MockEC2APIMockRecorder) {
-				m.DeleteSecurityGroupWithContext(gomock.Any(), &ec2.DeleteSecurityGroupInput{
+			ec2Mocks: func(m *mocksv2.MockEC2APIMockRecorder) {
+				m.DeleteSecurityGroup(gomock.Any(), &ec2.DeleteSecurityGroupInput{
 					GroupId: aws.String("sg-123456"),
-				})
+				}).Return(&ec2.DeleteSecurityGroupOutput{}, nil)
 			},
 			expectErr: false,
 		},
@@ -543,7 +584,7 @@ func TestReconcileDelete(t *testing.T) {
 			},
 			elbMocks:   func(m *mocks.MockELBAPIMockRecorder) {},
 			elbv2Mocks: func(m *mocks.MockELBV2APIMockRecorder) {},
-			ec2Mocks:   func(m *mocks.MockEC2APIMockRecorder) {},
+			ec2Mocks:   func(m *mocksv2.MockEC2APIMockRecorder) {},
 			expectErr:  false,
 		},
 		{
@@ -583,7 +624,7 @@ func TestReconcileDelete(t *testing.T) {
 			},
 			elbMocks:   func(m *mocks.MockELBAPIMockRecorder) {},
 			elbv2Mocks: func(m *mocks.MockELBV2APIMockRecorder) {},
-			ec2Mocks:   func(m *mocks.MockEC2APIMockRecorder) {},
+			ec2Mocks:   func(m *mocksv2.MockEC2APIMockRecorder) {},
 			expectErr:  false,
 		},
 		{
@@ -651,12 +692,12 @@ func TestReconcileDelete(t *testing.T) {
 			elbv2Mocks: func(m *mocks.MockELBV2APIMockRecorder) {
 				m.DeleteTargetGroup(gomock.Any(), &elbv2.DeleteTargetGroupInput{
 					TargetGroupArn: aws.String("arn:aws:elasticloadbalancing:eu-west-2:1234567890:targetgroup/k8s-default-podinfo-2c868b281a/e979fe9bd6825433"),
-				})
+				}).Return(&elbv2.DeleteTargetGroupOutput{}, nil)
 			},
-			ec2Mocks: func(m *mocks.MockEC2APIMockRecorder) {
-				m.DeleteSecurityGroupWithContext(gomock.Any(), &ec2.DeleteSecurityGroupInput{
+			ec2Mocks: func(m *mocksv2.MockEC2APIMockRecorder) {
+				m.DeleteSecurityGroup(gomock.Any(), &ec2.DeleteSecurityGroupInput{
 					GroupId: aws.String("sg-123456"),
-				})
+				}).Return(&ec2.DeleteSecurityGroupOutput{}, nil)
 			},
 			expectErr: false,
 		},
@@ -725,9 +766,9 @@ func TestReconcileDelete(t *testing.T) {
 			elbv2Mocks: func(m *mocks.MockELBV2APIMockRecorder) {
 				m.DeleteTargetGroup(gomock.Any(), &elbv2.DeleteTargetGroupInput{
 					TargetGroupArn: aws.String("arn:aws:elasticloadbalancing:eu-west-2:1234567890:targetgroup/k8s-default-podinfo-2c868b281a/e979fe9bd6825433"),
-				})
+				}).Return(&elbv2.DeleteTargetGroupOutput{}, nil)
 			},
-			ec2Mocks:  func(m *mocks.MockEC2APIMockRecorder) {},
+			ec2Mocks:  func(m *mocksv2.MockEC2APIMockRecorder) {},
 			expectErr: false,
 		},
 		{
@@ -793,7 +834,7 @@ func TestReconcileDelete(t *testing.T) {
 				}).Return(&elb.DeleteLoadBalancerOutput{}, nil)
 			},
 			elbv2Mocks: func(m *mocks.MockELBV2APIMockRecorder) {},
-			ec2Mocks:   func(m *mocks.MockEC2APIMockRecorder) {},
+			ec2Mocks:   func(m *mocksv2.MockEC2APIMockRecorder) {},
 			expectErr:  false,
 		},
 	}
@@ -807,7 +848,7 @@ func TestReconcileDelete(t *testing.T) {
 			rgapiMock := mocks.NewMockResourceGroupsTaggingAPIAPI(mockCtrl)
 			elbapiMock := mocks.NewMockELBAPI(mockCtrl)
 			elbv2Mock := mocks.NewMockELBV2API(mockCtrl)
-			ec2Mock := mocks.NewMockEC2API(mockCtrl)
+			ec2Mock := mocksv2.NewMockEC2API(mockCtrl)
 
 			tc.rgAPIMocks(rgapiMock.EXPECT())
 			tc.elbMocks(elbapiMock.EXPECT())
diff --git a/test/e2e/data/e2e_conf.yaml b/test/e2e/data/e2e_conf.yaml
index 2bce7f423..d6e3838b2 100644
--- a/test/e2e/data/e2e_conf.yaml
+++ b/test/e2e/data/e2e_conf.yaml
@@ -209,6 +209,7 @@ variables:
   # Enabling the feature flags by setting the env variables.
   EXP_CLUSTER_RESOURCE_SET: "true"
   EXP_MACHINE_POOL: "true"
+  EXP_MACHINE_POOL_MACHINES: "true"
   CLUSTER_TOPOLOGY: "true"
   INIT_WITH_BINARY_V1BETA1: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.2.0/clusterctl-{OS}-{ARCH}"
   # INIT_WITH_KUBERNETES_VERSION are only used by the clusterctl upgrade test to initialize
diff --git a/test/e2e/data/e2e_eks_conf.yaml b/test/e2e/data/e2e_eks_conf.yaml
index 173223b15..4697f9b7f 100644
--- a/test/e2e/data/e2e_eks_conf.yaml
+++ b/test/e2e/data/e2e_eks_conf.yaml
@@ -123,6 +123,7 @@ variables:
   UPGRADE_TO_VERSION: "v1.32.0"
   UPGRADE_FROM_VERSION: "v1.31.0"
   EXP_MACHINE_POOL: "true"
+  EXP_MACHINE_POOL_MACHINES: "true"
   EXP_CLUSTER_RESOURCE_SET: "true"
   EVENT_BRIDGE_INSTANCE_STATE: "true"
   AWS_NODE_MACHINE_TYPE: t3.large
diff --git a/test/mocks/generate_aws.go b/test/mocks/generate_aws.go
index 8f8477cc0..f5e551547 100644
--- a/test/mocks/generate_aws.go
+++ b/test/mocks/generate_aws.go
@@ -24,4 +24,6 @@ limitations under the License.
 //go:generate /usr/bin/env bash -c "cat ../../hack/boilerplate/boilerplate.generatego.txt aws_rgtagging_mock.go > _aws_rgtagging_mock.go && mv _aws_rgtagging_mock.go aws_rgtagging_mock.go"
 //go:generate ../../hack/tools/bin/mockgen -destination aws_ec2api_mock.go -package mocks github.com/aws/aws-sdk-go/service/ec2/ec2iface EC2API
 //go:generate /usr/bin/env bash -c "cat ../../hack/boilerplate/boilerplate.generatego.txt aws_ec2api_mock.go > _aws_ec2api_mock.go && mv _aws_ec2api_mock.go aws_ec2api_mock.go"
+//go:generate ../../hack/tools/bin/mockgen -destination aws_ec2apiv2_mock.go -package mocks sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/scope EC2API
+//go:generate /usr/bin/env bash -c "cat ../../hack/boilerplate/boilerplate.generatego.txt aws_ec2apiv2_mock.go > _aws_ec2apiv2_mock.go && mv _aws_ec2apiv2_mock.go v2/aws_ec2api_mock.go && rm aws_ec2apiv2_mock.go"
 package mocks
diff --git a/test/mocks/v2/aws_ec2api_mock.go b/test/mocks/v2/aws_ec2api_mock.go
new file mode 100644
index 000000000..78ab199a3
--- /dev/null
+++ b/test/mocks/v2/aws_ec2api_mock.go
@@ -0,0 +1,92 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by MockGen. DO NOT EDIT.
+// Source: sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/scope (interfaces: EC2API)
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+	context "context"
+	reflect "reflect"
+
+	ec2 "github.com/aws/aws-sdk-go-v2/service/ec2"
+	gomock "github.com/golang/mock/gomock"
+)
+
+// MockEC2API is a mock of EC2API interface.
+type MockEC2API struct {
+	ctrl     *gomock.Controller
+	recorder *MockEC2APIMockRecorder
+}
+
+// MockEC2APIMockRecorder is the mock recorder for MockEC2API.
+type MockEC2APIMockRecorder struct {
+	mock *MockEC2API
+}
+
+// NewMockEC2API creates a new mock instance.
+func NewMockEC2API(ctrl *gomock.Controller) *MockEC2API {
+	mock := &MockEC2API{ctrl: ctrl}
+	mock.recorder = &MockEC2APIMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockEC2API) EXPECT() *MockEC2APIMockRecorder {
+	return m.recorder
+}
+
+// DeleteSecurityGroup mocks base method.
+func (m *MockEC2API) DeleteSecurityGroup(arg0 context.Context, arg1 *ec2.DeleteSecurityGroupInput, arg2 ...func(*ec2.Options)) (*ec2.DeleteSecurityGroupOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "DeleteSecurityGroup", varargs...)
+	ret0, _ := ret[0].(*ec2.DeleteSecurityGroupOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// DeleteSecurityGroup indicates an expected call of DeleteSecurityGroup.
+func (mr *MockEC2APIMockRecorder) DeleteSecurityGroup(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSecurityGroup", reflect.TypeOf((*MockEC2API)(nil).DeleteSecurityGroup), varargs...)
+}
+
+// DescribeSecurityGroups mocks base method.
+func (m *MockEC2API) DescribeSecurityGroups(arg0 context.Context, arg1 *ec2.DescribeSecurityGroupsInput, arg2 ...func(*ec2.Options)) (*ec2.DescribeSecurityGroupsOutput, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1}
+	for _, a := range arg2 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "DescribeSecurityGroups", varargs...)
+	ret0, _ := ret[0].(*ec2.DescribeSecurityGroupsOutput)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// DescribeSecurityGroups indicates an expected call of DescribeSecurityGroups.
+func (mr *MockEC2APIMockRecorder) DescribeSecurityGroups(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1}, arg2...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeSecurityGroups", reflect.TypeOf((*MockEC2API)(nil).DescribeSecurityGroups), varargs...)
+}