diff --git a/ansible/group_vars/all/kolla b/ansible/group_vars/all/kolla
index e1fb3e6b750b9056b8ab75566cce9665baea363b..8279fb80ba518309e2c4318a8cccf3282c6b43da 100644
--- a/ansible/group_vars/all/kolla
+++ b/ansible/group_vars/all/kolla
@@ -337,6 +337,10 @@ kolla_ansible_group: kolla
 # Ansible.
 kolla_ansible_become: false
 
+# Whether to create a user account, configure passwordless sudo and authorise
+# an SSH key for Kolla Ansible. Default is 'true'.
+kolla_ansible_create_user: true
+
 ###############################################################################
 # Kolla feature flag configuration.
 
@@ -402,8 +406,7 @@ kolla_ansible_default_custom_passwords:
   bifrost_ssh_key:
     private_key: "{{ lookup('file', ssh_private_key_path) }}"
     public_key: "{{ lookup('file', ssh_public_key_path) }}"
-  # SSH key authorized by kolla user on Kolla hosts during
-  # kolla-ansible bootstrap-servers.
+  # SSH key authorized by kolla user on Kolla hosts.
   kolla_ssh_key:
     private_key: "{{ lookup('file', ssh_private_key_path) }}"
     public_key: "{{ lookup('file', ssh_public_key_path) }}"
diff --git a/ansible/kolla-ansible-user.yml b/ansible/kolla-ansible-user.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e9802d87242ac1cb2523f06ad7ba3251853d80f2
--- /dev/null
+++ b/ansible/kolla-ansible-user.yml
@@ -0,0 +1,47 @@
+---
+- name: Ensure the Kolla Ansible user account exists
+  hosts: seed:overcloud
+  gather_facts: false
+  tags:
+    - kolla-ansible
+    - kolla-ansible-user
+  vars:
+    # kolla_overcloud_inventory_top_level_group_map looks like:
+    # kolla_overcloud_inventory_top_level_group_map:
+    #  control:
+    #    groups:
+    #      - controllers
+    hosts_in_kolla_inventory: >-
+      {{ kolla_overcloud_inventory_top_level_group_map.values() |
+         map(attribute='groups') | flatten | unique | union(['seed']) | join(':') }}
+  tasks:
+    - block:
+        - name: Ensure the Kolla Ansible user account exists
+          include_role:
+            name: singleplatform-eng.users
+            apply:
+              become: True
+          vars:
+            groups_to_create:
+              - name: docker
+              - name: "{{ kolla_ansible_group }}"
+              - name: sudo
+            users:
+              - username: "{{ kolla_ansible_user }}"
+                group: "{{ kolla_ansible_group }}"
+                groups:
+                  - docker
+                  - sudo
+                append: True
+                ssh_key:
+                  - "{{ kolla_ansible_custom_passwords.kolla_ssh_key.public_key }}"
+
+        - name: Ensure the Kolla Ansible user has passwordless sudo
+          copy:
+            content: "{{ kolla_ansible_user }} ALL=(ALL) NOPASSWD: ALL"
+            dest: "/etc/sudoers.d/kolla-ansible-users"
+            mode: 0640
+          become: True
+      when:
+        - inventory_hostname in query('inventory_hostnames', hosts_in_kolla_inventory)
+        - kolla_ansible_create_user | bool
diff --git a/ansible/kolla-pip.yml b/ansible/kolla-pip.yml
new file mode 100644
index 0000000000000000000000000000000000000000..54c08bdd398a540d67095641df68d263907bc8b1
--- /dev/null
+++ b/ansible/kolla-pip.yml
@@ -0,0 +1,26 @@
+---
+- name: Configure local PyPi mirror for Kolla Ansible
+  hosts: seed:overcloud
+  gather_facts: false
+  tags:
+    - kolla-ansible
+    - kolla-pip
+    - pip
+  vars:
+    # kolla_overcloud_inventory_top_level_group_map looks like:
+    # kolla_overcloud_inventory_top_level_group_map:
+    #  control:
+    #    groups:
+    #      - controllers
+    hosts_in_kolla_inventory: >-
+      {{ kolla_overcloud_inventory_top_level_group_map.values() |
+         map(attribute='groups') | flatten | unique | union(['seed']) | join(':') }}
+    ansible_python_interpreter: /usr/libexec/platform-python
+  tasks:
+    - import_role:
+        name: pip
+      vars:
+        pip_applicable_users:
+          - "{{ kolla_ansible_user }}"
+      when:
+        - inventory_hostname in query('inventory_hostnames', hosts_in_kolla_inventory)
diff --git a/ansible/roles/kolla-ansible/templates/globals.yml.j2 b/ansible/roles/kolla-ansible/templates/globals.yml.j2
index 6bc96d47947b555a0e03663996235c8be95c0460..ad02b68beea6bb63413fd609299c4a9a7ab57d0c 100644
--- a/ansible/roles/kolla-ansible/templates/globals.yml.j2
+++ b/ansible/roles/kolla-ansible/templates/globals.yml.j2
@@ -48,12 +48,6 @@ kolla_external_vip_address: "{{ kolla_external_vip_address }}"
 # kolla_external_vip_address.
 kolla_external_fqdn: "{{ kolla_external_fqdn }}"
 
