Repository: kubernetes-sigs/cluster-api-provider-aws
Pull request #5140 (2024-10-08 08:19:47)

:sparkles: Support setting maxHealthyPercentage to configure ASG instance refresh
<!-- 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**:

It allows to configure the `MaxHealthyPercentage`  setting of the ASGs instance refresh, to better control how instances are added and terminated.

**Special notes for your reviewer**:
This is adding a new field to the AWSMachinePool CRD, and it's not a breaking change.

**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
- [ ] includes documentation
- [X] includes [emojis](https://github.com/kubernetes-sigs/kubebuilder-release-tools?tab=readme-ov-file#kubebuilder-project-versioning)
- [X] adds unit tests
- [ ] adds or updates e2e tests

**Release note**:
<!--  Write your release note:
1. Enter your extended release note in the below block. If the PR requires additional action from users switching to the new release, include the string "action required".
2. If no release note is required, just write "NONE".
-->
```release-note
Support setting maxHealthyPercentage to configure ASG instance refresh
```


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 [dlipovetsky](https://github.com/dlipovetsky) for approval. 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":["dlipovetsky"]} -->
k8s-ci-robot: @fiunchinho: 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 | ee5f9ee51e5a5f77a60b388f7c48109f0eea05ab | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/5140/pull-cluster-api-provider-aws-test/1843566962328735744) | 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=5140). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Afiunchinho). 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-sigs/prow](https://github.com/kubernetes-sigs/prow/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: @fiunchinho: 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 | 5022162f1c49f50e6e31e0cf6261ed9026f9dc8e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/5140/pull-cluster-api-provider-aws-test/1843583807555375104) | 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=5140). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Afiunchinho). 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-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
richardcase: /cherrypick release-2.6
k8s-infra-cherrypick-robot: @richardcase: once the present PR merges, I will cherry-pick it on top of release-2.6 in a new PR and assign it to you.

<details>

In response to [this](https://github.com/kubernetes-sigs/cluster-api-provider-aws/pull/5140#issuecomment-2400009204):

>/cherrypick release-2.6


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-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
richardcase: Until the e2e pass:

/hold

But from my side:

/lgtm
/approve
richardcase: /test ?
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/cluster-api-provider-aws/pull/5140#issuecomment-2400165545" title="Approved">richardcase</a>*

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).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

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

- ~~[OWNERS](https://github.com/kubernetes-sigs/cluster-api-provider-aws/blob/main/OWNERS)~~ [richardcase]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
k8s-ci-robot: @richardcase: The following commands are available to trigger required jobs:
* `/test pull-cluster-api-provider-aws-build`
* `/test pull-cluster-api-provider-aws-build-docker`
* `/test pull-cluster-api-provider-aws-test`
* `/test pull-cluster-api-provider-aws-verify`

The following commands are available to trigger optional jobs:
* `/test pull-cluster-api-provider-aws-apidiff-main`
* `/test pull-cluster-api-provider-aws-e2e`
* `/test pull-cluster-api-provider-aws-e2e-blocking`
* `/test pull-cluster-api-provider-aws-e2e-clusterclass`
* `/test pull-cluster-api-provider-aws-e2e-conformance`
* `/test pull-cluster-api-provider-aws-e2e-conformance-with-ci-artifacts`
* `/test pull-cluster-api-provider-aws-e2e-eks`
* `/test pull-cluster-api-provider-aws-e2e-eks-gc`
* `/test pull-cluster-api-provider-aws-e2e-eks-testing`

Use `/test all` to run the following jobs that were automatically triggered:
* `pull-cluster-api-provider-aws-apidiff-main`
* `pull-cluster-api-provider-aws-build`
* `pull-cluster-api-provider-aws-build-docker`
* `pull-cluster-api-provider-aws-test`
* `pull-cluster-api-provider-aws-verify`



<details>

In response to [this](https://github.com/kubernetes-sigs/cluster-api-provider-aws/pull/5140#issuecomment-2400165826):

>/test ?


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-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
richardcase: /test pull-cluster-api-provider-aws-e2e
/test pull-cluster-api-provider-aws-e2e-eks
k8s-ci-robot: @fiunchinho: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-provider-aws-e2e | e0b119cb4ad4e1800ca4002518e1b5134bfe8ed8 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/5140/pull-cluster-api-provider-aws-e2e/1843674494632202240) | false | `/test pull-cluster-api-provider-aws-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=5140). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Afiunchinho). 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-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
fiunchinho: /test pull-cluster-api-provider-aws-e2e
k8s-ci-robot: @fiunchinho: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-provider-aws-e2e | e0b119cb4ad4e1800ca4002518e1b5134bfe8ed8 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/5140/pull-cluster-api-provider-aws-e2e/1843679313040248832) | false | `/test pull-cluster-api-provider-aws-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=5140). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Afiunchinho). 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-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
richardcase: The e2e is failing because the AMIs are no longer available 😢 We have 2 options:

