Repository: syself/cluster-api-provider-hetzner
Pull request #697 (2023-03-24 21:09:01)

:sparkles: Add remediation controller for hcloud
**What this PR does / why we need it**:
A remediation controller that reboots hcloud servers for a specified amount of times before it deletes and recreates the machine. This pattern is more efficient in the majority of cases where HCloud servers get healthy again after a rebootThis pattern is more efficient in the majority of cases where HCloud servers get healthy again after a reboot.

**TODOs**:

- [ ] squashed commits
- [ ] includes documentation
- [x] adds unit tests




Patch:
diff --git a/PROJECT b/PROJECT
index ca8d308f..e71e2f02 100644
--- a/PROJECT
+++ b/PROJECT
@@ -134,4 +134,30 @@ resources:
     defaulting: true
     validation: true
     webhookVersion: v1
+- api:
+    crdVersion: v1
+    namespaced: true
+  controller: true
+  domain: cluster.x-k8s.io
+  group: infrastructure
+  kind: HCloudRemediation
+  path: github.com/syself/cluster-api-provider-hetzner/api/v1beta1
+  version: v1beta1
+  webhooks:
+    defaulting: true
+    validation: true
+    webhookVersion: v1
+- api:
+    crdVersion: v1
+    namespaced: true
+  controller: true
+  domain: cluster.x-k8s.io
+  group: infrastructure
+  kind: HCloudRemediationTemplate
+  path: github.com/syself/cluster-api-provider-hetzner/api/v1beta1
+  version: v1beta1
+  webhooks:
+    defaulting: true
+    validation: true
+    webhookVersion: v1
 version: "3"
diff --git a/api/v1beta1/hcloudremediation_types.go b/api/v1beta1/hcloudremediation_types.go
new file mode 100644
index 00000000..db28294f
--- /dev/null
+++ b/api/v1beta1/hcloudremediation_types.go
@@ -0,0 +1,104 @@
+/*
+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 v1beta1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
+)
+
+const (
+	// HCloudRemediationFinalizer allows HCloudRemediationReconciler to clean up resources associated with HCloudRemediation before
+	// removing it from the apiserver.
+	HCloudRemediationFinalizer = "hcloudremediation.infrastructure.cluster.x-k8s.io"
+
+	// HCloudRebootAnnotation indicates that a bare metal host object should be rebooted.
+	HCloudRebootAnnotation = "reboot.hcloud.infrastructure.cluster.x-k8s.io"
+)
+
+// HCloudRemediationSpec defines the desired state of HCloudRemediation.
+type HCloudRemediationSpec struct {
+	// Strategy field defines remediation strategy.
+	Strategy *RemediationStrategy `json:"strategy,omitempty"`
+}
+
+// HCloudRemediationStatus defines the observed state of HCloudRemediation.
+type HCloudRemediationStatus struct {
+	// Phase represents the current phase of machine remediation.
+	// E.g. Pending, Running, Done etc.
+	// +optional
+	Phase string `json:"phase,omitempty"`
+
+	// RetryCount can be used as a counter during the remediation.
+	// Field can hold number of reboots etc.
+	// +optional
+	RetryCount int `json:"retryCount,omitempty"`
+
+	// LastRemediated identifies when the host was last remediated
+	// +optional
+	LastRemediated *metav1.Time `json:"lastRemediated,omitempty"`
+
+	// Conditions defines current service state of the HCloudRemediation.
+	// +optional
+	Conditions clusterv1.Conditions `json:"conditions,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// +kubebuilder:resource:path=hcloudremediations,scope=Namespaced,categories=cluster-api,shortName=hcr;hcloudremediation
+// +kubebuilder:storageversion
+// +kubebuilder:printcolumn:name="Timeout",type=string,JSONPath=".spec.strategy.timeout",description="Timeout for the remediation"
+// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=".status.phase",description="Phase of the remediation"
+// +kubebuilder:printcolumn:name="Last Remediated",type=string,JSONPath=".status.lastRemediated",description="Timestamp of the last remediation attempt"
+// +kubebuilder:printcolumn:name="Retry count",type=string,JSONPath=".status.retryCount",description="How many times remediation controller has tried to remediate the node"
+// +kubebuilder:printcolumn:name="Retry limit",type=string,JSONPath=".spec.strategy.retryLimit",description="How many times remediation controller should attempt to remediate the node"
+
+// HCloudRemediation is the Schema for the hcloudremediations API.
+type HCloudRemediation struct {
+	metav1.TypeMeta `json:",inline"`
+	// +optional
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+	// +optional
+	Spec HCloudRemediationSpec `json:"spec,omitempty"`
+	// +optional
+	Status HCloudRemediationStatus `json:"status,omitempty"`
+}
+
+// GetConditions returns the observations of the operational state of the HCloudRemediation resource.
+func (r *HCloudRemediation) GetConditions() clusterv1.Conditions {
+	return r.Status.Conditions
+}
+
+// SetConditions sets the underlying service state of the HCloudRemediation to the predescribed clusterv1.Conditions.
+func (r *HCloudRemediation) SetConditions(conditions clusterv1.Conditions) {
+	r.Status.Conditions = conditions
+}
+
+//+kubebuilder:object:root=true
+
+// HCloudRemediationList contains a list of HCloudRemediation.
+type HCloudRemediationList struct {
+	metav1.TypeMeta `json:",inline"`
+	// +optional
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []HCloudRemediation `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&HCloudRemediation{}, &HCloudRemediationList{})
+}
diff --git a/api/v1beta1/hcloudremediation_webhook.go b/api/v1beta1/hcloudremediation_webhook.go
new file mode 100644
index 00000000..920577c6
--- /dev/null
+++ b/api/v1beta1/hcloudremediation_webhook.go
@@ -0,0 +1,57 @@
+/*
+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 v1beta1
+
+import (
+	"k8s.io/apimachinery/pkg/runtime"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/webhook"
+)
+
+// SetupWebhookWithManager initializes webhook manager for HCloudRemediation.
+func (r *HCloudRemediation) SetupWebhookWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewWebhookManagedBy(mgr).
+		For(r).
+		Complete()
+}
+
+//+kubebuilder:webhook:path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-hcloudremediation,mutating=true,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=hcloudremediations,verbs=create;update,versions=v1beta1,name=mutation.hcloudremediation.infrastructure.cluster.x-k8s.io,admissionReviewVersions={v1,v1beta1}
+
+var _ webhook.Defaulter = &HCloudRemediation{}
+
+// Default implements webhook.Defaulter so a webhook will be registered for the type.
+func (r *HCloudRemediation) Default() {
+}
+
+//+kubebuilder:webhook:path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-hcloudremediation,mutating=false,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=hcloudremediations,verbs=create;update,versions=v1beta1,name=validation.hcloudremediation.infrastructure.cluster.x-k8s.io,admissionReviewVersions={v1,v1beta1}
+
+var _ webhook.Validator = &HCloudRemediation{}
+
+// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
+func (r *HCloudRemediation) ValidateCreate() error {
+	return nil
+}
+
+// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
+func (r *HCloudRemediation) ValidateUpdate(old runtime.Object) error {
+	return nil
+}
+
+// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
+func (r *HCloudRemediation) ValidateDelete() error {
+	return nil
+}
diff --git a/api/v1beta1/hcloudremediationtemplate_types.go b/api/v1beta1/hcloudremediationtemplate_types.go
new file mode 100644
index 00000000..228c4bbb
--- /dev/null
+++ b/api/v1beta1/hcloudremediationtemplate_types.go
@@ -0,0 +1,72 @@
+/*
+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 v1beta1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// HCloudRemediationTemplateSpec defines the desired state of HCloudRemediationTemplate.
+type HCloudRemediationTemplateSpec struct {
+	Template HCloudRemediationTemplateResource `json:"template"`
+}
+
+// HCloudRemediationTemplateResource describes the data needed to create a HCloudRemediation from a template.
+type HCloudRemediationTemplateResource struct {
+	// Spec is the specification of the desired behavior of the HCloudRemediation.
+	Spec HCloudRemediationSpec `json:"spec"`
+}
+
+// HCloudRemediationTemplateStatus defines the observed state of HCloudRemediationTemplate.
+type HCloudRemediationTemplateStatus struct {
+	// HCloudRemediationStatus defines the observed state of HCloudRemediation
+	Status HCloudRemediationStatus `json:"status"`
+}
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// +kubebuilder:resource:path=hcloudremediationtemplates,scope=Namespaced,categories=cluster-api,shortName=hcrt;hcremediationtemplate;hcremediationtemplates;hcloudrt;hcloudremediationtemplate
+// +kubebuilder:subresource:status
+// +kubebuilder:object:root=true
+// +kubebuilder:storageversion
+// +kubebuilder:printcolumn:name="Strategy",type=string,JSONPath=".spec.template.spec.strategy.type",description="Type of the remediation strategy"
+// +kubebuilder:printcolumn:name="Retry limit",type=string,JSONPath=".spec.template.spec.strategy.retryLimit",description="How many times remediation controller should attempt to remediate the node"
+// +kubebuilder:printcolumn:name="Timeout",type=string,JSONPath=".spec.template.spec.strategy.timeout",description="Timeout for the remediation"
+
+// HCloudRemediationTemplate is the Schema for the hcloudremediationtemplates API.
+type HCloudRemediationTemplate struct {
+	metav1.TypeMeta `json:",inline"`
+	// +optional
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+	// +optional
+	Spec HCloudRemediationTemplateSpec `json:"spec,omitempty"`
+	// +optional
+	Status HCloudRemediationTemplateStatus `json:"status,omitempty"`
+}
+
+//+kubebuilder:object:root=true
+
+// HCloudRemediationTemplateList contains a list of HCloudRemediationTemplate.
+type HCloudRemediationTemplateList struct {
+	metav1.TypeMeta `json:",inline"`
+	// +optional
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []HCloudRemediationTemplate `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&HCloudRemediationTemplate{}, &HCloudRemediationTemplateList{})
+}
diff --git a/api/v1beta1/hcloudremediationtemplate_webhook.go b/api/v1beta1/hcloudremediationtemplate_webhook.go
new file mode 100644
index 00000000..defff1b9
--- /dev/null
+++ b/api/v1beta1/hcloudremediationtemplate_webhook.go
@@ -0,0 +1,57 @@
+/*
+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 v1beta1
+
+import (
+	"k8s.io/apimachinery/pkg/runtime"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/webhook"
+)
+
+// SetupWebhookWithManager initializes webhook manager for HCloudRemediationTemplate.
+func (r *HCloudRemediationTemplate) SetupWebhookWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewWebhookManagedBy(mgr).
+		For(r).
+		Complete()
+}
+
+//+kubebuilder:webhook:path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-hcloudremediationtemplate,mutating=true,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=hcloudremediationtemplates,verbs=create;update,versions=v1beta1,name=mhcloudremediationtemplate.kb.io,admissionReviewVersions=v1
+
+var _ webhook.Defaulter = &HCloudRemediationTemplate{}
+
+// Default implements webhook.Defaulter so a webhook will be registered for the type.
+func (r *HCloudRemediationTemplate) Default() {
+}
+
+//+kubebuilder:webhook:path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-hcloudremediationtemplate,mutating=false,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=hcloudremediationtemplates,verbs=create;update,versions=v1beta1,name=vhcloudremediationtemplate.kb.io,admissionReviewVersions=v1
+
+var _ webhook.Validator = &HCloudRemediationTemplate{}
+
+// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
+func (r *HCloudRemediationTemplate) ValidateCreate() error {
+	return nil
+}
+
+// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
+func (r *HCloudRemediationTemplate) ValidateUpdate(old runtime.Object) error {
+	return nil
+}
+
+// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
+func (r *HCloudRemediationTemplate) ValidateDelete() error {
+	return nil
+}
diff --git a/api/v1beta1/hetznerbaremetalremediation_types.go b/api/v1beta1/hetznerbaremetalremediation_types.go
index f7f1968b..4044eb40 100644
--- a/api/v1beta1/hetznerbaremetalremediation_types.go
+++ b/api/v1beta1/hetznerbaremetalremediation_types.go
@@ -20,9 +20,6 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-// RemediationType defines the type of remediation.
-type RemediationType string
-
 const (
 	// RemediationFinalizer allows HetznerBareMetalRemediationReconciler to clean up resources associated with HetznerBareMetalRemediation before
 	// removing it from the apiserver.
@@ -30,20 +27,6 @@ const (
 
 	// RebootAnnotation indicates that a bare metal host object should be rebooted.
 	RebootAnnotation = "reboot.hetznerbaremetalhost.infrastructure.cluster.x-k8s.io"
-
-	// RebootRemediationStrategy sets RemediationType to Reboot.
-	RebootRemediationStrategy RemediationType = "Reboot"
-)
-
-const (
-	// PhaseRunning represents the running state during remediation.
-	PhaseRunning = "Running"
-
-	// PhaseWaiting represents the state during remediation when the controller has done its job but still waiting for the result of the last remediation step.
-	PhaseWaiting = "Waiting"
-
-	// PhaseDeleting represents the state where host remediation has failed and the controller is deleting the unhealthy Machine object from the cluster.
-	PhaseDeleting = "Deleting machine"
 )
 
 // HetznerBareMetalRemediationSpec defines the desired state of HetznerBareMetalRemediation.
@@ -52,24 +35,8 @@ type HetznerBareMetalRemediationSpec struct {
 	Strategy *RemediationStrategy `json:"strategy,omitempty"`
 }
 
-// RemediationStrategy describes how to remediate machines.
-type RemediationStrategy struct {
-	// Type of remediation.
-	// +kubebuilder:default=Reboot
-	// +optional
-	Type RemediationType `json:"type,omitempty"`
-
-	// Sets maximum number of remediation retries.
-	// +optional
-	RetryLimit int `json:"retryLimit,omitempty"`
-
-	// Sets the timeout between remediation retries.
-	Timeout *metav1.Duration `json:"timeout"`
-}
-
 // HetznerBareMetalRemediationStatus defines the observed state of HetznerBareMetalRemediation.
 type HetznerBareMetalRemediationStatus struct {
-
 	// Phase represents the current phase of machine remediation.
 	// E.g. Pending, Running, Done etc.
 	// +optional
diff --git a/api/v1beta1/remediation_types.go b/api/v1beta1/remediation_types.go
new file mode 100644
index 00000000..ddb337b8
--- /dev/null
+++ b/api/v1beta1/remediation_types.go
@@ -0,0 +1,53 @@
+/*
+Copyright 2023 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 v1beta1
+
+import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+// RemediationType defines the type of remediation.
+type RemediationType string
+
+const (
+	// RemediationTypeReboot sets RemediationType to Reboot.
+	RemediationTypeReboot RemediationType = "Reboot"
+)
+
+const (
+	// PhaseRunning represents the running state during remediation.
+	PhaseRunning = "Running"
+
+	// PhaseWaiting represents the state during remediation when the controller has done its job but still waiting for the result of the last remediation step.
+	PhaseWaiting = "Waiting"
+
+	// PhaseDeleting represents the state where host remediation has failed and the controller is deleting the unhealthy Machine object from the cluster.
+	PhaseDeleting = "Deleting machine"
+)
+
+// RemediationStrategy describes how to remediate machines.
+type RemediationStrategy struct {
+	// Type of remediation.
+	// +kubebuilder:default=Reboot
+	// +optional
+	Type RemediationType `json:"type,omitempty"`
+
+	// Sets maximum number of remediation retries.
+	// +optional
+	RetryLimit int `json:"retryLimit,omitempty"`
+
+	// Sets the timeout between remediation retries.
+	Timeout *metav1.Duration `json:"timeout"`
+}
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index c9bd370f..d71b07ae 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -424,6 +424,218 @@ func (in *HCloudPlacementGroupStatus) DeepCopy() *HCloudPlacementGroupStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HCloudRemediation) DeepCopyInto(out *HCloudRemediation) {
+	*out = *in
+	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 HCloudRemediation.
+func (in *HCloudRemediation) DeepCopy() *HCloudRemediation {
+	if in == nil {
+		return nil
+	}
+	out := new(HCloudRemediation)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *HCloudRemediation) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HCloudRemediationList) DeepCopyInto(out *HCloudRemediationList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]HCloudRemediation, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCloudRemediationList.
+func (in *HCloudRemediationList) DeepCopy() *HCloudRemediationList {
+	if in == nil {
+		return nil
+	}
+	out := new(HCloudRemediationList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *HCloudRemediationList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HCloudRemediationSpec) DeepCopyInto(out *HCloudRemediationSpec) {
+	*out = *in
+	if in.Strategy != nil {
+		in, out := &in.Strategy, &out.Strategy
+		*out = new(RemediationStrategy)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCloudRemediationSpec.
+func (in *HCloudRemediationSpec) DeepCopy() *HCloudRemediationSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(HCloudRemediationSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HCloudRemediationStatus) DeepCopyInto(out *HCloudRemediationStatus) {
+	*out = *in
+	if in.LastRemediated != nil {
+		in, out := &in.LastRemediated, &out.LastRemediated
+		*out = (*in).DeepCopy()
+	}
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make(apiv1beta1.Conditions, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCloudRemediationStatus.
+func (in *HCloudRemediationStatus) DeepCopy() *HCloudRemediationStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(HCloudRemediationStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HCloudRemediationTemplate) DeepCopyInto(out *HCloudRemediationTemplate) {
+	*out = *in
+	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 HCloudRemediationTemplate.
+func (in *HCloudRemediationTemplate) DeepCopy() *HCloudRemediationTemplate {
+	if in == nil {
+		return nil
+	}
+	out := new(HCloudRemediationTemplate)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *HCloudRemediationTemplate) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HCloudRemediationTemplateList) DeepCopyInto(out *HCloudRemediationTemplateList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]HCloudRemediationTemplate, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCloudRemediationTemplateList.
+func (in *HCloudRemediationTemplateList) DeepCopy() *HCloudRemediationTemplateList {
+	if in == nil {
+		return nil
+	}
+	out := new(HCloudRemediationTemplateList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *HCloudRemediationTemplateList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HCloudRemediationTemplateResource) DeepCopyInto(out *HCloudRemediationTemplateResource) {
+	*out = *in
+	in.Spec.DeepCopyInto(&out.Spec)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCloudRemediationTemplateResource.
+func (in *HCloudRemediationTemplateResource) DeepCopy() *HCloudRemediationTemplateResource {
+	if in == nil {
+		return nil
+	}
+	out := new(HCloudRemediationTemplateResource)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HCloudRemediationTemplateSpec) DeepCopyInto(out *HCloudRemediationTemplateSpec) {
+	*out = *in
+	in.Template.DeepCopyInto(&out.Template)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCloudRemediationTemplateSpec.
+func (in *HCloudRemediationTemplateSpec) DeepCopy() *HCloudRemediationTemplateSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(HCloudRemediationTemplateSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HCloudRemediationTemplateStatus) DeepCopyInto(out *HCloudRemediationTemplateStatus) {
+	*out = *in
+	in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCloudRemediationTemplateStatus.
+func (in *HCloudRemediationTemplateStatus) DeepCopy() *HCloudRemediationTemplateStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(HCloudRemediationTemplateStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *HardwareDetails) DeepCopyInto(out *HardwareDetails) {
 	*out = *in
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_hcloudremediations.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_hcloudremediations.yaml
new file mode 100644
index 00000000..8e7730ad
--- /dev/null
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_hcloudremediations.yaml
@@ -0,0 +1,148 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.10.0
+  creationTimestamp: null
+  name: hcloudremediations.infrastructure.cluster.x-k8s.io
+spec:
+  group: infrastructure.cluster.x-k8s.io
+  names:
+    categories:
+    - cluster-api
+    kind: HCloudRemediation
+    listKind: HCloudRemediationList
+    plural: hcloudremediations
+    shortNames:
+    - hcr
+    - hcloudremediation
+    singular: hcloudremediation
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: Timeout for the remediation
+      jsonPath: .spec.strategy.timeout
+      name: Timeout
+      type: string
+    - description: Phase of the remediation
+      jsonPath: .status.phase
+      name: Phase
+      type: string
+    - description: Timestamp of the last remediation attempt
+      jsonPath: .status.lastRemediated
+      name: Last Remediated
+      type: string
+    - description: How many times remediation controller has tried to remediate the
+        node
+      jsonPath: .status.retryCount
+      name: Retry count
+      type: string
+    - description: How many times remediation controller should attempt to remediate
+        the node
+      jsonPath: .spec.strategy.retryLimit
+      name: Retry limit
+      type: string
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: HCloudRemediation is the Schema for the hcloudremediations API.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: HCloudRemediationSpec defines the desired state of HCloudRemediation.
+            properties:
+              strategy:
+                description: Strategy field defines remediation strategy.
+                properties:
+                  retryLimit:
+                    description: Sets maximum number of remediation retries.
+                    type: integer
+                  timeout:
+                    description: Sets the timeout between remediation retries.
+                    type: string
+                  type:
+                    default: Reboot
+                    description: Type of remediation.
+                    type: string
+                required:
+                - timeout
+                type: object
+            type: object
+          status:
+            description: HCloudRemediationStatus defines the observed state of HCloudRemediation.
+            properties:
+              conditions:
+                description: Conditions defines current service state of the HCloudRemediation.
+                items:
+                  description: Condition defines an observation of a Cluster API resource
+                    operational state.
+                  properties:
+                    lastTransitionTime:
+                      description: Last time the condition transitioned from one status
+                        to another. This should be when the underlying condition changed.
+                        If that is not known, then using the time when the API field
+                        changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: A human readable message indicating details about
+                        the transition. This field may be empty.
+                      type: string
+                    reason:
+                      description: The reason for the condition's last transition
+                        in CamelCase. The specific API may choose whether or not this
+                        field is considered a guaranteed API. This field may not be
+                        empty.
+                      type: string
+                    severity:
+                      description: Severity provides an explicit classification of
+                        Reason code, so the users or machines can immediately understand
+                        the current situation and act accordingly. The Severity field
+                        MUST be set only when Status=False.
+                      type: string
+                    status:
+                      description: Status of the condition, one of True, False, Unknown.
+                      type: string
+                    type:
+                      description: Type of condition in CamelCase or in foo.example.com/CamelCase.
+                        Many .condition.type values are consistent across resources
+                        like Available, but because arbitrary conditions can be useful
+                        (see .node.status.conditions), the ability to deconflict is
+                        important.
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - status
+                  - type
+                  type: object
+                type: array
+              lastRemediated:
+                description: LastRemediated identifies when the host was last remediated
+                format: date-time
+                type: string
+              phase:
+                description: Phase represents the current phase of machine remediation.
+                  E.g. Pending, Running, Done etc.
+                type: string
+              retryCount:
+                description: RetryCount can be used as a counter during the remediation.
+                  Field can hold number of reboots etc.
+                type: integer
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_hcloudremediationtemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_hcloudremediationtemplates.yaml
new file mode 100644
index 00000000..af2211c1
--- /dev/null
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_hcloudremediationtemplates.yaml
@@ -0,0 +1,168 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.10.0
+  creationTimestamp: null
+  name: hcloudremediationtemplates.infrastructure.cluster.x-k8s.io
+spec:
+  group: infrastructure.cluster.x-k8s.io
+  names:
+    categories:
+    - cluster-api
+    kind: HCloudRemediationTemplate
+    listKind: HCloudRemediationTemplateList
+    plural: hcloudremediationtemplates
+    shortNames:
+    - hcrt
+    - hcremediationtemplate
+    - hcremediationtemplates
+    - hcloudrt
+    - hcloudremediationtemplate
+    singular: hcloudremediationtemplate
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: Type of the remediation strategy
+      jsonPath: .spec.template.spec.strategy.type
+      name: Strategy
+      type: string
+    - description: How many times remediation controller should attempt to remediate
+        the node
+      jsonPath: .spec.template.spec.strategy.retryLimit
+      name: Retry limit
+      type: string
+    - description: Timeout for the remediation
+      jsonPath: .spec.template.spec.strategy.timeout
+      name: Timeout
+      type: string
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: HCloudRemediationTemplate is the Schema for the hcloudremediationtemplates
+          API.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: HCloudRemediationTemplateSpec defines the desired state of
+              HCloudRemediationTemplate.
+            properties:
+              template:
+                description: HCloudRemediationTemplateResource describes the data
+                  needed to create a HCloudRemediation from a template.
+                properties:
+                  spec:
+                    description: Spec is the specification of the desired behavior
+                      of the HCloudRemediation.
+                    properties:
+                      strategy:
+                        description: Strategy field defines remediation strategy.
+                        properties:
+                          retryLimit:
+                            description: Sets maximum number of remediation retries.
+                            type: integer
+                          timeout:
+                            description: Sets the timeout between remediation retries.
+                            type: string
+                          type:
+                            default: Reboot
+                            description: Type of remediation.
+                            type: string
+                        required:
+                        - timeout
+                        type: object
+                    type: object
+                required:
+                - spec
+                type: object
+            required:
+            - template
+            type: object
+          status:
+            description: HCloudRemediationTemplateStatus defines the observed state
+              of HCloudRemediationTemplate.
+            properties:
+              status:
+                description: HCloudRemediationStatus defines the observed state of
+                  HCloudRemediation
+                properties:
+                  conditions:
+                    description: Conditions defines current service state of the HCloudRemediation.
+                    items:
+                      description: Condition defines an observation of a Cluster API
+                        resource operational state.
+                      properties:
+                        lastTransitionTime:
+                          description: Last time the condition transitioned from one
+                            status to another. This should be when the underlying
+                            condition changed. If that is not known, then using the
+                            time when the API field changed is acceptable.
+                          format: date-time
+                          type: string
+                        message:
+                          description: A human readable message indicating details
+                            about the transition. This field may be empty.
+                          type: string
+                        reason:
+                          description: The reason for the condition's last transition
+                            in CamelCase. The specific API may choose whether or not
+                            this field is considered a guaranteed API. This field
+                            may not be empty.
+                          type: string
+                        severity:
+                          description: Severity provides an explicit classification
+                            of Reason code, so the users or machines can immediately
+                            understand the current situation and act accordingly.
+                            The Severity field MUST be set only when Status=False.
+                          type: string
+                        status:
+                          description: Status of the condition, one of True, False,
+                            Unknown.
+                          type: string
+                        type:
+                          description: Type of condition in CamelCase or in foo.example.com/CamelCase.
+                            Many .condition.type values are consistent across resources
+                            like Available, but because arbitrary conditions can be
+                            useful (see .node.status.conditions), the ability to deconflict
+                            is important.
+                          type: string
+                      required:
+                      - lastTransitionTime
+                      - status
+                      - type
+                      type: object
+                    type: array
+                  lastRemediated:
+                    description: LastRemediated identifies when the host was last
+                      remediated
+                    format: date-time
+                    type: string
+                  phase:
+                    description: Phase represents the current phase of machine remediation.
+                      E.g. Pending, Running, Done etc.
+                    type: string
+                  retryCount:
+                    description: RetryCount can be used as a counter during the remediation.
+                      Field can hold number of reboots etc.
+                    type: integer
+                type: object
+            required:
+            - status
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index 599df517..9f29f608 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -14,6 +14,8 @@ resources:
   - bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalremediationtemplates.yaml
   - bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalhosts.yaml
   - bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalremediations.yaml
+  - bases/infrastructure.cluster.x-k8s.io_hcloudremediationtemplates.yaml
+  - bases/infrastructure.cluster.x-k8s.io_hcloudremediations.yaml
 #+kubebuilder:scaffold:crdkustomizeresource
 
 patchesStrategicMerge:
@@ -28,6 +30,8 @@ patchesStrategicMerge:
   - patches/webhook_in_hetznerbaremetalremediationtemplates.yaml
   - patches/webhook_in_hetznerbaremetalhosts.yaml
   - patches/webhook_in_hetznerbaremetalremediations.yaml
+  - patches/webhook_in_hcloudremediationtemplates.yaml
+  - patches/webhook_in_hcloudremediations.yaml
   #+kubebuilder:scaffold:crdkustomizewebhookpatch
 
   # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix.
@@ -41,6 +45,8 @@ patchesStrategicMerge:
   - patches/cainjection_in_hetznerbaremetalremediationtemplates.yaml
   - patches/cainjection_in_hetznerbaremetalhosts.yaml
   - patches/cainjection_in_hetznerbaremetalremediations.yaml
+  - patches/cainjection_in_hcloudremediationtemplates.yaml
+  - patches/cainjection_in_hcloudremediations.yaml
 #+kubebuilder:scaffold:crdkustomizecainjectionpatch
 
 # the following config is for teaching kustomize how to do kustomization for CRDs.
diff --git a/config/crd/patches/cainjection_in_hcloudremediations.yaml b/config/crd/patches/cainjection_in_hcloudremediations.yaml
new file mode 100644
index 00000000..65b90776
--- /dev/null
+++ b/config/crd/patches/cainjection_in_hcloudremediations.yaml
@@ -0,0 +1,7 @@
+# The following patch adds a directive for certmanager to inject CA into the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
+  name: hcloudremediations.infrastructure.cluster.x-k8s.io
diff --git a/config/crd/patches/cainjection_in_hcloudremediationtemplates.yaml b/config/crd/patches/cainjection_in_hcloudremediationtemplates.yaml
new file mode 100644
index 00000000..f8a528f5
--- /dev/null
+++ b/config/crd/patches/cainjection_in_hcloudremediationtemplates.yaml
@@ -0,0 +1,7 @@
+# The following patch adds a directive for certmanager to inject CA into the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
+  name: hcloudremediationtemplates.infrastructure.cluster.x-k8s.io
diff --git a/config/crd/patches/webhook_in_hcloudremediations.yaml b/config/crd/patches/webhook_in_hcloudremediations.yaml
new file mode 100644
index 00000000..6fd58525
--- /dev/null
+++ b/config/crd/patches/webhook_in_hcloudremediations.yaml
@@ -0,0 +1,16 @@
+# The following patch enables a conversion webhook for the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: hcloudremediations.infrastructure.cluster.x-k8s.io
+spec:
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          namespace: system
+          name: webhook-service
+          path: /convert
+      conversionReviewVersions:
+      - v1
diff --git a/config/crd/patches/webhook_in_hcloudremediationtemplates.yaml b/config/crd/patches/webhook_in_hcloudremediationtemplates.yaml
new file mode 100644
index 00000000..0d852fc7
--- /dev/null
+++ b/config/crd/patches/webhook_in_hcloudremediationtemplates.yaml
@@ -0,0 +1,16 @@
+# The following patch enables a conversion webhook for the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: hcloudremediationtemplates.infrastructure.cluster.x-k8s.io
+spec:
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          namespace: system
+          name: webhook-service
+          path: /convert
+      conversionReviewVersions:
+      - v1
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 3d10916c..910397fc 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -113,6 +113,32 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - infrastructure.cluster.x-k8s.io
+  resources:
+  - hcloudremediations
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - infrastructure.cluster.x-k8s.io
+  resources:
+  - hcloudremediations/finalizers
+  verbs:
+  - update
+- apiGroups:
+  - infrastructure.cluster.x-k8s.io
+  resources:
+  - hcloudremediations/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - infrastructure.cluster.x-k8s.io
   resources:
diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml
index 8fa4b114..f91578da 100644
--- a/config/webhook/manifests.yaml
+++ b/config/webhook/manifests.yaml
@@ -26,6 +26,47 @@ webhooks:
     resources:
     - hcloudmachines
   sideEffects: None
+- admissionReviewVersions:
+  - v1
+  - v1beta1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: system
+      path: /mutate-infrastructure-cluster-x-k8s-io-v1beta1-hcloudremediation
+  failurePolicy: Fail
+  name: mutation.hcloudremediation.infrastructure.cluster.x-k8s.io
+  rules:
+  - apiGroups:
+    - infrastructure.cluster.x-k8s.io
+    apiVersions:
+    - v1beta1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - hcloudremediations
+  sideEffects: None
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: system
+      path: /mutate-infrastructure-cluster-x-k8s-io-v1beta1-hcloudremediationtemplate
+  failurePolicy: Fail
+  name: mhcloudremediationtemplate.kb.io
+  rules:
+  - apiGroups:
+    - infrastructure.cluster.x-k8s.io
+    apiVersions:
+    - v1beta1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - hcloudremediationtemplates
+  sideEffects: None
 - admissionReviewVersions:
   - v1
   - v1beta1
@@ -179,6 +220,47 @@ webhooks:
     resources:
     - hcloudmachinetemplates
   sideEffects: None
+- admissionReviewVersions:
+  - v1
+  - v1beta1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: system
+      path: /validate-infrastructure-cluster-x-k8s-io-v1beta1-hcloudremediation
+  failurePolicy: Fail
+  name: validation.hcloudremediation.infrastructure.cluster.x-k8s.io
+  rules:
+  - apiGroups:
+    - infrastructure.cluster.x-k8s.io
+    apiVersions:
+    - v1beta1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - hcloudremediations
+  sideEffects: None
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: system
+      path: /validate-infrastructure-cluster-x-k8s-io-v1beta1-hcloudremediationtemplate
+  failurePolicy: Fail
+  name: vhcloudremediationtemplate.kb.io
+  rules:
+  - apiGroups:
+    - infrastructure.cluster.x-k8s.io
+    apiVersions:
+    - v1beta1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - hcloudremediationtemplates
+  sideEffects: None
 - admissionReviewVersions:
   - v1
   - v1beta1
diff --git a/controllers/hcloudremediation_controller.go b/controllers/hcloudremediation_controller.go
new file mode 100644
index 00000000..29f5ba46
--- /dev/null
+++ b/controllers/hcloudremediation_controller.go
@@ -0,0 +1,193 @@
+/*
+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 controllers
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	infrav1 "github.com/syself/cluster-api-provider-hetzner/api/v1beta1"
+	"github.com/syself/cluster-api-provider-hetzner/pkg/scope"
+	secretutil "github.com/syself/cluster-api-provider-hetzner/pkg/secrets"
+	hcloudclient "github.com/syself/cluster-api-provider-hetzner/pkg/services/hcloud/client"
+	hcloudremediation "github.com/syself/cluster-api-provider-hetzner/pkg/services/hcloud/remediation"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/klog/v2"
+	"sigs.k8s.io/cluster-api/util"
+	"sigs.k8s.io/cluster-api/util/annotations"
+	"sigs.k8s.io/cluster-api/util/patch"
+	"sigs.k8s.io/cluster-api/util/predicates"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+)
+
+// HCloudRemediationReconciler reconciles a HCloudRemediation object.
+type HCloudRemediationReconciler struct {
+	client.Client
+	APIReader           client.Reader
+	HCloudClientFactory hcloudclient.Factory
+	WatchFilterValue    string
+}
+
+//+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=hcloudremediations,verbs=get;list;watch;create;update;patch;delete
+//+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=hcloudremediations/status,verbs=get;update;patch
+//+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=hcloudremediations/finalizers,verbs=update
+//+kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machines;machines/status,verbs=get;update;patch
+
+// Reconcile reconciles the hetznerHCloudRemediation object.
+func (r *HCloudRemediationReconciler) Reconcile(ctx context.Context, req reconcile.Request) (_ reconcile.Result, reterr error) {
+	log := ctrl.LoggerFrom(ctx)
+
+	// Fetch the Hetzner bare metal host instance.
+	hcloudRemediation := &infrav1.HCloudRemediation{}
+	err := r.Get(ctx, req.NamespacedName, hcloudRemediation)
+	if err != nil {
+		if apierrors.IsNotFound(err) {
+			return reconcile.Result{}, nil
+		}
+		return reconcile.Result{}, err
+	}
+
+	log = log.WithValues("HCloudRemediation", klog.KObj(hcloudRemediation))
+
+	// Fetch the Machine.
+	machine, err := util.GetOwnerMachine(ctx, r.Client, hcloudRemediation.ObjectMeta)
+	if err != nil {
+		return reconcile.Result{}, err
+	}
+	if machine == nil {
+		log.Info("Machine Controller has not yet set OwnerRef")
+		return reconcile.Result{}, nil
+	}
+
+	log = log.WithValues("Machine", klog.KObj(machine))
+
+	// Fetch the HCloudMachine instance.
+	hcloudMachine := &infrav1.HCloudMachine{}
+
+	key := client.ObjectKey{
+		Name:      machine.Spec.InfrastructureRef.Name,
+		Namespace: machine.Spec.InfrastructureRef.Namespace,
+	}
+
+	if err := r.Get(ctx, key, hcloudMachine); err != nil {
+		if apierrors.IsNotFound(err) {
+			return reconcile.Result{}, nil
+		}
+		return reconcile.Result{}, err
+	}
+
+	log = log.WithValues("HCloudMachine", klog.KObj(hcloudMachine))
+
+	// Fetch the Cluster.
+	cluster, err := util.GetClusterFromMetadata(ctx, r.Client, machine.ObjectMeta)
+	if err != nil {
+		log.Info("Machine is missing cluster label or cluster does not exist")
+		return reconcile.Result{}, nil
+	}
+
+	if annotations.IsPaused(cluster, hcloudMachine) {
+		log.Info("HCloudMachine or linked Cluster is marked as paused. Won't reconcile")
+		return reconcile.Result{}, nil
+	}
+
+	log = log.WithValues("Cluster", klog.KObj(cluster))
+
+	hetznerCluster := &infrav1.HetznerCluster{}
+
+	hetznerClusterName := client.ObjectKey{
+		Namespace: hcloudMachine.Namespace,
+		Name:      cluster.Spec.InfrastructureRef.Name,
+	}
+	if err := r.Client.Get(ctx, hetznerClusterName, hetznerCluster); err != nil {
+		log.Info("HetznerCluster is not available yet")
+		return reconcile.Result{}, nil
+	}
+
+	log = log.WithValues("HetznerCluster", klog.KObj(hetznerCluster))
+	ctx = ctrl.LoggerInto(ctx, log)
+
+	// Create the scope.
+	secretManager := secretutil.NewSecretManager(log, r.Client, r.APIReader)
+	hcloudToken, _, err := getAndValidateHCloudToken(ctx, req.Namespace, hetznerCluster, secretManager)
+	if err != nil {
+		return hcloudTokenErrorResult(ctx, err, hcloudMachine, infrav1.InstanceReadyCondition, r.Client)
+	}
+
+	hcc := r.HCloudClientFactory.NewClient(hcloudToken)
+
+	remediationScope, err := scope.NewHCloudRemediationScope(ctx, scope.HCloudRemediationScopeParams{
+		Client:            r.Client,
+		Logger:            log,
+		Machine:           machine,
+		HCloudMachine:     hcloudMachine,
+		HetznerCluster:    hetznerCluster,
+		HCloudRemediation: hcloudRemediation,
+		HCloudClient:      hcc,
+	})
+	if err != nil {
+		return reconcile.Result{}, fmt.Errorf("failed to create scope: %w", err)
+	}
+
+	// Always close the scope when exiting this function so we can persist any HCloudRemediation changes.
+	defer func() {
+		// Always attempt to Patch the Remediation object and status after each reconciliation.
+		// Patch ObservedGeneration only if the reconciliation completed successfully
+		patchOpts := []patch.Option{}
+		patchOpts = append(patchOpts, patch.WithStatusObservedGeneration{})
+
+		if err := remediationScope.Close(ctx, patchOpts...); err != nil && reterr == nil {
+			reterr = err
+		}
+	}()
+
+	// Check whether rate limit has been reached and if so, then wait.
+	if wait := reconcileRateLimit(hcloudRemediation); wait {
+		return reconcile.Result{RequeueAfter: 30 * time.Second}, nil
+	}
+
+	if !hcloudRemediation.ObjectMeta.DeletionTimestamp.IsZero() {
+		// Nothing to do
+		return reconcile.Result{}, nil
+	}
+
+	return r.reconcileNormal(ctx, remediationScope)
+}
+
+func (r *HCloudRemediationReconciler) reconcileNormal(ctx context.Context, remediationScope *scope.HCloudRemediationScope) (reconcile.Result, error) {
+	remediationScope.Info("Reconciling HCloudRemediation")
+	hcloudRemediation := remediationScope.HCloudRemediation
+
+	// reconcile bare metal remediation
+	if result, brk, err := breakReconcile(hcloudremediation.NewService(remediationScope).Reconcile(ctx)); brk {
+		return result, fmt.Errorf("failed to reconcile server for HCloudRemediation %s/%s: %w",
+			hcloudRemediation.Namespace, hcloudRemediation.Name, err)
+	}
+
+	return reconcile.Result{}, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *HCloudRemediationReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&infrav1.HCloudRemediation{}).
+		WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(ctrl.LoggerFrom(ctx), r.WatchFilterValue)).
+		Complete(r)
+}
diff --git a/docs/reference/hcloud-remediation-template copy.md b/docs/reference/hcloud-remediation-template copy.md
new file mode 100644
index 00000000..4d4e9955
--- /dev/null
+++ b/docs/reference/hcloud-remediation-template copy.md
@@ -0,0 +1,11 @@
+## HCloudRemediationTemplate
+
+In ```HCloudRemediationTemplate``` you can define all important properties for ```HCloudRemediations```. With this remediation, you can define a custom method for the manner of how Machine Health Checks treat the unhealthy objects - `HCloudMachines` in this case. For more information about how to use remdiations, see [Advanced CAPH](/docs/topics/advanced-caph.md). ```HCloudRemediations``` are reconciled by the ```HCloudRemediationController```, which reconciles the remediatons and triggers the requested type of remediation on the relevant `HCloudMachine`.
+
+### Overview of HCloudRemediationTemplate.Spec
+| Key | Type | Default | Required | Description |
+|-----|-----|------|---------|-------------|
+| template.spec.strategy | object |  | yes | Remediation strategy to be applied |
+| template.spec.strategy.type | string | Reboot  | no | Type of the remediation strategy. At the moment, only "Reboot" is supported |
+| template.spec.strategy.retryLimit | int | 0 | no | Set maximum of remediation retries. Zero retries if not set. |
+| template.spec.strategy.timeout | string | | yes | Timeout of one remediation try. Should be of the form "10m", or "40s" |
diff --git a/docs/topics/advanced-caph.md b/docs/topics/advanced-caph.md
index fba797b2..19094b77 100644
--- a/docs/topics/advanced-caph.md
+++ b/docs/topics/advanced-caph.md
@@ -26,11 +26,14 @@ We support multi-tenancy. You can start multiple clusters in one Hetzner project
 
 ## Machine Health Checks with Custom Remediation Template
 
-Cluster API allows to [configure Machine Health Checks](https://cluster-api.sigs.k8s.io/tasks/automated-machine-management/healthchecking.html) with custom remediation strategies. This is helpful for our bare metal servers. If the health checks find out that one server cannot be reached, the normal strategy would be to delete it. In that case it would need to be provisioned again. This takes, of course, longer for bare metal servers than for virtual cloud servers. Therefore, we want to try to avoid this with the help of our `HetznerBareMetalRemediationController`. Instead of deleting the object and deprovisioning it, we first try to reboot it and see whether this helps. If it solves the problem, we save a lot of time that is required for re-provisioning it.
+Cluster API allows to [configure Machine Health Checks](https://cluster-api.sigs.k8s.io/tasks/automated-machine-management/healthchecking.html) with custom remediation strategies. This is helpful for our bare metal servers. If the health checks find out that one server cannot be reached, the normal strategy would be to delete it. In that case it would need to be provisioned again. This takes, of course, longer for bare metal servers than for virtual cloud servers. Therefore, we want to try to avoid this with the help of our `HetznerBareMetalRemediationController` and `HCloudRemediationController`. Instead of deleting the object and deprovisioning it, we first try to reboot it and see whether this helps. If it solves the problem, we save a lot of time that is required for re-provisioning it.
 
-If the MHC are configured to be used with the `HetznerBareMetalRemediationTemplate` (also see the [reference of the object](/docs/reference/hetzner-bare-metal-remediation-template.md)), then such an object is created every time the MHC finds an unhealthy machine. The controller that reconciles this object, then sets an annotation in the relevant `HetznerBareMetalHost` object that specifies the desired remediation strategy. At the moment, only "reboot" is supported.
+If the MHC are configured to be used with the `HetznerBareMetalRemediationTemplate` (also see the [reference of the object](/docs/reference/hetzner-bare-metal-remediation-template.md)) and `HCloudRemediationTemplate` (also see the [reference of the object](/docs/reference/hcloud-remediation-template.md)), then such an object is created every time the MHC finds an unhealthy machine. 
 
-Here is an example of how to configure the Machine Health Check and remediation template:
+The `HetznerBareMetalRemediationController` reconciles this object, then sets an annotation in the relevant `HetznerBareMetalHost` object that specifies the desired remediation strategy. At the moment, only "reboot" is supported.
+The `HCloudRemediationController` reboots the HCloudMachine directly via HCloud API. For HCloud servers, there is no other strategy than "reboot" either.
+
+Here is an example of how to configure the Machine Health Check and `HetznerBareMetalRemediationTemplate`:
 
 ```yaml
 apiVersion: cluster.x-k8s.io/v1beta1
@@ -69,6 +72,3 @@ spec:
         timeout: 300s
 
 ```
-
-Please also refer to the [reference](/docs/reference/hetzner-bare-metal-remediation-template.md) for more details on how to configure the `HetznerBareMetalRemediationTemplate`
-
diff --git a/main.go b/main.go
index fae6929e..1aa689c8 100644
--- a/main.go
+++ b/main.go
@@ -169,6 +169,16 @@ func main() {
 		os.Exit(1)
 	}
 
+	if err = (&controllers.HCloudRemediationReconciler{
+		Client:              mgr.GetClient(),
+		APIReader:           mgr.GetAPIReader(),
+		HCloudClientFactory: hcloudClientFactory,
+		WatchFilterValue:    watchFilterValue,
+	}).SetupWithManager(ctx, mgr, controller.Options{}); err != nil {
+		setupLog.Error(err, "unable to create controller", "controller", "HCloudRemediation")
+		os.Exit(1)
+	}
+
 	setUpWebhookWithManager(mgr)
 
 	//+kubebuilder:scaffold:builder
@@ -231,4 +241,12 @@ func setUpWebhookWithManager(mgr ctrl.Manager) {
 		setupLog.Error(err, "unable to create webhook", "webhook", "HetznerBareMetalRemediationTemplate")
 		os.Exit(1)
 	}
+	if err := (&infrastructurev1beta1.HCloudRemediation{}).SetupWebhookWithManager(mgr); err != nil {
+		setupLog.Error(err, "unable to create webhook", "webhook", "HCloudRemediation")
+		os.Exit(1)
+	}
+	if err := (&infrastructurev1beta1.HCloudRemediationTemplate{}).SetupWebhookWithManager(mgr); err != nil {
+		setupLog.Error(err, "unable to create webhook", "webhook", "HCloudRemediationTemplate")
+		os.Exit(1)
+	}
 }
diff --git a/pkg/scope/hcloudremediation.go b/pkg/scope/hcloudremediation.go
new file mode 100644
index 00000000..8b387fa2
--- /dev/null
+++ b/pkg/scope/hcloudremediation.go
@@ -0,0 +1,129 @@
+/*
+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 (
+	"context"
+
+	"github.com/go-logr/logr"
+	"github.com/pkg/errors"
+	infrav1 "github.com/syself/cluster-api-provider-hetzner/api/v1beta1"
+	hcloudclient "github.com/syself/cluster-api-provider-hetzner/pkg/services/hcloud/client"
+	hcloudutil "github.com/syself/cluster-api-provider-hetzner/pkg/services/hcloud/util"
+	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
+	"sigs.k8s.io/cluster-api/util/patch"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// HCloudRemediationScopeParams defines the input parameters used to create a new Scope.
+type HCloudRemediationScopeParams struct {
+	Logger            logr.Logger
+	Client            client.Client
+	HCloudClient      hcloudclient.Client
+	Machine           *clusterv1.Machine
+	HCloudMachine     *infrav1.HCloudMachine
+	HetznerCluster    *infrav1.HetznerCluster
+	HCloudRemediation *infrav1.HCloudRemediation
+}
+
+// NewHCloudRemediationScope creates a new Scope from the supplied parameters.
+// This is meant to be called for each reconcile iteration.
+func NewHCloudRemediationScope(ctx context.Context, params HCloudRemediationScopeParams) (*HCloudRemediationScope, error) {
+	if params.HCloudRemediation == nil {
+		return nil, errors.New("failed to generate new scope from nil HCloudRemediation")
+	}
+	if params.Client == nil {
+		return nil, errors.New("failed to generate new scope from nil client")
+	}
+	if params.HCloudClient == nil {
+		return nil, errors.New("failed to generate new scope from nil HCloudClient")
+	}
+	if params.Machine == nil {
+		return nil, errors.New("failed to generate new scope from nil Machine")
+	}
+	if params.HCloudMachine == nil {
+		return nil, errors.New("failed to generate new scope from nil HCloudMachine")
+	}
+
+	emptyLogger := logr.Logger{}
+	if params.Logger == emptyLogger {
+		return nil, errors.New("failed to generate new scope from nil Logger")
+	}
+
+	patchHelper, err := patch.NewHelper(params.HCloudRemediation, params.Client)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to init patch helper")
+	}
+
+	machinePatchHelper, err := patch.NewHelper(params.Machine, params.Client)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to init machine patch helper")
+	}
+
+	return &HCloudRemediationScope{
+		Logger:             params.Logger,
+		Client:             params.Client,
+		HCloudClient:       params.HCloudClient,
+		patchHelper:        patchHelper,
+		machinePatchHelper: machinePatchHelper,
+		Machine:            params.Machine,
+		HCloudMachine:      params.HCloudMachine,
+		HCloudRemediation:  params.HCloudRemediation,
+	}, nil
+}
+
+// HCloudRemediationScope defines the basic context for an actuator to operate upon.
+type HCloudRemediationScope struct {
+	logr.Logger
+	Client             client.Client
+	patchHelper        *patch.Helper
+	machinePatchHelper *patch.Helper
+	HCloudClient       hcloudclient.Client
+	Machine            *clusterv1.Machine
+	HCloudMachine      *infrav1.HCloudMachine
+	HCloudRemediation  *infrav1.HCloudRemediation
+}
+
+// Close closes the current scope persisting the cluster configuration and status.
+func (m *HCloudRemediationScope) Close(ctx context.Context, opts ...patch.Option) error {
+	return m.patchHelper.Patch(ctx, m.HCloudRemediation, opts...)
+}
+
+// Name returns the HCloudMachine name.
+func (m *HCloudRemediationScope) Name() string {
+	return m.HCloudRemediation.Name
+}
+
+// Namespace returns the namespace name.
+func (m *HCloudRemediationScope) Namespace() string {
+	return m.HCloudRemediation.Namespace
+}
+
+// ServerIDFromProviderID returns the namespace name.
+func (m *HCloudRemediationScope) ServerIDFromProviderID() (int, error) {
+	return hcloudutil.ServerIDFromProviderID(m.HCloudMachine.Spec.ProviderID)
+}
+
+// PatchObject persists the remediation spec and status.
+func (m *HCloudRemediationScope) PatchObject(ctx context.Context, opts ...patch.Option) error {
+	return m.patchHelper.Patch(ctx, m.HCloudRemediation, opts...)
+}
+
+// PatchMachine persists the machine spec and status.
+func (m *HCloudRemediationScope) PatchMachine(ctx context.Context, opts ...patch.Option) error {
+	return m.machinePatchHelper.Patch(ctx, m.Machine, opts...)
+}
diff --git a/pkg/services/baremetal/remediation/remediation.go b/pkg/services/baremetal/remediation/remediation.go
index c11fe2d1..8e6bb0db 100644
--- a/pkg/services/baremetal/remediation/remediation.go
+++ b/pkg/services/baremetal/remediation/remediation.go
@@ -73,12 +73,12 @@ func (s *Service) Reconcile(ctx context.Context) (_ *ctrl.Result, err error) {
 
 	remediationType := s.scope.BareMetalRemediation.Spec.Strategy.Type
 
-	if remediationType != infrav1.RebootRemediationStrategy {
+	if remediationType != infrav1.RemediationTypeReboot {
 		s.scope.Info("unsupported remediation strategy")
 		return &ctrl.Result{}, nil
 	}
 
-	if remediationType == infrav1.RebootRemediationStrategy {
+	if remediationType == infrav1.RemediationTypeReboot {
 		// If no phase set, default to running
 		if s.scope.BareMetalRemediation.Status.Phase == "" {
 			s.scope.BareMetalRemediation.Status.Phase = infrav1.PhaseRunning
diff --git a/pkg/services/hcloud/client/client.go b/pkg/services/hcloud/client/client.go
index 4c1d8806..82add046 100644
--- a/pkg/services/hcloud/client/client.go
+++ b/pkg/services/hcloud/client/client.go
@@ -45,10 +45,12 @@ type Client interface {
 	CreateServer(context.Context, hcloud.ServerCreateOpts) (hcloud.ServerCreateResult, error)
 	AttachServerToNetwork(context.Context, *hcloud.Server, hcloud.ServerAttachToNetworkOpts) (*hcloud.Action, error)
 	ListServers(context.Context, hcloud.ServerListOpts) ([]*hcloud.Server, error)
+	GetServer(context.Context, int) (*hcloud.Server, error)
 	DeleteServer(context.Context, *hcloud.Server) error
 	ListServerTypes(context.Context) ([]*hcloud.ServerType, error)
 	PowerOnServer(context.Context, *hcloud.Server) (*hcloud.Action, error)
 	ShutdownServer(context.Context, *hcloud.Server) (*hcloud.Action, error)
+	RebootServer(context.Context, *hcloud.Server) (*hcloud.Action, error)
 	CreateNetwork(context.Context, hcloud.NetworkCreateOpts) (*hcloud.Network, error)
 	ListNetworks(context.Context, hcloud.NetworkListOpts) ([]*hcloud.Network, error)
 	DeleteNetwork(context.Context, *hcloud.Network) error
@@ -171,6 +173,11 @@ func (c *realClient) ListServers(ctx context.Context, opts hcloud.ServerListOpts
 	return c.client.Server.AllWithOpts(ctx, opts)
 }
 
+func (c *realClient) GetServer(ctx context.Context, id int) (*hcloud.Server, error) {
+	res, _, err := c.client.Server.GetByID(ctx, id)
+	return res, err
+}
+
 func (c *realClient) ListServerTypes(ctx context.Context) ([]*hcloud.ServerType, error) {
 	return c.client.ServerType.All(ctx)
 }
@@ -180,6 +187,11 @@ func (c *realClient) ShutdownServer(ctx context.Context, server *hcloud.Server)
 	return res, err
 }
 
+func (c *realClient) RebootServer(ctx context.Context, server *hcloud.Server) (*hcloud.Action, error) {
+	res, _, err := c.client.Server.Reboot(ctx, server)
+	return res, err
+}
+
 func (c *realClient) PowerOnServer(ctx context.Context, server *hcloud.Server) (*hcloud.Action, error) {
 	res, _, err := c.client.Server.Poweron(ctx, server)
 	return res, err
diff --git a/pkg/services/hcloud/client/fake/hcloud_client.go b/pkg/services/hcloud/client/fake/hcloud_client.go
index 284aa219..78ec2163 100644
--- a/pkg/services/hcloud/client/fake/hcloud_client.go
+++ b/pkg/services/hcloud/client/fake/hcloud_client.go
@@ -464,6 +464,10 @@ func (c *cacheHCloudClient) ListServers(ctx context.Context, opts hcloud.ServerL
 	return servers, nil
 }
 
+func (c *cacheHCloudClient) GetServer(ctx context.Context, id int) (*hcloud.Server, error) {
+	return c.serverCache.idMap[id], nil
+}
+
 func (c *cacheHCloudClient) ShutdownServer(ctx context.Context, server *hcloud.Server) (*hcloud.Action, error) {
 	if _, found := c.serverCache.idMap[server.ID]; !found {
 		return nil, hcloud.Error{Code: hcloud.ErrorCodeNotFound, Message: "not found"}
@@ -472,6 +476,10 @@ func (c *cacheHCloudClient) ShutdownServer(ctx context.Context, server *hcloud.S
 	return &hcloud.Action{}, nil
 }
 
+func (c *cacheHCloudClient) RebootServer(ctx context.Context, server *hcloud.Server) (*hcloud.Action, error) {
+	return nil, nil
+}
+
 func (c *cacheHCloudClient) PowerOnServer(ctx context.Context, server *hcloud.Server) (*hcloud.Action, error) {
 	if _, found := c.serverCache.idMap[server.ID]; !found {
 		return nil, hcloud.Error{Code: hcloud.ErrorCodeNotFound, Message: "not found"}
diff --git a/pkg/services/hcloud/remediation/remediation.go b/pkg/services/hcloud/remediation/remediation.go
new file mode 100644
index 00000000..77a4042a
--- /dev/null
+++ b/pkg/services/hcloud/remediation/remediation.go
@@ -0,0 +1,215 @@
+/*
+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 remediation implements functions to manage the lifecycle of hcloud remediation.
+package remediation
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/hetznercloud/hcloud-go/hcloud"
+	infrav1 "github.com/syself/cluster-api-provider-hetzner/api/v1beta1"
+	"github.com/syself/cluster-api-provider-hetzner/pkg/scope"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	capi "sigs.k8s.io/cluster-api/api/v1beta1"
+	"sigs.k8s.io/cluster-api/util/conditions"
+	"sigs.k8s.io/cluster-api/util/record"
+	ctrl "sigs.k8s.io/controller-runtime"
+)
+
+// Service defines struct with machine scope to reconcile HCloudRemediation.
+type Service struct {
+	scope *scope.HCloudRemediationScope
+}
+
+// NewService outs a new service with machine scope.
+func NewService(scope *scope.HCloudRemediationScope) *Service {
+	return &Service{
+		scope: scope,
+	}
+}
+
+// Reconcile implements reconcilement of HCloudRemediation.
+func (s *Service) Reconcile(ctx context.Context) (_ *ctrl.Result, err error) {
+	s.scope.Info("Reconciling hcloud remediation", "name", s.scope.HCloudRemediation.Name)
+
+	server, err := s.findServer(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("failed to find the server of unhealthy machine: %w", err)
+	}
+
+	remediationType := s.scope.HCloudRemediation.Spec.Strategy.Type
+
+	if remediationType != infrav1.RemediationTypeReboot {
+		s.scope.Info("unsupported remediation strategy")
+		record.Warnf(s.scope.HCloudRemediation, "UnsupportedRemdiationStrategy", "remediation strategy %q is unsupported", remediationType)
+		return nil, nil
+	}
+
+	if remediationType == infrav1.RemediationTypeReboot {
+		// If no phase set, default to running
+		if s.scope.HCloudRemediation.Status.Phase == "" {
+			s.scope.HCloudRemediation.Status.Phase = infrav1.PhaseRunning
+		}
+
+		switch s.scope.HCloudRemediation.Status.Phase {
+		case infrav1.PhaseRunning:
+			return s.handlePhaseRunning(ctx, server)
+		case infrav1.PhaseWaiting:
+			return s.handlePhaseWaiting(ctx)
+		default:
+		}
+	}
+	return nil, nil
+}
+
+func (s *Service) handlePhaseRunning(ctx context.Context, server *hcloud.Server) (*ctrl.Result, error) {
+	// server is not rebooted yet
+	if s.scope.HCloudRemediation.Status.LastRemediated == nil {
+		s.scope.Info("Rebooting the server")
+
+		if err := s.rebootServer(ctx, server); err != nil {
+			return nil, fmt.Errorf("failed to reboot server: %w", err)
+		}
+
+		now := metav1.Now()
+		s.scope.HCloudRemediation.Status.LastRemediated = &now
+		s.scope.HCloudRemediation.Status.RetryCount++
+	}
+
+	retryLimit := s.scope.HCloudRemediation.Spec.Strategy.RetryLimit
+	retryCount := s.scope.HCloudRemediation.Status.RetryCount
+
+	if retryLimit > 0 && retryLimit > retryCount {
+		okToRemediate, nextRemediation := s.timeToRemediate()
+
+		if okToRemediate {
+			s.scope.Info("Rebooting the server")
+
+			if err := s.rebootServer(ctx, server); err != nil {
+				return nil, fmt.Errorf("failed to reboot server: %w", err)
+			}
+
+			now := metav1.Now()
+			s.scope.HCloudRemediation.Status.LastRemediated = &now
+			s.scope.HCloudRemediation.Status.RetryCount++
+		}
+
+		if nextRemediation > 0 {
+			// Not yet time to remediate, requeue
+			return &ctrl.Result{RequeueAfter: nextRemediation}, nil
+		}
+	} else {
+		s.scope.HCloudRemediation.Status.Phase = infrav1.PhaseWaiting
+	}
+	return nil, nil
+}
+
+func (s *Service) handlePhaseWaiting(ctx context.Context) (*ctrl.Result, error) {
+	okToStop, nextCheck := s.timeToRemediate()
+
+	if okToStop {
+		s.scope.HCloudRemediation.Status.Phase = infrav1.PhaseDeleting
+		// When machine is still unhealthy after remediation, setting of OwnerRemediatedCondition
+		// moves control to CAPI machine controller. The owning controller will do
+		// preflight checks and handles the Machine deletion
+
+		if err := s.setOwnerRemediatedCondition(ctx); err != nil {
+			return nil, fmt.Errorf("failed to set conditions on CAPI machine: %w", err)
+		}
+	}
+
+	if nextCheck > 0 {
+		// Not yet time to stop remediation, requeue
+		return &ctrl.Result{RequeueAfter: nextCheck}, nil
+	}
+	return nil, nil
+}
+
+func (s *Service) findServer(ctx context.Context) (*hcloud.Server, error) {
+	serverID, err := s.scope.ServerIDFromProviderID()
+	if err != nil {
+		return nil, fmt.Errorf("failed to get serverID from providerID: %w", err)
+	}
+
+	server, err := s.scope.HCloudClient.GetServer(ctx, serverID)
+	if err != nil {
+		if hcloud.IsError(err, hcloud.ErrorCodeRateLimitExceeded) {
+			conditions.MarkTrue(s.scope.HCloudRemediation, infrav1.RateLimitExceeded)
+			record.Event(s.scope.HCloudRemediation,
+				"RateLimitExceeded",
+				"exceeded rate limit with calling hcloud function GetServer",
+			)
+		}
+		return nil, fmt.Errorf("failed to get server: %w", err)
+	}
+
+	return server, nil
+}
+
+func (s *Service) rebootServer(ctx context.Context, server *hcloud.Server) error {
+	s.scope.Info("Rebooting server", "server", server.ID)
+	if _, err := s.scope.HCloudClient.RebootServer(ctx, server); err != nil {
+		if hcloud.IsError(err, hcloud.ErrorCodeRateLimitExceeded) {
+			conditions.MarkTrue(s.scope.HCloudRemediation, infrav1.RateLimitExceeded)
+			record.Event(s.scope.HCloudRemediation,
+				"RateLimitExceeded",
+				"exceeded rate limit with calling hcloud function RebootServer",
+			)
+		}
+		return fmt.Errorf("failed to reboot server %v: %w", server.ID, err)
+	}
+	return nil
+}
+
+// timeToRemediate checks if it is time to execute a next remediation step
+// and returns seconds to next remediation time.
+func (s *Service) timeToRemediate() (bool, time.Duration) {
+	now := time.Now()
+	timeout := s.scope.HCloudRemediation.Spec.Strategy.Timeout.Duration
+	lastRemediated := s.scope.HCloudRemediation.Status.LastRemediated
+
+	// status is not updated yet
+	if lastRemediated == nil {
+		return false, timeout
+	}
+
+	if lastRemediated.Add(timeout).Before(now) {
+		return true, time.Duration(0)
+	}
+
+	timeElapsedSinceLastRemediation := now.Sub(lastRemediated.Time)
+	nextRemediation := timeout - timeElapsedSinceLastRemediation + time.Second
+	return false, nextRemediation
+}
+
+// setOwnerRemediatedCondition sets MachineOwnerRemediatedCondition on CAPI machine object
+// that have failed a healthcheck.
+func (s *Service) setOwnerRemediatedCondition(ctx context.Context) error {
+	conditions.MarkFalse(
+		s.scope.Machine,
+		capi.MachineOwnerRemediatedCondition,
+		capi.WaitingForRemediationReason,
+		capi.ConditionSeverityWarning,
+		"",
+	)
+	if err := s.scope.PatchMachine(ctx); err != nil {
+		return fmt.Errorf("failed to patch machine: %w", err)
+	}
+	return nil
+}
diff --git a/pkg/services/hcloud/util/utils.go b/pkg/services/hcloud/util/utils.go
new file mode 100644
index 00000000..5c73f524
--- /dev/null
+++ b/pkg/services/hcloud/util/utils.go
@@ -0,0 +1,56 @@
+/*
+Copyright 2023 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 hcloudutil contains utility functions for hcloud servers.
+package hcloudutil
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+)
+
+const providerIDPrefix = "hcloud://"
+
+var (
+	// ErrInvalidProviderID indicates that the providerID is invalid.
+	ErrInvalidProviderID = fmt.Errorf("invalid providerID")
+	// ErrNilProviderID indicates that the providerID is nil.
+	ErrNilProviderID = fmt.Errorf("nil providerID")
+)
+
+// ProviderIDFromServerID returns the providerID of a hcloud server from a serverID.
+func ProviderIDFromServerID(serverID int) string {
+	return fmt.Sprintf("%s%v", providerIDPrefix, serverID)
+}
+
+// ServerIDFromProviderID returns the serverID from a providerID.
+func ServerIDFromProviderID(providerID *string) (int, error) {
+	if providerID == nil {
+		return 0, ErrNilProviderID
+	}
+	stringParts := strings.Split(*providerID, "://")
+	if len(stringParts) != 2 || stringParts[0] == "" || stringParts[1] == "" {
+		return 0, ErrInvalidProviderID
+	}
+	idString := stringParts[1]
+	id, err := strconv.Atoi(idString)
+	if err != nil {
+		return 0, fmt.Errorf("failed to convert serverID to int - %w: %w", ErrInvalidProviderID, err)
+	}
+
+	return id, nil
+}


Test patch:
diff --git a/pkg/services/hcloud/client/fake/hcloud_client_test.go b/pkg/services/hcloud/client/fake/hcloud_client_test.go
index 54314f9b..405bddc2 100644
--- a/pkg/services/hcloud/client/fake/hcloud_client_test.go
+++ b/pkg/services/hcloud/client/fake/hcloud_client_test.go
@@ -28,11 +28,13 @@ import (
 
 const labelSelector = "key1==val1"
 
-var factory = fake.NewHCloudClientFactory()
-var ctx = context.Background()
+var (
+	factory = fake.NewHCloudClientFactory()
+	ctx     = context.Background()
+)
 
 var _ = Describe("Load balancer", func() {
-	var opts = hcloud.LoadBalancerCreateOpts{
+	opts := hcloud.LoadBalancerCreateOpts{
 		Name: "lb-name",
 		Labels: map[string]string{
 			"key1": "val1",
@@ -56,7 +58,7 @@ var _ = Describe("Load balancer", func() {
 	var listOpts hcloud.LoadBalancerListOpts
 	listOpts.LabelSelector = labelSelector
 
-	var networkOpts = hcloud.NetworkCreateOpts{
+	networkOpts := hcloud.NetworkCreateOpts{
 		Name: "network-name",
 		Labels: map[string]string{
 			"key1": "val1",
@@ -66,7 +68,7 @@ var _ = Describe("Load balancer", func() {
 		Subnets: []hcloud.NetworkSubnet{{IPRange: &net.IPNet{IP: net.IP("1.2.3.6")}}},
 	}
 
-	var client = factory.NewClient("")
+	client := factory.NewClient("")
 
 	var lb *hcloud.LoadBalancer
 	var network *hcloud.Network
@@ -407,9 +409,9 @@ var _ = Describe("Load balancer", func() {
 		Expect(err).ToNot(Succeed())
 	})
 
-	var _ = Describe("Images", func() {
+	_ = Describe("Images", func() {
 		var listOpts hcloud.ImageListOpts
-		var client = factory.NewClient("")
+		client := factory.NewClient("")
 		BeforeEach(func() {
 			listOpts.LabelSelector = labelSelector
 		})
@@ -425,9 +427,9 @@ var _ = Describe("Server", func() {
 	var listOpts hcloud.ServerListOpts
 	listOpts.LabelSelector = labelSelector
 
-	var client = factory.NewClient("")
+	client := factory.NewClient("")
 
-	var opts = hcloud.ServerCreateOpts{
+	opts := hcloud.ServerCreateOpts{
 		Name: "test-server",
 		Labels: map[string]string{
 			"key1": "val1",
@@ -504,6 +506,13 @@ var _ = Describe("Server", func() {
 		Expect(resp[0].ID).To(Equal(server.ID))
 	})
 
+	It("gets a server", func() {
+		resp, err := client.GetServer(ctx, 1)
+		Expect(err).To(Succeed())
+		Expect(resp).ToNot(BeNil())
+		Expect(resp.ID).To(Equal(server.ID))
+	})
+
 	It("shuts a server down", func() {
 		Expect(server.Status).To(Equal(hcloud.ServerStatusRunning))
 		_, err := client.ShutdownServer(ctx, server)
@@ -584,9 +593,9 @@ var _ = Describe("Network", func() {
 	var listOpts hcloud.NetworkListOpts
 	listOpts.LabelSelector = labelSelector
 
-	var client = factory.NewClient("")
+	client := factory.NewClient("")
 
-	var opts = hcloud.NetworkCreateOpts{
+	opts := hcloud.NetworkCreateOpts{
 		Name: "test-network",
 		Labels: map[string]string{
 			"key1": "val1",
@@ -641,7 +650,7 @@ var _ = Describe("Placement groups", func() {
 	var listOpts hcloud.PlacementGroupListOpts
 	listOpts.LabelSelector = labelSelector
 
-	var opts = hcloud.PlacementGroupCreateOpts{
+	opts := hcloud.PlacementGroupCreateOpts{
 		Name: "placement-group-name",
 		Labels: map[string]string{
 			"key1": "val1",
@@ -650,7 +659,7 @@ var _ = Describe("Placement groups", func() {
 		Type: "stream",
 	}
 
-	var client = factory.NewClient("")
+	client := factory.NewClient("")
 
 	var server *hcloud.Server
 	var placementGroup *hcloud.PlacementGroup
diff --git a/pkg/services/hcloud/util/utils_test.go b/pkg/services/hcloud/util/utils_test.go
new file mode 100644
index 00000000..e839a368
--- /dev/null
+++ b/pkg/services/hcloud/util/utils_test.go
@@ -0,0 +1,81 @@
+/*
+Copyright 2023 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 hcloudutil
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+func TestHCloudUtils(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "HCloud utils tests")
+}
+
+var _ = Describe("Test ProviderIDFromServerID", func() {
+	It("generates a correct providerID from a serverID", func() {
+		Expect(ProviderIDFromServerID(42)).To(Equal("hcloud://42"))
+	})
+})
+
+var _ = Describe("Test ServerIDFromProviderID", func() {
+	It("gives the correct error when providerID is nil", func() {
+		_, err := ServerIDFromProviderID(nil)
+		Expect(err).ToNot(BeNil())
+		Expect(err).To(MatchError(ErrNilProviderID))
+	})
+	type testCaseServerIDFromProviderID struct {
+		providerID     string
+		expectServerID int
+		expectError    error
+	}
+
+	DescribeTable("Test ServerIDFromProviderID",
+		func(tc testCaseServerIDFromProviderID) {
+			serverID, err := ServerIDFromProviderID(&tc.providerID)
+			Expect(serverID).Should(Equal(tc.expectServerID))
+			if tc.expectError != nil {
+				Expect(err).ToNot(BeNil())
+				Expect(err).Should(MatchError(tc.expectError))
+			} else {
+				Expect(err).To(BeNil())
+			}
+		},
+		Entry("valid providerID", testCaseServerIDFromProviderID{
+			providerID:     "hcloud://42",
+			expectServerID: 42,
+			expectError:    nil,
+		}),
+		Entry("invalid serverID", testCaseServerIDFromProviderID{
+			providerID:     "hcloud://serverID",
+			expectServerID: 0,
+			expectError:    ErrInvalidProviderID,
+		}),
+		Entry("invalid providerID 1", testCaseServerIDFromProviderID{
+			providerID:     "hcloud::serverID",
+			expectServerID: 0,
+			expectError:    ErrInvalidProviderID,
+		}),
+		Entry("invalid providerID 2", testCaseServerIDFromProviderID{
+			providerID:     "hcloud://serverID://s",
+			expectServerID: 0,
+			expectError:    ErrInvalidProviderID,
+		}),
+	)
+})
diff --git a/test/helpers/envtest.go b/test/helpers/envtest.go
index f34b7f99..8ca7a782 100644
--- a/test/helpers/envtest.go
+++ b/test/helpers/envtest.go
@@ -168,6 +168,12 @@ func NewTestEnvironment() *TestEnvironment {
 	if err := (&infrav1.HetznerBareMetalRemediationTemplate{}).SetupWebhookWithManager(mgr); err != nil {
 		klog.Fatalf("failed to set up webhook with manager for HetznerBareMetalRemediationTemplate: %s", err)
 	}
+	if err := (&infrav1.HCloudRemediation{}).SetupWebhookWithManager(mgr); err != nil {
+		klog.Fatalf("failed to set up webhook with manager for HCloudRemediation: %s", err)
+	}
+	if err := (&infrav1.HCloudRemediationTemplate{}).SetupWebhookWithManager(mgr); err != nil {
+		klog.Fatalf("failed to set up webhook with manager for HCloudRemediationTemplate: %s", err)
+	}
 	// Create a fake HCloudClientFactory
 	hcloudClientFactory := fakeclient.NewHCloudClientFactory()