-# User account to use for Kolla SSH access.
-kolla_user: "{{ kolla_ansible_user }}"
-
-# Primary group of Kolla SSH user.
-kolla_group: "{{ kolla_ansible_group }}"
-
 ################
 # Docker options
 ################
@@ -549,6 +543,10 @@ bifrost_install_type: source
 grafana_admin_username: "{{ grafana_local_admin_user_name }}"
 {% endif %}
 
+#########################################
+# Bootstrap-servers - Host Configuration
+#########################################
+
 {% if kolla_selinux_state is not none %}
 selinux_state: {{ kolla_selinux_state }}
 {% endif %}
@@ -559,6 +557,20 @@ install_epel: {{ kolla_ansible_install_epel | bool }}
 enable_host_ntp: {{ kolla_enable_host_ntp | bool }}
 {% endif %}
 
+# Kayobe performs creation of the Kolla Ansible user account, so there is no
+# need for Kolla Ansible to repeat this.
+create_kolla_user: false
+
+# User account to use for Kolla SSH access.
+kolla_user: "{{ kolla_ansible_user }}"
+
+# Primary group of Kolla SSH user.
+kolla_group: "{{ kolla_ansible_group }}"
+
+{% if kolla_ansible_target_venv %}
+virtualenv: {{ kolla_ansible_target_venv }}
+{% endif %}
+
 {% if kolla_extra_globals %}
 #######################
 # Extra configuration
diff --git a/doc/source/configuration/hosts.rst b/doc/source/configuration/hosts.rst
index 08ea2158eb7c853b3708503d2bc132022db8aefb..b0be9fd36485fd131ea2da0512276f22e591c2c7 100644
--- a/doc/source/configuration/hosts.rst
+++ b/doc/source/configuration/hosts.rst
@@ -720,16 +720,18 @@ Kolla-Ansible bootstrap-servers
 ===============================
 
 Kolla Ansible provides some host configuration functionality via the
-``bootstrap-servers`` command, which may be leveraged by Kayobe. Due to the
-bootstrapping nature of the command, Kayobe uses ``kayobe_ansible_user`` to
-execute it, and uses the Kayobe remote Python virtual environment (or the
-system Python interpreter if no virtual environment is in use).
+``bootstrap-servers`` command, which may be leveraged by Kayobe.
 
 See the :kolla-ansible-doc:`Kolla Ansible documentation
 <reference/deployment-and-bootstrapping/bootstrap-servers.html>`
 for more information on the functions performed by this command, and how to
 configure it.
 
+Note that from the Ussuri release, Kayobe creates a user account for Kolla
+Ansible rather than this being done by Kolla Ansible during
+``bootstrap-servers``. See :ref:`configuration-kolla-ansible-user-creation` for
+details.
+
 Kolla-Ansible Remote Virtual Environment
 ========================================
 *tags:*