1. Wait for the new AMIs to be published and then rebase this PR. This may take a few days to 1 week
2. Merge without the e2e passing.

Personally, i'd  lean towards 2). But would be good to get others view on this? @nrb @dlipovetsky @AndiDog any thoughts?
damdo: /test pull-cluster-api-provider-aws-e2e
k8s-ci-robot: @fiunchinho: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-provider-aws-e2e | e0b119cb4ad4e1800ca4002518e1b5134bfe8ed8 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/5140/pull-cluster-api-provider-aws-e2e/1844307949095227392) | false | `/test pull-cluster-api-provider-aws-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=5140). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Afiunchinho). 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-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
fiunchinho: I don't think this PR will break the e2e tests since it's adding a new field. Having said that, waiting 1 week for this to get merged is fine by me.
k8s-ci-robot: @fiunchinho: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-provider-aws-e2e | e0b119cb4ad4e1800ca4002518e1b5134bfe8ed8 | [link](https://prow.k8s.io/view/gs/kubernetes-ci-logs/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/5140/pull-cluster-api-provider-aws-e2e/1845750197481639936) | false | `/test pull-cluster-api-provider-aws-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=5140). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Afiunchinho). 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-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->

Patch:
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 e28522622..e70f54453 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml
@@ -969,6 +969,18 @@ spec:
                       The default is to use the value for the health check grace period defined for the group.
                     format: int64
                     type: integer
+                  maxHealthyPercentage:
+                    description: |-
+                      The amount of capacity as a percentage in ASG that can be in service and healthy, or pending,
+                      to support your workload when replacing instances.
+                      The value is expressed as a percentage of the desired capacity of the ASG. Value range is 100 to 200.
+                      If you specify MaxHealthyPercentage , you must also specify MinHealthyPercentage , and the difference between
+                      them cannot be greater than 100.
+                      A larger range increases the number of instances that can be replaced at the same time.
+                    format: int64
+                    maximum: 200
+                    minimum: 100
+                    type: integer
                   minHealthyPercentage:
                     description: |-
                       The amount of capacity as a percentage in ASG that must remain healthy
