diff --git a/roles/kubernetes/master/defaults/main.yml b/roles/kubernetes/master/defaults/main.yml
index 527b168b927cec1602659fe6cd96fd05122ef4fe..659dcc847e66f1f12898ffaae3310ad56806825a 100644
--- a/roles/kubernetes/master/defaults/main.yml
+++ b/roles/kubernetes/master/defaults/main.yml
@@ -13,6 +13,9 @@ kube_apiserver_node_port_range: "30000-32767"
 etcd_config_dir: /etc/ssl/etcd
 etcd_cert_dir: "{{ etcd_config_dir }}/ssl"
 
+# ETCD backend for k8s data
+kube_apiserver_storage_backend: etcd3
+
 # Limits for kube components
 kube_controller_memory_limit: 512M
 kube_controller_cpu_limit: 250m
@@ -29,7 +32,6 @@ kube_apiserver_memory_limit: 2000M
 kube_apiserver_cpu_limit: 800m
 kube_apiserver_memory_requests: 256M
 kube_apiserver_cpu_requests: 300m
-kube_apiserver_storage_backend: etcd2
 
 ## Variables for OpenID Connect Configuration https://kubernetes.io/docs/admin/authentication/
 ## To use OpenID you have to deploy additional an OpenID Provider (e.g Dex, Keycloak, ...)
diff --git a/roles/kubernetes/master/tasks/main.yml b/roles/kubernetes/master/tasks/main.yml
index 67a64d4a60a58ed8dd545606a3f6292e1996ea7d..baf3b5c7ce544c5f51e68acb11a89759534e0622 100644
--- a/roles/kubernetes/master/tasks/main.yml
+++ b/roles/kubernetes/master/tasks/main.yml
@@ -70,3 +70,7 @@
     dest: "{{ kube_manifest_dir }}/kube-scheduler.manifest"
   notify: Master | wait for kube-scheduler
   tags: kube-scheduler
+
+- include: post-upgrade.yml
+  tags: k8s-post-upgrade
+
diff --git a/roles/kubernetes/master/tasks/post-upgrade.yml b/roles/kubernetes/master/tasks/post-upgrade.yml
new file mode 100644
index 0000000000000000000000000000000000000000..07fc57b96daa979ac96596fbc02109f871b7e565
--- /dev/null
+++ b/roles/kubernetes/master/tasks/post-upgrade.yml
@@ -0,0 +1,6 @@
+---
+- name: "Post-upgrade | etcd3 upgrade | purge etcd2 k8s data"
+  command: "{{ bin_dir }}/etcdctl --endpoints={{ etcd_access_addresses }} rm -r /registry"
+  environment:
+    ETCDCTL_API: 2
+  when: kube_apiserver_storage_backend == "etcd3" and  needs_etcd_migration|bool|default(false)
diff --git a/roles/kubernetes/master/tasks/pre-upgrade.yml b/roles/kubernetes/master/tasks/pre-upgrade.yml
index 1bb0c0344809ee0259a75805400763836d1b334b..244c8b13e06b664675e890bfb21d39390bc4f91e 100644
--- a/roles/kubernetes/master/tasks/pre-upgrade.yml
+++ b/roles/kubernetes/master/tasks/pre-upgrade.yml
@@ -32,19 +32,64 @@
   stat:
     path: /etc/kubernetes/manifests/kube-apiserver.manifest
   register: kube_apiserver_manifest
-  when: secret_changed|default(false) or etcd_secret_changed|default(false)
 
-- name: "Pre-upgrade | Write invalid image to kube-apiserver manifest if secrets were changed"
+- name: "Pre-upgrade | etcd3 upgrade | see if old config exists"
+  command: "{{ bin_dir }}/etcdctl --peers={{ etcd_access_addresses }} ls /registry/minions"
+  environment:
+    ETCDCTL_API: 2
+  register: old_data_exists
+  delegate_to: "{{groups['kube-master'][0]}}"
+  when: kube_apiserver_storage_backend == "etcd3"
+  failed_when: false
+
+- name: "Pre-upgrade | etcd3 upgrade | see if data was already migrated"
+  command: "{{ bin_dir }}/etcdctl --endpoints={{ etcd_access_addresses }} get --limit=1 --prefix=true /registry/minions"
+  environment:
+    ETCDCTL_API: 3
+  register: data_migrated
+  delegate_to: "{{groups['etcd'][0]}}"
+  when: kube_apiserver_storage_backend == "etcd3"
+  failed_when: false
+
+- name: "Pre-upgrade | etcd3 upgrade | set needs_etcd_migration"
+  set_fact:
+    needs_etcd_migration: "{{ kube_apiserver_storage_backend == 'etcd3' and data_migrated.stdout_lines|length == 0 and old_data_exists.rc == 0 }}"
+
+- name: "Pre-upgrade | Write invalid image to kube-apiserver manifest if necessary"
   replace:
     dest: /etc/kubernetes/manifests/kube-apiserver.manifest
     regexp: '(\s+)image:\s+.*?$'
     replace: '\1image: kill.apiserver.using.fake.image.in:manifest'
   register: kube_apiserver_manifest_replaced
-  when: (secret_changed|default(false) or etcd_secret_changed|default(false)) and kube_apiserver_manifest.stat.exists
+  when: (secret_changed|default(false) or etcd_secret_changed|default(false) or needs_etcd_migration|bool) and kube_apiserver_manifest.stat.exists
 
 - name: "Pre-upgrade | Pause while waiting for kubelet to delete kube-apiserver pod"
   pause:
     seconds: 20
-  when: (secret_changed|default(false) or etcd_secret_changed|default(false)) and kube_apiserver_manifest.stat.exists
+  when: kube_apiserver_manifest_replaced.changed
   tags: kube-apiserver
 
+- name: "Pre-upgrade | etcd3 upgrade | stop etcd"
+  service:
+    name: etcd
+    state: stopped
+  delegate_to: "{{item}}"
+  with_items: "{{groups['etcd']}}"
+  when: needs_etcd_migration|bool
+
+- name: "Pre-upgrade | etcd3 upgrade | migrate data"
+  command: "{{ bin_dir }}/etcdctl migrate --data-dir=\"{{ etcd_data_dir }}\" --wal-dir=\"{{ etcd_data_dir }}/member/wal\""
+  environment:
+    ETCDCTL_API: 3
+  delegate_to: "{{item}}"
+  with_items: "{{groups['etcd']}}"
+  register: etcd_migrated
+  when: needs_etcd_migration|bool
+
+- name: "Pre-upgrade | etcd3 upgrade | start etcd"
+  service:
+    name: etcd
+    state: started
+  delegate_to: "{{item}}"
+  with_items: "{{groups['etcd']}}"
+  when: needs_etcd_migration|bool