diff --git a/roles/kubernetes/control-plane/tasks/kubeadm-upgrade.yml b/roles/kubernetes/control-plane/tasks/kubeadm-upgrade.yml
index 343724c473c7875c4d2ddefb28c149caf535079f..9609c2f3d853ddf42b3e4a9b36a411b83bf97af2 100644
--- a/roles/kubernetes/control-plane/tasks/kubeadm-upgrade.yml
+++ b/roles/kubernetes/control-plane/tasks/kubeadm-upgrade.yml
@@ -18,7 +18,7 @@
     --ignore-preflight-errors=all
     --allow-experimental-upgrades
     --etcd-upgrade={{ (etcd_deployment_type == "kubeadm") | bool | lower }}
-    {% if kubeadm_patches is defined and kubeadm_patches.enabled %}--patches={{ kubeadm_patches.dest_dir }}{% endif %}
+    {% if kubeadm_patches | length > 0 %}--patches={{ kubeadm_patches_dir }}{% endif %}
     --force
   register: kubeadm_upgrade
   # Retry is because upload config sometimes fails
@@ -39,7 +39,7 @@
     --ignore-preflight-errors=all
     --allow-experimental-upgrades
     --etcd-upgrade={{ (etcd_deployment_type == "kubeadm") | bool | lower }}
-    {% if kubeadm_patches is defined and kubeadm_patches.enabled %}--patches={{ kubeadm_patches.dest_dir }}{% endif %}
+    {% if kubeadm_patches | length > 0 %}--patches={{ kubeadm_patches_dir }}{% endif %}
     --force
   register: kubeadm_upgrade
   # Retry is because upload config sometimes fails
diff --git a/roles/kubernetes/control-plane/templates/kubeadm-config.v1beta3.yaml.j2 b/roles/kubernetes/control-plane/templates/kubeadm-config.v1beta3.yaml.j2
index ca48a3a911656607f09f48319e8c4232eda756b9..9dd5e43764e29ac6dc0697548ee0798238e5983b 100644
--- a/roles/kubernetes/control-plane/templates/kubeadm-config.v1beta3.yaml.j2
+++ b/roles/kubernetes/control-plane/templates/kubeadm-config.v1beta3.yaml.j2
@@ -28,9 +28,9 @@ nodeRegistration:
   kubeletExtraArgs:
     cloud-provider: external
 {% endif %}
-{% if kubeadm_patches is defined and kubeadm_patches.enabled %}
+{% if kubeadm_patches | length > 0 %}
 patches:
-  directory: {{ kubeadm_patches.dest_dir }}
+  directory: {{ kubeadm_patches_dir }}
 {% endif %}
 ---
 apiVersion: kubeadm.k8s.io/v1beta3
diff --git a/roles/kubernetes/control-plane/templates/kubeadm-controlplane.v1beta3.yaml.j2 b/roles/kubernetes/control-plane/templates/kubeadm-controlplane.v1beta3.yaml.j2
index cd19b5c2e5f1fb620fbd49c4befb7d9d54e0bb6b..bc9f3bdf929900b75c50d7152205395bea08b850 100644
--- a/roles/kubernetes/control-plane/templates/kubeadm-controlplane.v1beta3.yaml.j2
+++ b/roles/kubernetes/control-plane/templates/kubeadm-controlplane.v1beta3.yaml.j2
@@ -31,7 +31,7 @@ nodeRegistration:
 {% else %}
   taints: []
 {% endif %}
-{% if kubeadm_patches is defined and kubeadm_patches.enabled %}
+{% if kubeadm_patches | length > 0 %}
 patches:
-  directory: {{ kubeadm_patches.dest_dir }}
+  directory: {{ kubeadm_patches_dir }}
 {% endif %}
diff --git a/roles/kubernetes/kubeadm/templates/kubeadm-client.conf.v1beta3.j2 b/roles/kubernetes/kubeadm/templates/kubeadm-client.conf.v1beta3.j2
index 3b3bc57de4688c4fe9e753a0d78be737df0f1009..5016df9c3dc5939b4d4408964e7690226185cc6f 100644
--- a/roles/kubernetes/kubeadm/templates/kubeadm-client.conf.v1beta3.j2
+++ b/roles/kubernetes/kubeadm/templates/kubeadm-client.conf.v1beta3.j2
@@ -38,7 +38,7 @@ nodeRegistration:
   - effect: NoSchedule
     key: node-role.kubernetes.io/calico-rr
 {% endif %}
-{% if kubeadm_patches is defined and kubeadm_patches.enabled %}
+{% if kubeadm_patches | length > 0 %}
 patches:
-  directory: {{ kubeadm_patches.dest_dir }}
+  directory: {{ kubeadm_patches_dir }}
 {% endif %}
diff --git a/roles/kubernetes/kubeadm_common/defaults/main.yml b/roles/kubernetes/kubeadm_common/defaults/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f7d70691a27d4e6eef028eeac8856fc69bb0f9bc
--- /dev/null
+++ b/roles/kubernetes/kubeadm_common/defaults/main.yml
@@ -0,0 +1,14 @@
+---
+kubeadm_patches_dir: "{{ kube_config_dir }}/patches"
+kubeadm_patches: []
+# kubeadm_patches:
+# - target: kube-apiserver|kube-controller-manager|kube-scheduler|etcd|kubeletconfiguration
+#   type: strategic(default)|json|merge
+#   patch:
+#    metadata:
+#      annotations:
+#        example.com/test: "true"
+#      labels:
+#        example.com/prod_level: "{{ prod_level }}"
+# - ...
+# Patches are applied in the order they are specified.
diff --git a/roles/kubernetes/kubeadm_common/tasks/main.yml b/roles/kubernetes/kubeadm_common/tasks/main.yml
index b1f316e225d550a814e956147a11863efa6fc9fc..0f8d3b0a0af0e613461138c676496e268a03bd1a 100644
--- a/roles/kubernetes/kubeadm_common/tasks/main.yml
+++ b/roles/kubernetes/kubeadm_common/tasks/main.yml
@@ -1,15 +1,17 @@
 ---
 - name: Kubeadm | Create directory to store kubeadm patches
   file:
-    path: "{{ kubeadm_patches.dest_dir }}"
+    path: "{{ kubeadm_patches_dir }}"
     state: directory
     mode: "0640"
-  when: kubeadm_patches is defined and kubeadm_patches.enabled
+  when: kubeadm_patches | length > 0
 
 - name: Kubeadm | Copy kubeadm patches from inventory files
   copy:
-    src: "{{ kubeadm_patches.source_dir }}/"
-    dest: "{{ kubeadm_patches.dest_dir }}"
+    content: "{{ item.patch | to_yaml }}"
+    dest: "{{ kubeadm_patches_dir }}/{{ item.target }}{{ suffix }}+{{ item.type | d('strategic') }}.yaml"
     owner: "root"
     mode: "0644"
-  when: kubeadm_patches is defined and kubeadm_patches.enabled
+  loop: "{{ kubeadm_patches }}"
+  loop_control:
+    index_var: suffix