`ansible_host` with `delegate_to` points to `inventory_hostname`, not delegated host

Hello,
I am quite new to Ansible, hence knowledge might be a bit rough on the edges. As follows is a simple reproduction example.

inventory.yml:

# Inventory has inlined variables for ease of use.
# In practice these are defined within host_vars.
all:
  hosts:
    localhost:
      ansible_host: 127.0.0.1
      ansible_connection: local
    myhost:
      ansible_host: 192.168.100.186

main.yml:

- name: Example play
  gather_facts: false
  hosts: myhost
  tasks:

    - ansible.builtin.shell:
        cmd: >-
          echo "Delegated from {{ inventory_hostname }} to {{ ansible_host }},"
          "target hostname is $(hostnamectl hostname)"
      delegate_to: localhost
      register: result

    - ansible.builtin.debug:
        msg: "{{ result.stdout }}"

$ ansible-playbook -i inventory.yml main.yml
 
PLAY [Example play] ************************************************************************************************************************************************************************
 
TASK [ansible.builtin.shell] ***************************************************************************************************************************************************************
changed: [myhost -> localhost(127.0.0.1)]
 
TASK [ansible.builtin.debug] ***************************************************************************************************************************************************************
ok: [myhost] => {
    "msg": "Delegated from myhost to 192.168.100.186, target hostname is test-ansible-controller"     
}

Expected: {{ ansible_host }} is 127.0.0.1.
Actual: {{ ansible_host }} is 192.168.100.186 , i.e. inventory_hostname.

Having read https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_delegation.html#templating-in-delegation-context , this was surprising to me:

The ansible_host variable and other connection variables, if present, reflects information about the host a task is delegated to, not the inventory_hostname.

All variables except inventory_hostname will now be consumed from this host and not the original task host.

I also encountered ansible_host is incorrect when using delegate_to · Issue #14958 · ansible/ansible · GitHub (old):

So the confusion here is not about connection but display, for templating ansible_host will reflect inventory_hostname.

, as well as Variables not consumed from delegated host - #2 by sivel :

delegation really only impacts connection related variables.

The task definition itself is templated under the actual host, but connection related vars are pulled from the delegated host.

Do I interpret that correctly, that ansible only uses ansible_host variable to establish connection itself, but not for interpolating in templates of this task with delegate_to? In other words: “connection related variables” != template variables used for interpolation?

I cannot use hostvars[somehost], as somehost is dynamic - localhost and myhost in this example - , task has conditional actions depending on the delegated host, like

temp_copy_directory: >-
  {{
    lookup('env','HOME')
    if ansible_host == 'localhost'
    else ansible_user_dir
  }}/tmp__ansible

So: Is there any way to refer to current delegated ansible host from within task template ?

Thank you very much.

ansible 2.19.4

The relationship between ansible_host and delegate_to and the observed behavior is by design.

For a solution, I see no problem. You said that delegated host can be dynamic - in your example either localhost or myhost. That means that you have a task like:

- name: Delegated task
  <some_module>:
    <some_module_params>
    ...
  delegate_to: "{{ var_holding_delegated_host }}"

If so, than you can use var_holding_delegated_host in other expressions like:

hostvars[var_holding_delegated_host]

In other words, even if delegated host is dynamic, you should at all times have an information of which host the tasks were delegated to. You can use that information to extract vars related to the delegated host.

Yes right, that is one good solution. Actually in my case , the distinction between ansible controller and nodes is sufficient, so I should be able to use condition ansible_connection == “local” .

Update: Ah damn, ansible_connection oc has the same issue. And infos from previous ansible.builtin.setup with delegate_facts: true cannot be used either to get ansible_user_dir on delegated host. It seems passing that dynamic hostvar really is the only way.


What I also take away from your post: delegate_to: D uses connection-related variables like ansible_host etc. from D to only establish a connection to D, but then completely runs task template in variable context of original host / inventory_hostname - no substitution or adding of variables from delegated host context.

Thanks for the quick response!

Actually the situation with connection variables like ansible_host, ansible_connection etc. in combination with delegate_to: D is a bit more nuanced. Let’s take ansible_host as example:

  • ansible_host from D is always used to establish a connection.
  • For templating, if inventory_hostname has ansible_host explicitely declared (e.g. via host_vars), then ansible_host is used from here.
  • Otherwise ansible_host is taken from D.
  • Other variable types are not taken at all from D.

That leads to the curiosity, that when resolving ansible_host via DNS, my playbook works, but when declaring ansible_host explicitely in host_vars, it fails.

As connection variables are already handled in a specialized way for delegate_to directive and are always taken from delegated host context for connection establishment, why not adhere to same principles the other way round and always return delegate host values for ansible_host & Co. during template interpolation?

