diff --git a/ansible/compute-node-discovery.yml b/ansible/compute-node-discovery.yml
index 3d00efd87..6dded81c1 100644
--- a/ansible/compute-node-discovery.yml
+++ b/ansible/compute-node-discovery.yml
@@ -5,7 +5,7 @@
   hosts: compute
   gather_facts: no
   vars:
-    delegate_host: "{{ groups['controllers'][0] }}"
+    controller_host: "{{ groups['controllers'][0] }}"
   tasks:
     - name: Ensure ipmitool is installed
       yum:
@@ -13,15 +13,19 @@
         state: installed
       become: True
       run_once: True
-      delegate_to: "{{ item }}"
-      with_items:
-        - "{{ hostvars[delegate_host].ansible_host }}"
+      delegate_to: "{{ controller_host }}"
+      vars:
+        # NOTE: Without this, the controller's ansible_host variable will not
+        # be respected when using delegate_to.
+        ansible_host: "{{ hostvars[controller_host].ansible_host | default(controller_host) }}"
 
     - name: Ensure compute nodes are powered off
       command: ipmitool -U {{ ipmi_username }} -P {{ ipmi_password }} -H {{ ipmi_address }} -I lanplus chassis power off
-      delegate_to: "{{ item }}"
-      with_items:
-        - "{{ hostvars[delegate_host].ansible_host }}"
+      delegate_to: "{{ controller_host }}"
+      vars:
+        # NOTE: Without this, the controller's ansible_host variable will not
+        # be respected when using delegate_to.
+        ansible_host: "{{ hostvars[controller_host].ansible_host | default(controller_host) }}"
 
     - name: Pause to prevent overwhelming BMCs
       pause:
@@ -29,9 +33,11 @@
 
     - name: Ensure compute nodes are set to boot via PXE
       command: ipmitool -U {{ ipmi_username }} -P {{ ipmi_password }} -H {{ ipmi_address }} -I lanplus chassis bootdev pxe
-      delegate_to: "{{ item }}"
-      with_items:
-        - "{{ hostvars[delegate_host].ansible_host }}"
+      delegate_to: "{{ controller_host }}"
+      vars:
+        # NOTE: Without this, the controller's ansible_host variable will not
+        # be respected when using delegate_to.
+        ansible_host: "{{ hostvars[controller_host].ansible_host | default(controller_host) }}"
 
     - name: Pause to prevent overwhelming BMCs
       pause:
@@ -39,6 +45,8 @@
 
     - name: Ensure compute nodes are powered on
       command: ipmitool -U {{ ipmi_username }} -P {{ ipmi_password }} -H {{ ipmi_address }} -I lanplus chassis power on
-      delegate_to: "{{ item }}"
-      with_items:
-        - "{{ hostvars[delegate_host].ansible_host }}"
+      delegate_to: "{{ controller_host }}"
+      vars:
+        # NOTE: Without this, the controller's ansible_host variable will not
+        # be respected when using delegate_to.
+        ansible_host: "{{ hostvars[controller_host].ansible_host | default(controller_host) }}"
diff --git a/ansible/kolla-bifrost-hostvars.yml b/ansible/kolla-bifrost-hostvars.yml
index cf4e22718..ff7b92f11 100644
--- a/ansible/kolla-bifrost-hostvars.yml
+++ b/ansible/kolla-bifrost-hostvars.yml
@@ -45,7 +45,9 @@
           # Bifrost host variables for {{ inventory_hostname }}
           {{ bifrost_hostvars | to_nice_yaml }}
         dest: "/etc/kolla/bifrost/inventory/host_vars/{{ inventory_hostname }}"
-      delegate_to: "{{ item }}"
-      with_items:
-        - "{{ hostvars[seed_host].ansible_host }}"
+      delegate_to: "{{ seed_host }}"
+      vars:
+        # NOTE: Without this, the seed's ansible_host variable will not be
+        # respected when using delegate_to.
+        ansible_host: "{{ hostvars[seed_host].ansible_host | default(seed_host) }}"
       become: True
diff --git a/ansible/overcloud-bios-raid.yml b/ansible/overcloud-bios-raid.yml
index 4053a7471..2ed5d0a35 100644
--- a/ansible/overcloud-bios-raid.yml
+++ b/ansible/overcloud-bios-raid.yml
@@ -53,6 +53,7 @@
   vars:
     # Set this to False to avoid rebooting the nodes after configuration.
     drac_reboot: True