diff --git a/doc/source/configuration/kolla-ansible.rst b/doc/source/configuration/kolla-ansible.rst
index 6e235b211817657e37d11e23f2c3188229b28723..23e0a3ab9213b29e8a7b0cffc1634f60aed5730b 100644
--- a/doc/source/configuration/kolla-ansible.rst
+++ b/doc/source/configuration/kolla-ansible.rst
@@ -174,6 +174,23 @@ The variable ``kolla_ansible_target_venv`` configures the use of a virtual
 environment on the remote hosts. The default configuration should work in most
 cases.
 
+.. _configuration-kolla-ansible-user-creation:
+
+User account creation
+---------------------
+
+Since the Ussuri release, Kayobe creates a user account for Kolla Ansible
+rather than this being done during Kolla Ansible's ``bootstrap-servers``
+command. This workflow is more compatible with `Ansible fact caching
+<https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#caching-facts>`__,
+but does mean that Kolla Ansible's ``create_kolla_user`` variable cannot be
+used to disable creation of the user account. Instead, set
+``kolla_ansible_create_user`` to ``false``.
+
+``kolla_ansible_create_user``
+    Whether to create a user account, configure passwordless sudo and authorise
+    an SSH key for Kolla Ansible. Default is ``true``.
+
 OpenStack Logging
 -----------------
 
diff --git a/etc/kayobe/kolla.yml b/etc/kayobe/kolla.yml
index f5eb9a9992b5bd5c279cc2513574fd93b3abff22..56327aa515f10ae79cbf0b675a1709fad0209147 100644
--- a/etc/kayobe/kolla.yml
+++ b/etc/kayobe/kolla.yml
@@ -172,6 +172,10 @@
 # Ansible. Default is 'false'.
 #kolla_ansible_become:
 
+# Whether to create a user account, configure passwordless sudo and authorise
+# an SSH key for Kolla Ansible. Default is 'true'.
+#kolla_ansible_create_user:
+
 ###############################################################################
 # Kolla feature flag configuration.
 
diff --git a/kayobe/cli/commands.py b/kayobe/cli/commands.py
index 751234a7a9f82a43c54e70320618fc4f73276fee..42e35d7ad36b500fdd34de91c41599b815b5e714 100644
--- a/kayobe/cli/commands.py
+++ b/kayobe/cli/commands.py
@@ -516,23 +516,6 @@ class SeedHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
     def take_action(self, parsed_args):
         self.app.LOG.debug("Configuring seed host OS")
 
-        # Query some kayobe ansible variables.
-        # Explicitly request the dump-config tag to ensure this play runs even
-        # if the user specified tags.
-        hostvars = self.run_kayobe_config_dump(parsed_args, hosts="seed",
-                                               tags="dump-config")
-        if not hostvars:
-            self.app.LOG.error("No hosts in the seed group")
-            sys.exit(1)
-        hostvars = list(hostvars.values())[0]
-        ansible_user = hostvars.get("kayobe_ansible_user")
-        if not ansible_user:
-            self.app.LOG.error("Could not determine kayobe_ansible_user "
-                               "variable for seed host")
-            sys.exit(1)
-        python_interpreter = hostvars.get("ansible_python_interpreter")
-        kolla_target_venv = hostvars.get("kolla_ansible_target_venv")
-
         # Allocate IP addresses.
         playbooks = _build_playbook_list("ip-allocation")
         self.run_kayobe_playbooks(parsed_args, playbooks, limit="seed")
@@ -546,38 +529,19 @@ class SeedHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
         playbooks += _build_playbook_list(
             "users", "yum", "dnf", "dev-tools", "disable-selinux", "network",
             "sysctl", "ip-routing", "snat", "disable-glean", "ntp", "mdadm",
-            "lvm", "docker-devicemapper")
+            "lvm", "docker-devicemapper", "kolla-ansible-user", "kolla-pip",
+            "kolla-target-venv")
         self.run_kayobe_playbooks(parsed_args, playbooks, limit="seed")
 
         self.generate_kolla_ansible_config(parsed_args, service_config=False)
 
         # Run kolla-ansible bootstrap-servers.