Is this purely based on historical decisions? I might be missing something - please correct me otherwise, but from bare user perspective current design chocie seems to be the most inconsistent and least intuitive.

See also Variables not consumed from delegated host - #3 by kristianheljas .

While I can’t speak for the history or design decisions, I have a sense that you are using delegate_to the way it is not intended to be used. Host targeting should be done via play (hosts: key), not via delegate_to. delegate_to is mostly used for modules that must run on an Ansible controller host and communicate with some remote API or if you want to write some local files on your controller host. It can also be used in combination with run_once to target a specific host in some clustered setup (a master) so that your code does not run on other hosts in the cluster (slaves).

You have not described what you are really trying to do so I can’t tell if there is some alternative approach.

UPDATE: Don’t expect for this behavior to be changed as it would break other people’s code. It can only be documented better not to confuse people.

1 Like

Yes, I am aware of the hosts directive and its relation to delegate_to. In principle my use case fits in the category described by Controlling where tasks run: delegation and local actions:

By default, Ansible gathers facts and executes all tasks on the machines that match the hosts line of your playbook. This page shows you how to delegate tasks to a different machine or group, delegate facts to specific machines or groups, or run an entire playbook locally.

There is a build host B with compile and development dependencies, and an execution host E with runtime dependencies. B compiles artifacts, which are then run by E. Each host has its own separate playbook assigned. Playbook for B and E can be started independently on their own. Playbook for E needs to copy artifacts over from B. Though of course B has to be run at least once in order to deliver artifacts to E.

Thus playbook for E is executed via hosts: E, but needs to use delegate_to in order to trigger copy process from B. B does not have SSH permissions to access E, hence copy process flow goes like B → Controller → E.

That is understandable. But current behavior also is quite inconsistent. I better not count the time I needed to investigate into this issue.

What I mean by inconsistent: You use inventory_hostname, when referring to original host in a task with delegate_to. In this task, if intention is to refer to delegated host, you would expect, the host variable already used for the ssh connection ansible_host points to the same host during template. Getting a different host back is confusing.

If I read correctly, in the past, there existed ansible_delegated_host variable or similar, which always pointed at the delegated host - does it still exist (if not, why)? IMO It would be a good idea to (re)introduce it for a more consistent way to reference the delegate host for the long run, while not breaking anything in short-term. In future, a breaking release might “fix” ansible_host, and ansible_delegated_host could just point back to it, but there is no hurry about this.

Does such a variable still exist?

While I cannot answer all your questions (I’m leaving that to Ansible core devs), I can tell you how to alleviate your problem given your example of B, Controller (let’s call it C) and E hosts.

You have two playbooks, let’s call them B.yml and E.yml, that are meant to be run against host B and E respectively.

# B.yml
---
- name: Build artifacts on B
  hosts: B

  tasks:
  - name: Build
    <some_module>:
      <some_module_params>
      ...
    # no delegate_to
# E.yml
---
- name: Fetch artifacts from B to C
  hosts: localhost
  gather_facts: false
  
  tasks:
  - name: Fetch artifacts
    shell:
      <shell_params>
      ...
    # shell params specify B host as source
    # no delegate_to

- name: Run artifacts on E
  hosts: E

  tasks:
  - name: Copy artifacts from C to E
    copy:
      <copy_module_params>
      ...
    # no delegate_to    

  - name: Run artifacts
    <some_module>:
      <some_module_params>
      ...
    # no delegate_to

So the idea is to move the artifact fetching stage to run in a separate play (still E.yml playbook) that runs on C. This removes the need to use delegate_to at all. You can play with the idea.

UPDATE: I assume that fetching stage involves some additional steps that must run on C before E can run. If only file copying is involved, E.yml can also be implemented this way:

# E.yml
---
- name: Fetch artifacts from B to C
  hosts: B             # <- note this change
  gather_facts: false
  
  tasks:
  - name: Fetch artifacts
    fetch:             # <- and this
      <fetch_params>
      ...
    # no delegate_to

- name: Run artifacts on E
  hosts: E

  tasks:
  - name: Copy artifacts from C to E
    copy:
      <copy_module_params>
      ...
    # no delegate_to    

  - name: Run artifacts
    <some_module>:
      <some_module_params>
      ...
    # no delegate_to
2 Likes

Thank you so much for your time @bvitnik !

You are right, dividing tasks amongst different plays in proper order with specific hosts: makes things easier. This way, I probably can get rid of delegate_to.

If there should be need to reference delegated host from task template, it is probably best practice to just use explicit variables and avoid ansible_host & Co. in tasks that use delegate_to.

I still would be curious about devs’ opinion about fixing connection variables to always point to delegated host for the sake of consistency and not being overly confusing. Currently lacking a use case though :slight_smile: . I’ll tinker with your solution, and see how it goes.