diff --git a/contrib/kvm-setup/README.md b/contrib/kvm-setup/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..61e6265900a7e0024257fe02a5c8f60c8934f0ca
--- /dev/null
+++ b/contrib/kvm-setup/README.md
@@ -0,0 +1,11 @@
+# Kargo on KVM Virtual Machines hypervisor preparation
+
+A simple playbook to ensure your system has the right settings to enable Kargo
+deployment on VMs.
+
+This playbook does not create Virtual Machines, nor does it run Kargo itself.
+
+### User creation
+
+If you want to create a user for running Kargo deployment, you should specify
+both `k8s_deployment_user` and `k8s_deployment_user_pkey_path`.
diff --git a/contrib/kvm-setup/group_vars/all b/contrib/kvm-setup/group_vars/all
new file mode 100644
index 0000000000000000000000000000000000000000..d08c2c3d3fb3efadc9ec5359dac6e1f1a624b6e8
--- /dev/null
+++ b/contrib/kvm-setup/group_vars/all
@@ -0,0 +1,3 @@
+#k8s_deployment_user: kargo
+#k8s_deployment_user_pkey_path: /tmp/ssh_rsa
+
diff --git a/contrib/kvm-setup/kvm-setup.yml b/contrib/kvm-setup/kvm-setup.yml
new file mode 100644
index 0000000000000000000000000000000000000000..18b7206684659b93aeea71b432f24cf8a1ab1b81
--- /dev/null
+++ b/contrib/kvm-setup/kvm-setup.yml
@@ -0,0 +1,8 @@
+---
+- hosts: localhost
+  gather_facts: False
+  become: yes
+  vars:
+    - bootstrap_os: none
+  roles:
+    - kvm-setup
diff --git a/contrib/kvm-setup/roles/kvm-setup/tasks/main.yml b/contrib/kvm-setup/roles/kvm-setup/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..24cb16284b381882d29d70552ed0a4c4bc6a44eb
--- /dev/null
+++ b/contrib/kvm-setup/roles/kvm-setup/tasks/main.yml
@@ -0,0 +1,46 @@
+---
+
+- name: Upgrade all packages to the latest version (yum)
+  yum:
+   name: '*'
+   state: latest
+  when: ansible_os_family == "RedHat"
+
+- name: Install required packages
+  yum:
+    name: "{{ item }}"
+    state: latest
+  with_items:
+    - bind-utils
+    - ntp
+  when: ansible_os_family == "RedHat"
+
+- name: Install required packages
+  apt:
+    upgrade: yes
+    update_cache: yes
+    cache_valid_time: 3600
+    name: "{{ item }}"
+    state: latest
+    install_recommends: no
+  with_items:
+    - dnsutils
+    - ntp
+  when: ansible_os_family == "Debian"
+
+- name: Upgrade all packages to the latest version (apt)
+  shell: apt-get -o \
+       Dpkg::Options::=--force-confdef -o \
+       Dpkg::Options::=--force-confold -q -y \
+       dist-upgrade
+  environment:
+    DEBIAN_FRONTEND: noninteractive
+  when: ansible_os_family == "Debian"
+
+
+# Create deployment user if required
+- include: user.yml
+  when: k8s_deployment_user is defined
+
+# Set proper sysctl values
+- include: sysctl.yml
diff --git a/contrib/kvm-setup/roles/kvm-setup/tasks/sysctl.yml b/contrib/kvm-setup/roles/kvm-setup/tasks/sysctl.yml
new file mode 100644
index 0000000000000000000000000000000000000000..11f464bdfd23445d188709f6ba2fda7f17067e47
--- /dev/null
+++ b/contrib/kvm-setup/roles/kvm-setup/tasks/sysctl.yml
@@ -0,0 +1,46 @@
+---
+- name: Load br_netfilter module
+  modprobe:
+    name: br_netfilter
+    state: present
+  register: br_netfilter
+
+- name: Add br_netfilter into /etc/modules
+  lineinfile:
+    dest: /etc/modules
+    state: present
+    line: 'br_netfilter'
+  when: br_netfilter is defined and ansible_os_family == 'Debian'
+
+- name: Add br_netfilter into /etc/modules-load.d/kargo.conf
+  copy:
+    dest: /etc/modules-load.d/kargo.conf
+    content: |-
+      ### This file is managed by Ansible
+      br-netfilter
+    owner: root
+    group: root
+    mode: 0644
+  when: br_netfilter is defined
+
+
+- name: Enable net.ipv4.ip_forward in sysctl
+  sysctl:
+    name: net.ipv4.ip_forward
+    value: 1
+    sysctl_file: /etc/sysctl.d/ipv4-ip_forward.conf
+    state: present
+    reload: yes
+
+- name: Set bridge-nf-call-{arptables,iptables} to 0
+  sysctl:
+    name: "{{ item }}"
+    state: present
+    value: 0
+    sysctl_file: /etc/sysctl.d/bridge-nf-call.conf
+    reload: yes
+  with_items:
+    - net.bridge.bridge-nf-call-arptables
+    - net.bridge.bridge-nf-call-ip6tables
+    - net.bridge.bridge-nf-call-iptables
+  when: br_netfilter is defined
diff --git a/contrib/kvm-setup/roles/kvm-setup/tasks/user.yml b/contrib/kvm-setup/roles/kvm-setup/tasks/user.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f259c7f071b426be7df9681143d04ec611d68bad
--- /dev/null
+++ b/contrib/kvm-setup/roles/kvm-setup/tasks/user.yml
@@ -0,0 +1,46 @@
+---
+- name: Create user {{ k8s_deployment_user }}
+  user:
+    name: "{{ k8s_deployment_user }}"
+    groups: adm
+    shell: /bin/bash
+
+- name: Ensure that .ssh exists
+  file:
+    path: "/home/{{ k8s_deployment_user }}/.ssh"
+    state: directory
+    owner: "{{ k8s_deployment_user }}"
+    group: "{{ k8s_deployment_user }}"
+
+- name: Configure sudo for deployment user
+  copy:
+    content: |
+      %{{ k8s_deployment_user }} ALL=(ALL) NOPASSWD: ALL
+    dest: "/etc/sudoers.d/55-k8s-deployment"
+    owner: root
+    group: root
+    mode: 0644
+
+- name: Write private SSH key
+  copy:
+    src: "{{ k8s_deployment_user_pkey_path }}"
+    dest: "/home/{{ k8s_deployment_user }}/.ssh/id_rsa"
+    mode: 0400
+    owner: "{{ k8s_deployment_user }}"
+    group: "{{ k8s_deployment_user }}"
+  when: k8s_deployment_user_pkey_path is defined
+
+- name: Write public SSH key
+  shell: "ssh-keygen -y -f /home/{{ k8s_deployment_user }}/.ssh/id_rsa \
+            > /home/{{ k8s_deployment_user }}/.ssh/authorized_keys"
+  args:
+    creates: "/home/{{ k8s_deployment_user }}/.ssh/authorized_keys"
+  when: k8s_deployment_user_pkey_path is defined
+
+- name: Fix ssh-pub-key permissions
+  file:
+    path: "/home/{{ k8s_deployment_user }}/.ssh/authorized_keys"
+    mode: 0600
+    owner: "{{ k8s_deployment_user }}"
+    group: "{{ k8s_deployment_user }}"
+  when: k8s_deployment_user_pkey_path is defined
diff --git a/tests/cloud_playbooks/delete-gce.yml b/tests/cloud_playbooks/delete-gce.yml
index ee0ffad183cb26baa39de939e399115646b90772..fa05610824989ed0991d71de775ba60c99a1d6c7 100644
--- a/tests/cloud_playbooks/delete-gce.yml
+++ b/tests/cloud_playbooks/delete-gce.yml
@@ -21,12 +21,14 @@
     - name: delete gce instances
       gce:
         instance_names: "{{instance_names}}"
-        image: "{{ cloud_image }}"
+        image: "{{ cloud_image | default(omit) }}"
         service_account_email: "{{ gce_service_account_email }}"
         pem_file: "{{ gce_pem_file | default(omit)}}"
         credentials_file: "{{gce_credentials_file | default(omit)}}"
         project_id: "{{ gce_project_id }}"
         zone: "{{cloud_region | default('europe-west1-b')}}"
-        metadata: '{"test_id": "{{test_id}}", "network": "{{kube_network_plugin}}"}'
         state: 'absent'
+      async: 120
+      poll: 3
+      retries: 3
       register: gce