-        # This command should be run as the kayobe ansible user because at this
-        # point the kolla user may not exist.
-        extra_vars = {"ansible_user": ansible_user}
-        if python_interpreter:
-            # Use the kayobe virtualenv, as this is the executing user.
-            extra_vars["ansible_python_interpreter"] = python_interpreter
-        elif kolla_target_venv:
-            # Override the kolla-ansible virtualenv, use the system python
-            # instead.
-            extra_vars["ansible_python_interpreter"] = "/usr/bin/python"
-        if kolla_target_venv:
-            # Specify a virtualenv in which to install python packages.
-            extra_vars["virtualenv"] = kolla_target_venv
-        self.run_kolla_ansible_seed(parsed_args, "bootstrap-servers",
-                                    extra_vars=extra_vars)
-
-        # Re-run the Pip role after we've bootstrapped the Kolla user
-        extra_vars = {}
-        kolla_ansible_user = hostvars.get("kolla_ansible_user")
-        extra_vars["pip_applicable_users"] = [kolla_ansible_user]
+        self.run_kolla_ansible_seed(parsed_args, "bootstrap-servers")
 
         # Run final kayobe playbooks.
         playbooks = _build_playbook_list(
-            "pip", "kolla-target-venv", "kolla-host", "docker")
-        self.run_kayobe_playbooks(parsed_args, playbooks,
-                                  extra_vars=extra_vars, limit="seed")
+            "kolla-host", "docker")
+        self.run_kayobe_playbooks(parsed_args, playbooks, limit="seed")
 
         # Optionally, deploy a Docker Registry.
         playbooks = _build_playbook_list("docker-registry")
@@ -916,23 +880,6 @@ class OvercloudHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
     def take_action(self, parsed_args):
         self.app.LOG.debug("Configuring overcloud host OS")
 
-        # Query some kayobe ansible variables.
-        # Explicitly request the dump-config tag to ensure this play runs even
-        # if the user specified tags.
-        hostvars = self.run_kayobe_config_dump(parsed_args, hosts="overcloud",
-                                               tags="dump-config")
-        if not hostvars:
-            self.app.LOG.error("No hosts in the overcloud group")
-            sys.exit(1)
-        hostvars = list(hostvars.values())[0]
-        ansible_user = hostvars.get("kayobe_ansible_user")
-        if not ansible_user:
-            self.app.LOG.error("Could not determine kayobe_ansible_user "
-                               "variable for overcloud hosts")
-            sys.exit(1)
-        python_interpreter = hostvars.get("ansible_python_interpreter")
-        kolla_target_venv = hostvars.get("kolla_ansible_target_venv")
-
         # Allocate IP addresses.
         playbooks = _build_playbook_list("ip-allocation")
         self.run_kayobe_playbooks(parsed_args, playbooks, limit="overcloud")
@@ -946,40 +893,19 @@ class OvercloudHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
         playbooks += _build_playbook_list(
             "users", "yum", "dnf", "dev-tools", "disable-selinux", "network",
             "sysctl", "disable-glean", "disable-cloud-init", "ntp", "mdadm",
-            "lvm", "docker-devicemapper")
+            "lvm", "docker-devicemapper", "kolla-ansible-user", "kolla-pip",
+            "kolla-target-venv")
         self.run_kayobe_playbooks(parsed_args, playbooks, limit="overcloud")
 
         self.generate_kolla_ansible_config(parsed_args, service_config=False)
 
         # Kolla-ansible bootstrap-servers.