+    seed_host: "{{ groups['seed'][0] }}"
   pre_tasks:
     - name: Set the overcloud nodes' maintenance mode
       command: >
@@ -69,12 +70,11 @@
         --limit {{ inventory_hostname }}
         -m command
         -a "openstack baremetal node maintenance set {% raw %}{{ inventory_hostname }}{% endraw %} --reason BIOS-RAID"'
-      register: show_result
-      # We use this convoluted construct to work around Ansible's limitations
-      # in evaluation of the delegate_to keyword.
-      delegate_to: "{{ item }}"
-      with_items:
-        - "{{ hostvars[groups['seed'][0]].ansible_host }}"
+      delegate_to: "{{ seed_host }}"
+      vars:
+        # NOTE: Without this, the seed's ansible_host variable will not be
+        # respected when using delegate_to.
+        ansible_host: "{{ hostvars[seed_host].ansible_host | default(seed_host) }}"
       when: "{{ bios_or_raid_change | bool }}"
 
   roles:
@@ -102,10 +102,9 @@
         --limit {{ inventory_hostname }}
         -m command
         -a "openstack baremetal node maintenance unset {% raw %}{{ inventory_hostname }}{% endraw %}"'
-      register: show_result
-      # We use this convoluted construct to work around Ansible's limitations
-      # in evaluation of the delegate_to keyword.
-      delegate_to: "{{ item }}"
-      with_items:
-        - "{{ hostvars[groups['seed'][0]].ansible_host }}"
+      delegate_to: "{{ seed_host }}"
+      vars:
+        # NOTE: Without this, the seed's ansible_host variable will not be
+        # respected when using delegate_to.
+        ansible_host: "{{ hostvars[seed_host].ansible_host | default(seed_host) }}"
       when: "{{ bios_or_raid_change | bool }}"
diff --git a/ansible/overcloud-deprovision.yml b/ansible/overcloud-deprovision.yml
index 249c1f4a1..45f53cd3c 100644
--- a/ansible/overcloud-deprovision.yml
+++ b/ansible/overcloud-deprovision.yml
@@ -26,6 +26,7 @@
     # Retries to use when using Ironic API and hitting node locked errors.
     ironic_retries: 6
     ironic_retry_interval: 5
+    seed_host: "{{ groups['seed'][0] }}"
   gather_facts: no
   tasks:
     - name: Check the ironic node's initial provision state
@@ -45,15 +46,15 @@
         -a "openstack baremetal node show {% raw %}{{ inventory_hostname }}{% endraw %} -f value -c provision_state"'
       register: show_result
       changed_when: False
-      # We use this convoluted construct to work around Ansible's limitations
-      # in evaluation of the delegate_to keyword.
-      delegate_to: "{{ item }}"
-      with_items:
-        - "{{ hostvars[groups['seed'][0]].ansible_host }}"
+      delegate_to: "{{ seed_host }}"
+      vars:
+        # NOTE: Without this, the seed's ansible_host variable will not be
+        # respected when using delegate_to.
+        ansible_host: "{{ hostvars[seed_host].ansible_host | default(seed_host) }}"
 
     - name: Set a fact containing the ironic node's initial provision state
       set_fact:
-        initial_provision_state: "{{ show_result.results[0].stdout_lines[1] }}"
+        initial_provision_state: "{{ show_result.stdout_lines[1] }}"
 
     - name: Fail if the ironic node is in an unexpected provision state
       fail:
@@ -81,9 +82,11 @@
       retries: "{{ ironic_retries }}"
       delay: "{{ ironic_retry_interval }}"
       when: "{{ initial_provision_state != 'available' }}"
-      delegate_to: "{{ item }}"
-      with_items:
-        - "{{ hostvars[groups['seed'][0]].ansible_host }}"
+      delegate_to: "{{ seed_host }}"
+      vars:
+        # NOTE: Without this, the seed's ansible_host variable will not be
+        # respected when using delegate_to.
+        ansible_host: "{{ hostvars[seed_host].ansible_host | default(seed_host) }}"
 
     - name: Wait for the ironic node to become available
       command: >
@@ -109,13 +112,15 @@
         - "{{ wait_available | bool }}"
         - "{{ initial_provision_state != 'available' }}"
       changed_when: False
