diff --git a/ansible/roles/docker-registry/tasks/pull.yml b/ansible/roles/docker-registry/tasks/pull.yml
index 35d46a9618341ab415e82e98fb42732a1d6fc3a0..22dbc471dcb0bba5eb7b5df30c16c1e58d0ac8ed 100644
--- a/ansible/roles/docker-registry/tasks/pull.yml
+++ b/ansible/roles/docker-registry/tasks/pull.yml
@@ -3,6 +3,7 @@
   docker_image:
     name: "{{ item.value.image }}"
     repository: "{{ item.value.image }}"
+    source: pull
     state: present
   with_dict: "{{ docker_registry_services }}"
   when:
diff --git a/ansible/roles/inspection-store/tasks/pull.yml b/ansible/roles/inspection-store/tasks/pull.yml
index 1b00c90b4af88df0699a4f1b85f7f005cde8693c..1abd7a5b0e2be74255739aaf89a3e2cc755c7f54 100644
--- a/ansible/roles/inspection-store/tasks/pull.yml
+++ b/ansible/roles/inspection-store/tasks/pull.yml
@@ -3,6 +3,7 @@
   docker_image:
     name: "{{ item.value.image }}"
     repository: "{{ item.value.image }}"
+    source: pull
     state: present
   with_dict: "{{ inspection_store_services }}"
   when:
diff --git a/ansible/roles/kolla-ansible/tasks/install.yml b/ansible/roles/kolla-ansible/tasks/install.yml
index 5c96170a3487be8a56ab8d2a5f0fa8571f8e5ab4..79fb210ed410858e57bbe35357bfc2d86b018441 100644
--- a/ansible/roles/kolla-ansible/tasks/install.yml
+++ b/ansible/roles/kolla-ansible/tasks/install.yml
@@ -54,6 +54,36 @@
   with_items:
     - { name: pip }
 
+- block:
+    - name: Gather list of installed Python packages
+      pip_package_info:
+        clients: "{{ kolla_ansible_pip }}"
+      register: pip_packages
+
+    # Upgrading directly from Ansible 2.9 to Ansible 2.10 or from Ansible 2.10
+    # to Ansible 4 is known to cause problems. Uninstall Ansible first if its
+    # version is lower than 4.0.0. Although 2.10 is allowed by version limits,
+    # this is needed even it is present from Wallaby, because we request
+    # `state: latest`.
+    - name: Uninstall Ansible if an old version is present
+      pip:
+        name: ansible
+        state: absent
+        virtualenv: "{{ kolla_ansible_venv }}"
+        virtualenv_python: "{{ kolla_ansible_venv_python }}"
+      when:
+        - "'ansible' in pip_packages.packages[kolla_ansible_pip]"
+        - pip_packages.packages[kolla_ansible_pip].ansible[0].version is version('4.0.0', '<')
+
+    - name: Uninstall ansible-base
+      pip:
+        name: ansible-base
+        state: absent
+        virtualenv: "{{ kolla_ansible_venv }}"
+        virtualenv_python: "{{ kolla_ansible_venv_python }}"
+  vars:
+    kolla_ansible_pip: "{{ kolla_ansible_venv }}/bin/pip"
+
 - name: Ensure required Python packages are installed
   vars:
     kolla_ansible_packages:
@@ -67,7 +97,7 @@
       # Limit the version of ansible used by kolla-ansible to avoid new
       # releases from breaking tested code. Changes to this limit should be
       # tested.
-      - ansible>=2.9,<2.11,!=2.9.8,!=2.9.12
+      - ansible>=2.10.0,<5.0
       - selinux
   pip:
     name: "{{ (kolla_ansible_packages + kolla_ansible_venv_extra_requirements) | select | list }}"
diff --git a/ansible/roles/kolla-openstack/molecule/default/create.yml b/ansible/roles/kolla-openstack/molecule/default/create.yml
index f44cd2fc086d589af40b0cd646ced5553f02b74e..4f02a8194ee7825f872d4c528e0d1231ea9b2b1c 100644
--- a/ansible/roles/kolla-openstack/molecule/default/create.yml
+++ b/ansible/roles/kolla-openstack/molecule/default/create.yml
@@ -17,17 +17,20 @@
       register: platforms
 
     - name: Discover local Docker images
-      docker_image_facts:
+      docker_image_info:
         name: "molecule_local/{{ item.item.name }}"
       with_items: "{{ platforms.results }}"
       register: docker_images
 
     - name: Build an Ansible compatible image
       docker_image:
-        path: "{{ molecule_ephemeral_directory }}"
         name: "molecule_local/{{ item.item.image }}"
-        dockerfile: "{{ item.item.dockerfile | default(item.invocation.module_args.dest) }}"
-        force: "{{ item.item.force | default(true) }}"
+        source: build
+        build:
+          path: "{{ molecule_ephemeral_directory }}"
+          dockerfile: "{{ item.item.dockerfile | default(item.invocation.module_args.dest) }}"
+        force_source: "{{ item.item.force | default(true) }}"
+        force_tag: "{{ item.item.force | default(true) }}"
       with_items: "{{ platforms.results }}"
       when: platforms.changed or docker_images.results | map(attribute='images') | select('equalto', []) | list | count >= 0
 
