diff --git a/roles/kubernetes/preinstall/defaults/main.yml b/roles/kubernetes/preinstall/defaults/main.yml
index 09da2ec9bf7e0397fb531818cfb9fb3490e3152e..77de0b70249cc6b668fd385ceff2b2acc871e276 100644
--- a/roles/kubernetes/preinstall/defaults/main.yml
+++ b/roles/kubernetes/preinstall/defaults/main.yml
@@ -6,9 +6,6 @@ epel_enabled: false
 # Kubespray sets this to true after clusterDNS is running to apply changes to the host resolv.conf
 dns_late: false
 
-common_required_pkgs:
-  - "{{ kube_proxy_mode == 'ipvs' | ternary(['ipvsadm', 'ipset'], []) }}"
-
 # Set to true if your network does not support IPv6
 # This may be necessary for pulling Docker images from
 # GCE docker repository
diff --git a/roles/kubernetes/preinstall/files/pkgs-schema.json b/roles/kubernetes/preinstall/files/pkgs-schema.json
index 22fd0fa192e21cd1587fbf159a9ac8e0a999fb08..1fb9e28de2097ae599af89dd931d77a63eaed9d0 100644
--- a/roles/kubernetes/preinstall/files/pkgs-schema.json
+++ b/roles/kubernetes/preinstall/files/pkgs-schema.json
@@ -9,6 +9,11 @@
             "type": "object",
             "additionalProperties": false,
             "properties": {
+                "enabled": {
+                    "description": "Escape hatch to filter packages. The value is expected to be pre-resolved to a boolean by Jinja",
+                    "type": "boolean",
+                    "default": true
+                },
                 "groups": {
                     "description": "Match if the host is in one of these groups. If not specified match any host.",
                     "type": "array",
diff --git a/roles/kubernetes/preinstall/tasks/0070-system-packages.yml b/roles/kubernetes/preinstall/tasks/0070-system-packages.yml
index 1e27c6b7af2b0850ec6b6c44c572a7a12dc353f6..7085ffb0c4966166e4c6eaf526db6b84fe3dce6a 100644
--- a/roles/kubernetes/preinstall/tasks/0070-system-packages.yml
+++ b/roles/kubernetes/preinstall/tasks/0070-system-packages.yml
@@ -64,7 +64,7 @@
     # The json_query for selecting packages name is split for readability
     # see files/pkgs-schema.json for the structure of `pkgs`
     # and the matching semantics
-    full_query: "[? value | ( {{ filters_os }} ) && ( {{ filters_groups }} ) ].key"
+    full_query: "[? value | (enabled == null || enabled) && ( {{ filters_os }} ) && ( {{ filters_groups }} ) ].key"
     filters_groups: "groups | @ == null || [? contains(`{{ group_names }}`, @)]"
     filters_os: "os == null || (os | ( {{ filters_family }} ) || ( {{ filters_distro }} ))"
     dquote: !unsafe '"'
diff --git a/roles/kubernetes/preinstall/vars/main.yml b/roles/kubernetes/preinstall/vars/main.yml
index 7c83d855e72a3dfdde29aa1872b66bbbadf37e10..28ee56a278635f246bf8de7b9d472a3ead6d1b38 100644
--- a/roles/kubernetes/preinstall/vars/main.yml
+++ b/roles/kubernetes/preinstall/vars/main.yml
@@ -54,7 +54,15 @@ pkgs:
           major_versions:
           - "11"
           - "12"
+  ipset:
+    enabled: "{{ kube_proxy_mode != 'ipvs' }}"
+    groups:
+    - k8s_cluster
   iptables: *deb_redhat
+  ipvsadm:
+    enabled: "{{ kube_proxy_mode == 'ipvs' }}"
+    groups:
+    - k8s_cluster
   libseccomp: *redhat_family
   libseccomp2:
     groups: