Repository: kubernetes-sigs/cluster-api-provider-aws
Pull request #3684 (2022-08-22 14:21:36)

Add scaling from 0 support
<!-- 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**:

Adds scaling from 0 to CAPA.

Proposal -> https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20210310-opt-in-autoscaling-from-zero.md#upgrade-strategy
Reference implementation -> https://github.com/kubernetes-sigs/cluster-api-provider-kubemark/pull/30/files

<!-- 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/3679

**Special notes for your reviewer**:

**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 -->

- [x] squashed commits
- [x] includes documentation
- [ ] adds unit tests
- [x] adds or updates e2e tests
  ~~- I will try adding one!~~
    - I decided not to considering how convoluted the setup gets


k8s-ci-robot: @Skarlso: This issue is currently awaiting triage.

If CAPA/CAPI contributors determines this is a relevant issue, they will accept it by applying the `triage/accepted` label and provide further guidance.

The `triage/accepted` label can be added by org members by writing `/triage accepted` in a comment.


<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: [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 assign fabriziopandini for approval by writing `/assign @fabriziopandini` 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":["fabriziopandini"]} -->
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 | ea6f0a106d44a922278882bbdb41d54ac3b608b3 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3684/pull-cluster-api-provider-aws-test/1561720253145157632) | 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=3684). [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-verify | 0f0f4b7cf576aa7ac20b2d06604abb74e2411e2f | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/3684/pull-cluster-api-provider-aws-verify/1561721667846148096) | 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=3684). [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 reluctant to auto-generate anything. That would be magic and implicit. Or do you mean, that a user should set something like, enable scale from 0 and that should autofill some data?

Patch:
diff --git a/api/v1alpha3/awsmachine_conversion.go b/api/v1alpha3/awsmachine_conversion.go
index 5984814ca..83d1c4957 100644
--- a/api/v1alpha3/awsmachine_conversion.go
+++ b/api/v1alpha3/awsmachine_conversion.go
@@ -106,6 +106,8 @@ func (r *AWSMachineTemplate) ConvertTo(dstRaw conversion.Hub) error {
 
 	restoreSpec(&restored.Spec.Template.Spec, &dst.Spec.Template.Spec)
 
+	dst.Status = restored.Status
+
 	return nil
 }
 
@@ -123,6 +125,10 @@ func (r *AWSMachineTemplate) ConvertFrom(srcRaw conversion.Hub) error {
 	return nil
 }
 
+func Convert_v1beta1_AWSMachineTemplate_To_v1alpha3_AWSMachineTemplate(in *infrav1.AWSMachineTemplate, out *AWSMachineTemplate, s apiconversion.Scope) error {
+	return autoConvert_v1beta1_AWSMachineTemplate_To_v1alpha3_AWSMachineTemplate(in, out, s)
+}
+
 // ConvertTo converts the v1alpha3 AWSMachineTemplateList receiver to a v1beta1 AWSMachineTemplateList.
 func (r *AWSMachineTemplateList) ConvertTo(dstRaw conversion.Hub) error {
 	dst := dstRaw.(*infrav1.AWSMachineTemplateList)
diff --git a/api/v1alpha3/zz_generated.conversion.go b/api/v1alpha3/zz_generated.conversion.go
index 638fd46bf..9d27d6ae4 100644
--- a/api/v1alpha3/zz_generated.conversion.go
+++ b/api/v1alpha3/zz_generated.conversion.go
@@ -221,11 +221,6 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
-	if err := s.AddGeneratedConversionFunc((*v1beta1.AWSMachineTemplate)(nil), (*AWSMachineTemplate)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_v1beta1_AWSMachineTemplate_To_v1alpha3_AWSMachineTemplate(a.(*v1beta1.AWSMachineTemplate), b.(*AWSMachineTemplate), scope)
-	}); err != nil {
-		return err
-	}
 	if err := s.AddGeneratedConversionFunc((*AWSMachineTemplateList)(nil), (*v1beta1.AWSMachineTemplateList)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1alpha3_AWSMachineTemplateList_To_v1beta1_AWSMachineTemplateList(a.(*AWSMachineTemplateList), b.(*v1beta1.AWSMachineTemplateList), scope)
 	}); err != nil {
@@ -506,6 +501,11 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddConversionFunc((*v1beta1.AWSMachineTemplate)(nil), (*AWSMachineTemplate)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta1_AWSMachineTemplate_To_v1alpha3_AWSMachineTemplate(a.(*v1beta1.AWSMachineTemplate), b.(*AWSMachineTemplate), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddConversionFunc((*v1beta1.Instance)(nil), (*Instance)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1beta1_Instance_To_v1alpha3_Instance(a.(*v1beta1.Instance), b.(*Instance), scope)
 	}); err != nil {
@@ -1321,14 +1321,10 @@ func autoConvert_v1beta1_AWSMachineTemplate_To_v1alpha3_AWSMachineTemplate(in *v
 	if err := Convert_v1beta1_AWSMachineTemplateSpec_To_v1alpha3_AWSMachineTemplateSpec(&in.Spec, &out.Spec, s); err != nil {
 		return err
 	}
+	// WARNING: in.Status requires manual conversion: does not exist in peer-type
 	return nil
 }
 
-// Convert_v1beta1_AWSMachineTemplate_To_v1alpha3_AWSMachineTemplate is an autogenerated conversion function.
-func Convert_v1beta1_AWSMachineTemplate_To_v1alpha3_AWSMachineTemplate(in *v1beta1.AWSMachineTemplate, out *AWSMachineTemplate, s conversion.Scope) error {
-	return autoConvert_v1beta1_AWSMachineTemplate_To_v1alpha3_AWSMachineTemplate(in, out, s)
-}
-
 func autoConvert_v1alpha3_AWSMachineTemplateList_To_v1beta1_AWSMachineTemplateList(in *AWSMachineTemplateList, out *v1beta1.AWSMachineTemplateList, s conversion.Scope) error {
 	out.ListMeta = in.ListMeta
 	if in.Items != nil {
diff --git a/api/v1alpha4/awsmachine_conversion.go b/api/v1alpha4/awsmachine_conversion.go
index c6b629462..422b54789 100644
--- a/api/v1alpha4/awsmachine_conversion.go
+++ b/api/v1alpha4/awsmachine_conversion.go
@@ -83,6 +83,7 @@ func (r *AWSMachineTemplate) ConvertTo(dstRaw conversion.Hub) error {
 
 	dst.Spec.Template.ObjectMeta = restored.Spec.Template.ObjectMeta
 	dst.Spec.Template.Spec.Ignition = restored.Spec.Template.Spec.Ignition
+	dst.Status = restored.Status
 
 	return nil
 }
@@ -103,6 +104,10 @@ func (r *AWSMachineTemplate) ConvertFrom(srcRaw conversion.Hub) error {
 	return nil
 }
 
+func Convert_v1beta1_AWSMachineTemplate_To_v1alpha4_AWSMachineTemplate(in *v1beta1.AWSMachineTemplate, out *AWSMachineTemplate, s apiconversion.Scope) error {
+	return autoConvert_v1beta1_AWSMachineTemplate_To_v1alpha4_AWSMachineTemplate(in, out, s)
+}
+
 // ConvertTo converts the v1alpha4 AWSMachineTemplateList receiver to a v1beta1 AWSMachineTemplateList.
 func (src *AWSMachineTemplateList) ConvertTo(dstRaw conversion.Hub) error {
 	dst := dstRaw.(*infrav1.AWSMachineTemplateList)
diff --git a/api/v1alpha4/zz_generated.conversion.go b/api/v1alpha4/zz_generated.conversion.go
index 0b0fd48f8..aaef32997 100644
--- a/api/v1alpha4/zz_generated.conversion.go
+++ b/api/v1alpha4/zz_generated.conversion.go
@@ -275,11 +275,6 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
-	if err := s.AddGeneratedConversionFunc((*v1beta1.AWSMachineTemplate)(nil), (*AWSMachineTemplate)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_v1beta1_AWSMachineTemplate_To_v1alpha4_AWSMachineTemplate(a.(*v1beta1.AWSMachineTemplate), b.(*AWSMachineTemplate), scope)
-	}); err != nil {
-		return err
-	}
 	if err := s.AddGeneratedConversionFunc((*AWSMachineTemplateList)(nil), (*v1beta1.AWSMachineTemplateList)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1alpha4_AWSMachineTemplateList_To_v1beta1_AWSMachineTemplateList(a.(*AWSMachineTemplateList), b.(*v1beta1.AWSMachineTemplateList), scope)
 	}); err != nil {
@@ -560,6 +555,11 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddConversionFunc((*v1beta1.AWSMachineTemplate)(nil), (*AWSMachineTemplate)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta1_AWSMachineTemplate_To_v1alpha4_AWSMachineTemplate(a.(*v1beta1.AWSMachineTemplate), b.(*AWSMachineTemplate), scope)
+	}); err != nil {
+		return err
+	}
 	return nil
 }
 
@@ -1448,14 +1448,10 @@ func autoConvert_v1beta1_AWSMachineTemplate_To_v1alpha4_AWSMachineTemplate(in *v
 	if err := Convert_v1beta1_AWSMachineTemplateSpec_To_v1alpha4_AWSMachineTemplateSpec(&in.Spec, &out.Spec, s); err != nil {
 		return err
 	}
+	// WARNING: in.Status requires manual conversion: does not exist in peer-type
 	return nil
 }
 
-// Convert_v1beta1_AWSMachineTemplate_To_v1alpha4_AWSMachineTemplate is an autogenerated conversion function.
-func Convert_v1beta1_AWSMachineTemplate_To_v1alpha4_AWSMachineTemplate(in *v1beta1.AWSMachineTemplate, out *AWSMachineTemplate, s conversion.Scope) error {
-	return autoConvert_v1beta1_AWSMachineTemplate_To_v1alpha4_AWSMachineTemplate(in, out, s)
-}
-
 func autoConvert_v1alpha4_AWSMachineTemplateList_To_v1beta1_AWSMachineTemplateList(in *AWSMachineTemplateList, out *v1beta1.AWSMachineTemplateList, s conversion.Scope) error {
 	out.ListMeta = in.ListMeta
 	if in.Items != nil {
diff --git a/api/v1beta1/awsmachinetemplate_types.go b/api/v1beta1/awsmachinetemplate_types.go
index 5ca8ae986..69b62f9a6 100644
--- a/api/v1beta1/awsmachinetemplate_types.go
+++ b/api/v1beta1/awsmachinetemplate_types.go
@@ -17,11 +17,21 @@ limitations under the License.
 package v1beta1
 
 import (
+	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
 )
 
+// AWSMachineTemplateStatus defines a status for an AWSMachineTemplate.
+type AWSMachineTemplateStatus struct {
+	// Capacity defines the resource capacity for this machine.
+	// This value is used for autoscaling from zero operations as defined in:
+	// https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20210310-opt-in-autoscaling-from-zero.md
+	// +optional
+	Capacity corev1.ResourceList `json:"capacity,omitempty"`
+}
+
 // AWSMachineTemplateSpec defines the desired state of AWSMachineTemplate.
 type AWSMachineTemplateSpec struct {
 	Template AWSMachineTemplateResource `json:"template"`
@@ -37,7 +47,8 @@ type AWSMachineTemplate struct {
 	metav1.TypeMeta   `json:",inline"`
 	metav1.ObjectMeta `json:"metadata,omitempty"`
 
-	Spec AWSMachineTemplateSpec `json:"spec,omitempty"`
+	Spec   AWSMachineTemplateSpec   `json:"spec,omitempty"`
+	Status AWSMachineTemplateStatus `json:"status,omitempty"`
 }
 
 // +kubebuilder:object:root=true
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index 28901c638..6f6162ab0 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -22,6 +22,7 @@ limitations under the License.
 package v1beta1
 
 import (
+	"k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	apiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1"
 	"sigs.k8s.io/cluster-api/errors"
@@ -785,6 +786,7 @@ func (in *AWSMachineTemplate) DeepCopyInto(out *AWSMachineTemplate) {
 	out.TypeMeta = in.TypeMeta
 	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
 	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSMachineTemplate.
@@ -870,6 +872,28 @@ func (in *AWSMachineTemplateSpec) DeepCopy() *AWSMachineTemplateSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AWSMachineTemplateStatus) DeepCopyInto(out *AWSMachineTemplateStatus) {
+	*out = *in
+	if in.Capacity != nil {
+		in, out := &in.Capacity, &out.Capacity
+		*out = make(v1.ResourceList, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val.DeepCopy()
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSMachineTemplateStatus.
+func (in *AWSMachineTemplateStatus) DeepCopy() *AWSMachineTemplateStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(AWSMachineTemplateStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *AWSResourceReference) DeepCopyInto(out *AWSResourceReference) {
 	*out = *in
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml
index 11d1de8ba..90d29a422 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml
@@ -1413,9 +1413,6 @@ spec:
               launchTemplateID:
                 description: The ID of the launch template
                 type: string
-              launchTemplateVersion:
-                description: The version of the launch template
-                type: string
               ready:
                 description: Ready is true when the provider resource is ready.
                 type: boolean
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml
index a59ce2a01..5b347af9d 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml
@@ -1115,6 +1115,21 @@ spec:
             required:
             - template
             type: object
+          status:
+            description: AWSMachineTemplateStatus defines a status for an AWSMachineTemplate.
+            properties:
+              capacity:
+                additionalProperties:
+                  anyOf:
+                  - type: integer
+                  - type: string
+                  pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
+                  x-kubernetes-int-or-string: true
+                description: 'Capacity defines the resource capacity for this machine.
+                  This value is used for autoscaling from zero operations as defined
+                  in: https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20210310-opt-in-autoscaling-from-zero.md'
+                type: object
+            type: object
         type: object
     served: true
     storage: true
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml
index 2899a8cd4..8b2a16a0d 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml
@@ -540,7 +540,6 @@ spec:
                 - AL2_x86_64
                 - AL2_x86_64_GPU
                 - AL2_ARM_64
-                - CUSTOM
                 type: string
               amiVersion:
                 description: AMIVersion defines the desired AMI release version. If
@@ -554,169 +553,6 @@ spec:
                 items:
                   type: string
                 type: array
-              awsLaunchTemplate:
-                description: AWSLaunchTemplate specifies the launch template to use
-                  to create the managed node group. If AWSLaunchTemplate is specified,
-                  certain node group configuraions outside of launch template are
-                  prohibited (https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html).
-                properties:
-                  additionalSecurityGroups:
-                    description: AdditionalSecurityGroups is an array of references
-                      to security groups that should be applied to the instances.
-                      These security groups would be set in addition to any security
-                      groups defined at the cluster level or in the actuator.
-                    items:
-                      description: AWSResourceReference is a reference to a specific
-                        AWS resource by ID or filters. Only one of ID or Filters may
-                        be specified. Specifying more than one will result in a validation
-                        error.
-                      properties:
-                        arn:
-                          description: 'ARN of resource. Deprecated: This field has
-                            no function and is going to be removed in the next release.'
-                          type: string
-                        filters:
-                          description: 'Filters is a set of key/value pairs used to
-                            identify a resource They are applied according to the
-                            rules defined by the AWS API: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Filtering.html'
-                          items:
-                            description: Filter is a filter used to identify an AWS
-                              resource.
-                            properties:
-                              name:
-                                description: Name of the filter. Filter names are
-                                  case-sensitive.
-                                type: string
-                              values:
-                                description: Values includes one or more filter values.
-                                  Filter values are case-sensitive.
-                                items:
-                                  type: string
-                                type: array
-                            required:
-                            - name
-                            - values
-                            type: object
-                          type: array
-                        id:
-                          description: ID of resource
-                          type: string
-                      type: object
-                    type: array
-                  ami:
-                    description: AMI is the reference to the AMI from which to create
-                      the machine instance.
-                    properties:
-                      eksLookupType:
-                        description: EKSOptimizedLookupType If specified, will look
-                          up an EKS Optimized image in SSM Parameter store
-                        enum:
-                        - AmazonLinux
-                        - AmazonLinuxGPU
-                        type: string
-                      id:
-                        description: ID of resource
-                        type: string
-                    type: object
-                  iamInstanceProfile:
-                    description: The name or the Amazon Resource Name (ARN) of the
-                      instance profile associated with the IAM role for the instance.
-                      The instance profile contains the IAM role.
-                    type: string
-                  imageLookupBaseOS:
-                    description: ImageLookupBaseOS is the name of the base operating
-                      system to use for image lookup the AMI is not set.
-                    type: string
-                  imageLookupFormat:
-                    description: 'ImageLookupFormat is the AMI naming format to look
-                      up the image for this machine It will be ignored if an explicit
-                      AMI is set. Supports substitutions for {{.BaseOS}} and {{.K8sVersion}}
-                      with the base OS and kubernetes version, respectively. The BaseOS
-                      will be the value in ImageLookupBaseOS or ubuntu (the default),
-                      and the kubernetes version as defined by the packages produced
-                      by kubernetes/release without v as a prefix: 1.13.0, 1.12.5-mybuild.1,
-                      or 1.17.3. For example, the default image format of capa-ami-{{.BaseOS}}-?{{.K8sVersion}}-*
-                      will end up searching for AMIs that match the pattern capa-ami-ubuntu-?1.18.0-*
-                      for a Machine that is targeting kubernetes v1.18.0 and the ubuntu
-                      base OS. See also: https://golang.org/pkg/text/template/'
-                    type: string
-                  imageLookupOrg:
-                    description: ImageLookupOrg is the AWS Organization ID to use
-                      for image lookup if AMI is not set.
-                    type: string
-                  instanceType:
-                    description: 'InstanceType is the type of instance to create.
-                      Example: m4.xlarge'
-                    type: string
-                  name:
-                    description: The name of the launch template.
-                    type: string
-                  rootVolume:
-                    description: RootVolume encapsulates the configuration options
-                      for the root volume
-                    properties:
-                      deviceName:
-                        description: Device name
-                        type: string
-                      encrypted:
-                        description: Encrypted is whether the volume should be encrypted
-                          or not.
-                        type: boolean
-                      encryptionKey:
-                        description: EncryptionKey is the KMS key to use to encrypt
-                          the volume. Can be either a KMS key ID or ARN. If Encrypted
-                          is set and this is omitted, the default AWS key will be
-                          used. The key must already exist and be accessible by the
-                          controller.
-                        type: string
-                      iops:
-                        description: IOPS is the number of IOPS requested for the
-                          disk. Not applicable to all types.
-                        format: int64
-                        type: integer
-                      size:
-                        description: Size specifies size (in Gi) of the storage device.
-                          Must be greater than the image snapshot size or 8 (whichever
-                          is greater).
-                        format: int64
-                        minimum: 8
-                        type: integer
-                      throughput:
-                        description: Throughput to provision in MiB/s supported for
-                          the volume type. Not applicable to all types.
-                        format: int64
-                        type: integer
-                      type:
-                        description: Type is the type of the volume (e.g. gp2, io1,
-                          etc...).
-                        type: string
-                    required:
-                    - size
-                    type: object
-                  spotMarketOptions:
-                    description: SpotMarketOptions are options for configuring AWSMachinePool
-                      instances to be run using AWS Spot instances.
-                    properties:
-                      maxPrice:
-                        description: MaxPrice defines the maximum price the user is
-                          willing to pay for Spot VM instances
-                        type: string
-                    type: object
-                  sshKeyName:
-                    description: SSHKeyName is the name of the ssh key to attach to
-                      the instance. Valid values are empty string (do not use SSH
-                      keys), a valid SSH key name, or omitted (use the default SSH
-                      key name)
-                    type: string
-                  versionNumber:
-                    description: 'VersionNumber is the version of the launch template
-                      that is applied. Typically a new version is created when at
-                      least one of the following happens: 1) A new launch template
-                      spec is applied. 2) One or more parameters in an existing template
-                      is changed. 3) A new AMI is discovered.'
-                    format: int64
-                    type: integer
-                type: object
               capacityType:
                 default: onDemand
                 description: CapacityType specifies the capacity type for the ASG
@@ -925,12 +761,6 @@ spec:
                   events to the MachinePool object and/or logged in the controller's
                   output."
                 type: string
-              launchTemplateID:
-                description: The ID of the launch template
-                type: string
-              launchTemplateVersion:
-                description: The version of the launch template
-                type: string
               ready:
                 default: false
                 description: Ready denotes that the AWSManagedMachinePool nodegroup
diff --git a/controlplane/eks/api/v1beta1/awsmanagedcontrolplane_types.go b/controlplane/eks/api/v1beta1/awsmanagedcontrolplane_types.go
index 3be3e756e..47005872a 100644
--- a/controlplane/eks/api/v1beta1/awsmanagedcontrolplane_types.go
+++ b/controlplane/eks/api/v1beta1/awsmanagedcontrolplane_types.go
@@ -27,9 +27,6 @@ import (
 const (
 	// ManagedControlPlaneFinalizer allows the controller to clean up resources on delete.
 	ManagedControlPlaneFinalizer = "awsmanagedcontrolplane.controlplane.cluster.x-k8s.io"
-
-	// AWSManagedControlPlaneKind is the Kind of AWSManagedControlPlane.
-	AWSManagedControlPlaneKind = "AWSManagedControlPlane"
 )
 
 // AWSManagedControlPlaneSpec defines the desired state of an Amazon EKS Cluster.
diff --git a/docs/book/src/crd/index.md b/docs/book/src/crd/index.md
index c1ad8c30f..92eda1adc 100644
--- a/docs/book/src/crd/index.md
+++ b/docs/book/src/crd/index.md
@@ -18283,7 +18283,7 @@ FargateProfileStatus
 <h3 id="infrastructure.cluster.x-k8s.io/v1beta1.AWSLaunchTemplate">AWSLaunchTemplate
 </h3>
 <p>
-(<em>Appears on:</em><a href="#infrastructure.cluster.x-k8s.io/v1beta1.AWSMachinePoolSpec">AWSMachinePoolSpec</a>, <a href="#infrastructure.cluster.x-k8s.io/v1beta1.AWSManagedMachinePoolSpec">AWSManagedMachinePoolSpec</a>)
+(<em>Appears on:</em><a href="#infrastructure.cluster.x-k8s.io/v1beta1.AWSMachinePoolSpec">AWSMachinePoolSpec</a>)
 </p>
 <p>
 <p>AWSLaunchTemplate defines the desired state of AWSLaunchTemplate.</p>
@@ -18956,18 +18956,6 @@ string
 </tr>
 <tr>
 <td>
-<code>launchTemplateVersion</code><br/>
-<em>
-string
-</em>
-</td>
-<td>
-<em>(Optional)</em>
-<p>The version of the launch template</p>
-</td>
-</tr>
-<tr>
-<td>
 <code>failureReason</code><br/>
 <em>
 <a href="https://pkg.go.dev/sigs.k8s.io/cluster-api@v1.0.0/errors#MachineStatusError">
@@ -19304,22 +19292,6 @@ UpdateConfig
 to the nodegroup.</p>
 </td>
 </tr>
-<tr>
-<td>
-<code>awsLaunchTemplate</code><br/>
-<em>
-<a href="#infrastructure.cluster.x-k8s.io/v1beta1.AWSLaunchTemplate">
-AWSLaunchTemplate
-</a>
-</em>
-</td>
-<td>
-<em>(Optional)</em>
-<p>AWSLaunchTemplate specifies the launch template to use to create the managed node group.
-If AWSLaunchTemplate is specified, certain node group configuraions outside of launch template
-are prohibited (<a href="https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html">https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html</a>).</p>
-</td>
-</tr>
 </table>
 </td>
 </tr>
@@ -19585,22 +19557,6 @@ UpdateConfig
 to the nodegroup.</p>
 </td>
 </tr>
-<tr>
-<td>
-<code>awsLaunchTemplate</code><br/>
-<em>
-<a href="#infrastructure.cluster.x-k8s.io/v1beta1.AWSLaunchTemplate">
-AWSLaunchTemplate
-</a>
-</em>
-</td>
-<td>
-<em>(Optional)</em>
-<p>AWSLaunchTemplate specifies the launch template to use to create the managed node group.
-If AWSLaunchTemplate is specified, certain node group configuraions outside of launch template
-are prohibited (<a href="https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html">https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html</a>).</p>
-</td>
-</tr>
 </tbody>
 </table>
 <h3 id="infrastructure.cluster.x-k8s.io/v1beta1.AWSManagedMachinePoolStatus">AWSManagedMachinePoolStatus
@@ -19645,30 +19601,6 @@ int32
 </tr>
 <tr>
 <td>
-<code>launchTemplateID</code><br/>
-<em>
-string
-</em>
-</td>
-<td>
-<em>(Optional)</em>
-<p>The ID of the launch template</p>
-</td>
-</tr>
-<tr>
-<td>
-<code>launchTemplateVersion</code><br/>
-<em>
-string
-</em>
-</td>
-<td>
-<em>(Optional)</em>
-<p>The version of the launch template</p>
-</td>
-</tr>
-<tr>
-<td>
 <code>failureReason</code><br/>
 <em>
 <a href="https://pkg.go.dev/sigs.k8s.io/cluster-api@v1.0.0/errors#MachineStatusError">
diff --git a/docs/book/src/topics/scale-from-0.md b/docs/book/src/topics/scale-from-0.md
new file mode 100644
index 000000000..b579a7604
--- /dev/null
+++ b/docs/book/src/topics/scale-from-0.md
@@ -0,0 +1,199 @@
+# Scaling from 0
+
+With the changes introduce into `cluster-api` described in [this](https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20210310-opt-in-autoscaling-from-zero.md#upgrade-strategy) proposal, a user can now opt in to scaling nodes from 0.
+
+This entails a number of things which I will describe in detail.
+
+The following actions need to be taken to enabled cluster autoscaling:
+
+## Set Capacity field
+
+To do that, simply define some values to the new field called `capacity` in the `AWSMachineTemplate` like this:
+
+```yaml
+---
+apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+kind: AWSMachineTemplate
+metadata:
+  name: "${CLUSTER_NAME}-md-0"
+spec:
+  template:
+    spec:
+      instanceType: "${AWS_NODE_MACHINE_TYPE}"
+      iamInstanceProfile: "nodes.cluster-api-provider-aws.sigs.k8s.io"
+      sshKeyName: "${AWS_SSH_KEY_NAME}"
+status:
+  capacity:
+    memory: "500m"
+    cpu: "1"
+    nvidia.com/gpu: "1"
+```
+
+To read more about what values are available, consult the proposal. These values can be overridden by selected annotations
+on the MachineTemplate.
+
+## Add two necessary annotations to MachineDeployment
+
+There are two annotations which need to be applied to the MachineDeployment like this:
+
+```yaml
+apiVersion: cluster.x-k8s.io/v1beta1
+kind: MachineDeployment
+metadata:
+  name: "managed-cluster-md-0"
+  annotations:
+    cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size: "5"
+    cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size: "0"
+```
+
+These are necessary for the autoscaler to be able to pick up the deployment and scale it. Read more about these [here](https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/cloudprovider/clusterapi/README.md#enabling-autoscaling).
+
+## Install and start cluster-autoscaler
+
+Now comes the tricky part. In order for this to work, you need the cluster-autoscaler binary located [here](https://github.com/kubernetes/autoscaler).
+You have to options. Use Helm to install autoscaler, or use the command line ( which is faster in if you are testing ).
+
+In either cases, you need the following options:
+- namespace
+- cloud-provider
+- scale-down-delay-after-add
+- scale-down-delay-after-delete
+- scale-down-delay-after-failure
+- scale-down-unneeded-time
+- expander
+- kubeconfig
+- cloud-config
+
+These last two values are crucial for the autoscaler to work. `cloud-config` is the kubeconfig of the management cluster.
+If you are using a service account to access it, you also have an option to define that. Read more about it on the
+autoscaler's repository. The second one is the workload cluster. It needs both because the MachineDeployment is in the
+control-plane while the actual node and pods are in the workload cluster.
+
+Therefor, you have to install cluster-autoscaler into the _control-plane_ cluster.
+
+I have a handy script to launch autoscaler which looks like this:
+
+```bash
+#!/bin/sh
+# usage: start-autoscaler management.kubeconfig workload.kubeconfig
+cluster-autoscaler \
+	--cloud-provider=clusterapi \
+	--v=4 \
+	--namespace=default \
+	--max-nodes-total=30 \
+	--scale-down-delay-after-add=10s \
+	--scale-down-delay-after-delete=10s \
+	--scale-down-delay-after-failure=10s \
+	--scale-down-unneeded-time=23s \
+	--max-node-provision-time=2m \
+	--balance-similar-node-groups \
+	--expander=random \
+	--kubeconfig=$2 \
+	--cloud-config=$1
+```
+
+Courtesy of [@elmiko](https://github.com/elmiko).
+
+The Helm equivalent is a bit more complex and either needs to mount in the kubeconfig from somewhere or be pointed to it.
+
+## Permissions
+
+This depends on your scenario. Read about it more [here](https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler).
+Since this is Cluster API Provider AWS, you would need to look for the AWS provider settings [here](https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/cloudprovider/aws/README.md).
+
+Further, the service account associated with cluster-autoscaler requires permissions to access `get` and `list` the
+Cluster API machine template infrastructure objects.
+
+## Putting it together
+
+The whole yaml looks like this:
+
+```yaml
+---
+apiVersion: cluster.x-k8s.io/v1beta1
+kind: Cluster
+metadata:
+  name: "managed-cluster"
+spec:
+  infrastructureRef:
+    kind: AWSManagedControlPlane
+    apiVersion: controlplane.cluster.x-k8s.io/v1beta1
+    name: "managed-cluster-control-plane"
+  controlPlaneRef:
+    kind: AWSManagedControlPlane
+    apiVersion: controlplane.cluster.x-k8s.io/v1beta1
+    name: "managed-cluster-control-plane"
+---
+kind: AWSManagedControlPlane
+apiVersion: controlplane.cluster.x-k8s.io/v1beta1
+metadata:
+  name: "managed-cluster-control-plane"
+spec:
+  region: "eu-central-1"
+  version: "v1.22.0"
+---
+apiVersion: cluster.x-k8s.io/v1beta1
+kind: MachineDeployment
+metadata:
+  name: "managed-cluster-md-0"
+  annotations:
+    cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size: "5"
+    cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size: "0"
+spec:
+  clusterName: "managed-cluster"
+  replicas: 0 # _NOTE_ that we set the initial replicas size to *ZERO*.
+  selector:
+    matchLabels:
+  template:
+    spec:
+      clusterName: "managed-cluster"
+      version: "v1.22.0"
+      bootstrap:
+        configRef:
+          name: "managed-cluster-md-0"
+          apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
+          kind: EKSConfigTemplate
+      infrastructureRef:
+        name: "managed-cluster-md-0"
+        apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+        kind: AWSMachineTemplate
+---
+apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+kind: AWSMachineTemplate
+metadata:
+  name: "managed-cluster-md-0"
+spec:
+  template:
+    spec:
+      instanceType: "t3.small"
+      iamInstanceProfile: "nodes.cluster-api-provider-aws.sigs.k8s.io"
+status:
+  capacity:
+    memory: "500m"
+    cpu: "1"
+---
+apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
+kind: EKSConfigTemplate
+metadata:
+  name: "managed-cluster-md-0"
+spec:
+  template: {}
+```
+
+## When will it not scale?
+
+There is a document describing under what circumstances it won't be able to scale located [here](https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/FAQ.md#what-types-of-pods-can-prevent-ca-from-removing-a-node). Read this carefully.
+
+It has some ramifications when scaling back down to 0. Which will only work if all pods are removed from the node and
+the node cannot schedule even the aws-node and kube-proxy pods. There is this tiny manual step of cordoning off the last
+node in order to scale back down to 0.
+
+## Conclusion
+
+Once the cluster-autoscaler is running, you will start seeing nodes pop-in as soon as there is some load on the cluster.
+To test it, simply create and inflate a deployment like this:
+
+```bash
+kubectl create deployment inflate --image=public.ecr.aws/eks-distro/kubernetes/pause:3.2 --kubeconfig workload.kubeconfig
+kubectl scale deployment inflate --replicas=50 --kubeconfig workload.kubeconfig
+```
diff --git a/exp/api/v1alpha3/conversion.go b/exp/api/v1alpha3/conversion.go
index 1a1227291..3e1ac7937 100644
--- a/exp/api/v1alpha3/conversion.go
+++ b/exp/api/v1alpha3/conversion.go
@@ -46,7 +46,6 @@ func (r *AWSMachinePool) ConvertTo(dstRaw conversion.Hub) error {
 		infrav1alpha3.RestoreRootVolume(restored.Spec.AWSLaunchTemplate.RootVolume, dst.Spec.AWSLaunchTemplate.RootVolume)
 	}
 	dst.Spec.AWSLaunchTemplate.SpotMarketOptions = restored.Spec.AWSLaunchTemplate.SpotMarketOptions
-	dst.Status.LaunchTemplateVersion = restored.Status.LaunchTemplateVersion
 	return nil
 }
 
@@ -94,9 +93,6 @@ func (r *AWSManagedMachinePool) ConvertTo(dstRaw conversion.Hub) error {
 	dst.Spec.CapacityType = restored.Spec.CapacityType
 	dst.Spec.RoleAdditionalPolicies = restored.Spec.RoleAdditionalPolicies
 	dst.Spec.UpdateConfig = restored.Spec.UpdateConfig
-	dst.Spec.AWSLaunchTemplate = restored.Spec.AWSLaunchTemplate
-	dst.Status.LaunchTemplateID = restored.Status.LaunchTemplateID
-	dst.Status.LaunchTemplateVersion = restored.Status.LaunchTemplateVersion
 
 	return nil
 }
@@ -173,21 +169,6 @@ func Convert_v1beta1_AWSManagedMachinePoolSpec_To_v1alpha3_AWSManagedMachinePool
 	return autoConvert_v1beta1_AWSManagedMachinePoolSpec_To_v1alpha3_AWSManagedMachinePoolSpec(in, out, s)
 }
 
-// Convert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha3_AWSManagedMachinePoolStatus is a conversion function.
-func Convert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha3_AWSManagedMachinePoolStatus(in *infrav1exp.AWSManagedMachinePoolStatus, out *AWSManagedMachinePoolStatus, s apiconversion.Scope) error {
-	return autoConvert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha3_AWSManagedMachinePoolStatus(in, out, s)
-}
-
-// Convert_v1beta1_AWSMachinePoolStatus_To_v1alpha3_AWSMachinePoolStatus is a conversion function.
-func Convert_v1beta1_AWSMachinePoolStatus_To_v1alpha3_AWSMachinePoolStatus(in *infrav1exp.AWSMachinePoolStatus, out *AWSMachinePoolStatus, s apiconversion.Scope) error {
-	return autoConvert_v1beta1_AWSMachinePoolStatus_To_v1alpha3_AWSMachinePoolStatus(in, out, s)
-}
-
-// Convert_v1beta1_Instance_To_v1alpha3_Instance is a conversion function.
-func Convert_v1beta1_Instance_To_v1alpha3_Instance(in *infrav1.Instance, out *infrav1alpha3.Instance, s apiconversion.Scope) error {
-	return infrav1alpha3.Convert_v1beta1_Instance_To_v1alpha3_Instance(in, out, s)
-}
-
 // Convert_v1alpha3_Instance_To_v1beta1_Instance is a conversion function.
 func Convert_v1alpha3_Instance_To_v1beta1_Instance(in *infrav1alpha3.Instance, out *infrav1.Instance, s apiconversion.Scope) error {
 	return infrav1alpha3.Convert_v1alpha3_Instance_To_v1beta1_Instance(in, out, s)
diff --git a/exp/api/v1alpha3/zz_generated.conversion.go b/exp/api/v1alpha3/zz_generated.conversion.go
index f30aa7692..0f646e94c 100644
--- a/exp/api/v1alpha3/zz_generated.conversion.go
+++ b/exp/api/v1alpha3/zz_generated.conversion.go
@@ -111,6 +111,11 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddGeneratedConversionFunc((*v1beta1.AWSMachinePoolStatus)(nil), (*AWSMachinePoolStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta1_AWSMachinePoolStatus_To_v1alpha3_AWSMachinePoolStatus(a.(*v1beta1.AWSMachinePoolStatus), b.(*AWSMachinePoolStatus), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddGeneratedConversionFunc((*AWSManagedMachinePool)(nil), (*v1beta1.AWSManagedMachinePool)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1alpha3_AWSManagedMachinePool_To_v1beta1_AWSManagedMachinePool(a.(*AWSManagedMachinePool), b.(*v1beta1.AWSManagedMachinePool), scope)
 	}); err != nil {
@@ -141,6 +146,11 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddGeneratedConversionFunc((*v1beta1.AWSManagedMachinePoolStatus)(nil), (*AWSManagedMachinePoolStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha3_AWSManagedMachinePoolStatus(a.(*v1beta1.AWSManagedMachinePoolStatus), b.(*AWSManagedMachinePoolStatus), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddGeneratedConversionFunc((*AutoScalingGroup)(nil), (*v1beta1.AutoScalingGroup)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1alpha3_AutoScalingGroup_To_v1beta1_AutoScalingGroup(a.(*AutoScalingGroup), b.(*v1beta1.AutoScalingGroup), scope)
 	}); err != nil {
@@ -281,21 +291,11 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
-	if err := s.AddConversionFunc((*v1beta1.AWSMachinePoolStatus)(nil), (*AWSMachinePoolStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_v1beta1_AWSMachinePoolStatus_To_v1alpha3_AWSMachinePoolStatus(a.(*v1beta1.AWSMachinePoolStatus), b.(*AWSMachinePoolStatus), scope)
-	}); err != nil {
-		return err
-	}
 	if err := s.AddConversionFunc((*v1beta1.AWSManagedMachinePoolSpec)(nil), (*AWSManagedMachinePoolSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1beta1_AWSManagedMachinePoolSpec_To_v1alpha3_AWSManagedMachinePoolSpec(a.(*v1beta1.AWSManagedMachinePoolSpec), b.(*AWSManagedMachinePoolSpec), scope)
 	}); err != nil {
 		return err
 	}
-	if err := s.AddConversionFunc((*v1beta1.AWSManagedMachinePoolStatus)(nil), (*AWSManagedMachinePoolStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha3_AWSManagedMachinePoolStatus(a.(*v1beta1.AWSManagedMachinePoolStatus), b.(*AWSManagedMachinePoolStatus), scope)
-	}); err != nil {
-		return err
-	}
 	if err := s.AddConversionFunc((*apiv1beta1.AWSResourceReference)(nil), (*apiv1alpha3.AWSResourceReference)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1beta1_AWSResourceReference_To_v1alpha3_AWSResourceReference(a.(*apiv1beta1.AWSResourceReference), b.(*apiv1alpha3.AWSResourceReference), scope)
 	}); err != nil {
@@ -659,13 +659,17 @@ func autoConvert_v1beta1_AWSMachinePoolStatus_To_v1alpha3_AWSMachinePoolStatus(i
 	}
 	out.Instances = *(*[]AWSMachinePoolInstanceStatus)(unsafe.Pointer(&in.Instances))
 	out.LaunchTemplateID = in.LaunchTemplateID
-	// WARNING: in.LaunchTemplateVersion requires manual conversion: does not exist in peer-type
 	out.FailureReason = (*errors.MachineStatusError)(unsafe.Pointer(in.FailureReason))
 	out.FailureMessage = (*string)(unsafe.Pointer(in.FailureMessage))
 	out.ASGStatus = (*ASGStatus)(unsafe.Pointer(in.ASGStatus))
 	return nil
 }
 
+// Convert_v1beta1_AWSMachinePoolStatus_To_v1alpha3_AWSMachinePoolStatus is an autogenerated conversion function.
+func Convert_v1beta1_AWSMachinePoolStatus_To_v1alpha3_AWSMachinePoolStatus(in *v1beta1.AWSMachinePoolStatus, out *AWSMachinePoolStatus, s conversion.Scope) error {
+	return autoConvert_v1beta1_AWSMachinePoolStatus_To_v1alpha3_AWSMachinePoolStatus(in, out, s)
+}
+
 func autoConvert_v1alpha3_AWSManagedMachinePool_To_v1beta1_AWSManagedMachinePool(in *AWSManagedMachinePool, out *v1beta1.AWSManagedMachinePool, s conversion.Scope) error {
 	out.ObjectMeta = in.ObjectMeta
 	if err := Convert_v1alpha3_AWSManagedMachinePoolSpec_To_v1beta1_AWSManagedMachinePoolSpec(&in.Spec, &out.Spec, s); err != nil {
@@ -780,7 +784,6 @@ func autoConvert_v1beta1_AWSManagedMachinePoolSpec_To_v1alpha3_AWSManagedMachine
 	out.ProviderIDList = *(*[]string)(unsafe.Pointer(&in.ProviderIDList))
 	// WARNING: in.CapacityType requires manual conversion: does not exist in peer-type
 	// WARNING: in.UpdateConfig requires manual conversion: does not exist in peer-type
-	// WARNING: in.AWSLaunchTemplate requires manual conversion: does not exist in peer-type
 	return nil
 }
 
@@ -811,8 +814,6 @@ func Convert_v1alpha3_AWSManagedMachinePoolStatus_To_v1beta1_AWSManagedMachinePo
 func autoConvert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha3_AWSManagedMachinePoolStatus(in *v1beta1.AWSManagedMachinePoolStatus, out *AWSManagedMachinePoolStatus, s conversion.Scope) error {
 	out.Ready = in.Ready
 	out.Replicas = in.Replicas
-	// WARNING: in.LaunchTemplateID requires manual conversion: does not exist in peer-type
-	// WARNING: in.LaunchTemplateVersion requires manual conversion: does not exist in peer-type
 	out.FailureReason = (*errors.MachineStatusError)(unsafe.Pointer(in.FailureReason))
 	out.FailureMessage = (*string)(unsafe.Pointer(in.FailureMessage))
 	if in.Conditions != nil {
@@ -829,6 +830,11 @@ func autoConvert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha3_AWSManagedMachi
 	return nil
 }
 
+// Convert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha3_AWSManagedMachinePoolStatus is an autogenerated conversion function.
+func Convert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha3_AWSManagedMachinePoolStatus(in *v1beta1.AWSManagedMachinePoolStatus, out *AWSManagedMachinePoolStatus, s conversion.Scope) error {
+	return autoConvert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha3_AWSManagedMachinePoolStatus(in, out, s)
+}
+
 func autoConvert_v1alpha3_AutoScalingGroup_To_v1beta1_AutoScalingGroup(in *AutoScalingGroup, out *v1beta1.AutoScalingGroup, s conversion.Scope) error {
 	out.ID = in.ID
 	out.Tags = *(*apiv1beta1.Tags)(unsafe.Pointer(&in.Tags))
diff --git a/exp/api/v1alpha4/conversion.go b/exp/api/v1alpha4/conversion.go
index 913092bc2..ffdf35a0a 100644
--- a/exp/api/v1alpha4/conversion.go
+++ b/exp/api/v1alpha4/conversion.go
@@ -28,19 +28,16 @@ import (
 // ConvertTo converts the v1alpha4 AWSMachinePool receiver to a v1beta1 AWSMachinePool.
 func (src *AWSMachinePool) ConvertTo(dstRaw conversion.Hub) error {
 	dst := dstRaw.(*infrav1exp.AWSMachinePool)
-
 	if err := Convert_v1alpha4_AWSMachinePool_To_v1beta1_AWSMachinePool(src, dst, nil); err != nil {
 		return err
 	}
-
+	
 	restored := &infrav1exp.AWSMachinePool{}
 	if ok, err := utilconversion.UnmarshalData(src, restored); err != nil || !ok {
 		return err
 	}
 	
 	dst.Spec.AWSLaunchTemplate.SpotMarketOptions = restored.Spec.AWSLaunchTemplate.SpotMarketOptions
-	dst.Status.LaunchTemplateVersion = restored.Status.LaunchTemplateVersion
-
 	return nil
 }
 
@@ -51,11 +48,10 @@ func (r *AWSMachinePool) ConvertFrom(srcRaw conversion.Hub) error {
 	if err := Convert_v1beta1_AWSMachinePool_To_v1alpha4_AWSMachinePool(src, r, nil); err != nil {
 		return err
 	}
-
+	
 	if err := utilconversion.MarshalData(src, r); err != nil {
 		return err
 	}
-
 	return nil
 }
 
@@ -86,9 +82,6 @@ func (src *AWSManagedMachinePool) ConvertTo(dstRaw conversion.Hub) error {
 
 	dst.Spec.RoleAdditionalPolicies = restored.Spec.RoleAdditionalPolicies
 	dst.Spec.UpdateConfig = restored.Spec.UpdateConfig
-	dst.Spec.AWSLaunchTemplate = restored.Spec.AWSLaunchTemplate
-	dst.Status.LaunchTemplateID = restored.Status.LaunchTemplateID
-	dst.Status.LaunchTemplateVersion = restored.Status.LaunchTemplateVersion
 
 	return nil
 }
@@ -113,16 +106,6 @@ func Convert_v1beta1_AWSManagedMachinePoolSpec_To_v1alpha4_AWSManagedMachinePool
 	return autoConvert_v1beta1_AWSManagedMachinePoolSpec_To_v1alpha4_AWSManagedMachinePoolSpec(in, out, s)
 }
 
-// Convert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha4_AWSManagedMachinePoolStatus is a conversion function.
-func Convert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha4_AWSManagedMachinePoolStatus(in *infrav1exp.AWSManagedMachinePoolStatus, out *AWSManagedMachinePoolStatus, s apiconversion.Scope) error {
-	return autoConvert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha4_AWSManagedMachinePoolStatus(in, out, s)
-}
-
-// Convert_v1beta1_AWSMachinePoolStatus_To_v1alpha4_AWSMachinePoolStatus is a conversion function.
-func Convert_v1beta1_AWSMachinePoolStatus_To_v1alpha4_AWSMachinePoolStatus(in *infrav1exp.AWSMachinePoolStatus, out *AWSMachinePoolStatus, s apiconversion.Scope) error {
-	return autoConvert_v1beta1_AWSMachinePoolStatus_To_v1alpha4_AWSMachinePoolStatus(in, out, s)
-}
-
 // ConvertTo converts the v1alpha4 AWSManagedMachinePoolList receiver to a v1beta1 AWSManagedMachinePoolList.
 func (src *AWSManagedMachinePoolList) ConvertTo(dstRaw conversion.Hub) error {
 	dst := dstRaw.(*infrav1exp.AWSManagedMachinePoolList)
diff --git a/exp/api/v1alpha4/zz_generated.conversion.go b/exp/api/v1alpha4/zz_generated.conversion.go
index d61f53386..0255e7f1c 100644
--- a/exp/api/v1alpha4/zz_generated.conversion.go
+++ b/exp/api/v1alpha4/zz_generated.conversion.go
@@ -111,6 +111,11 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddGeneratedConversionFunc((*v1beta1.AWSMachinePoolStatus)(nil), (*AWSMachinePoolStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta1_AWSMachinePoolStatus_To_v1alpha4_AWSMachinePoolStatus(a.(*v1beta1.AWSMachinePoolStatus), b.(*AWSMachinePoolStatus), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddGeneratedConversionFunc((*AWSManagedMachinePool)(nil), (*v1beta1.AWSManagedMachinePool)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1alpha4_AWSManagedMachinePool_To_v1beta1_AWSManagedMachinePool(a.(*AWSManagedMachinePool), b.(*v1beta1.AWSManagedMachinePool), scope)
 	}); err != nil {
@@ -141,6 +146,11 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddGeneratedConversionFunc((*v1beta1.AWSManagedMachinePoolStatus)(nil), (*AWSManagedMachinePoolStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha4_AWSManagedMachinePoolStatus(a.(*v1beta1.AWSManagedMachinePoolStatus), b.(*AWSManagedMachinePoolStatus), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddGeneratedConversionFunc((*AutoScalingGroup)(nil), (*v1beta1.AutoScalingGroup)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1alpha4_AutoScalingGroup_To_v1beta1_AutoScalingGroup(a.(*AutoScalingGroup), b.(*v1beta1.AutoScalingGroup), scope)
 	}); err != nil {
@@ -291,21 +301,11 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
-	if err := s.AddConversionFunc((*v1beta1.AWSMachinePoolStatus)(nil), (*AWSMachinePoolStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_v1beta1_AWSMachinePoolStatus_To_v1alpha4_AWSMachinePoolStatus(a.(*v1beta1.AWSMachinePoolStatus), b.(*AWSMachinePoolStatus), scope)
-	}); err != nil {
-		return err
-	}
 	if err := s.AddConversionFunc((*v1beta1.AWSManagedMachinePoolSpec)(nil), (*AWSManagedMachinePoolSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1beta1_AWSManagedMachinePoolSpec_To_v1alpha4_AWSManagedMachinePoolSpec(a.(*v1beta1.AWSManagedMachinePoolSpec), b.(*AWSManagedMachinePoolSpec), scope)
 	}); err != nil {
 		return err
 	}
-	if err := s.AddConversionFunc((*v1beta1.AWSManagedMachinePoolStatus)(nil), (*AWSManagedMachinePoolStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha4_AWSManagedMachinePoolStatus(a.(*v1beta1.AWSManagedMachinePoolStatus), b.(*AWSManagedMachinePoolStatus), scope)
-	}); err != nil {
-		return err
-	}
 	if err := s.AddConversionFunc((*apiv1beta1.Instance)(nil), (*apiv1alpha4.Instance)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1beta1_Instance_To_v1alpha4_Instance(a.(*apiv1beta1.Instance), b.(*apiv1alpha4.Instance), scope)
 	}); err != nil {
@@ -613,13 +613,17 @@ func autoConvert_v1beta1_AWSMachinePoolStatus_To_v1alpha4_AWSMachinePoolStatus(i
 	}
 	out.Instances = *(*[]AWSMachinePoolInstanceStatus)(unsafe.Pointer(&in.Instances))
 	out.LaunchTemplateID = in.LaunchTemplateID
-	// WARNING: in.LaunchTemplateVersion requires manual conversion: does not exist in peer-type
 	out.FailureReason = (*errors.MachineStatusError)(unsafe.Pointer(in.FailureReason))
 	out.FailureMessage = (*string)(unsafe.Pointer(in.FailureMessage))
 	out.ASGStatus = (*ASGStatus)(unsafe.Pointer(in.ASGStatus))
 	return nil
 }
 
+// Convert_v1beta1_AWSMachinePoolStatus_To_v1alpha4_AWSMachinePoolStatus is an autogenerated conversion function.
+func Convert_v1beta1_AWSMachinePoolStatus_To_v1alpha4_AWSMachinePoolStatus(in *v1beta1.AWSMachinePoolStatus, out *AWSMachinePoolStatus, s conversion.Scope) error {
+	return autoConvert_v1beta1_AWSMachinePoolStatus_To_v1alpha4_AWSMachinePoolStatus(in, out, s)
+}
+
 func autoConvert_v1alpha4_AWSManagedMachinePool_To_v1beta1_AWSManagedMachinePool(in *AWSManagedMachinePool, out *v1beta1.AWSManagedMachinePool, s conversion.Scope) error {
 	out.ObjectMeta = in.ObjectMeta
 	if err := Convert_v1alpha4_AWSManagedMachinePoolSpec_To_v1beta1_AWSManagedMachinePoolSpec(&in.Spec, &out.Spec, s); err != nil {
@@ -736,7 +740,6 @@ func autoConvert_v1beta1_AWSManagedMachinePoolSpec_To_v1alpha4_AWSManagedMachine
 	out.ProviderIDList = *(*[]string)(unsafe.Pointer(&in.ProviderIDList))
 	out.CapacityType = (*ManagedMachinePoolCapacityType)(unsafe.Pointer(in.CapacityType))
 	// WARNING: in.UpdateConfig requires manual conversion: does not exist in peer-type
-	// WARNING: in.AWSLaunchTemplate requires manual conversion: does not exist in peer-type
 	return nil
 }
 
@@ -767,8 +770,6 @@ func Convert_v1alpha4_AWSManagedMachinePoolStatus_To_v1beta1_AWSManagedMachinePo
 func autoConvert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha4_AWSManagedMachinePoolStatus(in *v1beta1.AWSManagedMachinePoolStatus, out *AWSManagedMachinePoolStatus, s conversion.Scope) error {
 	out.Ready = in.Ready
 	out.Replicas = in.Replicas
-	// WARNING: in.LaunchTemplateID requires manual conversion: does not exist in peer-type
-	// WARNING: in.LaunchTemplateVersion requires manual conversion: does not exist in peer-type
 	out.FailureReason = (*errors.MachineStatusError)(unsafe.Pointer(in.FailureReason))
 	out.FailureMessage = (*string)(unsafe.Pointer(in.FailureMessage))
 	if in.Conditions != nil {
@@ -785,6 +786,11 @@ func autoConvert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha4_AWSManagedMachi
 	return nil
 }
 
+// Convert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha4_AWSManagedMachinePoolStatus is an autogenerated conversion function.
+func Convert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha4_AWSManagedMachinePoolStatus(in *v1beta1.AWSManagedMachinePoolStatus, out *AWSManagedMachinePoolStatus, s conversion.Scope) error {
+	return autoConvert_v1beta1_AWSManagedMachinePoolStatus_To_v1alpha4_AWSManagedMachinePoolStatus(in, out, s)
+}
+
 func autoConvert_v1alpha4_AutoScalingGroup_To_v1beta1_AutoScalingGroup(in *AutoScalingGroup, out *v1beta1.AutoScalingGroup, s conversion.Scope) error {
 	out.ID = in.ID
 	out.Tags = *(*apiv1beta1.Tags)(unsafe.Pointer(&in.Tags))
diff --git a/exp/api/v1beta1/awsmachinepool_types.go b/exp/api/v1beta1/awsmachinepool_types.go
index 46e98dd91..55764b905 100644
--- a/exp/api/v1beta1/awsmachinepool_types.go
+++ b/exp/api/v1beta1/awsmachinepool_types.go
@@ -126,10 +126,6 @@ type AWSMachinePoolStatus struct {
 	// The ID of the launch template
 	LaunchTemplateID string `json:"launchTemplateID,omitempty"`
 
-	// The version of the launch template
-	// +optional
-	LaunchTemplateVersion *string `json:"launchTemplateVersion,omitempty"`
-
 	// FailureReason will be set in the event that there is a terminal problem
 	// reconciling the Machine and will contain a succinct value suitable
 	// for machine interpretation.
diff --git a/exp/api/v1beta1/awsmanagedmachinepool_types.go b/exp/api/v1beta1/awsmanagedmachinepool_types.go
index f5b5a639b..93f8ab73c 100644
--- a/exp/api/v1beta1/awsmanagedmachinepool_types.go
+++ b/exp/api/v1beta1/awsmanagedmachinepool_types.go
@@ -99,7 +99,7 @@ type AWSManagedMachinePoolSpec struct {
 	AMIVersion *string `json:"amiVersion,omitempty"`
 
 	// AMIType defines the AMI type
-	// +kubebuilder:validation:Enum:=AL2_x86_64;AL2_x86_64_GPU;AL2_ARM_64;CUSTOM
+	// +kubebuilder:validation:Enum:=AL2_x86_64;AL2_x86_64_GPU;AL2_ARM_64
 	// +kubebuilder:default:=AL2_x86_64
 	// +optional
 	AMIType *ManagedMachineAMIType `json:"amiType,omitempty"`
@@ -144,12 +144,6 @@ type AWSManagedMachinePoolSpec struct {
 	// to the nodegroup.
 	// +optional
 	UpdateConfig *UpdateConfig `json:"updateConfig,omitempty"`
-
-	// AWSLaunchTemplate specifies the launch template to use to create the managed node group.
-	// If AWSLaunchTemplate is specified, certain node group configuraions outside of launch template
-	// are prohibited (https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html).
-	// +optional
-	AWSLaunchTemplate *AWSLaunchTemplate `json:"awsLaunchTemplate,omitempty"`
 }
 
 // ManagedMachinePoolScaling specifies scaling options.
@@ -182,14 +176,6 @@ type AWSManagedMachinePoolStatus struct {
 	// +optional
 	Replicas int32 `json:"replicas"`
 
-	// The ID of the launch template
-	// +optional
-	LaunchTemplateID *string `json:"launchTemplateID,omitempty"`
-
-	// The version of the launch template
-	// +optional
-	LaunchTemplateVersion *string `json:"launchTemplateVersion,omitempty"`
-
 	// FailureReason will be set in the event that there is a terminal problem
 	// reconciling the MachinePool and will contain a succinct value suitable
 	// for machine interpretation.
diff --git a/exp/api/v1beta1/awsmanagedmachinepool_webhook.go b/exp/api/v1beta1/awsmanagedmachinepool_webhook.go
index f8625df36..b7f17b6a0 100644
--- a/exp/api/v1beta1/awsmanagedmachinepool_webhook.go
+++ b/exp/api/v1beta1/awsmanagedmachinepool_webhook.go
@@ -117,26 +117,6 @@ func (r *AWSManagedMachinePool) validateRemoteAccess() field.ErrorList {
 	return allErrs
 }
 
-func (r *AWSManagedMachinePool) validateLaunchTemplate() field.ErrorList {
-	var allErrs field.ErrorList
-	if r.Spec.AWSLaunchTemplate == nil {
-		return allErrs
-	}
-
-	if r.Spec.InstanceType != nil {
-		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "InstanceType"), r.Spec.InstanceType, "InstanceType cannot be specified when LaunchTemplate is specified"))
-	}
-	if r.Spec.DiskSize != nil {
-		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "DiskSize"), r.Spec.DiskSize, "DiskSize cannot be specified when LaunchTemplate is specified"))
-	}
-
-	if r.Spec.AWSLaunchTemplate.IamInstanceProfile != "" {
-		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "AWSLaunchTemplate", "IamInstanceProfile"), r.Spec.AWSLaunchTemplate.IamInstanceProfile, "IAM instance profile in launch template is prohibited in EKS managed node group"))
-	}
-
-	return allErrs
-}
-
 // ValidateCreate will do any extra validation when creating a AWSManagedMachinePool.
 func (r *AWSManagedMachinePool) ValidateCreate() error {
 	mmpLog.Info("AWSManagedMachinePool validate create", "name", r.Name)
@@ -155,9 +135,6 @@ func (r *AWSManagedMachinePool) ValidateCreate() error {
 	if errs := r.validateNodegroupUpdateConfig(); len(errs) > 0 {
 		allErrs = append(allErrs, errs...)
 	}
-	if errs := r.validateLaunchTemplate(); len(errs) > 0 {
-		allErrs = append(allErrs, errs...)
-	}
 
 	allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...)
 
@@ -192,9 +169,6 @@ func (r *AWSManagedMachinePool) ValidateUpdate(old runtime.Object) error {
 	if errs := r.validateNodegroupUpdateConfig(); len(errs) > 0 {
 		allErrs = append(allErrs, errs...)
 	}
-	if errs := r.validateLaunchTemplate(); len(errs) > 0 {
-		allErrs = append(allErrs, errs...)
-	}
 
 	if len(allErrs) == 0 {
 		return nil
@@ -243,16 +217,6 @@ func (r *AWSManagedMachinePool) validateImmutable(old *AWSManagedMachinePool) fi
 	appendErrorIfMutated(old.Spec.AMIType, r.Spec.AMIType, "amiType")
 	appendErrorIfMutated(old.Spec.RemoteAccess, r.Spec.RemoteAccess, "remoteAccess")
 	appendErrorIfSetAndMutated(old.Spec.CapacityType, r.Spec.CapacityType, "capacityType")
-	if (old.Spec.AWSLaunchTemplate != nil && r.Spec.AWSLaunchTemplate == nil) ||
-		(old.Spec.AWSLaunchTemplate == nil && r.Spec.AWSLaunchTemplate != nil) {
-		allErrs = append(
-			allErrs,
-			field.Invalid(field.NewPath("spec", "AWSLaunchTemplate"), old.Spec.AWSLaunchTemplate, "field is immutable"),
-		)
-	}
-	if old.Spec.AWSLaunchTemplate != nil && r.Spec.AWSLaunchTemplate != nil {
-		appendErrorIfMutated(old.Spec.AWSLaunchTemplate.Name, r.Spec.AWSLaunchTemplate.Name, "awsLaunchTemplate.name")
-	}
 
 	return allErrs
 }
diff --git a/exp/api/v1beta1/conditions_consts.go b/exp/api/v1beta1/conditions_consts.go
index 534ebb2bf..dcf44a1d7 100644
--- a/exp/api/v1beta1/conditions_consts.go
+++ b/exp/api/v1beta1/conditions_consts.go
@@ -34,18 +34,6 @@ const (
 	LaunchTemplateNotFoundReason = "LaunchTemplateNotFound"
 	// LaunchTemplateCreateFailedReason used for failures during Launch Template creation.
 	LaunchTemplateCreateFailedReason = "LaunchTemplateCreateFailed"
-	// LaunchTemplateReconcileFailedReason used for failures during Launch Template reconciliation.
-	LaunchTemplateReconcileFailedReason = "LaunchTemplateReconcileFailed"
-
-	// PreLaunchTemplateUpdateCheckCondition reports if all prerequisite are met for launch template update.
-	PreLaunchTemplateUpdateCheckCondition clusterv1.ConditionType = "PreLaunchTemplateUpdateCheckSuccess"
-	// PostLaunchTemplateUpdateOperationCondition reports on successfully completes post launch template update operation.
-	PostLaunchTemplateUpdateOperationCondition clusterv1.ConditionType = "PostLaunchTemplateUpdateOperationSuccess"
-
-	// PreLaunchTemplateUpdateCheckFailedReason used to report when not all prerequisite are met for launch template update.
-	PreLaunchTemplateUpdateCheckFailedReason = "PreLaunchTemplateUpdateCheckFailed"
-	// PostLaunchTemplateUpdateOperationFailedReason used to report when post launch template update operation failed.
-	PostLaunchTemplateUpdateOperationFailedReason = "PostLaunchTemplateUpdateOperationFailed"
 
 	// InstanceRefreshStartedCondition reports on successfully starting instance refresh.
 	InstanceRefreshStartedCondition clusterv1.ConditionType = "InstanceRefreshStarted"
diff --git a/exp/api/v1beta1/zz_generated.deepcopy.go b/exp/api/v1beta1/zz_generated.deepcopy.go
index dc1ffbb1d..2ba9c4ab7 100644
--- a/exp/api/v1beta1/zz_generated.deepcopy.go
+++ b/exp/api/v1beta1/zz_generated.deepcopy.go
@@ -277,11 +277,6 @@ func (in *AWSMachinePoolStatus) DeepCopyInto(out *AWSMachinePoolStatus) {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
-	if in.LaunchTemplateVersion != nil {
-		in, out := &in.LaunchTemplateVersion, &out.LaunchTemplateVersion
-		*out = new(string)
-		**out = **in
-	}
 	if in.FailureReason != nil {
 		in, out := &in.FailureReason, &out.FailureReason
 		*out = new(errors.MachineStatusError)
@@ -450,11 +445,6 @@ func (in *AWSManagedMachinePoolSpec) DeepCopyInto(out *AWSManagedMachinePoolSpec
 		*out = new(UpdateConfig)
 		(*in).DeepCopyInto(*out)
 	}
-	if in.AWSLaunchTemplate != nil {
-		in, out := &in.AWSLaunchTemplate, &out.AWSLaunchTemplate
-		*out = new(AWSLaunchTemplate)
-		(*in).DeepCopyInto(*out)
-	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedMachinePoolSpec.
@@ -470,16 +460,6 @@ func (in *AWSManagedMachinePoolSpec) DeepCopy() *AWSManagedMachinePoolSpec {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *AWSManagedMachinePoolStatus) DeepCopyInto(out *AWSManagedMachinePoolStatus) {
 	*out = *in
-	if in.LaunchTemplateID != nil {
-		in, out := &in.LaunchTemplateID, &out.LaunchTemplateID
-		*out = new(string)
-		**out = **in
-	}
-	if in.LaunchTemplateVersion != nil {
-		in, out := &in.LaunchTemplateVersion, &out.LaunchTemplateVersion
-		*out = new(string)
-		**out = **in
-	}
 	if in.FailureReason != nil {
 		in, out := &in.FailureReason, &out.FailureReason
 		*out = new(errors.MachineStatusError)
diff --git a/exp/controllers/awsmachinepool_controller.go b/exp/controllers/awsmachinepool_controller.go
index e4788cfbc..a0778a572 100644
--- a/exp/controllers/awsmachinepool_controller.go
+++ b/exp/controllers/awsmachinepool_controller.go
@@ -45,6 +45,7 @@ import (
 	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services"
 	asg "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/autoscaling"
 	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/ec2"
+	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/userdata"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
 	expclusterv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
 	"sigs.k8s.io/cluster-api/util"
@@ -221,29 +222,7 @@ func (r *AWSMachinePoolReconciler) reconcileNormal(ctx context.Context, machineP
 		return ctrl.Result{}, nil
 	}
 
-	ec2Svc := r.getEC2Service(ec2Scope)
-	asgsvc := r.getASGService(clusterScope)
-
-	canUpdateLaunchTemplate := func() (bool, error) {
-		// If there is a change: before changing the template, check if there exist an ongoing instance refresh,
-		// because only 1 instance refresh can be "InProgress". If template is updated when refresh cannot be started,
-		// that change will not trigger a refresh. Do not start an instance refresh if only userdata changed.
-		return asgsvc.CanStartASGInstanceRefresh(machinePoolScope)
-	}
-	runPostLaunchTemplateUpdateOperation := func() error {
-		// After creating a new version of launch template, instance refresh is required
-		// to trigger a rolling replacement of all previously launched instances.
-		// If ONLY the userdata changed, previously launched instances continue to use the old launch
-		// template.
-		//
-		// FIXME(dlipovetsky,sedefsavas): If the controller terminates, or the StartASGInstanceRefresh returns an error,
-		// this conditional will not evaluate to true the next reconcile. If any machines use an older
-		// Launch Template version, and the difference between the older and current versions is _more_
-		// than userdata, we should start an Instance Refresh.
-		machinePoolScope.Info("starting instance refresh", "number of instances", machinePoolScope.MachinePool.Spec.Replicas)
-		return asgsvc.StartASGInstanceRefresh(machinePoolScope)
-	}
-	if err := ec2Svc.ReconcileLaunchTemplate(machinePoolScope, canUpdateLaunchTemplate, runPostLaunchTemplateUpdateOperation); err != nil {
+	if err := r.reconcileLaunchTemplate(machinePoolScope, ec2Scope); err != nil {
 		r.Recorder.Eventf(machinePoolScope.AWSMachinePool, corev1.EventTypeWarning, "FailedLaunchTemplateReconcile", "Failed to reconcile launch template: %v", err)
 		machinePoolScope.Error(err, "failed to reconcile launch template")
 		return ctrl.Result{}, err
@@ -252,6 +231,9 @@ func (r *AWSMachinePoolReconciler) reconcileNormal(ctx context.Context, machineP
 	// set the LaunchTemplateReady condition
 	conditions.MarkTrue(machinePoolScope.AWSMachinePool, expinfrav1.LaunchTemplateReadyCondition)
 
+	// Initialize ASG client
+	asgsvc := r.getASGService(clusterScope)
+
 	// Find existing ASG
 	asg, err := r.findASG(machinePoolScope, asgsvc)
 	if err != nil {
@@ -273,19 +255,7 @@ func (r *AWSMachinePoolReconciler) reconcileNormal(ctx context.Context, machineP
 		return ctrl.Result{}, err
 	}
 
-	launchTemplateID := machinePoolScope.GetLaunchTemplateIDStatus()
-	asgName := machinePoolScope.Name()
-	resourceServiceToUpdate := []scope.ResourceServiceToUpdate{
-		{
-			ResourceID:      &launchTemplateID,
-			ResourceService: ec2Svc,
-		},
-		{
-			ResourceID:      &asgName,
-			ResourceService: asgsvc,
-		},
-	}
-	err = ec2Svc.ReconcileTags(machinePoolScope, resourceServiceToUpdate)
+	err = r.reconcileTags(machinePoolScope, clusterScope, ec2Scope)
 	if err != nil {
 		return ctrl.Result{}, errors.Wrap(err, "error updating tags")
 	}
@@ -346,7 +316,7 @@ func (r *AWSMachinePoolReconciler) reconcileDelete(machinePoolScope *scope.Machi
 	}
 
 	launchTemplateID := machinePoolScope.AWSMachinePool.Status.LaunchTemplateID
-	launchTemplate, _, err := ec2Svc.GetLaunchTemplate(machinePoolScope.LaunchTemplateName())
+	launchTemplate, _, err := ec2Svc.GetLaunchTemplate(machinePoolScope.Name())
 	if err != nil {
 		return ctrl.Result{}, err
 	}
@@ -410,6 +380,134 @@ func (r *AWSMachinePoolReconciler) findASG(machinePoolScope *scope.MachinePoolSc
 	return asg, nil
 }
 
+func (r *AWSMachinePoolReconciler) reconcileLaunchTemplate(machinePoolScope *scope.MachinePoolScope, ec2Scope scope.EC2Scope) error {
+	bootstrapData, err := machinePoolScope.GetRawBootstrapData()
+	if err != nil {
+		r.Recorder.Eventf(machinePoolScope.AWSMachinePool, corev1.EventTypeWarning, "FailedGetBootstrapData", err.Error())
+	}
+	bootstrapDataHash := userdata.ComputeHash(bootstrapData)
+
+	ec2svc := r.getEC2Service(ec2Scope)
+
+	machinePoolScope.Info("checking for existing launch template")
+	launchTemplate, launchTemplateUserDataHash, err := ec2svc.GetLaunchTemplate(machinePoolScope.Name())
+	if err != nil {
+		conditions.MarkUnknown(machinePoolScope.AWSMachinePool, expinfrav1.LaunchTemplateReadyCondition, expinfrav1.LaunchTemplateNotFoundReason, err.Error())
+		return err
+	}
+
+	imageID, err := ec2svc.DiscoverLaunchTemplateAMI(machinePoolScope)
+	if err != nil {
+		conditions.MarkFalse(machinePoolScope.AWSMachinePool, expinfrav1.LaunchTemplateReadyCondition, expinfrav1.LaunchTemplateCreateFailedReason, clusterv1.ConditionSeverityError, err.Error())
+		return err
+	}
+
+	if launchTemplate == nil {
+		machinePoolScope.Info("no existing launch template found, creating")
+		launchTemplateID, err := ec2svc.CreateLaunchTemplate(machinePoolScope, imageID, bootstrapData)
+		if err != nil {
+			conditions.MarkFalse(machinePoolScope.AWSMachinePool, expinfrav1.LaunchTemplateReadyCondition, expinfrav1.LaunchTemplateCreateFailedReason, clusterv1.ConditionSeverityError, err.Error())
+			return err
+		}
+
+		machinePoolScope.SetLaunchTemplateIDStatus(launchTemplateID)
+		return machinePoolScope.PatchObject()
+	}
+
+	// LaunchTemplateID is set during LaunchTemplate creation, but for a scenario such as `clusterctl move`, status fields become blank.
+	// If launchTemplate already exists but LaunchTemplateID field in the status is empty, get the ID and update the status.
+	if machinePoolScope.AWSMachinePool.Status.LaunchTemplateID == "" {
+		launchTemplateID, err := ec2svc.GetLaunchTemplateID(machinePoolScope.Name())
+		if err != nil {
+			conditions.MarkUnknown(machinePoolScope.AWSMachinePool, expinfrav1.LaunchTemplateReadyCondition, expinfrav1.LaunchTemplateNotFoundReason, err.Error())
+			return err
+		}
+		machinePoolScope.SetLaunchTemplateIDStatus(launchTemplateID)
+		return machinePoolScope.PatchObject()
+	}
+
+	annotation, err := r.machinePoolAnnotationJSON(machinePoolScope.AWSMachinePool, TagsLastAppliedAnnotation)
+	if err != nil {
+		return err
+	}
+
+	// Check if the instance tags were changed. If they were, create a new LaunchTemplate.
+	tagsChanged, _, _, _ := tagsChanged(annotation, machinePoolScope.AdditionalTags()) //nolint:dogsled
+
+	needsUpdate, err := ec2svc.LaunchTemplateNeedsUpdate(machinePoolScope, &machinePoolScope.AWSMachinePool.Spec.AWSLaunchTemplate, launchTemplate)
+	if err != nil {
+		return err
+	}
+
+	// If there is a change: before changing the template, check if there exist an ongoing instance refresh,
+	// because only 1 instance refresh can be "InProgress". If template is updated when refresh cannot be started,
+	// that change will not trigger a refresh. Do not start an instance refresh if only userdata changed.
+	if needsUpdate || tagsChanged || *imageID != *launchTemplate.AMI.ID {
+		asgSvc := r.getASGService(ec2Scope)
+		canStart, err := asgSvc.CanStartASGInstanceRefresh(machinePoolScope)
+		if err != nil {
+			return err
+		}
+		if !canStart {
+			conditions.MarkFalse(machinePoolScope.AWSMachinePool, expinfrav1.InstanceRefreshStartedCondition, expinfrav1.InstanceRefreshNotReadyReason, clusterv1.ConditionSeverityWarning, "")
+			return errors.New("Cannot start a new instance refresh. Unfinished instance refresh exist")
+		}
+	}
+
+	// Create a new launch template version if there's a difference in configuration, tags,
+	// userdata, OR we've discovered a new AMI ID.
+	if needsUpdate || tagsChanged || *imageID != *launchTemplate.AMI.ID || launchTemplateUserDataHash != bootstrapDataHash {
+		machinePoolScope.Info("creating new version for launch template", "existing", launchTemplate, "incoming", machinePoolScope.AWSMachinePool.Spec.AWSLaunchTemplate)
+		// There is a limit to the number of Launch Template Versions.
+		// We ensure that the number of versions does not grow without bound by following a simple rule: Before we create a new version, we delete one old version, if there is at least one old version that is not in use.
+		if err := ec2svc.PruneLaunchTemplateVersions(machinePoolScope.AWSMachinePool.Status.LaunchTemplateID); err != nil {
+			return err
+		}
+		if err := ec2svc.CreateLaunchTemplateVersion(machinePoolScope, imageID, bootstrapData); err != nil {
+			return err
+		}
+	}
+
+	// After creating a new version of launch template, instance refresh is required
+	// to trigger a rolling replacement of all previously launched instances.
+	// If ONLY the userdata changed, previously launched instances continue to use the old launch
+	// template.
+	//
+	// FIXME(dlipovetsky,sedefsavas): If the controller terminates, or the StartASGInstanceRefresh returns an error,
+	// this conditional will not evaluate to true the next reconcile. If any machines use an older
+	// Launch Template version, and the difference between the older and current versions is _more_
+	// than userdata, we should start an Instance Refresh.
+	if needsUpdate || tagsChanged || *imageID != *launchTemplate.AMI.ID {
+		machinePoolScope.Info("starting instance refresh", "number of instances", machinePoolScope.MachinePool.Spec.Replicas)
+		asgSvc := r.getASGService(ec2Scope)
+		if err := asgSvc.StartASGInstanceRefresh(machinePoolScope); err != nil {
+			conditions.MarkFalse(machinePoolScope.AWSMachinePool, expinfrav1.InstanceRefreshStartedCondition, expinfrav1.InstanceRefreshFailedReason, clusterv1.ConditionSeverityError, err.Error())
+			return err
+		}
+		conditions.MarkTrue(machinePoolScope.AWSMachinePool, expinfrav1.InstanceRefreshStartedCondition)
+	}
+
+	return nil
+}
+
+func (r *AWSMachinePoolReconciler) reconcileTags(machinePoolScope *scope.MachinePoolScope, clusterScope cloud.ClusterScoper, ec2Scope scope.EC2Scope) error {
+	ec2Svc := r.getEC2Service(ec2Scope)
+	asgSvc := r.getASGService(clusterScope)
+
+	launchTemplateID := machinePoolScope.AWSMachinePool.Status.LaunchTemplateID
+	asgName := machinePoolScope.Name()
+	additionalTags := machinePoolScope.AdditionalTags()
+
+	tagsChanged, err := r.ensureTags(ec2Svc, asgSvc, machinePoolScope.AWSMachinePool, &launchTemplateID, &asgName, additionalTags)
+	if err != nil {
+		return err
+	}
+	if tagsChanged {
+		r.Recorder.Eventf(machinePoolScope.AWSMachinePool, corev1.EventTypeNormal, "UpdatedTags", "updated tags on resources")
+	}
+	return nil
+}
+
 // asgNeedsUpdates compares incoming AWSMachinePool and compares against existing ASG.
 func asgNeedsUpdates(machinePoolScope *scope.MachinePoolScope, existingASG *expinfrav1.AutoScalingGroup) bool {
 	if machinePoolScope.MachinePool.Spec.Replicas != nil {
diff --git a/exp/controllers/awsmachinepool_tags.go b/exp/controllers/awsmachinepool_tags.go
new file mode 100644
index 000000000..8a6a0ad9d
--- /dev/null
+++ b/exp/controllers/awsmachinepool_tags.go
@@ -0,0 +1,191 @@
+/*
+Copyright 2020 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 controllers
+
+import (
+	"encoding/json"
+
+	expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/exp/api/v1beta1"
+	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services"
+)
+
+const (
+	// TagsLastAppliedAnnotation is the key for the AWSMachinePool object annotation
+	// which tracks the tags that the AWSMachinePool actuator is responsible
+	// for. These are the tags that have been handled by the
+	// AdditionalTags in the AWSMachinePool Provider Config.
+	// See https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
+	// for annotation formatting rules.
+	TagsLastAppliedAnnotation = "sigs.k8s.io/cluster-api-provider-aws-last-applied-tags"
+)
+
+// Ensure that the tags of the AWSMachinePool are correct
+// Returns bool, error
+// Bool indicates if changes were made or not, allowing the caller to decide
+// if the machine should be updated.
+func (r *AWSMachinePoolReconciler) ensureTags(ec2svc services.EC2Interface, asgsvc services.ASGInterface, machinePool *expinfrav1.AWSMachinePool, launchTemplateID, asgName *string, additionalTags map[string]string) (bool, error) {
+	annotation, err := r.machinePoolAnnotationJSON(machinePool, TagsLastAppliedAnnotation)
+	if err != nil {
+		return false, err
+	}
+
+	// Check if the instance tags were changed. If they were, update them.
+	// It would be possible here to only send new/updated tags, but for the
+	// moment we send everything, even if only a single tag was created or
+	// upated.
+	changed, created, deleted, newAnnotation := tagsChanged(annotation, additionalTags)
+	if changed {
+		err = ec2svc.UpdateResourceTags(launchTemplateID, created, deleted)
+		if err != nil {
+			return false, err
+		}
+
+		if err := asgsvc.UpdateResourceTags(asgName, created, deleted); err != nil {
+			return false, err
+		}
+
+		// We also need to update the annotation if anything changed.
+		err = r.updateMachinePoolAnnotationJSON(machinePool, TagsLastAppliedAnnotation, newAnnotation)
+		if err != nil {
+			return false, err
+		}
+	}
+
+	return changed, nil
+}
+
+// tagsChanged determines which tags to delete and which to add.
+func tagsChanged(annotation map[string]interface{}, src map[string]string) (bool, map[string]string, map[string]string, map[string]interface{}) {
+	// Bool tracking if we found any changed state.
+	changed := false
+
+	// Tracking for created/updated
+	created := map[string]string{}
+
+	// Tracking for tags that were deleted.
+	deleted := map[string]string{}
+
+	// The new annotation that we need to set if anything is created/updated.
+	newAnnotation := map[string]interface{}{}
+
+	// Loop over annotation, checking if entries are in src.
+	// If an entry is present in annotation but not src, it has been deleted
+	// since last time. We flag this in the deleted map.
+	for t, v := range annotation {
+		_, ok := src[t]
+
+		// Entry isn't in src, it has been deleted.
+		if !ok {
+			// Cast v to a string here. This should be fine, tags are always
+			// strings.
+			deleted[t] = v.(string)
+			changed = true
+		}
+	}
+
+	// Loop over src, checking for entries in annotation.
+	//
+	// If an entry is in src, but not annotation, it has been created since
+	// last time.
+	//
+	// If an entry is in both src and annotation, we compare their values, if
+	// the value in src differs from that in annotation, the tag has been
+	// updated since last time.
+	for t, v := range src {
+		av, ok := annotation[t]
+
+		// Entries in the src always need to be noted in the newAnnotation. We
+		// know they're going to be created or updated.
+		newAnnotation[t] = v
+
+		// Entry isn't in annotation, it's new.
+		if !ok {
+			created[t] = v
+			newAnnotation[t] = v
+			changed = true
+			continue
+		}
+
+		// Entry is in annotation, has the value changed?
+		if v != av {
+			created[t] = v
+			changed = true
+		}
+
+		// Entry existed in both src and annotation, and their values were
+		// equal. Nothing to do.
+	}
+
+	// We made it through the loop, and everything that was in src, was also
+	// in dst. Nothing changed.
+	return changed, created, deleted, newAnnotation
+}
+
+// updateMachinePoolAnnotationJSON updates the `annotation` on `machinePool` with
+// `content`. `content` in this case should be a `map[string]interface{}`
+// suitable for turning into JSON. This `content` map will be marshalled into a
+// JSON string before being set as the given `annotation`.
+func (r *AWSMachinePoolReconciler) updateMachinePoolAnnotationJSON(machinePool *expinfrav1.AWSMachinePool, annotation string, content map[string]interface{}) error {
+	b, err := json.Marshal(content)
+	if err != nil {
+		return err
+	}
+
+	r.updateMachinePoolAnnotation(machinePool, annotation, string(b))
+	return nil
+}
+
+// updateMachinePoolAnnotation updates the `annotation` on the given `machinePool` with
+// `content`.
+func (r *AWSMachinePoolReconciler) updateMachinePoolAnnotation(machinePool *expinfrav1.AWSMachinePool, annotation, content string) {
+	// Get the annotations
+	annotations := machinePool.GetAnnotations()
+
+	if annotations == nil {
+		annotations = make(map[string]string)
+	}
+
+	// Set our annotation to the given content.
+	annotations[annotation] = content
+
+	// Update the machine object with these annotations
+	machinePool.SetAnnotations(annotations)
+}
+
+// Returns a map[string]interface from a JSON annotation.
+// This method gets the given `annotation` from the `machinePool` and unmarshalls it
+// from a JSON string into a `map[string]interface{}`.
+func (r *AWSMachinePoolReconciler) machinePoolAnnotationJSON(machinePool *expinfrav1.AWSMachinePool, annotation string) (map[string]interface{}, error) {
+	out := map[string]interface{}{}
+
+	jsonAnnotation := r.machinePoolAnnotation(machinePool, annotation)
+	if len(jsonAnnotation) == 0 {
+		return out, nil
+	}
+
+	err := json.Unmarshal([]byte(jsonAnnotation), &out)
+	if err != nil {
+		return out, err
+	}
+
+	return out, nil
+}
+
+// Fetches the specific machine annotation.
+func (r *AWSMachinePoolReconciler) machinePoolAnnotation(machinePool *expinfrav1.AWSMachinePool, annotation string) string {
+	return machinePool.GetAnnotations()[annotation]
+}
diff --git a/exp/controllers/awsmanagedmachinepool_controller.go b/exp/controllers/awsmanagedmachinepool_controller.go
index 19ca5e218..50f5ebe9b 100644
--- a/exp/controllers/awsmanagedmachinepool_controller.go
+++ b/exp/controllers/awsmanagedmachinepool_controller.go
@@ -22,7 +22,6 @@ import (
 
 	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
-	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime/schema"
@@ -39,8 +38,6 @@ import (
 	ekscontrolplanev1 "sigs.k8s.io/cluster-api-provider-aws/controlplane/eks/api/v1beta1"
 	expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/exp/api/v1beta1"
 	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/scope"
-	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services"
-	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/ec2"
 	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/eks"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
 	expclusterv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
@@ -137,17 +134,6 @@ func (r *AWSManagedMachinePoolReconciler) Reconcile(ctx context.Context, req ctr
 		return reconcile.Result{}, nil
 	}
 
-	managedControlPlaneScope, err := scope.NewManagedControlPlaneScope(scope.ManagedControlPlaneScopeParams{
-		Client:         r.Client,
-		Logger:         &log,
-		Cluster:        cluster,
-		ControlPlane:   controlPlane,
-		ControllerName: "awsManagedControlPlane",
-	})
-	if err != nil {
-		return ctrl.Result{}, errors.New("error getting managed control plane scope")
-	}
-
 	if !controlPlane.Status.Ready {
 		log.Info("Control plane is not ready yet")
 		conditions.MarkFalse(awsPool, expinfrav1.EKSNodegroupReadyCondition, expinfrav1.WaitingForEKSControlPlaneReason, clusterv1.ConditionSeverityInfo, "")
@@ -164,7 +150,6 @@ func (r *AWSManagedMachinePoolReconciler) Reconcile(ctx context.Context, req ctr
 		EnableIAM:            r.EnableIAM,
 		AllowAdditionalRoles: r.AllowAdditionalRoles,
 		Endpoints:            r.Endpoints,
-		InfraCluster:         managedControlPlaneScope,
 	})
 	if err != nil {
 		return ctrl.Result{}, errors.Wrap(err, "failed to create scope")
@@ -174,7 +159,6 @@ func (r *AWSManagedMachinePoolReconciler) Reconcile(ctx context.Context, req ctr
 		applicableConditions := []clusterv1.ConditionType{
 			expinfrav1.EKSNodegroupReadyCondition,
 			expinfrav1.IAMNodegroupRolesReadyCondition,
-			expinfrav1.LaunchTemplateReadyCondition,
 		}
 
 		conditions.SetSummary(machinePoolScope.ManagedMachinePool, conditions.WithConditions(applicableConditions...), conditions.WithStepCounter())
@@ -185,16 +169,15 @@ func (r *AWSManagedMachinePoolReconciler) Reconcile(ctx context.Context, req ctr
 	}()
 
 	if !awsPool.ObjectMeta.DeletionTimestamp.IsZero() {
-		return r.reconcileDelete(ctx, machinePoolScope, managedControlPlaneScope)
+		return r.reconcileDelete(ctx, machinePoolScope)
 	}
 
-	return r.reconcileNormal(ctx, machinePoolScope, managedControlPlaneScope)
+	return r.reconcileNormal(ctx, machinePoolScope)
 }
 
 func (r *AWSManagedMachinePoolReconciler) reconcileNormal(
 	_ context.Context,
 	machinePoolScope *scope.ManagedMachinePoolScope,
-	ec2Scope scope.EC2Scope,
 ) (ctrl.Result, error) {
 	machinePoolScope.Info("Reconciling AWSManagedMachinePool")
 
@@ -204,34 +187,6 @@ func (r *AWSManagedMachinePoolReconciler) reconcileNormal(
 	}
 
 	ekssvc := eks.NewNodegroupService(machinePoolScope)
-	ec2svc := r.getEC2Service(ec2Scope)
-
-	if machinePoolScope.ManagedMachinePool.Spec.AWSLaunchTemplate != nil {
-		canUpdateLaunchTemplate := func() (bool, error) {
-			return true, nil
-		}
-		runPostLaunchTemplateUpdateOperation := func() error {
-			return nil
-		}
-		if err := ec2svc.ReconcileLaunchTemplate(machinePoolScope, canUpdateLaunchTemplate, runPostLaunchTemplateUpdateOperation); err != nil {
-			r.Recorder.Eventf(machinePoolScope.ManagedMachinePool, corev1.EventTypeWarning, "FailedLaunchTemplateReconcile", "Failed to reconcile launch template: %v", err)
-			machinePoolScope.Error(err, "failed to reconcile launch template")
-			conditions.MarkFalse(machinePoolScope.ManagedMachinePool, expinfrav1.LaunchTemplateReadyCondition, expinfrav1.LaunchTemplateReconcileFailedReason, clusterv1.ConditionSeverityError, "")
-			return ctrl.Result{}, err
-		}
-
-		launchTemplateID := machinePoolScope.GetLaunchTemplateIDStatus()
-		resourceServiceToUpdate := []scope.ResourceServiceToUpdate{{
-			ResourceID:      &launchTemplateID,
-			ResourceService: ec2svc,
-		}}
-		if err := ec2svc.ReconcileTags(machinePoolScope, resourceServiceToUpdate); err != nil {
-			return ctrl.Result{}, errors.Wrap(err, "error updating tags")
-		}
-
-		// set the LaunchTemplateReady condition
-		conditions.MarkTrue(machinePoolScope.ManagedMachinePool, expinfrav1.LaunchTemplateReadyCondition)
-	}
 
 	if err := ekssvc.ReconcilePool(); err != nil {
 		return reconcile.Result{}, errors.Wrapf(err, "failed to reconcile machine pool for AWSManagedMachinePool %s/%s", machinePoolScope.ManagedMachinePool.Namespace, machinePoolScope.ManagedMachinePool.Name)
@@ -243,40 +198,15 @@ func (r *AWSManagedMachinePoolReconciler) reconcileNormal(
 func (r *AWSManagedMachinePoolReconciler) reconcileDelete(
 	_ context.Context,
 	machinePoolScope *scope.ManagedMachinePoolScope,
-	ec2Scope scope.EC2Scope,
 ) (ctrl.Result, error) {
 	machinePoolScope.Info("Reconciling deletion of AWSManagedMachinePool")
 
 	ekssvc := eks.NewNodegroupService(machinePoolScope)
-	ec2Svc := ec2.NewService(ec2Scope)
 
 	if err := ekssvc.ReconcilePoolDelete(); err != nil {
 		return reconcile.Result{}, errors.Wrapf(err, "failed to reconcile machine pool deletion for AWSManagedMachinePool %s/%s", machinePoolScope.ManagedMachinePool.Namespace, machinePoolScope.ManagedMachinePool.Name)
 	}
 
-	if machinePoolScope.ManagedMachinePool.Spec.AWSLaunchTemplate != nil {
-		launchTemplateID := machinePoolScope.ManagedMachinePool.Status.LaunchTemplateID
-		launchTemplate, _, err := ec2Svc.GetLaunchTemplate(machinePoolScope.LaunchTemplateName())
-		if err != nil {
-			return ctrl.Result{}, err
-		}
-
-		if launchTemplate == nil {
-			machinePoolScope.V(2).Info("Unable to find matching launch template")
-			r.Recorder.Eventf(machinePoolScope.ManagedMachinePool, corev1.EventTypeNormal, "NoLaunchTemplateFound", "Unable to find matching launch template")
-			controllerutil.RemoveFinalizer(machinePoolScope.ManagedMachinePool, expinfrav1.ManagedMachinePoolFinalizer)
-			return ctrl.Result{}, nil
-		}
-
-		machinePoolScope.Info("deleting launch template", "name", launchTemplate.Name)
-		if err := ec2Svc.DeleteLaunchTemplate(*launchTemplateID); err != nil {
-			r.Recorder.Eventf(machinePoolScope.ManagedMachinePool, corev1.EventTypeWarning, "FailedDelete", "Failed to delete launch template %q: %v", launchTemplate.Name, err)
-			return ctrl.Result{}, errors.Wrap(err, "failed to delete launch template")
-		}
-
-		machinePoolScope.Info("successfully deleted launch template")
-	}
-
 	controllerutil.RemoveFinalizer(machinePoolScope.ManagedMachinePool, expinfrav1.ManagedMachinePoolFinalizer)
 
 	return reconcile.Result{}, nil
@@ -342,7 +272,3 @@ func managedControlPlaneToManagedMachinePoolMapFunc(c client.Client, gvk schema.
 		return results
 	}
 }
-
-func (r *AWSManagedMachinePoolReconciler) getEC2Service(scope scope.EC2Scope) services.EC2Interface {
-	return ec2.NewService(scope)
-}
diff --git a/pkg/cloud/scope/launchtemplate.go b/pkg/cloud/scope/launchtemplate.go
deleted file mode 100644
index 4faf82297..000000000
--- a/pkg/cloud/scope/launchtemplate.go
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
-Copyright 2022 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package scope
-
-import (
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-
-	infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1"
-	expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/exp/api/v1beta1"
-	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud"
-	expclusterv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
-	"sigs.k8s.io/cluster-api/util/conditions"
-)
-
-// LaunchTemplateScope defines a scope defined around a launch template.
-type LaunchTemplateScope interface {
-	GetMachinePool() *expclusterv1.MachinePool
-	GetLaunchTemplate() *expinfrav1.AWSLaunchTemplate
-	LaunchTemplateName() string
-	GetLaunchTemplateIDStatus() string
-	SetLaunchTemplateIDStatus(id string)
-	GetLaunchTemplateLatestVersionStatus() string
-	SetLaunchTemplateLatestVersionStatus(version string)
-	GetRawBootstrapData() ([]byte, error)
-
-	IsEKSManaged() bool
-	AdditionalTags() infrav1.Tags
-
-	GetObjectMeta() *metav1.ObjectMeta
-	GetSetter() conditions.Setter
-	PatchObject() error
-	GetEC2Scope() EC2Scope
-
-	client.Client
-	cloud.Logger
-}
-
-type ResourceServiceToUpdate struct {
-	ResourceID      *string
-	ResourceService ResourceService
-}
-
-type ResourceService interface {
-	UpdateResourceTags(resourceID *string, create, remove map[string]string) error
-}
diff --git a/pkg/cloud/scope/machine.go b/pkg/cloud/scope/machine.go
index 1832572f3..189e3d7f8 100644
--- a/pkg/cloud/scope/machine.go
+++ b/pkg/cloud/scope/machine.go
@@ -30,7 +30,6 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1"
-	ekscontrolplanev1 "sigs.k8s.io/cluster-api-provider-aws/controlplane/eks/api/v1beta1"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
 	"sigs.k8s.io/cluster-api/controllers/noderefutil"
 	capierrors "sigs.k8s.io/cluster-api/errors"
@@ -364,7 +363,7 @@ func (m *MachineScope) AWSMachineIsDeleted() bool {
 
 // IsEKSManaged checks if the machine is EKS managed.
 func (m *MachineScope) IsEKSManaged() bool {
-	return m.InfraCluster.InfraCluster().GetObjectKind().GroupVersionKind().Kind == ekscontrolplanev1.AWSManagedControlPlaneKind
+	return m.InfraCluster.InfraCluster().GetObjectKind().GroupVersionKind().Kind == "AWSManagedControlPlane"
 }
 
 // IsExternallyManaged checks if the machine is externally managed.
diff --git a/pkg/cloud/scope/machinepool.go b/pkg/cloud/scope/machinepool.go
index d7c834214..16b35db93 100644
--- a/pkg/cloud/scope/machinepool.go
+++ b/pkg/cloud/scope/machinepool.go
@@ -24,29 +24,25 @@ import (
 	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/klog/v2/klogr"
 	"k8s.io/utils/pointer"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1"
-	ekscontrolplanev1 "sigs.k8s.io/cluster-api-provider-aws/controlplane/eks/api/v1beta1"
 	expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/exp/api/v1beta1"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
 	"sigs.k8s.io/cluster-api/controllers/remote"
 	capierrors "sigs.k8s.io/cluster-api/errors"
 	expclusterv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/conditions"
 	"sigs.k8s.io/cluster-api/util/patch"
 )
 
 // MachinePoolScope defines a scope defined around a machine and its cluster.
 type MachinePoolScope struct {
 	logr.Logger
-	client.Client
+	client      client.Client
 	patchHelper *patch.Helper
 
 	Cluster        *clusterv1.Cluster
@@ -57,7 +53,7 @@ type MachinePoolScope struct {
 
 // MachinePoolScopeParams defines a scope defined around a machine and its cluster.
 type MachinePoolScopeParams struct {
-	client.Client
+	Client client.Client
 	Logger *logr.Logger
 
 	Cluster        *clusterv1.Cluster
@@ -105,7 +101,7 @@ func NewMachinePoolScope(params MachinePoolScopeParams) (*MachinePoolScope, erro
 
 	return &MachinePoolScope{
 		Logger:      *params.Logger,
-		Client:      params.Client,
+		client:      params.Client,
 		patchHelper: helper,
 
 		Cluster:        params.Cluster,
@@ -145,7 +141,7 @@ func (m *MachinePoolScope) getBootstrapData() ([]byte, string, error) {
 	secret := &corev1.Secret{}
 	key := types.NamespacedName{Namespace: m.Namespace(), Name: *m.MachinePool.Spec.Template.Spec.Bootstrap.DataSecretName}
 
-	if err := m.Client.Get(context.TODO(), key, secret); err != nil {
+	if err := m.client.Get(context.TODO(), key, secret); err != nil {
 		return nil, "", errors.Wrapf(err, "failed to retrieve bootstrap data secret for AWSMachine %s/%s", m.Namespace(), m.Name())
 	}
 
@@ -224,41 +220,14 @@ func (m *MachinePoolScope) SetASGStatus(v expinfrav1.ASGStatus) {
 	m.AWSMachinePool.Status.ASGStatus = &v
 }
 
-func (m *MachinePoolScope) GetObjectMeta() *metav1.ObjectMeta {
-	return &m.AWSMachinePool.ObjectMeta
-}
-
-func (m *MachinePoolScope) GetSetter() conditions.Setter {
-	return m.AWSMachinePool
-}
-
-func (m *MachinePoolScope) GetEC2Scope() EC2Scope {
-	return m.InfraCluster
-}
-
-func (m *MachinePoolScope) GetLaunchTemplateIDStatus() string {
-	return m.AWSMachinePool.Status.LaunchTemplateID
-}
-
+// SetLaunchTemplateIDStatus sets the AWSMachinePool LaunchTemplateID status.
 func (m *MachinePoolScope) SetLaunchTemplateIDStatus(id string) {
 	m.AWSMachinePool.Status.LaunchTemplateID = id
 }
 
-func (m *MachinePoolScope) GetLaunchTemplateLatestVersionStatus() string {
-	if m.AWSMachinePool.Status.LaunchTemplateVersion != nil {
-		return *m.AWSMachinePool.Status.LaunchTemplateVersion
-	} else {
-		return ""
-	}
-}
-
-func (m *MachinePoolScope) SetLaunchTemplateLatestVersionStatus(version string) {
-	m.AWSMachinePool.Status.LaunchTemplateVersion = &version
-}
-
 // IsEKSManaged checks if the AWSMachinePool is EKS managed.
 func (m *MachinePoolScope) IsEKSManaged() bool {
-	return m.InfraCluster.InfraCluster().GetObjectKind().GroupVersionKind().Kind == ekscontrolplanev1.AWSManagedControlPlaneKind
+	return m.InfraCluster.InfraCluster().GetObjectKind().GroupVersionKind().Kind == "AWSManagedControlPlane"
 }
 
 // SubnetIDs returns the machine pool subnet IDs.
@@ -322,7 +291,7 @@ func (m *MachinePoolScope) getNodeStatusByProviderID(ctx context.Context, provid
 		nodeStatusMap[id] = &NodeStatus{}
 	}
 
-	workloadClient, err := remote.NewClusterClient(ctx, "", m.Client, util.ObjectKey(m.Cluster))
+	workloadClient, err := remote.NewClusterClient(ctx, "", m.client, util.ObjectKey(m.Cluster))
 	if err != nil {
 		return nil, err
 	}
@@ -358,19 +327,3 @@ func nodeIsReady(node corev1.Node) bool {
 	}
 	return false
 }
-
-func (m *MachinePoolScope) GetLaunchTemplate() *expinfrav1.AWSLaunchTemplate {
-	return &m.AWSMachinePool.Spec.AWSLaunchTemplate
-}
-
-func (m *MachinePoolScope) GetMachinePool() *expclusterv1.MachinePool {
-	return m.MachinePool
-}
-
-func (m *MachinePoolScope) LaunchTemplateName() string {
-	return m.Name()
-}
-
-func (m *MachinePoolScope) GetRuntimeObject() runtime.Object {
-	return m.AWSMachinePool
-}
diff --git a/pkg/cloud/scope/managednodegroup.go b/pkg/cloud/scope/managednodegroup.go
index bff8e3b64..ca91a7f48 100644
--- a/pkg/cloud/scope/managednodegroup.go
+++ b/pkg/cloud/scope/managednodegroup.go
@@ -23,10 +23,6 @@ import (
 	awsclient "github.com/aws/aws-sdk-go/aws/client"
 	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
-	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/klog/v2/klogr"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
@@ -55,8 +51,6 @@ type ManagedMachinePoolScopeParams struct {
 
 	EnableIAM            bool
 	AllowAdditionalRoles bool
-
-	InfraCluster EC2Scope
 }
 
 // NewManagedMachinePoolScope creates a new Scope from the supplied parameters.
@@ -100,7 +94,6 @@ func NewManagedMachinePoolScope(params ManagedMachinePoolScopeParams) (*ManagedM
 		ControlPlane:         params.ControlPlane,
 		ManagedMachinePool:   params.ManagedMachinePool,
 		MachinePool:          params.MachinePool,
-		EC2Scope:             params.InfraCluster,
 		patchHelper:          helper,
 		session:              session,
 		serviceLimiters:      serviceLimiters,
@@ -113,14 +106,13 @@ func NewManagedMachinePoolScope(params ManagedMachinePoolScopeParams) (*ManagedM
 // ManagedMachinePoolScope defines the basic context for an actuator to operate upon.
 type ManagedMachinePoolScope struct {
 	logr.Logger
-	client.Client
+	Client      client.Client
 	patchHelper *patch.Helper
 
 	Cluster            *clusterv1.Cluster
 	ControlPlane       *ekscontrolplanev1.AWSManagedControlPlane
 	ManagedMachinePool *expinfrav1.AWSManagedMachinePool
 	MachinePool        *expclusterv1.MachinePool
-	EC2Scope           EC2Scope
 
 	session         awsclient.ConfigProvider
 	serviceLimiters throttle.ServiceLimiters
@@ -166,14 +158,11 @@ func (s *ManagedMachinePoolScope) IdentityRef() *infrav1.AWSIdentityReference {
 // AdditionalTags returns AdditionalTags from the scope's ManagedMachinePool
 // The returned value will never be nil.
 func (s *ManagedMachinePoolScope) AdditionalTags() infrav1.Tags {
-	tags := make(infrav1.Tags)
-
-	// Start with the cluster-wide tags...
-	tags.Merge(s.EC2Scope.AdditionalTags())
-	// ... and merge in the Machine's
-	tags.Merge(s.ManagedMachinePool.Spec.AdditionalTags)
+	if s.ManagedMachinePool.Spec.AdditionalTags == nil {
+		s.ManagedMachinePool.Spec.AdditionalTags = infrav1.Tags{}
+	}
 
-	return tags
+	return s.ManagedMachinePool.Spec.AdditionalTags.DeepCopy()
 }
 
 // RoleName returns the node group role name.
@@ -292,87 +281,3 @@ func (s *ManagedMachinePoolScope) KubernetesClusterName() string {
 func (s *ManagedMachinePoolScope) NodegroupName() string {
 	return s.ManagedMachinePool.Spec.EKSNodegroupName
 }
-
-func (s *ManagedMachinePoolScope) Name() string {
-	return s.ManagedMachinePool.Name
-}
-
-func (s *ManagedMachinePoolScope) Namespace() string {
-	return s.ManagedMachinePool.Namespace
-}
-
-func (s *ManagedMachinePoolScope) GetRawBootstrapData() ([]byte, error) {
-	if s.MachinePool.Spec.Template.Spec.Bootstrap.DataSecretName == nil {
-		return nil, errors.New("error retrieving bootstrap data: linked Machine's bootstrap.dataSecretName is nil")
-	}
-
-	secret := &corev1.Secret{}
-	key := types.NamespacedName{Namespace: s.Namespace(), Name: *s.MachinePool.Spec.Template.Spec.Bootstrap.DataSecretName}
-
-	if err := s.Client.Get(context.TODO(), key, secret); err != nil {
-		return nil, errors.Wrapf(err, "failed to retrieve bootstrap data secret for AWSManagedMachinePool %s/%s", s.Namespace(), s.Name())
-	}
-
-	value, ok := secret.Data["value"]
-	if !ok {
-		return nil, errors.New("error retrieving bootstrap data: secret value key is missing")
-	}
-
-	return value, nil
-}
-
-func (s *ManagedMachinePoolScope) GetObjectMeta() *metav1.ObjectMeta {
-	return &s.ManagedMachinePool.ObjectMeta
-}
-
-func (s *ManagedMachinePoolScope) GetSetter() conditions.Setter {
-	return s.ManagedMachinePool
-}
-
-func (s *ManagedMachinePoolScope) GetEC2Scope() EC2Scope {
-	return s.EC2Scope
-}
-
-func (s *ManagedMachinePoolScope) IsEKSManaged() bool {
-	return true
-}
-
-func (s *ManagedMachinePoolScope) GetLaunchTemplateIDStatus() string {
-	if s.ManagedMachinePool.Status.LaunchTemplateID != nil {
-		return *s.ManagedMachinePool.Status.LaunchTemplateID
-	} else {
-		return ""
-	}
-}
-
-func (s *ManagedMachinePoolScope) SetLaunchTemplateIDStatus(id string) {
-	s.ManagedMachinePool.Status.LaunchTemplateID = &id
-}
-
-func (s *ManagedMachinePoolScope) GetLaunchTemplateLatestVersionStatus() string {
-	if s.ManagedMachinePool.Status.LaunchTemplateVersion != nil {
-		return *s.ManagedMachinePool.Status.LaunchTemplateVersion
-	} else {
-		return ""
-	}
-}
-
-func (s *ManagedMachinePoolScope) SetLaunchTemplateLatestVersionStatus(version string) {
-	s.ManagedMachinePool.Status.LaunchTemplateVersion = &version
-}
-
-func (s *ManagedMachinePoolScope) GetLaunchTemplate() *expinfrav1.AWSLaunchTemplate {
-	return s.ManagedMachinePool.Spec.AWSLaunchTemplate
-}
-
-func (s *ManagedMachinePoolScope) GetMachinePool() *expclusterv1.MachinePool {
-	return s.MachinePool
-}
-
-func (s *ManagedMachinePoolScope) LaunchTemplateName() string {
-	return fmt.Sprintf("%s-%s", s.ControlPlane.Name, s.ManagedMachinePool.Name)
-}
-
-func (s *ManagedMachinePoolScope) GetRuntimeObject() runtime.Object {
-	return s.ManagedMachinePool
-}
diff --git a/pkg/cloud/services/ec2/instances.go b/pkg/cloud/services/ec2/instances.go
index dfea944cc..11892679b 100644
--- a/pkg/cloud/services/ec2/instances.go
+++ b/pkg/cloud/services/ec2/instances.go
@@ -409,7 +409,7 @@ func (s *Service) GetCoreSecurityGroups(scope *scope.MachineScope) ([]string, er
 
 // GetCoreNodeSecurityGroups looks up the security group IDs managed by this actuator
 // They are considered "core" to its proper functioning.
-func (s *Service) GetCoreNodeSecurityGroups(scope scope.LaunchTemplateScope) ([]string, error) {
+func (s *Service) GetCoreNodeSecurityGroups(scope *scope.MachinePoolScope) ([]string, error) {
 	// These are common across both controlplane and node machines
 	sgRoles := []infrav1.SecurityGroupRole{
 		infrav1.SecurityGroupNode,
diff --git a/pkg/cloud/services/ec2/launchtemplate.go b/pkg/cloud/services/ec2/launchtemplate.go
index edb6f277b..dd77b7afc 100644
--- a/pkg/cloud/services/ec2/launchtemplate.go
+++ b/pkg/cloud/services/ec2/launchtemplate.go
@@ -18,7 +18,6 @@ package ec2
 
 import (
 	"encoding/base64"
-	"encoding/json"
 	"fmt"
 	"sort"
 	"strconv"
@@ -28,7 +27,6 @@ import (
 	"github.com/aws/aws-sdk-go/service/ec2"
 	"github.com/google/go-cmp/cmp"
 	"github.com/pkg/errors"
-	corev1 "k8s.io/api/core/v1"
 	"k8s.io/utils/pointer"
 
 	infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1"
@@ -36,290 +34,8 @@ import (
 	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/awserrors"
 	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/scope"
 	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/userdata"
-	"sigs.k8s.io/cluster-api-provider-aws/pkg/record"
-	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
-	"sigs.k8s.io/cluster-api/util/conditions"
 )
 
-const (
-	// TagsLastAppliedAnnotation is the key for the AWSMachinePool object annotation
-	// which tracks the tags that the AWSMachinePool actuator is responsible
-	// for. These are the tags that have been handled by the
-	// AdditionalTags in the AWSMachinePool Provider Config.
-	// See https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
-	// for annotation formatting rules.
-	TagsLastAppliedAnnotation = "sigs.k8s.io/cluster-api-provider-aws-last-applied-tags"
-)
-
-func (s *Service) ReconcileLaunchTemplate(
-	scope scope.LaunchTemplateScope,
-	canUpdateLaunchTemplate func() (bool, error),
-	runPostLaunchTemplateUpdateOperation func() error,
-) error {
-	bootstrapData, err := scope.GetRawBootstrapData()
-	if err != nil {
-		record.Eventf(scope.GetMachinePool(), corev1.EventTypeWarning, "FailedGetBootstrapData", err.Error())
-	}
-	bootstrapDataHash := userdata.ComputeHash(bootstrapData)
-
-	ec2svc := NewService(scope.GetEC2Scope())
-
-	scope.Info("checking for existing launch template")
-	launchTemplate, launchTemplateUserDataHash, err := ec2svc.GetLaunchTemplate(scope.LaunchTemplateName())
-	if err != nil {
-		conditions.MarkUnknown(scope.GetSetter(), expinfrav1.LaunchTemplateReadyCondition, expinfrav1.LaunchTemplateNotFoundReason, err.Error())
-		return err
-	}
-
-	imageID, err := ec2svc.DiscoverLaunchTemplateAMI(scope)
-	if err != nil {
-		conditions.MarkFalse(scope.GetSetter(), expinfrav1.LaunchTemplateReadyCondition, expinfrav1.LaunchTemplateCreateFailedReason, clusterv1.ConditionSeverityError, err.Error())
-		return err
-	}
-
-	if launchTemplate == nil {
-		scope.Info("no existing launch template found, creating")
-		launchTemplateID, err := ec2svc.CreateLaunchTemplate(scope, imageID, bootstrapData)
-		if err != nil {
-			conditions.MarkFalse(scope.GetSetter(), expinfrav1.LaunchTemplateReadyCondition, expinfrav1.LaunchTemplateCreateFailedReason, clusterv1.ConditionSeverityError, err.Error())
-			return err
-		}
-
-		scope.SetLaunchTemplateIDStatus(launchTemplateID)
-		return scope.PatchObject()
-	}
-
-	// LaunchTemplateID is set during LaunchTemplate creation, but for a scenario such as `clusterctl move`, status fields become blank.
-	// If launchTemplate already exists but LaunchTemplateID field in the status is empty, get the ID and update the status.
-	if scope.GetLaunchTemplateIDStatus() == "" {
-		launchTemplateID, err := ec2svc.GetLaunchTemplateID(scope.LaunchTemplateName())
-		if err != nil {
-			conditions.MarkUnknown(scope.GetSetter(), expinfrav1.LaunchTemplateReadyCondition, expinfrav1.LaunchTemplateNotFoundReason, err.Error())
-			return err
-		}
-		scope.SetLaunchTemplateIDStatus(launchTemplateID)
-		return scope.PatchObject()
-	}
-
-	if scope.GetLaunchTemplateLatestVersionStatus() == "" {
-		launchTemplateVersion, err := ec2svc.GetLaunchTemplateLatestVersion(scope.GetLaunchTemplateIDStatus())
-		if err != nil {
-			conditions.MarkUnknown(scope.GetSetter(), expinfrav1.LaunchTemplateReadyCondition, expinfrav1.LaunchTemplateNotFoundReason, err.Error())
-			return err
-		}
-		scope.SetLaunchTemplateLatestVersionStatus(launchTemplateVersion)
-		return scope.PatchObject()
-	}
-
-	annotation, err := MachinePoolAnnotationJSON(scope, TagsLastAppliedAnnotation)
-	if err != nil {
-		return err
-	}
-
-	// Check if the instance tags were changed. If they were, create a new LaunchTemplate.
-	tagsChanged, _, _, _ := tagsChanged(annotation, scope.AdditionalTags()) //nolint:dogsled
-
-	needsUpdate, err := ec2svc.LaunchTemplateNeedsUpdate(scope, scope.GetLaunchTemplate(), launchTemplate)
-	if err != nil {
-		return err
-	}
-
-	if needsUpdate || tagsChanged || *imageID != *launchTemplate.AMI.ID {
-		canUpdate, err := canUpdateLaunchTemplate()
-		if err != nil {
-			return err
-		}
-		if !canUpdate {
-			conditions.MarkFalse(scope.GetSetter(), expinfrav1.PreLaunchTemplateUpdateCheckCondition, expinfrav1.PreLaunchTemplateUpdateCheckFailedReason, clusterv1.ConditionSeverityWarning, "")
-			return errors.New("Cannot update the launch template, prerequisite not met")
-		}
-	}
-
-	// Create a new launch template version if there's a difference in configuration, tags,
-	// userdata, OR we've discovered a new AMI ID.
-	if needsUpdate || tagsChanged || *imageID != *launchTemplate.AMI.ID || launchTemplateUserDataHash != bootstrapDataHash {
-		scope.Info("creating new version for launch template", "existing", launchTemplate, "incoming", scope.GetLaunchTemplate())
-		// There is a limit to the number of Launch Template Versions.
-		// We ensure that the number of versions does not grow without bound by following a simple rule: Before we create a new version, we delete one old version, if there is at least one old version that is not in use.
-		if err := ec2svc.PruneLaunchTemplateVersions(scope.GetLaunchTemplateIDStatus()); err != nil {
-			return err
-		}
-		if err := ec2svc.CreateLaunchTemplateVersion(scope.GetLaunchTemplateIDStatus(), scope, imageID, bootstrapData); err != nil {
-			return err
-		}
-		version, err := ec2svc.GetLaunchTemplateLatestVersion(scope.GetLaunchTemplateIDStatus())
-		if err != nil {
-			return err
-		}
-
-		scope.SetLaunchTemplateLatestVersionStatus(version)
-		if err := scope.PatchObject(); err != nil {
-			return err
-		}
-	}
-
-	if needsUpdate || tagsChanged || *imageID != *launchTemplate.AMI.ID {
-		if err := runPostLaunchTemplateUpdateOperation(); err != nil {
-			conditions.MarkFalse(scope.GetSetter(), expinfrav1.PostLaunchTemplateUpdateOperationCondition, expinfrav1.PostLaunchTemplateUpdateOperationFailedReason, clusterv1.ConditionSeverityError, err.Error())
-			return err
-		}
-		conditions.MarkTrue(scope.GetSetter(), expinfrav1.PostLaunchTemplateUpdateOperationCondition)
-	}
-
-	return nil
-}
-
-func (s *Service) ReconcileTags(scope scope.LaunchTemplateScope, resourceServicesToUpdate []scope.ResourceServiceToUpdate) error {
-	additionalTags := scope.AdditionalTags()
-
-	_, err := s.ensureTags(scope, resourceServicesToUpdate, additionalTags)
-	if err != nil {
-		return err
-	}
-	return nil
-}
-
-func (s *Service) ensureTags(scope scope.LaunchTemplateScope, resourceServicesToUpdate []scope.ResourceServiceToUpdate, additionalTags map[string]string) (bool, error) {
-	annotation, err := MachinePoolAnnotationJSON(scope, TagsLastAppliedAnnotation)
-	if err != nil {
-		return false, err
-	}
-
-	// Check if the instance tags were changed. If they were, update them.
-	// It would be possible here to only send new/updated tags, but for the
-	// moment we send everything, even if only a single tag was created or
-	// upated.
-	changed, created, deleted, newAnnotation := tagsChanged(annotation, additionalTags)
-	if changed {
-		for _, resourceServiceToUpdate := range resourceServicesToUpdate {
-			err := resourceServiceToUpdate.ResourceService.UpdateResourceTags(resourceServiceToUpdate.ResourceID, created, deleted)
-			if err != nil {
-				return false, err
-			}
-		}
-
-		// We also need to update the annotation if anything changed.
-		err = UpdateMachinePoolAnnotationJSON(scope, TagsLastAppliedAnnotation, newAnnotation)
-		if err != nil {
-			return false, err
-		}
-	}
-
-	return changed, nil
-}
-
-func MachinePoolAnnotationJSON(lts scope.LaunchTemplateScope, annotation string) (map[string]interface{}, error) {
-	out := map[string]interface{}{}
-
-	jsonAnnotation := machinePoolAnnotation(lts, annotation)
-	if len(jsonAnnotation) == 0 {
-		return out, nil
-	}
-
-	err := json.Unmarshal([]byte(jsonAnnotation), &out)
-	if err != nil {
-		return out, err
-	}
-
-	return out, nil
-}
-
-func machinePoolAnnotation(lts scope.LaunchTemplateScope, annotation string) string {
-	return lts.GetObjectMeta().GetAnnotations()[annotation]
-}
-
-func UpdateMachinePoolAnnotationJSON(lts scope.LaunchTemplateScope, annotation string, content map[string]interface{}) error {
-	b, err := json.Marshal(content)
-	if err != nil {
-		return err
-	}
-
-	updateMachinePoolAnnotation(lts, annotation, string(b))
-	return nil
-}
-
-func updateMachinePoolAnnotation(lts scope.LaunchTemplateScope, annotation, content string) {
-	// Get the annotations
-	annotations := lts.GetObjectMeta().GetAnnotations()
-
-	if annotations == nil {
-		annotations = make(map[string]string)
-	}
-
-	// Set our annotation to the given content.
-	annotations[annotation] = content
-
-	// Update the machine object with these annotations
-	lts.GetObjectMeta().SetAnnotations(annotations)
-}
-
-// tagsChanged determines which tags to delete and which to add.
-func tagsChanged(annotation map[string]interface{}, src map[string]string) (bool, map[string]string, map[string]string, map[string]interface{}) {
-	// Bool tracking if we found any changed state.
-	changed := false
-
-	// Tracking for created/updated
-	created := map[string]string{}
-
-	// Tracking for tags that were deleted.
-	deleted := map[string]string{}
-
-	// The new annotation that we need to set if anything is created/updated.
-	newAnnotation := map[string]interface{}{}
-
-	// Loop over annotation, checking if entries are in src.
-	// If an entry is present in annotation but not src, it has been deleted
-	// since last time. We flag this in the deleted map.
-	for t, v := range annotation {
-		_, ok := src[t]
-
-		// Entry isn't in src, it has been deleted.
-		if !ok {
-			// Cast v to a string here. This should be fine, tags are always
-			// strings.
-			deleted[t] = v.(string)
-			changed = true
-		}
-	}
-
-	// Loop over src, checking for entries in annotation.
-	//
-	// If an entry is in src, but not annotation, it has been created since
-	// last time.
-	//
-	// If an entry is in both src and annotation, we compare their values, if
-	// the value in src differs from that in annotation, the tag has been
-	// updated since last time.
-	for t, v := range src {
-		av, ok := annotation[t]
-
-		// Entries in the src always need to be noted in the newAnnotation. We
-		// know they're going to be created or updated.
-		newAnnotation[t] = v
-
-		// Entry isn't in annotation, it's new.
-		if !ok {
-			created[t] = v
-			newAnnotation[t] = v
-			changed = true
-			continue
-		}
-
-		// Entry is in annotation, has the value changed?
-		if v != av {
-			created[t] = v
-			changed = true
-		}
-
-		// Entry existed in both src and annotation, and their values were
-		// equal. Nothing to do.
-	}
-
-	// We made it through the loop, and everything that was in src, was also
-	// in dst. Nothing changed.
-	return changed, created, deleted, newAnnotation
-}
-
 // GetLaunchTemplate returns the existing LaunchTemplate or nothing if it doesn't exist.
 // For now by name until we need the input to be something different.
 func (s *Service) GetLaunchTemplate(launchTemplateName string) (*expinfrav1.AWSLaunchTemplate, string, error) {
@@ -377,7 +93,7 @@ func (s *Service) GetLaunchTemplateID(launchTemplateName string) (string, error)
 }
 
 // CreateLaunchTemplate generates a launch template to be used with the autoscaling group.
-func (s *Service) CreateLaunchTemplate(scope scope.LaunchTemplateScope, imageID *string, userData []byte) (string, error) {
+func (s *Service) CreateLaunchTemplate(scope *scope.MachinePoolScope, imageID *string, userData []byte) (string, error) {
 	s.scope.Info("Create a new launch template")
 
 	launchTemplateData, err := s.createLaunchTemplateData(scope, imageID, userData)
@@ -387,17 +103,17 @@ func (s *Service) CreateLaunchTemplate(scope scope.LaunchTemplateScope, imageID
 
 	input := &ec2.CreateLaunchTemplateInput{
 		LaunchTemplateData: launchTemplateData,
-		LaunchTemplateName: aws.String(scope.LaunchTemplateName()),
+		LaunchTemplateName: aws.String(scope.Name()),
 	}
 
 	additionalTags := scope.AdditionalTags()
 	// Set the cloud provider tag
-	additionalTags[infrav1.ClusterAWSCloudProviderTagKey(s.scope.KubernetesClusterName())] = string(infrav1.ResourceLifecycleOwned)
+	additionalTags[infrav1.ClusterAWSCloudProviderTagKey(s.scope.Name())] = string(infrav1.ResourceLifecycleOwned)
 
 	tags := infrav1.Build(infrav1.BuildParams{
-		ClusterName: s.scope.KubernetesClusterName(),
+		ClusterName: s.scope.Name(),
 		Lifecycle:   infrav1.ResourceLifecycleOwned,
-		Name:        aws.String(scope.LaunchTemplateName()),
+		Name:        aws.String(scope.Name()),
 		Role:        aws.String("node"),
 		Additional:  additionalTags,
 	})
@@ -421,8 +137,8 @@ func (s *Service) CreateLaunchTemplate(scope scope.LaunchTemplateScope, imageID
 }
 
 // CreateLaunchTemplateVersion will create a launch template.
-func (s *Service) CreateLaunchTemplateVersion(id string, scope scope.LaunchTemplateScope, imageID *string, userData []byte) error {
-	s.scope.V(2).Info("creating new launch template version", "machine-pool", scope.LaunchTemplateName())
+func (s *Service) CreateLaunchTemplateVersion(scope *scope.MachinePoolScope, imageID *string, userData []byte) error {
+	s.scope.V(2).Info("creating new launch template version", "machine-pool", scope.Name())
 
 	launchTemplateData, err := s.createLaunchTemplateData(scope, imageID, userData)
 	if err != nil {
@@ -431,7 +147,7 @@ func (s *Service) CreateLaunchTemplateVersion(id string, scope scope.LaunchTempl
 
 	input := &ec2.CreateLaunchTemplateVersionInput{
 		LaunchTemplateData: launchTemplateData,
-		LaunchTemplateId:   &id,
+		LaunchTemplateId:   aws.String(scope.AWSMachinePool.Status.LaunchTemplateID),
 	}
 
 	_, err = s.EC2Client.CreateLaunchTemplateVersion(input)
@@ -442,8 +158,8 @@ func (s *Service) CreateLaunchTemplateVersion(id string, scope scope.LaunchTempl
 	return nil
 }
 
-func (s *Service) createLaunchTemplateData(scope scope.LaunchTemplateScope, imageID *string, userData []byte) (*ec2.RequestLaunchTemplateData, error) {
-	lt := scope.GetLaunchTemplate()
+func (s *Service) createLaunchTemplateData(scope *scope.MachinePoolScope, imageID *string, userData []byte) (*ec2.RequestLaunchTemplateData, error) {
+	lt := scope.AWSMachinePool.Spec.AWSLaunchTemplate
 
 	// An explicit empty string for SSHKeyName means do not specify a key in the ASG launch
 	var sshKeyNamePtr *string
@@ -453,14 +169,11 @@ func (s *Service) createLaunchTemplateData(scope scope.LaunchTemplateScope, imag
 
 	data := &ec2.RequestLaunchTemplateData{
 		InstanceType: aws.String(lt.InstanceType),
-		KeyName:      sshKeyNamePtr,
-		UserData:     pointer.StringPtr(base64.StdEncoding.EncodeToString(userData)),
-	}
-
-	if len(lt.IamInstanceProfile) > 0 {
-		data.IamInstanceProfile = &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{
+		IamInstanceProfile: &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{
 			Name: aws.String(lt.IamInstanceProfile),
-		}
+		},
+		KeyName:  sshKeyNamePtr,
+		UserData: pointer.StringPtr(base64.StdEncoding.EncodeToString(userData)),
 	}
 
 	ids, err := s.GetCoreNodeSecurityGroups(scope)
@@ -473,7 +186,7 @@ func (s *Service) createLaunchTemplateData(scope scope.LaunchTemplateScope, imag
 	}
 
 	// add additional security groups as well
-	securityGroupIDs, err := s.GetAdditionalSecurityGroupsIDs(scope.GetLaunchTemplate().AdditionalSecurityGroups)
+	securityGroupIDs, err := s.GetAdditionalSecurityGroupsIDs(scope.AWSMachinePool.Spec.AWSLaunchTemplate.AdditionalSecurityGroups)
 	if err != nil {
 		return nil, err
 	}
@@ -482,7 +195,7 @@ func (s *Service) createLaunchTemplateData(scope scope.LaunchTemplateScope, imag
 	// set the AMI ID
 	data.ImageId = imageID
 
-	data.InstanceMarketOptions = getLaunchTemplateInstanceMarketOptionsRequest(scope.GetLaunchTemplate().SpotMarketOptions)
+	data.InstanceMarketOptions = getLaunchTemplateInstanceMarketOptionsRequest(scope.AWSMachinePool.Spec.AWSLaunchTemplate.SpotMarketOptions)
 
 	// Set up root volume
 	if lt.RootVolume != nil {
@@ -585,25 +298,6 @@ func (s *Service) PruneLaunchTemplateVersions(id string) error {
 	return s.deleteLaunchTemplateVersion(id, versionToPrune)
 }
 
-func (s *Service) GetLaunchTemplateLatestVersion(id string) (string, error) {
-	input := &ec2.DescribeLaunchTemplateVersionsInput{
-		LaunchTemplateId: aws.String(id),
-		Versions:         aws.StringSlice([]string{expinfrav1.LaunchTemplateLatestVersion}),
-	}
-
-	out, err := s.EC2Client.DescribeLaunchTemplateVersions(input)
-	if err != nil {
-		s.scope.Info("", "aerr", err.Error())
-		return "", err
-	}
-
-	if len(out.LaunchTemplateVersions) == 0 {
-		return "", errors.Wrapf(err, "failed to get latest launch template version %q", id)
-	}
-
-	return strconv.Itoa(int(*out.LaunchTemplateVersions[0].VersionNumber)), nil
-}
-
 func (s *Service) deleteLaunchTemplateVersion(id string, version *int64) error {
 	s.scope.V(2).Info("Deleting launch template version", "id", id)
 
@@ -634,13 +328,10 @@ func (s *Service) SDKToLaunchTemplate(d *ec2.LaunchTemplateVersion) (*expinfrav1
 		AMI: infrav1.AMIReference{
 			ID: v.ImageId,
 		},
-		InstanceType:  aws.StringValue(v.InstanceType),
-		SSHKeyName:    v.KeyName,
-		VersionNumber: d.VersionNumber,
-	}
-
-	if v.IamInstanceProfile != nil {
-		i.IamInstanceProfile = aws.StringValue(v.IamInstanceProfile.Name)
+		IamInstanceProfile: aws.StringValue(v.IamInstanceProfile.Name),
+		InstanceType:       aws.StringValue(v.InstanceType),
+		SSHKeyName:         v.KeyName,
+		VersionNumber:      d.VersionNumber,
 	}
 
 	// Extract IAM Instance Profile name from ARN
@@ -673,7 +364,7 @@ func (s *Service) SDKToLaunchTemplate(d *ec2.LaunchTemplateVersion) (*expinfrav1
 //
 // FIXME(dlipovetsky): This check should account for changed userdata, but does not yet do so.
 // Although userdata is stored in an EC2 Launch Template, it is not a field of AWSLaunchTemplate.
-func (s *Service) LaunchTemplateNeedsUpdate(scope scope.LaunchTemplateScope, incoming *expinfrav1.AWSLaunchTemplate, existing *expinfrav1.AWSLaunchTemplate) (bool, error) {
+func (s *Service) LaunchTemplateNeedsUpdate(scope *scope.MachinePoolScope, incoming *expinfrav1.AWSLaunchTemplate, existing *expinfrav1.AWSLaunchTemplate) (bool, error) {
 	if incoming.IamInstanceProfile != existing.IamInstanceProfile {
 		return true, nil
 	}
@@ -708,14 +399,14 @@ func (s *Service) LaunchTemplateNeedsUpdate(scope scope.LaunchTemplateScope, inc
 }
 
 // DiscoverLaunchTemplateAMI will discover the AMI launch template.
-func (s *Service) DiscoverLaunchTemplateAMI(scope scope.LaunchTemplateScope) (*string, error) {
-	lt := scope.GetLaunchTemplate()
+func (s *Service) DiscoverLaunchTemplateAMI(scope *scope.MachinePoolScope) (*string, error) {
+	lt := scope.AWSMachinePool.Spec.AWSLaunchTemplate
 
 	if lt.AMI.ID != nil {
 		return lt.AMI.ID, nil
 	}
 
-	if scope.GetMachinePool().Spec.Template.Spec.Version == nil {
+	if scope.MachinePool.Spec.Template.Spec.Version == nil {
 		err := errors.New("Either AWSMachinePool's spec.awslaunchtemplate.ami.id or MachinePool's spec.template.spec.version must be defined")
 		s.scope.Error(err, "")
 		return nil, err
@@ -726,26 +417,26 @@ func (s *Service) DiscoverLaunchTemplateAMI(scope scope.LaunchTemplateScope) (*s
 
 	imageLookupFormat := lt.ImageLookupFormat
 	if imageLookupFormat == "" {
-		imageLookupFormat = scope.GetEC2Scope().ImageLookupFormat()
+		imageLookupFormat = scope.InfraCluster.ImageLookupFormat()
 	}
 
 	imageLookupOrg := lt.ImageLookupOrg
 	if imageLookupOrg == "" {
-		imageLookupOrg = scope.GetEC2Scope().ImageLookupOrg()
+		imageLookupOrg = scope.InfraCluster.ImageLookupOrg()
 	}
 
 	imageLookupBaseOS := lt.ImageLookupBaseOS
 	if imageLookupBaseOS == "" {
-		imageLookupBaseOS = scope.GetEC2Scope().ImageLookupBaseOS()
+		imageLookupBaseOS = scope.InfraCluster.ImageLookupBaseOS()
 	}
 
 	if scope.IsEKSManaged() && imageLookupFormat == "" && imageLookupOrg == "" && imageLookupBaseOS == "" {
-		lookupAMI, err = s.eksAMILookup(*scope.GetMachinePool().Spec.Template.Spec.Version, scope.GetLaunchTemplate().AMI.EKSOptimizedLookupType)
+		lookupAMI, err = s.eksAMILookup(*scope.MachinePool.Spec.Template.Spec.Version, scope.AWSMachinePool.Spec.AWSLaunchTemplate.AMI.EKSOptimizedLookupType)
 		if err != nil {
 			return nil, err
 		}
 	} else {
-		lookupAMI, err = s.defaultAMIIDLookup(imageLookupFormat, imageLookupOrg, imageLookupBaseOS, *scope.GetMachinePool().Spec.Template.Spec.Version)
+		lookupAMI, err = s.defaultAMIIDLookup(imageLookupFormat, imageLookupOrg, imageLookupBaseOS, *scope.MachinePool.Spec.Template.Spec.Version)
 		if err != nil {
 			return nil, err
 		}
@@ -773,16 +464,16 @@ func (s *Service) GetAdditionalSecurityGroupsIDs(securityGroups []infrav1.AWSRes
 	return additionalSecurityGroupsIDs, nil
 }
 
-func (s *Service) buildLaunchTemplateTagSpecificationRequest(scope scope.LaunchTemplateScope) []*ec2.LaunchTemplateTagSpecificationRequest {
+func (s *Service) buildLaunchTemplateTagSpecificationRequest(scope *scope.MachinePoolScope) []*ec2.LaunchTemplateTagSpecificationRequest {
 	tagSpecifications := make([]*ec2.LaunchTemplateTagSpecificationRequest, 0)
 	additionalTags := scope.AdditionalTags()
 	// Set the cloud provider tag
-	additionalTags[infrav1.ClusterAWSCloudProviderTagKey(s.scope.KubernetesClusterName())] = string(infrav1.ResourceLifecycleOwned)
+	additionalTags[infrav1.ClusterAWSCloudProviderTagKey(s.scope.Name())] = string(infrav1.ResourceLifecycleOwned)
 
 	tags := infrav1.Build(infrav1.BuildParams{
-		ClusterName: s.scope.KubernetesClusterName(),
+		ClusterName: s.scope.Name(),
 		Lifecycle:   infrav1.ResourceLifecycleOwned,
-		Name:        aws.String(scope.LaunchTemplateName()),
+		Name:        aws.String(scope.Name()),
 		Role:        aws.String("node"),
 		Additional:  additionalTags,
 	})
diff --git a/pkg/cloud/services/eks/cluster.go b/pkg/cloud/services/eks/cluster.go
index 896aa34ac..00afedae1 100644
--- a/pkg/cloud/services/eks/cluster.go
+++ b/pkg/cloud/services/eks/cluster.go
@@ -59,10 +59,10 @@ func (s *Service) reconcileCluster(ctx context.Context) error {
 			return errors.Wrap(err, "failed to create cluster")
 		}
 	} else {
-		tagKey := infrav1.ClusterAWSCloudProviderTagKey(eksClusterName)
+		tagKey := infrav1.ClusterAWSCloudProviderTagKey(s.scope.KubernetesClusterName())
 		ownedTag := cluster.Tags[tagKey]
 		if ownedTag == nil {
-			return fmt.Errorf("checking owner of %s is %s: %w", s.scope.KubernetesClusterName(), eksClusterName, err)
+			return fmt.Errorf("checking owner of %s is %s: %w", s.scope.KubernetesClusterName(), s.scope.Name(), err)
 		}
 
 		s.scope.V(2).Info("Found owned EKS cluster in AWS", "cluster-name", eksClusterName)
diff --git a/pkg/cloud/services/eks/nodegroup.go b/pkg/cloud/services/eks/nodegroup.go
index 3b3df02b3..a4edbbbd7 100644
--- a/pkg/cloud/services/eks/nodegroup.go
+++ b/pkg/cloud/services/eks/nodegroup.go
@@ -217,7 +217,7 @@ func (s *NodegroupService) createNodegroup() (*eks.Nodegroup, error) {
 		RemoteAccess:  remoteAccess,
 		UpdateConfig:  s.updateConfig(),
 	}
-	if managedPool.AMIType != nil && (managedPool.AWSLaunchTemplate == nil || managedPool.AWSLaunchTemplate.AMI.ID == nil) {
+	if managedPool.AMIType != nil {
 		input.AmiType = aws.String(string(*managedPool.AMIType))
 	}
 	if managedPool.DiskSize != nil {
@@ -241,12 +241,6 @@ func (s *NodegroupService) createNodegroup() (*eks.Nodegroup, error) {
 		}
 		input.CapacityType = aws.String(capacityType)
 	}
-	if managedPool.AWSLaunchTemplate != nil {
-		input.LaunchTemplate = &eks.LaunchTemplateSpecification{
-			Id:      s.scope.ManagedMachinePool.Status.LaunchTemplateID,
-			Version: s.scope.ManagedMachinePool.Status.LaunchTemplateVersion,
-		}
-	}
 
 	if err := input.Validate(); err != nil {
 		return nil, errors.Wrap(err, "created invalid CreateNodegroupInput")
@@ -331,14 +325,9 @@ func (s *NodegroupService) reconcileNodegroupVersion(ng *eks.Nodegroup) error {
 	ngVersion := version.MustParseGeneric(*ng.Version)
 	specAMI := s.scope.ManagedMachinePool.Spec.AMIVersion
 	ngAMI := *ng.ReleaseVersion
-	statusLaunchTemplateVersion := s.scope.ManagedMachinePool.Status.LaunchTemplateVersion
-	var ngLaunchTemplateVersion *string
-	if ng.LaunchTemplate != nil {
-		ngLaunchTemplateVersion = ng.LaunchTemplate.Version
-	}
 
 	eksClusterName := s.scope.KubernetesClusterName()
-	if (specVersion != nil && ngVersion.LessThan(specVersion)) || (specAMI != nil && *specAMI != ngAMI) || (statusLaunchTemplateVersion != nil && *statusLaunchTemplateVersion != *ngLaunchTemplateVersion) {
+	if (specVersion != nil && ngVersion.LessThan(specVersion)) || (specAMI != nil && *specAMI != ngAMI) {
 		input := &eks.UpdateNodegroupVersionInput{
 			ClusterName:   aws.String(eksClusterName),
 			NodegroupName: aws.String(s.scope.NodegroupName()),
@@ -346,21 +335,14 @@ func (s *NodegroupService) reconcileNodegroupVersion(ng *eks.Nodegroup) error {
 
 		var updateMsg string
 		// Either update k8s version or AMI version
-		switch {
-		case specVersion != nil && ngVersion.LessThan(specVersion):
+		if specVersion != nil && ngVersion.LessThan(specVersion) {
 			// NOTE: you can only upgrade increments of minor versions. If you want to upgrade 1.14 to 1.16 we
 			// need to go 1.14-> 1.15 and then 1.15 -> 1.16.
 			input.Version = aws.String(versionToEKS(ngVersion.WithMinor(ngVersion.Minor() + 1)))
 			updateMsg = fmt.Sprintf("to version %s", *input.Version)
-		case specAMI != nil && *specAMI != ngAMI:
+		} else if specAMI != nil && *specAMI != ngAMI {
 			input.ReleaseVersion = specAMI
 			updateMsg = fmt.Sprintf("to AMI version %s", *input.ReleaseVersion)
-		case statusLaunchTemplateVersion != nil && *statusLaunchTemplateVersion != *ngLaunchTemplateVersion:
-			input.LaunchTemplate = &eks.LaunchTemplateSpecification{
-				Id:      s.scope.ManagedMachinePool.Status.LaunchTemplateID,
-				Version: statusLaunchTemplateVersion,
-			}
-			updateMsg = fmt.Sprintf("to launch template version %s", *statusLaunchTemplateVersion)
 		}
 
 		if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) {
diff --git a/pkg/cloud/services/eks/tags.go b/pkg/cloud/services/eks/tags.go
index 290a1eaf0..e27e474b6 100644
--- a/pkg/cloud/services/eks/tags.go
+++ b/pkg/cloud/services/eks/tags.go
@@ -52,7 +52,7 @@ func (s *Service) getEKSTagParams(id string) *infrav1.BuildParams {
 	name := s.scope.KubernetesClusterName()
 
 	return &infrav1.BuildParams{
-		ClusterName: name,
+		ClusterName: s.scope.Name(),
 		ResourceID:  id,
 		Lifecycle:   infrav1.ResourceLifecycleOwned,
 		Name:        aws.String(name),
diff --git a/pkg/cloud/services/interfaces.go b/pkg/cloud/services/interfaces.go
index 1f2994816..080acea34 100644
--- a/pkg/cloud/services/interfaces.go
+++ b/pkg/cloud/services/interfaces.go
@@ -59,18 +59,14 @@ type EC2Interface interface {
 	TerminateInstanceAndWait(instanceID string) error
 	DetachSecurityGroupsFromNetworkInterface(groups []string, interfaceID string) error
 
-	ReconcileLaunchTemplate(scope scope.LaunchTemplateScope, canUpdateLaunchTemplate func() (bool, error), runPostLaunchTemplateUpdateOperation func() error) error
-	ReconcileTags(scope scope.LaunchTemplateScope, resourceServicesToUpdate []scope.ResourceServiceToUpdate) error
-
-	DiscoverLaunchTemplateAMI(scope scope.LaunchTemplateScope) (*string, error)
+	DiscoverLaunchTemplateAMI(scope *scope.MachinePoolScope) (*string, error)
 	GetLaunchTemplate(id string) (lt *expinfrav1.AWSLaunchTemplate, userDataHash string, err error)
 	GetLaunchTemplateID(id string) (string, error)
-	GetLaunchTemplateLatestVersion(id string) (string, error)
-	CreateLaunchTemplate(scope scope.LaunchTemplateScope, imageID *string, userData []byte) (string, error)
-	CreateLaunchTemplateVersion(id string, scope scope.LaunchTemplateScope, imageID *string, userData []byte) error
+	CreateLaunchTemplate(scope *scope.MachinePoolScope, imageID *string, userData []byte) (string, error)
+	CreateLaunchTemplateVersion(scope *scope.MachinePoolScope, imageID *string, userData []byte) error
 	PruneLaunchTemplateVersions(id string) error
 	DeleteLaunchTemplate(id string) error
-	LaunchTemplateNeedsUpdate(scope scope.LaunchTemplateScope, incoming *expinfrav1.AWSLaunchTemplate, existing *expinfrav1.AWSLaunchTemplate) (bool, error)
+	LaunchTemplateNeedsUpdate(scope *scope.MachinePoolScope, incoming *expinfrav1.AWSLaunchTemplate, existing *expinfrav1.AWSLaunchTemplate) (bool, error)
 	DeleteBastion() error
 	ReconcileBastion() error
 }
diff --git a/pkg/cloud/services/mock_services/ec2_interface_mock.go b/pkg/cloud/services/mock_services/ec2_interface_mock.go
index 46ba71bc7..a663f7b73 100644
--- a/pkg/cloud/services/mock_services/ec2_interface_mock.go
+++ b/pkg/cloud/services/mock_services/ec2_interface_mock.go
@@ -68,7 +68,7 @@ func (mr *MockEC2InterfaceMockRecorder) CreateInstance(arg0, arg1, arg2 interfac
 }
 
 // CreateLaunchTemplate mocks base method.
-func (m *MockEC2Interface) CreateLaunchTemplate(arg0 scope.LaunchTemplateScope, arg1 *string, arg2 []byte) (string, error) {
+func (m *MockEC2Interface) CreateLaunchTemplate(arg0 *scope.MachinePoolScope, arg1 *string, arg2 []byte) (string, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "CreateLaunchTemplate", arg0, arg1, arg2)
 	ret0, _ := ret[0].(string)
@@ -83,17 +83,17 @@ func (mr *MockEC2InterfaceMockRecorder) CreateLaunchTemplate(arg0, arg1, arg2 in
 }
 
 // CreateLaunchTemplateVersion mocks base method.
-func (m *MockEC2Interface) CreateLaunchTemplateVersion(arg0 string, arg1 scope.LaunchTemplateScope, arg2 *string, arg3 []byte) error {
+func (m *MockEC2Interface) CreateLaunchTemplateVersion(arg0 *scope.MachinePoolScope, arg1 *string, arg2 []byte) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "CreateLaunchTemplateVersion", arg0, arg1, arg2, arg3)
+	ret := m.ctrl.Call(m, "CreateLaunchTemplateVersion", arg0, arg1, arg2)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
 // CreateLaunchTemplateVersion indicates an expected call of CreateLaunchTemplateVersion.
-func (mr *MockEC2InterfaceMockRecorder) CreateLaunchTemplateVersion(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+func (mr *MockEC2InterfaceMockRecorder) CreateLaunchTemplateVersion(arg0, arg1, arg2 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLaunchTemplateVersion", reflect.TypeOf((*MockEC2Interface)(nil).CreateLaunchTemplateVersion), arg0, arg1, arg2, arg3)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLaunchTemplateVersion", reflect.TypeOf((*MockEC2Interface)(nil).CreateLaunchTemplateVersion), arg0, arg1, arg2)
 }
 
 // DeleteBastion mocks base method.
@@ -139,7 +139,7 @@ func (mr *MockEC2InterfaceMockRecorder) DetachSecurityGroupsFromNetworkInterface
 }
 
 // DiscoverLaunchTemplateAMI mocks base method.
-func (m *MockEC2Interface) DiscoverLaunchTemplateAMI(arg0 scope.LaunchTemplateScope) (*string, error) {
+func (m *MockEC2Interface) DiscoverLaunchTemplateAMI(arg0 *scope.MachinePoolScope) (*string, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "DiscoverLaunchTemplateAMI", arg0)
 	ret0, _ := ret[0].(*string)
@@ -229,21 +229,6 @@ func (mr *MockEC2InterfaceMockRecorder) GetLaunchTemplateID(arg0 interface{}) *g
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLaunchTemplateID", reflect.TypeOf((*MockEC2Interface)(nil).GetLaunchTemplateID), arg0)
 }
 
-// GetLaunchTemplateLatestVersion mocks base method.
-func (m *MockEC2Interface) GetLaunchTemplateLatestVersion(arg0 string) (string, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetLaunchTemplateLatestVersion", arg0)
-	ret0, _ := ret[0].(string)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// GetLaunchTemplateLatestVersion indicates an expected call of GetLaunchTemplateLatestVersion.
-func (mr *MockEC2InterfaceMockRecorder) GetLaunchTemplateLatestVersion(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLaunchTemplateLatestVersion", reflect.TypeOf((*MockEC2Interface)(nil).GetLaunchTemplateLatestVersion), arg0)
-}
-
 // GetRunningInstanceByTags mocks base method.
 func (m *MockEC2Interface) GetRunningInstanceByTags(arg0 *scope.MachineScope) (*v1beta1.Instance, error) {
 	m.ctrl.T.Helper()
@@ -275,7 +260,7 @@ func (mr *MockEC2InterfaceMockRecorder) InstanceIfExists(arg0 interface{}) *gomo
 }
 
 // LaunchTemplateNeedsUpdate mocks base method.
-func (m *MockEC2Interface) LaunchTemplateNeedsUpdate(arg0 scope.LaunchTemplateScope, arg1, arg2 *v1beta10.AWSLaunchTemplate) (bool, error) {
+func (m *MockEC2Interface) LaunchTemplateNeedsUpdate(arg0 *scope.MachinePoolScope, arg1, arg2 *v1beta10.AWSLaunchTemplate) (bool, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "LaunchTemplateNeedsUpdate", arg0, arg1, arg2)
 	ret0, _ := ret[0].(bool)
@@ -317,34 +302,6 @@ func (mr *MockEC2InterfaceMockRecorder) ReconcileBastion() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReconcileBastion", reflect.TypeOf((*MockEC2Interface)(nil).ReconcileBastion))
 }
 
-// ReconcileLaunchTemplate mocks base method.
-func (m *MockEC2Interface) ReconcileLaunchTemplate(arg0 scope.LaunchTemplateScope, arg1 func() (bool, error), arg2 func() error) error {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "ReconcileLaunchTemplate", arg0, arg1, arg2)
-	ret0, _ := ret[0].(error)
-	return ret0
-}
-
-// ReconcileLaunchTemplate indicates an expected call of ReconcileLaunchTemplate.
-func (mr *MockEC2InterfaceMockRecorder) ReconcileLaunchTemplate(arg0, arg1, arg2 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReconcileLaunchTemplate", reflect.TypeOf((*MockEC2Interface)(nil).ReconcileLaunchTemplate), arg0, arg1, arg2)
-}
-
-// ReconcileTags mocks base method.
-func (m *MockEC2Interface) ReconcileTags(arg0 scope.LaunchTemplateScope, arg1 []scope.ResourceServiceToUpdate) error {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "ReconcileTags", arg0, arg1)
-	ret0, _ := ret[0].(error)
-	return ret0
-}
-
-// ReconcileTags indicates an expected call of ReconcileTags.
-func (mr *MockEC2InterfaceMockRecorder) ReconcileTags(arg0, arg1 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReconcileTags", reflect.TypeOf((*MockEC2Interface)(nil).ReconcileTags), arg0, arg1)
-}
-
 // TerminateInstance mocks base method.
 func (m *MockEC2Interface) TerminateInstance(arg0 string) error {
 	m.ctrl.T.Helper()


Test patch:
diff --git a/exp/controllers/awsmachinepool_controller_test.go b/exp/controllers/awsmachinepool_controller_test.go
index ea9c82240..138e022d4 100644
--- a/exp/controllers/awsmachinepool_controller_test.go
+++ b/exp/controllers/awsmachinepool_controller_test.go
@@ -188,8 +188,6 @@ func TestAWSMachinePoolReconciler(t *testing.T) {
 				defer teardown(t, g)
 				getASG(t, g)
 
-				ec2Svc.EXPECT().ReconcileLaunchTemplate(gomock.Any(), gomock.Any(), gomock.Any())
-
 				_, _ = reconciler.reconcileNormal(context.Background(), ms, cs, cs)
 
 				g.Expect(ms.AWSMachinePool.Finalizers).To(ContainElement(expinfrav1.MachinePoolFinalizer))
@@ -244,7 +242,23 @@ func TestAWSMachinePoolReconciler(t *testing.T) {
 				setProviderID(t, g)
 
 				expectedErr := errors.New("no connection available ")
-				ec2Svc.EXPECT().ReconcileLaunchTemplate(gomock.Any(), gomock.Any(), gomock.Any()).Return(expectedErr)
+				var launchtemplate *expinfrav1.AWSLaunchTemplate
+				ec2Svc.EXPECT().GetLaunchTemplate(gomock.Any()).Return(launchtemplate, "", expectedErr)
+				_, err := reconciler.reconcileNormal(context.Background(), ms, cs, cs)
+				g.Expect(errors.Cause(err)).To(MatchError(expectedErr))
+			})
+			t.Run("should try to create a new machinepool if none exists", func(t *testing.T) {
+				g := NewWithT(t)
+				setup(t, g)
+				defer teardown(t, g)
+				setProviderID(t, g)
+
+				expectedErr := errors.New("Invalid instance")
+				asgSvc.EXPECT().ASGIfExists(gomock.Any()).Return(nil, nil).AnyTimes()
+				ec2Svc.EXPECT().GetLaunchTemplate(gomock.Any()).Return(nil, "", nil)
+				ec2Svc.EXPECT().DiscoverLaunchTemplateAMI(gomock.Any()).Return(nil, nil)
+				ec2Svc.EXPECT().CreateLaunchTemplate(gomock.Any(), gomock.Any(), gomock.Any()).Return("", expectedErr).AnyTimes()
+
 				_, err := reconciler.reconcileNormal(context.Background(), ms, cs, cs)
 				g.Expect(errors.Cause(err)).To(MatchError(expectedErr))
 			})
diff --git a/pkg/cloud/services/ec2/launchtemplate_test.go b/pkg/cloud/services/ec2/launchtemplate_test.go
index 2ed7d9f9d..bbfbe7fc9 100644
--- a/pkg/cloud/services/ec2/launchtemplate_test.go
+++ b/pkg/cloud/services/ec2/launchtemplate_test.go
@@ -1083,10 +1083,10 @@ func TestCreateLaunchTemplateVersion(t *testing.T) {
 			cs, err := setupClusterScope(client)
 			g.Expect(err).NotTo(HaveOccurred())
 
-			ms, err := setupMachinePoolScope(client, cs)
+			mpScope, err := setupMachinePoolScope(client, cs)
 			g.Expect(err).NotTo(HaveOccurred())
 
-			ms.AWSMachinePool.Spec.AWSLaunchTemplate.AdditionalSecurityGroups = tc.awsResourceReference
+			mpScope.AWSMachinePool.Spec.AWSLaunchTemplate.AdditionalSecurityGroups = tc.awsResourceReference
 
 			mockEC2Client := mocks.NewMockEC2API(mockCtrl)
 			s := NewService(cs)
@@ -1096,10 +1096,10 @@ func TestCreateLaunchTemplateVersion(t *testing.T) {
 				tc.expect(mockEC2Client.EXPECT())
 			}
 			if tc.wantErr {
-				g.Expect(s.CreateLaunchTemplateVersion("launch-template-id", ms, aws.String("imageID"), userData)).To(HaveOccurred())
+				g.Expect(s.CreateLaunchTemplateVersion(mpScope, aws.String("imageID"), userData)).To(HaveOccurred())
 				return
 			}
-			g.Expect(s.CreateLaunchTemplateVersion("launch-template-id", ms, aws.String("imageID"), userData)).NotTo(HaveOccurred())
+			g.Expect(s.CreateLaunchTemplateVersion(mpScope, aws.String("imageID"), userData)).NotTo(HaveOccurred())
 		})
 	}
 }
@@ -1144,11 +1144,11 @@ func TestBuildLaunchTemplateTagSpecificationRequest(t *testing.T) {
 			cs, err := setupClusterScope(client)
 			g.Expect(err).NotTo(HaveOccurred())
 
-			ms, err := setupMachinePoolScope(client, cs)
+			mpScope, err := setupMachinePoolScope(client, cs)
 			g.Expect(err).NotTo(HaveOccurred())
 
 			s := NewService(cs)
-			tc.check(g, s.buildLaunchTemplateTagSpecificationRequest(ms))
+			tc.check(g, s.buildLaunchTemplateTagSpecificationRequest(mpScope))
 		})
 	}
 }
diff --git a/pkg/internal/cidr/cidr_test.go b/pkg/internal/cidr/cidr_test.go
index a226d37bb..df1bed88d 100644
--- a/pkg/internal/cidr/cidr_test.go
+++ b/pkg/internal/cidr/cidr_test.go
@@ -14,84 +14,15 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package cidr
+package cidr_test
 
 import (
-	"net"
 	"testing"
 
 	. "github.com/onsi/gomega"
-)
-
-func TestSplitIntoSubnetsIPv4(t *testing.T) {
-	RegisterTestingT(t)
-	tests := []struct {
-		name        string
-		cidrblock   string
-		subnetcount int
-		expected    []*net.IPNet
-	}{
-		{
-			// https://aws.amazon.com/about-aws/whats-new/2018/10/amazon-eks-now-supports-additional-vpc-cidr-blocks/
-			name:        "default secondary cidr block configuration with primary cidr",
-			cidrblock:   "100.64.0.0/16",
-			subnetcount: 3,
-			expected: []*net.IPNet{
-				{
-					IP:   net.IPv4(100, 64, 0, 0).To4(),
-					Mask: net.IPv4Mask(255, 255, 192, 0),
-				},
-				{
-					IP:   net.IPv4(100, 64, 64, 0).To4(),
-					Mask: net.IPv4Mask(255, 255, 192, 0),
-				},
-				{
-					IP:   net.IPv4(100, 64, 128, 0).To4(),
-					Mask: net.IPv4Mask(255, 255, 192, 0),
-				},
-			},
-		},
-		{
-			// https://aws.amazon.com/about-aws/whats-new/2018/10/amazon-eks-now-supports-additional-vpc-cidr-blocks/
-			name:        "default secondary cidr block configuration with alternative cidr",
-			cidrblock:   "198.19.0.0/16",
-			subnetcount: 3,
-			expected: []*net.IPNet{
-				{
-					IP:   net.IPv4(198, 19, 0, 0).To4(),
-					Mask: net.IPv4Mask(255, 255, 192, 0),
-				},
-				{
-					IP:   net.IPv4(198, 19, 64, 0).To4(),
-					Mask: net.IPv4Mask(255, 255, 192, 0),
-				},
-				{
-					IP:   net.IPv4(198, 19, 128, 0).To4(),
-					Mask: net.IPv4Mask(255, 255, 192, 0),
-				},
-			},
-		},
-		{
-			name:        "slash 16 cidr with one subnet",
-			cidrblock:   "1.1.0.0/16",
-			subnetcount: 1,
-			expected: []*net.IPNet{
-				{
-					IP:   net.IPv4(1, 1, 0, 0).To4(),
-					Mask: net.IPv4Mask(255, 255, 0, 0),
-				},
-			},
-		},
-	}
 
-	for _, tc := range tests {
-		t.Run(tc.name, func(t *testing.T) {
-			output, err := SplitIntoSubnetsIPv4(tc.cidrblock, tc.subnetcount)
-			Expect(err).NotTo(HaveOccurred())
-			Expect(output).To(ConsistOf(tc.expected))
-		})
-	}
-}
+	"sigs.k8s.io/cluster-api-provider-aws/pkg/internal/cidr"
+)
 
 func TestParseIPv4CIDR(t *testing.T) {
 	RegisterTestingT(t)
@@ -102,7 +33,7 @@ func TestParseIPv4CIDR(t *testing.T) {
 		"193.168.3.20/7",
 	}
 
-	output, err := GetIPv4Cidrs(input)
+	output, err := cidr.GetIPv4Cidrs(input)
 	Expect(err).NotTo(HaveOccurred())
 	Expect(output).To(HaveLen(1))
 }
@@ -116,7 +47,7 @@ func TestParseIPv6CIDR(t *testing.T) {
 		"193.168.3.20/7",
 	}
 
-	output, err := GetIPv6Cidrs(input)
+	output, err := cidr.GetIPv6Cidrs(input)
 	Expect(err).NotTo(HaveOccurred())
 	Expect(output).To(HaveLen(2))
 }
diff --git a/test/e2e/data/e2e_eks_conf.yaml b/test/e2e/data/e2e_eks_conf.yaml
index 63083226e..57c4904f0 100644
--- a/test/e2e/data/e2e_eks_conf.yaml
+++ b/test/e2e/data/e2e_eks_conf.yaml
@@ -140,8 +140,6 @@ providers:
         targetName: "cluster-template-eks-managed-machinepool-only.yaml"
       - sourcePath: "./eks/cluster-template-eks-machinepool-only.yaml"
         targetName: "cluster-template-eks-machinepool-only.yaml"
-      - sourcePath: "./eks/cluster-template-eks-managed-machinepool-with-launch-template-only.yaml"
-        targetName: "cluster-template-eks-managed-machinepool-with-launch-template-only.yaml"
       - sourcePath: "./eks/cluster-template-eks-managedmachinepool.yaml"
         targetName: "cluster-template-eks-managedmachinepool.yaml"
 
diff --git a/test/e2e/data/eks/cluster-template-eks-managed-machinepool-with-launch-template-only.yaml b/test/e2e/data/eks/cluster-template-eks-managed-machinepool-with-launch-template-only.yaml
deleted file mode 100644
index 39a7bf93a..000000000
--- a/test/e2e/data/eks/cluster-template-eks-managed-machinepool-with-launch-template-only.yaml
+++ /dev/null
@@ -1,37 +0,0 @@
-apiVersion: cluster.x-k8s.io/v1beta1
-kind: MachinePool
-metadata:
-  name: "${CLUSTER_NAME}-pool-lt-0"
-spec:
-  clusterName: "${CLUSTER_NAME}"
-  replicas: ${WORKER_MACHINE_COUNT}
-  template:
-    spec:
-      version: "${KUBERNETES_VERSION}"
-      clusterName: "${CLUSTER_NAME}"
-      bootstrap:
-        dataSecretName: "${CLUSTER_NAME}-pool-lt-0-userdata"
-      infrastructureRef:
-        name: "${CLUSTER_NAME}-pool-lt-0"
-        apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
-        kind: AWSManagedMachinePool
----
-apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
-kind: AWSManagedMachinePool
-metadata:
-  name: "${CLUSTER_NAME}-pool-lt-0"
-spec:
-  amiType: CUSTOM
-  awsLaunchTemplate:
-    ami: {}
-  scaling:
-    minSize: 1
-    maxSize: 2
----
-apiVersion: v1
-kind: Secret
-metadata:
-  name: "${CLUSTER_NAME}-pool-lt-0-userdata"
-data:
-  value: "USER_DATA"
-type: Opaque
diff --git a/test/e2e/shared/template.go b/test/e2e/shared/template.go
index 89cb49635..efe08932b 100644
--- a/test/e2e/shared/template.go
+++ b/test/e2e/shared/template.go
@@ -198,13 +198,6 @@ func getBootstrapTemplate(e2eCtx *E2EContext) *cfn_bootstrap.Template {
 
 // ApplyTemplate will render a cluster template and apply it to the management cluster.
 func ApplyTemplate(ctx context.Context, configCluster clusterctl.ConfigClusterInput, clusterProxy framework.ClusterProxy) error {
-	workloadClusterTemplate := GetTemplate(ctx, configCluster)
-	Byf("Applying the %s cluster template yaml to the cluster", configCluster.Flavor)
-	return clusterProxy.Apply(ctx, workloadClusterTemplate)
-}
-
-// GetTemplate will render a cluster template.
-func GetTemplate(ctx context.Context, configCluster clusterctl.ConfigClusterInput) []byte {
 	Expect(ctx).NotTo(BeNil(), "ctx is required for ApplyClusterTemplateAndWait")
 
 	Byf("Getting the cluster template yaml")
@@ -222,5 +215,6 @@ func GetTemplate(ctx context.Context, configCluster clusterctl.ConfigClusterInpu
 	})
 	Expect(workloadClusterTemplate).ToNot(BeNil(), "Failed to get the cluster template")
 
-	return workloadClusterTemplate
+	Byf("Applying the %s cluster template yaml to the cluster", configCluster.Flavor)
+	return clusterProxy.Apply(ctx, workloadClusterTemplate)
 }
diff --git a/test/e2e/suites/managed/cluster.go b/test/e2e/suites/managed/cluster.go
index 47cc6a610..74427ef70 100644
--- a/test/e2e/suites/managed/cluster.go
+++ b/test/e2e/suites/managed/cluster.go
@@ -83,14 +83,14 @@ func ManagedClusterSpec(ctx context.Context, inputGetter func() ManagedClusterSp
 
 	shared.Byf("Checking EKS cluster is active")
 	eksClusterName := getEKSClusterName(input.Namespace.Name, input.ClusterName)
-	verifyClusterActiveAndOwned(eksClusterName, input.AWSSession)
+	verifyClusterActiveAndOwned(eksClusterName, input.ClusterName, input.AWSSession)
 
 	if input.CluserSpecificRoles {
 		ginkgo.By("Checking that the cluster specific IAM role exists")
-		VerifyRoleExistsAndOwned(fmt.Sprintf("%s-iam-service-role", input.ClusterName), eksClusterName, true, input.AWSSession)
+		VerifyRoleExistsAndOwned(fmt.Sprintf("%s-iam-service-role", input.ClusterName), input.ClusterName, true, input.AWSSession)
 	} else {
 		ginkgo.By("Checking that the cluster default IAM role exists")
-		VerifyRoleExistsAndOwned(ekscontrolplanev1.DefaultEKSControlPlaneRole, eksClusterName, false, input.AWSSession)
+		VerifyRoleExistsAndOwned(ekscontrolplanev1.DefaultEKSControlPlaneRole, input.ClusterName, false, input.AWSSession)
 	}
 
 	shared.Byf("Checking kubeconfig secrets exist")
diff --git a/test/e2e/suites/managed/eks_test.go b/test/e2e/suites/managed/eks_test.go
index 93a40a1ac..e4eff7744 100644
--- a/test/e2e/suites/managed/eks_test.go
+++ b/test/e2e/suites/managed/eks_test.go
@@ -38,7 +38,7 @@ var _ = ginkgo.Describe("[managed] [general] EKS cluster tests", func() {
 	var (
 		namespace        *corev1.Namespace
 		ctx              context.Context
-		specName         = "cluster"
+		specName         = "eks-nodes"
 		clusterName      string
 		cniAddonName     = "vpc-cni"
 		corednsAddonName = "coredns"
@@ -55,10 +55,9 @@ var _ = ginkgo.Describe("[managed] [general] EKS cluster tests", func() {
 		ctx = context.TODO()
 		namespace = shared.SetupSpecNamespace(ctx, specName, e2eCtx)
 		clusterName = fmt.Sprintf("%s-%s", specName, util.RandomString(6))
-		eksClusterName := getEKSClusterName(namespace.Name, clusterName)
 
 		ginkgo.By("default iam role should exist")
-		VerifyRoleExistsAndOwned(ekscontrolplanev1.DefaultEKSControlPlaneRole, eksClusterName, false, e2eCtx.BootstrapUserAWSSession)
+		VerifyRoleExistsAndOwned(ekscontrolplanev1.DefaultEKSControlPlaneRole, clusterName, false, e2eCtx.BootstrapUserAWSSession)
 
 		ginkgo.By("should create an EKS control plane")
 		ManagedClusterSpec(ctx, func() ManagedClusterSpecInput {
@@ -155,24 +154,6 @@ var _ = ginkgo.Describe("[managed] [general] EKS cluster tests", func() {
 				Cleanup:               true,
 				ManagedMachinePool:    false,
 				Flavor:                EKSMachinePoolOnlyFlavor,
-				UsesLaunchTemplate:    false,
-			}
-		})
-
-		ginkgo.By("should create a managed node pool with launch template and scale")
-		MachinePoolSpec(ctx, func() MachinePoolSpecInput {
-			return MachinePoolSpecInput{
-				E2EConfig:             e2eCtx.E2EConfig,
-				ConfigClusterFn:       defaultConfigCluster,
-				BootstrapClusterProxy: e2eCtx.Environment.BootstrapClusterProxy,
-				AWSSession:            e2eCtx.BootstrapUserAWSSession,
-				Namespace:             namespace,
-				ClusterName:           clusterName,
-				IncludeScaling:        true,
-				Cleanup:               true,
-				ManagedMachinePool:    true,
-				Flavor:                EKSManagedMachinePoolWithLaunchTemplateOnlyFlavor,
-				UsesLaunchTemplate:    true,
 			}
 		})
 
diff --git a/test/e2e/suites/managed/helpers.go b/test/e2e/suites/managed/helpers.go
index 19adad408..ed9431646 100644
--- a/test/e2e/suites/managed/helpers.go
+++ b/test/e2e/suites/managed/helpers.go
@@ -39,13 +39,12 @@ import (
 
 // EKS related constants.
 const (
-	EKSManagedPoolFlavor                              = "eks-managedmachinepool"
-	EKSControlPlaneOnlyFlavor                         = "eks-control-plane-only"
-	EKSControlPlaneOnlyWithAddonFlavor                = "eks-control-plane-only-withaddon"
-	EKSMachineDeployOnlyFlavor                        = "eks-machine-deployment-only"
-	EKSManagedMachinePoolOnlyFlavor                   = "eks-managed-machinepool-only"
-	EKSManagedMachinePoolWithLaunchTemplateOnlyFlavor = "eks-managed-machinepool-with-launch-template-only"
-	EKSMachinePoolOnlyFlavor                          = "eks-machinepool-only"
+	EKSManagedPoolFlavor               = "eks-managedmachinepool"
+	EKSControlPlaneOnlyFlavor          = "eks-control-plane-only"
+	EKSControlPlaneOnlyWithAddonFlavor = "eks-control-plane-only-withaddon"
+	EKSMachineDeployOnlyFlavor         = "eks-machine-deployment-only"
+	EKSManagedMachinePoolOnlyFlavor    = "eks-managed-machinepool-only"
+	EKSMachinePoolOnlyFlavor           = "eks-machinepool-only"
 )
 
 type DefaultConfigClusterFn func(clusterName, namespace string) clusterctl.ConfigClusterInput
@@ -58,10 +57,6 @@ func getEKSNodegroupName(namespace, clusterName string) string {
 	return fmt.Sprintf("%s_%s-pool-0", namespace, clusterName)
 }
 
-func getEKSNodegroupWithLaunchTemplateName(namespace, clusterName string) string {
-	return fmt.Sprintf("%s_%s-pool-lt-0", namespace, clusterName)
-}
-
 func getControlPlaneName(clusterName string) string {
 	return fmt.Sprintf("%s-control-plane", clusterName)
 }
@@ -70,11 +65,11 @@ func getASGName(clusterName string) string {
 	return fmt.Sprintf("%s-mp-0", clusterName)
 }
 
-func verifyClusterActiveAndOwned(eksClusterName string, sess client.ConfigProvider) {
+func verifyClusterActiveAndOwned(eksClusterName, clusterName string, sess client.ConfigProvider) {
 	cluster, err := getEKSCluster(eksClusterName, sess)
 	Expect(err).NotTo(HaveOccurred())
 
-	tagName := infrav1.ClusterTagKey(eksClusterName)
+	tagName := infrav1.ClusterTagKey(clusterName)
 	tagValue, ok := cluster.Tags[tagName]
 	Expect(ok).To(BeTrue(), "expecting the cluster owned tag to exist")
 	Expect(*tagValue).To(BeEquivalentTo(string(infrav1.ResourceLifecycleOwned)))
@@ -121,7 +116,7 @@ func verifyConfigMapExists(ctx context.Context, name, namespace string, k8sclien
 	Expect(err).ShouldNot(HaveOccurred())
 }
 
-func VerifyRoleExistsAndOwned(roleName string, eksClusterName string, checkOwned bool, sess client.ConfigProvider) {
+func VerifyRoleExistsAndOwned(roleName string, clusterName string, checkOwned bool, sess client.ConfigProvider) {
 	iamClient := iam.New(sess)
 	input := &iam.GetRoleInput{
 		RoleName: aws.String(roleName),
@@ -132,7 +127,7 @@ func VerifyRoleExistsAndOwned(roleName string, eksClusterName string, checkOwned
 
 	if checkOwned {
 		found := false
-		expectedTagName := infrav1.ClusterAWSCloudProviderTagKey(eksClusterName)
+		expectedTagName := infrav1.ClusterAWSCloudProviderTagKey(clusterName)
 		for _, tag := range output.Role.Tags {
 			if *tag.Key == expectedTagName && *tag.Value == string(infrav1.ResourceLifecycleOwned) {
 				found = true
diff --git a/test/e2e/suites/managed/machine_pool.go b/test/e2e/suites/managed/machine_pool.go
index 9c42405a7..dacdd53a2 100644
--- a/test/e2e/suites/managed/machine_pool.go
+++ b/test/e2e/suites/managed/machine_pool.go
@@ -21,9 +21,6 @@ package managed
 
 import (
 	"context"
-	"encoding/base64"
-	"fmt"
-	"strings"
 
 	"github.com/aws/aws-sdk-go/aws/client"
 	"github.com/onsi/ginkgo"
@@ -48,7 +45,6 @@ type MachinePoolSpecInput struct {
 	Cleanup               bool
 	ManagedMachinePool    bool
 	Flavor                string
-	UsesLaunchTemplate    bool
 }
 
 // MachinePoolSpec implements a test for creating a machine pool.
@@ -60,7 +56,6 @@ func MachinePoolSpec(ctx context.Context, inputGetter func() MachinePoolSpecInpu
 	Expect(input.AWSSession).ToNot(BeNil(), "Invalid argument. input.AWSSession can't be nil")
 	Expect(input.Namespace).NotTo(BeNil(), "Invalid argument. input.Namespace can't be nil")
 	Expect(input.ClusterName).ShouldNot(HaveLen(0), "Invalid argument. input.ClusterName can't be empty")
-	Expect(input.Flavor).ShouldNot(HaveLen(0), "Invalid argument. input.Flavor can't be empty")
 
 	shared.Byf("getting cluster with name %s", input.ClusterName)
 	cluster := framework.GetClusterByName(ctx, framework.GetClusterByNameInput{
@@ -74,20 +69,7 @@ func MachinePoolSpec(ctx context.Context, inputGetter func() MachinePoolSpecInpu
 	configCluster := input.ConfigClusterFn(input.ClusterName, input.Namespace.Name)
 	configCluster.Flavor = input.Flavor
 	configCluster.WorkerMachineCount = pointer.Int64Ptr(1)
-	workloadClusterTemplate := shared.GetTemplate(ctx, configCluster)
-	if input.UsesLaunchTemplate {
-		userDataTemplate := `#!/bin/bash
-/etc/eks/bootstrap.sh %s \
-  --container-runtime containerd
-`
-		eksClusterName := getEKSClusterName(input.Namespace.Name, input.ClusterName)
-		userData := fmt.Sprintf(userDataTemplate, eksClusterName)
-		userDataEncoded := base64.StdEncoding.EncodeToString([]byte(userData))
-		workloadClusterTemplate = []byte(strings.ReplaceAll(string(workloadClusterTemplate), "USER_DATA", userDataEncoded))
-	}
-	shared.Byf(string(workloadClusterTemplate))
-	shared.Byf("Applying the %s cluster template yaml to the cluster", configCluster.Flavor)
-	err := input.BootstrapClusterProxy.Apply(ctx, workloadClusterTemplate)
+	err := shared.ApplyTemplate(ctx, configCluster, input.BootstrapClusterProxy)
 	Expect(err).ShouldNot(HaveOccurred())
 
 	shared.Byf("Waiting for the machine pool to be running")
@@ -101,12 +83,7 @@ func MachinePoolSpec(ctx context.Context, inputGetter func() MachinePoolSpecInpu
 	shared.Byf("Check the status of the node group")
 	eksClusterName := getEKSClusterName(input.Namespace.Name, input.ClusterName)
 	if input.ManagedMachinePool {
-		var nodeGroupName string
-		if input.UsesLaunchTemplate {
-			nodeGroupName = getEKSNodegroupWithLaunchTemplateName(input.Namespace.Name, input.ClusterName)
-		} else {
-			nodeGroupName = getEKSNodegroupName(input.Namespace.Name, input.ClusterName)
-		}
+		nodeGroupName := getEKSNodegroupName(input.Namespace.Name, input.ClusterName)
 		verifyManagedNodeGroup(eksClusterName, nodeGroupName, true, input.AWSSession)
 	} else {
 		asgName := getASGName(input.ClusterName)