AnsibleII · Ansible ArchitectureThe execution model
Control node and managed node responsibilities
What you'll learn
- List exactly what the control node requires and why each item is there
- List exactly what a managed node requires, and what it explicitly does not
- Separate controller Python from managed-node Python, which have different support windows
- Diagnose a managed node that fails the prerequisites without guessing
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
The most persistent misconception about Ansible is that you install it on the servers you want to manage. You do not, and the fastest way to kill the idea for good is an exact list of what each side needs.
This lesson is that list. It is also the reference you will come back to when a host does not work, because almost every “Ansible cannot manage this host” problem is one of six specific prerequisites being absent, and knowing the six turns guessing into checking.
The architecture
flowchart LR
subgraph CONTROL["Control node"]
CORE["ansible-core 2.21<br/>Python 3.12-3.14"]
REPO["Repository<br/>inventory, playbooks, roles"]
KEYS["SSH private keys<br/>vault password"]
CFG["ansible.cfg"]
end
subgraph MANAGED["Managed nodes"]
N1["node1<br/>sshd + Python 3.9-3.14<br/>writable tmp"]
N2["node2<br/>sshd + Python 3.9-3.14<br/>writable tmp"]
N3["node3<br/>sshd + Python 3.9-3.14<br/>writable tmp"]
end
CORE -->|"SSH, port 22"| N1
CORE -->|"SSH, port 22"| N2
CORE -->|"SSH, port 22"| N3
REPO --- CORE
KEYS --- CORE
CFG --- CORE
Read the diagram as an asymmetry. Everything that makes the automation what it is — the code, the inventory, the credentials, the configuration — lives on one machine on the left. The machines on the right hold none of it and know nothing about it. All the arrows point one way, and they are all the same protocol.
Two consequences fall straight out of that picture, and both get their own parts later. The controller is a concentrated target, because it holds keys to everything on the right. And the arrows must be able to exist: if the controller cannot open a connection to a host, that host cannot be managed, regardless of how healthy it is.
What the control node needs
ansible-core, and a Python that can run it
ansible-core 2.21 requires Python 3.12, 3.13 or 3.14 on the
controller. That is a hard floor: the package will not install on an
older interpreter.
$ ansible --versionansible [core 2.21.3]
config file = None
configured module search path = ['/home/ebrandi/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /opt/estate/venv/lib/python3.14/site-packages/ansible
ansible collection location = /home/ebrandi/.ansible/collections:/usr/share/ansible/collections
executable location = /opt/estate/venv/bin/ansible
python version = 3.14.4 (main, Jun 18 2026, 14:25:02) [GCC 15.2.0] (/opt/estate/venv/bin/python3)
jinja version = 3.1.6
pyyaml version = 6.0.3 (with libyaml v0.2.5)The line worth reading carefully is config file. Here it says None,
which means no ansible.cfg was found anywhere in the search path and
every setting is at its built-in default. On a working controller this
line names a file, and if it names an unexpected file you have found the
cause of a surprising amount of behaviour. The configuration part of
this course covers the search order.
The repository
Inventories, playbooks, roles, group and host variables. The declaration from Part I lives here, and it lives on the controller because that is where it is read — the managed nodes never see it.
This has a consequence that catches people repeatedly: a task that reads
a file, such as template or copy, reads it from the controller,
not from the managed node. The next lesson explains why, from the
execution model, rather than asking you to memorise it.
Credentials
SSH private keys for the automation account, and the vault password if secrets are encrypted. Both are on the controller and both are what make it a concentrated target.
ansible.cfg
Forks, timeouts, the inventory path, connection defaults. Ansible finds exactly one of these and ignores the rest; it does not merge them. That single fact is the most common cause of “why is my setting not applying”, and the configuration part leads with it.
What the control node does not need
- Root. Ansible runs as an ordinary user. It escalates on the managed node, when a task asks it to.
- To be a server. No listening port, no service unit, no daemon.
- To be always on. It is a program you run. A laptop is a valid controller for a small estate, with the security consequences you would expect.
- The same operating system as the managed nodes. A Debian controller managing RHEL hosts is entirely normal.
What a managed node needs
Six things. Every failure to manage a host is one of these.
1. A reachable SSH service
The controller must be able to open a TCP connection to the SSH port and complete a handshake. “Reachable” means reachable from the controller, which is not the same as reachable from your desk.
2. Credentials that work non-interactively
An account the controller can authenticate as, without a human typing anything. In practice that means key-based authentication, and it means the key is not passphrase-protected unless an agent is holding it.
3. A usable Python interpreter
ansible-core 2.21 supports managed-node Python 3.9 through 3.14.
This is a different and wider window than the controller’s, and
conflating the two is a documented source of misdirected diagnosis.
Ansible finds the interpreter itself. With INTERPRETER_PYTHON at its
default of auto, it probes the node in a fixed order:
$ ansible-config dump | grep INTERPRETER_PYTHONINTERPRETER_PYTHON(default) = auto
INTERPRETER_PYTHON_FALLBACK(default) = ['python3.14', 'python3.13', 'python3.12', 'python3.11', 'python3.10', 'python3.9', '/usr/bin/python3', 'python3']Note where that list stops. It ends at python3.9, then falls back to
whatever /usr/bin/python3 and python3 resolve to. RHEL, Rocky and
AlmaLinux 9 ship Python 3.9, which is exactly at the floor — supported,
but with no margin.
The probe is visible in a verbose run. This is the actual command Ansible sends before it transfers anything:
$ ansible-playbook -i inventory site.yml -vvv | grep -A1 'python interpreter discovery'<node1> Attempting python interpreter discovery
<node1> EXEC /bin/sh -c 'echo FOUND; command -v '"'"'python3.14'"'"'; command -v '"'"'python3.13'"'"'; command -v '"'"'python3.12'"'"'; command -v '"'"'python3.11'"'"'; command -v '"'"'python3.10'"'"'; command -v '"'"'python3.9'"'"'; command -v '"'"'/usr/bin/python3'"'"'; command -v '"'"'python3'"'"'; echo ENDFOUND'That single line answers several questions at once. Discovery costs one
extra round trip per host. It runs through /bin/sh, so a node needs a
POSIX shell before it needs Python. And it is a probe, so its answer
depends on what is installed at that moment — which is why the warning
Ansible emits recommends setting the interpreter explicitly for
production.
4. A POSIX shell
Ansible runs its commands through /bin/sh. A node with an exotic or
absent shell needs the raw connection route to bootstrap, which the
interpreter part of this course covers.
5. A writable temporary directory
The module payload is written into a directory under the remote user’s
home, ~/.ansible/tmp by default, and deleted afterwards. A node whose
home directory is read-only, full, or mounted noexec fails here — and
the error message points at the module rather than at the filesystem,
which is why this is worth knowing in advance.
6. A way to become root, if the tasks need it
Most useful tasks do. That means sudo (or su, or doas) configured
for the automation account, non-interactively.
What a managed node does not need
- Ansible. No package, no agent, no service. This is the whole point.
- A listening management port. Only SSH.
- Outbound access to the controller. The connection is opened by the controller and is bidirectional once established.
- Python on the controller’s version. 3.9 is fine against a 3.14 controller.
- The same distribution as anything else. Mixed estates are normal.
$ ansible -i inventory node1 -m ansible.builtin.pingnode1 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.12"
},
"changed": false,
"ping": "pong"
}Add --become to prove item 6 as well. Note the discovered_interpreter_python
value in the result — that is the outcome of the probe, and it is worth
recording when you onboard a host, because it tells you which
interpreter the node will actually use.
Controller Python is not managed-node Python
This deserves its own section because it is the single most commonly confused pair of requirements in the whole architecture.
| Control node | Managed node | |
|---|---|---|
ansible-core 2.21 | Python 3.12 – 3.14 | Python 3.9 – 3.14 |
ansible-core 2.20 | Python 3.12 – 3.14 | Python 3.9 – 3.14 |
ansible-core 2.19 | Python 3.11 – 3.13 | Python 3.8 – 3.13 |
Two independent windows with two separate support policies. Read the table as answering a specific operational question: can this controller manage that host? The answer comes from the table, not from trying it — and getting it from the table means you find out during planning rather than during a run.
The direction of the confusion in practice is always the same. Somebody
learns that ansible-core 2.21 requires Python 3.12, concludes their
RHEL 9 fleet with Python 3.9 cannot be managed, and either upgrades
something they did not need to or abandons the plan. RHEL 9 at 3.9 is
supported — it is at the bottom of the supported range, which is a
reason to plan an upgrade, not a blocker.
Knowledge check
Knowledge check · 4 questions
Q1. A run against forty hosts fails on one with MODULE FAILURE and OSError: No space left on device. What is the most likely cause?
Q2. A fleet of RHEL 9 hosts shipping Python 3.9 can be managed by an ansible-core 2.21 controller running Python 3.14.
Q3. Which of these does a managed node genuinely require? Select all that apply.
Q4. What does a successful ansible.builtin.ping against a new host prove?
Passing score: 75%. Answers are checked in this browser.