-        # The kolla-ansible bootstrap-servers command should be run as the
-        # kayobe ansible user because at this point the kolla user may not
-        # exist.
-        extra_vars = {"ansible_user": ansible_user}
-        if python_interpreter:
-            # Use the kayobe virtualenv, as this is the executing user.
-            extra_vars["ansible_python_interpreter"] = python_interpreter
-        elif kolla_target_venv:
-            # Override the kolla-ansible virtualenv, use the system python
-            # instead.
-            extra_vars["ansible_python_interpreter"] = "/usr/bin/python"
-        if kolla_target_venv:
-            # Specify a virtualenv in which to install python packages.
-            extra_vars["virtualenv"] = kolla_target_venv
-        self.run_kolla_ansible_overcloud(parsed_args, "bootstrap-servers",
-                                         extra_vars=extra_vars)
-
-        # Re-run the Pip role after we've bootstrapped the Kolla user
-        extra_vars = {}
-        kolla_ansible_user = hostvars.get("kolla_ansible_user")
-        extra_vars["pip_applicable_users"] = [kolla_ansible_user]
+        self.run_kolla_ansible_overcloud(parsed_args, "bootstrap-servers")
 
         # Further kayobe playbooks.
         playbooks = _build_playbook_list(
-            "pip", "kolla-target-venv", "kolla-host",
-            "docker", "swift-block-devices")
-        self.run_kayobe_playbooks(parsed_args, playbooks,
-                                  extra_vars=extra_vars, limit="overcloud")
+            "kolla-host", "docker", "swift-block-devices")
+        self.run_kayobe_playbooks(parsed_args, playbooks, limit="overcloud")
 
 
 class OvercloudHostPackageUpdate(KayobeAnsibleMixin, VaultMixin, Command):
diff --git a/kayobe/tests/unit/cli/test_commands.py b/kayobe/tests/unit/cli/test_commands.py
index 1242b17c9ecb49b7bf4710112ff1d023bfb63390..93fce78530f14b0d4f7c748208a15e7e01e193e4 100644
--- a/kayobe/tests/unit/cli/test_commands.py
+++ b/kayobe/tests/unit/cli/test_commands.py
@@ -461,28 +461,18 @@ class TestCase(unittest.TestCase):
         ]
         self.assertEqual(expected_calls, mock_run.call_args_list)
 
-    @mock.patch.object(commands.KayobeAnsibleMixin,
-                       "run_kayobe_config_dump")
     @mock.patch.object(commands.KayobeAnsibleMixin,
                        "run_kayobe_playbooks")
     @mock.patch.object(commands.KollaAnsibleMixin,
                        "run_kolla_ansible_seed")
-    def test_seed_host_configure(self, mock_kolla_run, mock_run, mock_dump):
+    def test_seed_host_configure(self, mock_kolla_run, mock_run):
         command = commands.SeedHostConfigure(TestApp(), [])
         parser = command.get_parser("test")
         parsed_args = parser.parse_args([])
-        mock_dump.return_value = {
-            "seed": {"kayobe_ansible_user": "stack"}
-        }
 
         result = command.run(parsed_args)
         self.assertEqual(0, result)
 
-        expected_calls = [
-            mock.call(mock.ANY, hosts="seed", tags="dump-config")
-        ]
-        self.assertEqual(expected_calls, mock_dump.call_args_list)
-
         expected_calls = [
             mock.call(
                 mock.ANY,
@@ -514,6 +504,11 @@ class TestCase(unittest.TestCase):
                     utils.get_data_files_path("ansible", "lvm.yml"),
                     utils.get_data_files_path("ansible",
                                               "docker-devicemapper.yml"),
+                    utils.get_data_files_path(
+                        "ansible", "kolla-ansible-user.yml"),
+                    utils.get_data_files_path("ansible", "kolla-pip.yml"),
+                    utils.get_data_files_path(
+                        "ansible", "kolla-target-venv.yml"),
                 ],
                 limit="seed",
             ),