-      delegate_to: "{{ item }}"
-      with_items:
-        - "{{ hostvars[groups['seed'][0]].ansible_host }}"
+      delegate_to: "{{ seed_host }}"
+      vars:
+        # NOTE: Without this, the seed's ansible_host variable will not be
+        # respected when using delegate_to.
+        ansible_host: "{{ hostvars[seed_host].ansible_host | default(seed_host) }}"
 
     - name: Set a fact containing the ironic node's final provision state
       set_fact:
-        final_provision_state: "{{ show_result.results[0].stdout_lines[1] }}"
+        final_provision_state: "{{ show_result.stdout_lines[1] }}"
       when:
         - "{{ wait_available | bool }}"
         - "{{ initial_provision_state != 'available' }}"
diff --git a/ansible/overcloud-hardware-inspect.yml b/ansible/overcloud-hardware-inspect.yml
index 273b92e85..42642e9db 100644
--- a/ansible/overcloud-hardware-inspect.yml
+++ b/ansible/overcloud-hardware-inspect.yml
@@ -20,6 +20,7 @@
     # Retries to use when using Ironic API and hitting node locked errors.
     ironic_retries: 6
     ironic_retry_interval: 5
+    seed_host: "{{ groups['seed'][0] }}"
   gather_facts: no
   tasks:
     - name: Check the ironic node's initial provision state
@@ -39,15 +40,15 @@
         -a "openstack baremetal node show {% raw %}{{ inventory_hostname }}{% endraw %} -f value -c provision_state"'
       register: show_result
       changed_when: False
-      # We use this convoluted construct to work around Ansible's limitations
-      # in evaluation of the delegate_to keyword.
-      delegate_to: "{{ item }}"
-      with_items:
-        - "{{ hostvars[groups['seed'][0]].ansible_host }}"
+      delegate_to: "{{ seed_host }}"
+      vars:
+        # NOTE: Without this, the seed's ansible_host variable will not be
+        # respected when using delegate_to.
+        ansible_host: "{{ hostvars[seed_host].ansible_host | default(seed_host) }}"
 
     - name: Set a fact containing the ironic node's initial provision state
       set_fact:
-        initial_provision_state: "{{ show_result.results[0].stdout_lines[1] }}"
+        initial_provision_state: "{{ show_result.stdout_lines[1] }}"
 
     - name: Fail if the ironic node is in an unexpected provision state
       fail:
@@ -75,9 +76,11 @@
       retries: "{{ ironic_retries }}"
       delay: "{{ ironic_retry_interval }}"
       when: "{{ initial_provision_state != 'manageable' }}"
-      delegate_to: "{{ item }}"
-      with_items:
-        - "{{ hostvars[groups['seed'][0]].ansible_host }}"
+      delegate_to: "{{ seed_host }}"
+      vars:
+        # NOTE: Without this, the seed's ansible_host variable will not be
+        # respected when using delegate_to.
+        ansible_host: "{{ hostvars[seed_host].ansible_host | default(seed_host) }}"
 
     - name: Ensure the ironic node is inspected
       command: >
@@ -96,9 +99,11 @@
       until: "{{ provide_result | success or 'is locked by host' in provide_result.stdout }}"
       retries: "{{ ironic_retries }}"
       delay: "{{ ironic_retry_interval }}"
-      delegate_to: "{{ item }}"
-      with_items:
-        - "{{ hostvars[groups['seed'][0]].ansible_host }}"
+      delegate_to: "{{ seed_host }}"
+      vars:
+        # NOTE: Without this, the seed's ansible_host variable will not be
+        # respected when using delegate_to.
+        ansible_host: "{{ hostvars[seed_host].ansible_host | default(seed_host) }}"
 
     - name: Wait for the ironic node to be inspected
       command: >
@@ -123,13 +128,15 @@
       when:
         - "{{ wait_inspected | bool }}"
       changed_when: False
-      delegate_to: "{{ item }}"
-      with_items:
-        - "{{ hostvars[groups['seed'][0]].ansible_host }}"
+      delegate_to: "{{ seed_host }}"
+      vars:
+        # NOTE: Without this, the seed's ansible_host variable will not be
+        # respected when using delegate_to.
+        ansible_host: "{{ hostvars[seed_host].ansible_host | default(seed_host) }}"
 
     - name: Set a fact containing the final provision state
       set_fact:
-        final_provision_state: "{{ show_result.results[0].stdout_lines[1] }}"
+        final_provision_state: "{{ show_result.stdout_lines[1] }}"
       when:
         - "{{ wait_inspected | bool }}"
 
