Performing tasks locally is a common operation when working with an API of some
kind—typical use cases are cloud services, network devices, cluster
management. There are three ways of achieving this in Ansible: connection: local, delegate_to: localhost and local_action. The last is rarely seen these
days and can be deemed equivalent to delegate_to: localhost in terms of
advantages and disadvantages, but with the additional disadvantage of being
a very unusual style, adding a readability penalty.
In a previous post I talked about the
runner pattern
which allows better use of inventory for different scenarios even when the
controller is localhost. connection: local behaves very differently
if the host is localhost or a ‘runner’ host, which is surprising.
The main difference between connection and delegate_to is that connection can
be used at a play or task level, whereas delegate_to operates at a task level
only. This means that if you have a playbook with fifty tasks, each will need
delegate_to set. Worse, if you’re using someone else’s role, you’ll have to
hope they’ve provided for this eventuality.
The problem with connection: local for the runner pattern is that it assumes
that it’s an entirely new connection and will use the system python rather than
what ever python you prefer to use. Situations where this is a problem include
when using virtualenvs to install python libraries or on OS X where most rely
on python from brew. In this case, you might run pip install library, run
Ansible and find that that library can’t be found because it’s looking in the
wrong place.
To demonstrate this, I wrote a boto3_facts module,
which shows python location and version as well as boto3 and botocore versions.
- hosts: localhost
gather_facts: no
tasks:
- name: localhost without explicit connection
boto3_facts:
- hosts: fakehost
gather_facts: no
tasks:
- name: runner host using delegate_to
boto3_facts:
delegate_to: localhost
- hosts: fakehost
gather_facts: no
tasks:
- name: runner host using local_action
local_action:
module: boto3_facts
- hosts: fakehost
connection: local
gather_facts: no
tasks:
- name: runner host using local connection
boto3_facts:
$ ansible-playbook boto3_facts.yml -v -i fakehost,
Using /Users/will/tmp/ansible/boto3_facts/ansible.cfg as config file
PLAY [localhost] *****************************************************************************************************************
TASK [localhost without explicit connection] *************************************************************************************
ok: [localhost] => changed=false
boto3_version: 1.7.42
botocore_version: 1.10.42
python: /usr/local/Cellar/python@2/2.7.14_3/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python
python_version: |-
2.7.14 (default, Mar 9 2018, 23:57:12)
[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.39.2)]
PLAY [fakehost] ******************************************************************************************************************
TASK [runner host using delegate_to] *********************************************************************************************
ok: [fakehost -> localhost] => changed=false
boto3_version: 1.7.42
botocore_version: 1.10.42
python: /usr/local/Cellar/python@2/2.7.14_3/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python
python_version: |-
2.7.14 (default, Mar 9 2018, 23:57:12)
[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.39.2)]
PLAY [fakehost] ******************************************************************************************************************
TASK [runner host using local_action] ********************************************************************************************
ok: [fakehost -> localhost] => changed=false
boto3_version: 1.7.42
botocore_version: 1.10.42
python: /usr/local/Cellar/python@2/2.7.14_3/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python
python_version: |-
2.7.14 (default, Mar 9 2018, 23:57:12)
[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.39.2)]
PLAY [fakehost] ******************************************************************************************************************
TASK [runner host using local connection] ****************************************************************************************
ok: [fakehost] => changed=false
python: /usr/bin/python
python_version: |-
2.7.10 (default, Oct 6 2017, 22:29:07)
[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.31)]
PLAY RECAP ***********************************************************************************************************************
fakehost : ok=3 changed=0 unreachable=0 failed=0
localhost : ok=1 changed=0 unreachable=0 failed=0
The easiest way to fix this is to set ansible_python_interpreter: "{{ ansible_playbook_python }}".
My preferred approach is in group_vars/all if all tasks run locally, or
group_vars/runner if using the runner pattern—but, as with below, at playbook vars
level also works.
In conclusion, I much prefer connection: local for the runner pattern now that
ansible_python_interpreter can be set dynamically.
- hosts: fakehost
connection: local
vars:
ansible_python_interpreter: "{{ ansible_playbook_python }}"
gather_facts: no
tasks:
- name: runner host using local connection and ansible_python_interpreter set
boto3_facts:
PLAY [fakehost] ******************************************************************************************************************
TASK [runner host using local connection and ansible_python_interpreter set] *****************************************************
ok: [fakehost] => changed=false
boto3_version: 1.7.42
botocore_version: 1.10.42
python: /usr/local/Cellar/python@2/2.7.14_3/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python
python_version: |-
2.7.14 (default, Mar 9 2018, 23:57:12)
[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.39.2)]