@@ -526,14 +521,10 @@ class TestCase(unittest.TestCase):
             mock.call(
                 mock.ANY,
                 [
-                    utils.get_data_files_path("ansible", "pip.yml"),
-                    utils.get_data_files_path(
-                        "ansible", "kolla-target-venv.yml"),
                     utils.get_data_files_path("ansible", "kolla-host.yml"),
                     utils.get_data_files_path("ansible", "docker.yml"),
                 ],
                 limit="seed",
-                extra_vars={'pip_applicable_users': [None]},
             ),
             mock.call(
                 mock.ANY,
@@ -551,109 +542,6 @@ class TestCase(unittest.TestCase):
             mock.call(
                 mock.ANY,
                 "bootstrap-servers",
-                extra_vars={"ansible_user": "stack"},
-            ),
-        ]
-        self.assertEqual(expected_calls, mock_kolla_run.call_args_list)
-
-    @mock.patch.object(commands.KayobeAnsibleMixin,
-                       "run_kayobe_config_dump")
-    @mock.patch.object(commands.KayobeAnsibleMixin,
-                       "run_kayobe_playbooks")
-    @mock.patch.object(commands.KollaAnsibleMixin,
-                       "run_kolla_ansible_seed")
-    def test_seed_host_configure_kayobe_venv(self, mock_kolla_run, mock_run,
-                                             mock_dump):
-        command = commands.SeedHostConfigure(TestApp(), [])
-        parser = command.get_parser("test")
-        parsed_args = parser.parse_args([])
-        mock_dump.return_value = {
-            "seed": {
-                "ansible_python_interpreter": "/kayobe/venv/bin/python",
-                "kayobe_ansible_user": "stack",
-            }
-        }
-
-        result = command.run(parsed_args)
-        self.assertEqual(0, result)
-
-        expected_calls = [
-            mock.call(
-                mock.ANY,
-                "bootstrap-servers",
-                extra_vars={
-                    "ansible_python_interpreter": "/kayobe/venv/bin/python",
-                    "ansible_user": "stack",
-                },
-            ),
-        ]
-        self.assertEqual(expected_calls, mock_kolla_run.call_args_list)
-
-    @mock.patch.object(commands.KayobeAnsibleMixin,
-                       "run_kayobe_config_dump")
-    @mock.patch.object(commands.KayobeAnsibleMixin,
-                       "run_kayobe_playbooks")
-    @mock.patch.object(commands.KollaAnsibleMixin,
-                       "run_kolla_ansible_seed")
-    def test_seed_host_configure_kolla_venv(self, mock_kolla_run, mock_run,
-                                            mock_dump):
-        command = commands.SeedHostConfigure(TestApp(), [])
-        parser = command.get_parser("test")
-        parsed_args = parser.parse_args([])
-        mock_dump.return_value = {
-            "seed": {
-                "kayobe_ansible_user": "stack",
-                "kolla_ansible_target_venv": "/kolla/venv/bin/python",
-            }
-        }
-
-        result = command.run(parsed_args)
-        self.assertEqual(0, result)
-
-        expected_calls = [
-            mock.call(
-                mock.ANY,
-                "bootstrap-servers",
-                extra_vars={
-                    "ansible_python_interpreter": "/usr/bin/python",
-                    "ansible_user": "stack",
-                    "virtualenv": "/kolla/venv/bin/python",
-                },
-            ),
-        ]
-        self.assertEqual(expected_calls, mock_kolla_run.call_args_list)
-
-    @mock.patch.object(commands.KayobeAnsibleMixin,
-                       "run_kayobe_config_dump")
-    @mock.patch.object(commands.KayobeAnsibleMixin,
-                       "run_kayobe_playbooks")
-    @mock.patch.object(commands.KollaAnsibleMixin,
-                       "run_kolla_ansible_seed")
-    def test_seed_host_configure_both_venvs(self, mock_kolla_run, mock_run,
-                                            mock_dump):
-        command = commands.SeedHostConfigure(TestApp(), [])
-        parser = command.get_parser("test")
-        parsed_args = parser.parse_args([])
-        mock_dump.return_value = {
-            "seed": {
-                "ansible_python_interpreter": "/kayobe/venv/bin/python",
-                "kayobe_ansible_user": "stack",
-                "kolla_ansible_target_venv": "/kolla/venv/bin/python",
-            }
-        }
-
-        result = command.run(parsed_args)
-        self.assertEqual(0, result)
-
-        expected_calls = [
-            mock.call(
-                mock.ANY,
-                "bootstrap-servers",
-                extra_vars={
-                    "ansible_python_interpreter": "/kayobe/venv/bin/python",
-                    "ansible_user": "stack",
-                    "virtualenv": "/kolla/venv/bin/python",
-                },
             ),
         ]
         self.assertEqual(expected_calls, mock_kolla_run.call_args_list)
@@ -1088,29 +976,18 @@ class TestCase(unittest.TestCase):
         ]
         self.assertEqual(expected_calls, mock_run.call_args_list)
 
-    @mock.patch.object(commands.KayobeAnsibleMixin,
-                       "run_kayobe_config_dump")
     @mock.patch.object(commands.KayobeAnsibleMixin,
                        "run_kayobe_playbooks")
     @mock.patch.object(commands.KollaAnsibleMixin,
                        "run_kolla_ansible_overcloud")