diff --git a/ansible/roles/kolla-openstack/molecule/enable-everything/create.yml b/ansible/roles/kolla-openstack/molecule/enable-everything/create.yml
index 93bf57d105febb945acf1e72edb49531f3b8ea6b..7cb2f843cdc8efc4315e2385096f5567498e32f4 100644
--- a/ansible/roles/kolla-openstack/molecule/enable-everything/create.yml
+++ b/ansible/roles/kolla-openstack/molecule/enable-everything/create.yml
@@ -18,17 +18,20 @@
       register: platforms
 
     - name: Discover local Docker images
-      docker_image_facts:
+      docker_image_info:
         name: "molecule_local/{{ item.item.name }}"
       with_items: "{{ platforms.results }}"
       register: docker_images
 
     - name: Build an Ansible compatible image
       docker_image:
-        path: "{{ molecule_ephemeral_directory }}"
         name: "molecule_local/{{ item.item.image }}"
-        dockerfile: "{{ item.item.dockerfile | default(item.invocation.module_args.dest) }}"
-        force: "{{ item.item.force | default(true) }}"
+        source: build
+        build:
+          path: "{{ molecule_ephemeral_directory }}"
+          dockerfile: "{{ item.item.dockerfile | default(item.invocation.module_args.dest) }}"
+        force_source: "{{ item.item.force | default(true) }}"
+        force_tag: "{{ item.item.force | default(true) }}"
       with_items: "{{ platforms.results }}"
       when: platforms.changed or docker_images.results | map(attribute='images') | select('equalto', []) | list | count >= 0
 
diff --git a/ansible/roles/opensm/tasks/pull.yml b/ansible/roles/opensm/tasks/pull.yml
index bc23636e1d5535e4def4171c0527b1ed56e2c166..7313c262614f9208bfc9e8064e2efc066546e171 100644
--- a/ansible/roles/opensm/tasks/pull.yml
+++ b/ansible/roles/opensm/tasks/pull.yml
@@ -3,6 +3,7 @@
   docker_image:
     name: "{{ item.value.image }}"
     repository: "{{ item.value.image }}"
+    source: pull
     state: present
   with_dict: "{{ opensm_services }}"
   when:
diff --git a/doc/source/upgrading.rst b/doc/source/upgrading.rst
index bbfb4fb49cdf6de959a05ca5112cc7b0464c8947..70cfc0dbccb98d797c441866931a44f48a527c40 100644
--- a/doc/source/upgrading.rst
+++ b/doc/source/upgrading.rst
@@ -112,6 +112,14 @@ Update the pip package::
 
     (kayobe) $ pip install -U pip
 
+.. note::
+
+   When updating Ansible above version 2.9.x, first uninstall it with ``pip
+   uninstall ansible``. A newer version will be installed with the next
+   command, as a Kayobe dependency. If Ansible 2.10.x was installed and you
+   want to use a newer version, also uninstall the ``ansible-base`` package
+   with ``pip uninstall ansible-base``.
+
 If upgrading to the latest version of Kayobe::
 
     (kayobe) $ pip install -U kayobe
diff --git a/releasenotes/notes/ansible-max-4-f0666c5decc440f1.yaml b/releasenotes/notes/ansible-max-4-f0666c5decc440f1.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..fbc437a75d871ccc79046f9f8aa3543c7d0d56ef
--- /dev/null
+++ b/releasenotes/notes/ansible-max-4-f0666c5decc440f1.yaml
@@ -0,0 +1,21 @@
+---
+upgrade:
+  - |
+    Updates the maximum supported version of Ansible from 2.9 to 4.x
+    (ansible-core 2.11). The minimum supported version is updated from 2.9 to
+    2.10. This is true for both Kayobe and Kolla Ansible.
+  - |
+    Upgrading directly from Ansible 2.9 to Ansible 2.10 or from Ansible 2.10 to
+    Ansible 4 is known to cause problems. You should uninstall Ansible before
+    upgrading your Kayobe virtual environment:
+
+    .. code-block:: console
+
+       pip uninstall ansible
+
+    If upgrading from Ansible 2.10 to a newer version, also uninstall
+    ``ansible-base``:
+
+    .. code-block:: console
+
+       pip uninstall ansible-base
diff --git a/requirements.txt b/requirements.txt
index ed81d1d11939722affcdf1e7655dd7a63e80b453..cd95cfc0010f9f28878038d3ca97ee48fb39a540 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
 pbr>=2.0 # Apache-2.0
-ansible>=2.9.0,<2.11.0,!=2.9.8,!=2.9.12 # GPLv3
+ansible>=2.10.0,<5.0 # GPLv3
 cliff>=3.1.0 # Apache
 netaddr!=0.7.16,>=0.7.13 # BSD
 PyYAML>=3.10.0 # MIT