diff --git a/exp/api/v1beta1/conversion.go b/exp/api/v1beta1/conversion.go
index 50a62f6bb..7c39f1fcb 100644
--- a/exp/api/v1beta1/conversion.go
+++ b/exp/api/v1beta1/conversion.go
@@ -42,8 +42,9 @@ func (src *AWSMachinePool) ConvertTo(dstRaw conversion.Hub) error {
 	if restored.Spec.SuspendProcesses != nil {
 		dst.Spec.SuspendProcesses = restored.Spec.SuspendProcesses
 	}
-	if dst.Spec.RefreshPreferences != nil && restored.Spec.RefreshPreferences != nil {
+	if restored.Spec.RefreshPreferences != nil {
 		dst.Spec.RefreshPreferences.Disable = restored.Spec.RefreshPreferences.Disable
+		dst.Spec.RefreshPreferences.MaxHealthyPercentage = restored.Spec.RefreshPreferences.MaxHealthyPercentage
 	}
 	if restored.Spec.AWSLaunchTemplate.InstanceMetadataOptions != nil {
 		dst.Spec.AWSLaunchTemplate.InstanceMetadataOptions = restored.Spec.AWSLaunchTemplate.InstanceMetadataOptions
diff --git a/exp/api/v1beta1/zz_generated.conversion.go b/exp/api/v1beta1/zz_generated.conversion.go
index 4f50a8c20..a1a2d80e5 100644
--- a/exp/api/v1beta1/zz_generated.conversion.go
+++ b/exp/api/v1beta1/zz_generated.conversion.go
@@ -1066,6 +1066,7 @@ func autoConvert_v1beta2_RefreshPreferences_To_v1beta1_RefreshPreferences(in *v1
 	out.Strategy = (*string)(unsafe.Pointer(in.Strategy))
 	out.InstanceWarmup = (*int64)(unsafe.Pointer(in.InstanceWarmup))
 	out.MinHealthyPercentage = (*int64)(unsafe.Pointer(in.MinHealthyPercentage))
+	// WARNING: in.MaxHealthyPercentage requires manual conversion: does not exist in peer-type
 	return nil
 }
 
diff --git a/exp/api/v1beta2/awsmachinepool_types.go b/exp/api/v1beta2/awsmachinepool_types.go
index a9c26a3e6..526876bcf 100644
--- a/exp/api/v1beta2/awsmachinepool_types.go
+++ b/exp/api/v1beta2/awsmachinepool_types.go
@@ -172,6 +172,17 @@ type RefreshPreferences struct {
 	// during an instance refresh. The default is 90.
 	// +optional
 	MinHealthyPercentage *int64 `json:"minHealthyPercentage,omitempty"`
+
+	// The amount of capacity as a percentage in ASG that can be in service and healthy, or pending,
+	// to support your workload when replacing instances.
+	// The value is expressed as a percentage of the desired capacity of the ASG. Value range is 100 to 200.
+	// If you specify MaxHealthyPercentage , you must also specify MinHealthyPercentage , and the difference between
+	// them cannot be greater than 100.
+	// A larger range increases the number of instances that can be replaced at the same time.
+	// +optional
+	// +kubebuilder:validation:Minimum=100
+	// +kubebuilder:validation:Maximum=200
+	MaxHealthyPercentage *int64 `json:"maxHealthyPercentage,omitempty"`
 }
 
 // AWSMachinePoolStatus defines the observed state of AWSMachinePool.
diff --git a/exp/api/v1beta2/awsmachinepool_webhook.go b/exp/api/v1beta2/awsmachinepool_webhook.go
index 541243c53..a4f6a44d4 100644
--- a/exp/api/v1beta2/awsmachinepool_webhook.go
+++ b/exp/api/v1beta2/awsmachinepool_webhook.go
@@ -133,6 +133,7 @@ func (r *AWSMachinePool) validateAdditionalSecurityGroups() field.ErrorList {
 	}
 	return allErrs
 }
+
 func (r *AWSMachinePool) validateSpotInstances() field.ErrorList {
 	var allErrs field.ErrorList
 	if r.Spec.AWSLaunchTemplate.SpotMarketOptions != nil && r.Spec.MixedInstancesPolicy != nil {
@@ -141,6 +142,26 @@ func (r *AWSMachinePool) validateSpotInstances() field.ErrorList {
 	return allErrs
 }
 
+func (r *AWSMachinePool) validateRefreshPreferences() field.ErrorList {
+	var allErrs field.ErrorList
+
+	if r.Spec.RefreshPreferences == nil {
+		return allErrs
+	}
+
+	if r.Spec.RefreshPreferences.MaxHealthyPercentage != nil && r.Spec.RefreshPreferences.MinHealthyPercentage == nil {
+		allErrs = append(allErrs, field.Forbidden(field.NewPath("spec.refreshPreferences.maxHealthyPercentage"), "If you specify spec.refreshPreferences.maxHealthyPercentage, you must also specify spec.refreshPreferences.minHealthyPercentage"))
+	}
+
+	if r.Spec.RefreshPreferences.MaxHealthyPercentage != nil && r.Spec.RefreshPreferences.MinHealthyPercentage != nil {
+		if *r.Spec.RefreshPreferences.MaxHealthyPercentage-*r.Spec.RefreshPreferences.MinHealthyPercentage > 100 {
+			allErrs = append(allErrs, field.Forbidden(field.NewPath("spec.refreshPreferences.maxHealthyPercentage"), "the difference between spec.refreshPreferences.maxHealthyPercentage and spec.refreshPreferences.minHealthyPercentage cannot be greater than 100"))
+		}
+	}
+
+	return allErrs
+}
+
 // ValidateCreate will do any extra validation when creating a AWSMachinePool.
 func (r *AWSMachinePool) ValidateCreate() (admission.Warnings, error) {
 	log.Info("AWSMachinePool validate create", "machine-pool", klog.KObj(r))
@@ -154,6 +175,7 @@ func (r *AWSMachinePool) ValidateCreate() (admission.Warnings, error) {
 	allErrs = append(allErrs, r.validateSubnets()...)
 	allErrs = append(allErrs, r.validateAdditionalSecurityGroups()...)
 	allErrs = append(allErrs, r.validateSpotInstances()...)
+	allErrs = append(allErrs, r.validateRefreshPreferences()...)
 
 	if len(allErrs) == 0 {
 		return nil, nil
@@ -175,6 +197,7 @@ func (r *AWSMachinePool) ValidateUpdate(_ runtime.Object) (admission.Warnings, e
 	allErrs = append(allErrs, r.validateSubnets()...)
 	allErrs = append(allErrs, r.validateAdditionalSecurityGroups()...)
 	allErrs = append(allErrs, r.validateSpotInstances()...)
+	allErrs = append(allErrs, r.validateRefreshPreferences()...)
 
 	if len(allErrs) == 0 {
 		return nil, nil
diff --git a/exp/api/v1beta2/zz_generated.deepcopy.go b/exp/api/v1beta2/zz_generated.deepcopy.go
index 69ff149f4..f34e8f9d9 100644
--- a/exp/api/v1beta2/zz_generated.deepcopy.go
+++ b/exp/api/v1beta2/zz_generated.deepcopy.go
@@ -1062,6 +1062,11 @@ func (in *RefreshPreferences) DeepCopyInto(out *RefreshPreferences) {
 		*out = new(int64)
 		**out = **in
 	}
+	if in.MaxHealthyPercentage != nil {
+		in, out := &in.MaxHealthyPercentage, &out.MaxHealthyPercentage
+		*out = new(int64)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RefreshPreferences.
diff --git a/pkg/cloud/services/autoscaling/autoscalinggroup.go b/pkg/cloud/services/autoscaling/autoscalinggroup.go
index d473010d1..f8e8b1869 100644
--- a/pkg/cloud/services/autoscaling/autoscalinggroup.go
+++ b/pkg/cloud/services/autoscaling/autoscalinggroup.go
@@ -349,7 +349,7 @@ func (s *Service) CanStartASGInstanceRefresh(scope *scope.MachinePoolScope) (boo
 // StartASGInstanceRefresh will start an ASG instance with refresh.
 func (s *Service) StartASGInstanceRefresh(scope *scope.MachinePoolScope) error {
 	strategy := ptr.To[string](autoscaling.RefreshStrategyRolling)
-	var minHealthyPercentage, instanceWarmup *int64
+	var minHealthyPercentage, maxHealthyPercentage, instanceWarmup *int64
 	if scope.AWSMachinePool.Spec.RefreshPreferences != nil {
 		if scope.AWSMachinePool.Spec.RefreshPreferences.Strategy != nil {
 			strategy = scope.AWSMachinePool.Spec.RefreshPreferences.Strategy
@@ -360,6 +360,9 @@ func (s *Service) StartASGInstanceRefresh(scope *scope.MachinePoolScope) error {
 		if scope.AWSMachinePool.Spec.RefreshPreferences.MinHealthyPercentage != nil {
 			minHealthyPercentage = scope.AWSMachinePool.Spec.RefreshPreferences.MinHealthyPercentage
 		}
+		if scope.AWSMachinePool.Spec.RefreshPreferences.MaxHealthyPercentage != nil {
+			maxHealthyPercentage = scope.AWSMachinePool.Spec.RefreshPreferences.MaxHealthyPercentage
+		}
 	}
 
 	input := &autoscaling.StartInstanceRefreshInput{
@@ -368,6 +371,7 @@ func (s *Service) StartASGInstanceRefresh(scope *scope.MachinePoolScope) error {
 		Preferences: &autoscaling.RefreshPreferences{
 			InstanceWarmup:       instanceWarmup,
 			MinHealthyPercentage: minHealthyPercentage,
+			MaxHealthyPercentage: maxHealthyPercentage,
 		},
 	}
 


Test patch:
diff --git a/exp/api/v1beta2/awsmachinepool_webhook_test.go b/exp/api/v1beta2/awsmachinepool_webhook_test.go
index 3f7f30a10..0f14ad1c0 100644
--- a/exp/api/v1beta2/awsmachinepool_webhook_test.go
+++ b/exp/api/v1beta2/awsmachinepool_webhook_test.go
@@ -153,6 +153,27 @@ func TestAWSMachinePoolValidateCreate(t *testing.T) {
 			},
 			wantErr: true,
 		},
+		{
+			name: "Should fail if MaxHealthyPercentage is set, but MinHealthyPercentage is not set",
+			pool: &AWSMachinePool{
+				Spec: AWSMachinePoolSpec{
+					RefreshPreferences: &RefreshPreferences{MaxHealthyPercentage: aws.Int64(100)},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "Should fail if the difference between MaxHealthyPercentage and MinHealthyPercentage is greater than 100",
+			pool: &AWSMachinePool{
+				Spec: AWSMachinePoolSpec{
+					RefreshPreferences: &RefreshPreferences{
+						MaxHealthyPercentage: aws.Int64(150),
+						MinHealthyPercentage: aws.Int64(25),
+					},
+				},
+			},
+			wantErr: true,
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -287,6 +308,27 @@ func TestAWSMachinePoolValidateUpdate(t *testing.T) {
 			},
 			wantErr: true,
 		},
+		{
+			name: "Should fail if MaxHealthyPercentage is set, but MinHealthyPercentage is not set",
+			new: &AWSMachinePool{
+				Spec: AWSMachinePoolSpec{
+					RefreshPreferences: &RefreshPreferences{MaxHealthyPercentage: aws.Int64(100)},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "Should fail if the difference between MaxHealthyPercentage and MinHealthyPercentage is greater than 100",
+			new: &AWSMachinePool{
+				Spec: AWSMachinePoolSpec{
+					RefreshPreferences: &RefreshPreferences{
+						MaxHealthyPercentage: aws.Int64(150),
+						MinHealthyPercentage: aws.Int64(25),
+					},
+				},
+			},
+			wantErr: true,
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cloud/services/autoscaling/autoscalinggroup_test.go b/pkg/cloud/services/autoscaling/autoscalinggroup_test.go
index e116c8012..943469616 100644
--- a/pkg/cloud/services/autoscaling/autoscalinggroup_test.go
+++ b/pkg/cloud/services/autoscaling/autoscalinggroup_test.go
@@ -1203,6 +1203,7 @@ func TestServiceStartASGInstanceRefresh(t *testing.T) {
 					Preferences: &autoscaling.RefreshPreferences{
 						InstanceWarmup:       aws.Int64(100),
 						MinHealthyPercentage: aws.Int64(80),
+						MaxHealthyPercentage: aws.Int64(100),
 					},
 				})).
 					Return(nil, awserrors.NewNotFound("not found"))
@@ -1218,6 +1219,7 @@ func TestServiceStartASGInstanceRefresh(t *testing.T) {
 					Preferences: &autoscaling.RefreshPreferences{
 						InstanceWarmup:       aws.Int64(100),
 						MinHealthyPercentage: aws.Int64(80),
+						MaxHealthyPercentage: aws.Int64(100),
 					},
 				})).
 					Return(&autoscaling.StartInstanceRefreshOutput{}, nil)
@@ -1312,6 +1314,7 @@ func getMachinePoolScope(client client.Client, clusterScope *scope.ClusterScope)
 				Strategy:             aws.String("Rolling"),
 				InstanceWarmup:       aws.Int64(100),
 				MinHealthyPercentage: aws.Int64(80),
+				MaxHealthyPercentage: aws.Int64(100),
 			},
 			MixedInstancesPolicy: &expinfrav1.MixedInstancesPolicy{
 				InstancesDistribution: &expinfrav1.InstancesDistribution{