-    def test_overcloud_host_configure(self, mock_kolla_run, mock_run,
-                                      mock_dump):
+    def test_overcloud_host_configure(self, mock_kolla_run, mock_run):
         command = commands.OvercloudHostConfigure(TestApp(), [])
         parser = command.get_parser("test")
         parsed_args = parser.parse_args([])
-        mock_dump.return_value = {
-            "controller0": {"kayobe_ansible_user": "stack"}
-        }
 
         result = command.run(parsed_args)
         self.assertEqual(0, result)
 
-        expected_calls = [
-            mock.call(mock.ANY, hosts="overcloud", tags="dump-config")
-        ]
-        self.assertEqual(expected_calls, mock_dump.call_args_list)
-
         expected_calls = [
             mock.call(
                 mock.ANY,
@@ -1142,6 +1019,11 @@ class TestCase(unittest.TestCase):
                     utils.get_data_files_path("ansible", "lvm.yml"),
                     utils.get_data_files_path("ansible",
                                               "docker-devicemapper.yml"),
+                    utils.get_data_files_path(
+                        "ansible", "kolla-ansible-user.yml"),
+                    utils.get_data_files_path("ansible", "kolla-pip.yml"),
+                    utils.get_data_files_path(
+                        "ansible", "kolla-target-venv.yml"),
                 ],
                 limit="overcloud",
             ),
@@ -1154,16 +1036,12 @@ class TestCase(unittest.TestCase):
             mock.call(
                 mock.ANY,
                 [
-                    utils.get_data_files_path("ansible", "pip.yml"),
-                    utils.get_data_files_path(
-                        "ansible", "kolla-target-venv.yml"),
                     utils.get_data_files_path("ansible", "kolla-host.yml"),
                     utils.get_data_files_path("ansible", "docker.yml"),
                     utils.get_data_files_path(
                         "ansible", "swift-block-devices.yml"),
                 ],
                 limit="overcloud",
-                extra_vars={"pip_applicable_users": [None]},
             ),
         ]
         self.assertEqual(expected_calls, mock_run.call_args_list)