diff --git a/ansible/overcloud-provision.yml b/ansible/overcloud-provision.yml
index 1c0124645..b3a8c56ab 100644
--- a/ansible/overcloud-provision.yml
+++ b/ansible/overcloud-provision.yml
@@ -29,6 +29,7 @@
     # Retries to use when using Ironic API and hitting node locked errors.
     ironic_retries: 6
     ironic_retry_interval: 5
+    seed_host: "{{ groups['seed'][0] }}"
   gather_facts: no
   tasks:
     - name: Check the ironic node's initial provision state
@@ -48,15 +49,15 @@
         -a "openstack baremetal node show {% raw %}{{ inventory_hostname }}{% endraw %} -f value -c provision_state"'
       register: show_result
       changed_when: False
-      # We use this convoluted construct to work around Ansible's limitations
-      # in evaluation of the delegate_to keyword.
-      delegate_to: "{{ item }}"
-      with_items:
-        - "{{ hostvars[groups['seed'][0]].ansible_host }}"
+      delegate_to: "{{ seed_host }}"
+      vars:
+        # NOTE: Without this, the seed's ansible_host variable will not be
+        # respected when using delegate_to.
+        ansible_host: "{{ hostvars[seed_host].ansible_host | default(seed_host) }}"
 
     - name: Set a fact containing the ironic node's initial provision state
       set_fact:
-        initial_provision_state: "{{ show_result.results[0].stdout_lines[1] }}"
+        initial_provision_state: "{{ show_result.stdout_lines[1] }}"
 
     - name: Fail if the ironic node is in an unexpected provision state
       fail:
@@ -84,9 +85,11 @@
       retries: "{{ ironic_retries }}"
       delay: "{{ ironic_retry_interval }}"
       when: "{{ initial_provision_state == 'enroll' }}"
-      delegate_to: "{{ item }}"
-      with_items:
-        - "{{ hostvars[groups['seed'][0]].ansible_host }}"
+      delegate_to: "{{ seed_host }}"
+      vars:
+        # NOTE: Without this, the seed's ansible_host variable will not be
+        # respected when using delegate_to.
+        ansible_host: "{{ hostvars[seed_host].ansible_host | default(seed_host) }}"
 
     - name: Ensure the ironic node is available
       command: >
@@ -106,9 +109,11 @@
       retries: "{{ ironic_retries }}"
       delay: "{{ ironic_retry_interval }}"
       when: "{{ initial_provision_state in ['enroll', 'manageable'] }}"
-      delegate_to: "{{ item }}"
-      with_items:
-        - "{{ hostvars[groups['seed'][0]].ansible_host }}"
+      delegate_to: "{{ seed_host }}"
+      vars:
+        # NOTE: Without this, the seed's ansible_host variable will not be
+        # respected when using delegate_to.
+        ansible_host: "{{ hostvars[seed_host].ansible_host | default(seed_host) }}"
 
     - name: Set a fact containing the bifrost host list
       set_fact:
@@ -129,9 +134,11 @@
         -e @/etc/bifrost/dib.yml
         --limit {{ bifrost_limit | join(':') }}'
       when: "{{ bifrost_limit }}"
-      delegate_to: "{{ item }}"
-      with_items:
-        - "{{ hostvars[groups['seed'][0]].ansible_host }}"
+      delegate_to: "{{ seed_host }}"
+      vars:
+        # NOTE: Without this, the seed's ansible_host variable will not be
+        # respected when using delegate_to.
+        ansible_host: "{{ hostvars[seed_host].ansible_host | default(seed_host) }}"
       # We execute this only once, allowing the Bifrost Ansible to handle
       # multiple nodes.
       run_once: True
@@ -160,13 +167,15 @@
         - "{{ wait_active | bool }}"
         - "{{ initial_provision_state != 'active' }}"
       changed_when: False
-      delegate_to: "{{ item }}"
-      with_items:
-        - "{{ hostvars[groups['seed'][0]].ansible_host }}"
+      delegate_to: "{{ seed_host }}"
+      vars:
+        # NOTE: Without this, the seed's ansible_host variable will not be
+        # respected when using delegate_to.
+        ansible_host: "{{ hostvars[seed_host].ansible_host | default(seed_host) }}"
 
     - name: Set a fact containing the final provision state
       set_fact:
-        final_provision_state: "{{ show_result.results[0].stdout_lines[1] }}"
+        final_provision_state: "{{ show_result.stdout_lines[1] }}"
       when:
         - "{{ wait_active | bool }}"
         - "{{ initial_provision_state != 'active' }}"