@@ -1172,109 +1050,6 @@ class TestCase(unittest.TestCase):
             mock.call(
                 mock.ANY,
                 "bootstrap-servers",
-                extra_vars={"ansible_user": "stack"},
-            ),
-        ]
-        self.assertEqual(expected_calls, mock_kolla_run.call_args_list)
-
-    @mock.patch.object(commands.KayobeAnsibleMixin,
-                       "run_kayobe_config_dump")
-    @mock.patch.object(commands.KayobeAnsibleMixin,
-                       "run_kayobe_playbooks")
-    @mock.patch.object(commands.KollaAnsibleMixin,
-                       "run_kolla_ansible_overcloud")
-    def test_overcloud_host_configure_kayobe_venv(self, mock_kolla_run,
-                                                  mock_run, mock_dump):
-        command = commands.OvercloudHostConfigure(TestApp(), [])
-        parser = command.get_parser("test")
-        parsed_args = parser.parse_args([])
-        mock_dump.return_value = {
-            "controller0": {
-                "ansible_python_interpreter": "/kayobe/venv/bin/python",
-                "kayobe_ansible_user": "stack",
-            }
-        }
-
-        result = command.run(parsed_args)
-        self.assertEqual(0, result)
-
-        expected_calls = [
-            mock.call(
-                mock.ANY,
-                "bootstrap-servers",
-                extra_vars={
-                    "ansible_python_interpreter": "/kayobe/venv/bin/python",
-                    "ansible_user": "stack",
-                }
-            ),
-        ]
-        self.assertEqual(expected_calls, mock_kolla_run.call_args_list)
-
-    @mock.patch.object(commands.KayobeAnsibleMixin,
-                       "run_kayobe_config_dump")
-    @mock.patch.object(commands.KayobeAnsibleMixin,
-                       "run_kayobe_playbooks")
-    @mock.patch.object(commands.KollaAnsibleMixin,
-                       "run_kolla_ansible_overcloud")
-    def test_overcloud_host_configure_kolla_venv(self, mock_kolla_run,
-                                                 mock_run, mock_dump):
-        command = commands.OvercloudHostConfigure(TestApp(), [])
-        parser = command.get_parser("test")
-        parsed_args = parser.parse_args([])
-        mock_dump.return_value = {
-            "controller0": {
-                "kayobe_ansible_user": "stack",
-                "kolla_ansible_target_venv": "/kolla/venv/bin/python",
-            }
-        }
-
-        result = command.run(parsed_args)
-        self.assertEqual(0, result)
-
-        expected_calls = [
-            mock.call(
-                mock.ANY,
-                "bootstrap-servers",
-                extra_vars={
-                    "ansible_python_interpreter": "/usr/bin/python",
-                    "ansible_user": "stack",
-                    "virtualenv": "/kolla/venv/bin/python",
-                }
-            ),
-        ]
-        self.assertEqual(expected_calls, mock_kolla_run.call_args_list)
-
-    @mock.patch.object(commands.KayobeAnsibleMixin,
-                       "run_kayobe_config_dump")
-    @mock.patch.object(commands.KayobeAnsibleMixin,
-                       "run_kayobe_playbooks")
-    @mock.patch.object(commands.KollaAnsibleMixin,
-                       "run_kolla_ansible_overcloud")
-    def test_overcloud_host_configure_both_venvs(self, mock_kolla_run,
-                                                 mock_run, mock_dump):
-        command = commands.OvercloudHostConfigure(TestApp(), [])
-        parser = command.get_parser("test")
-        parsed_args = parser.parse_args([])
-        mock_dump.return_value = {
-            "controller0": {
-                "ansible_python_interpreter": "/kayobe/venv/bin/python",
-                "kayobe_ansible_user": "stack",
-                "kolla_ansible_target_venv": "/kolla/venv/bin/python",
-            }
-        }
-
-        result = command.run(parsed_args)
-        self.assertEqual(0, result)
-
-        expected_calls = [
-            mock.call(
-                mock.ANY,
-                "bootstrap-servers",
-                extra_vars={
-                    "ansible_python_interpreter": "/kayobe/venv/bin/python",
-                    "ansible_user": "stack",
-                    "virtualenv": "/kolla/venv/bin/python",
-                }
             ),
         ]
         self.assertEqual(expected_calls, mock_kolla_run.call_args_list)
diff --git a/releasenotes/notes/bootstrap-servers-user-8cb5114de1dd10ec.yaml b/releasenotes/notes/bootstrap-servers-user-8cb5114de1dd10ec.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..db32c9a754c374d9cc16e00524dddb2d3cc60d1b
--- /dev/null
+++ b/releasenotes/notes/bootstrap-servers-user-8cb5114de1dd10ec.yaml
@@ -0,0 +1,20 @@
+---
+upgrade:
+  - |
+    The ``kolla-ansible bootstrap-servers`` command is used by Kayobe during
+    the ``kayobe seed host configure`` and ``kayobe overcloud host configure``
+    tasks. In previous releases it was executed as the Kayobe Ansible user
+    (``kayobe_ansible_user``) and using the remote Kayobe Python interpreter
+    (``ansible_python_interpreter``) since it was responsible for creation of
+    the Kolla Ansible user account (``kolla_ansible_user``) and Python virtual
+    environment (``kolla_ansible_target_venv``). This mix of environments
+    causes problems for Ansible fact caching. To avoid this issue, Kayobe is
+    now responsible for creation of the Kolla Ansible user and Python virtual
+    environment, and ``kolla-ansible bootstrap-servers`` is run using the
+    normal Kolla Ansible user and remote Python interpreter.
+
+    Previously it was possible to avoid creation of the user account during
+    ``kolla-ansible bootstrap-servers`` by setting ``create_kolla_user`` to
+    ``false`` in ``${KAYOBE_CONFIG_PATH}/kolla/globals.yml``. The same may now
+    be achieved by setting ``kolla_ansible_create_user`` to ``false`` in
+    ``${KAYOBE_CONFIG_PATH}/kolla.yml``.