Skip to main content
RunBook Academy

AnsibleII · Ansible ArchitectureThe execution model

Control node and managed node responsibilities

Foundation⏱ ~18 minansiblesshpython3

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

Not yet marked complete on this device.

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.

Read-only / Safewhat the controller actually is
$ ansible --version
ansible [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:

Read-only / Safethe discovery order, verified on 2.21.3
$ ansible-config dump | grep INTERPRETER_PYTHON
INTERPRETER_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:

Read-only / Safeinterpreter discovery on the wire
$ 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.
Read-only / Safeprove the prerequisites before trusting the host
$ ansible -i inventory node1 -m ansible.builtin.ping
node1 | 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 nodeManaged node
ansible-core 2.21Python 3.12 – 3.14Python 3.9 – 3.14
ansible-core 2.20Python 3.12 – 3.14Python 3.9 – 3.14
ansible-core 2.19Python 3.11 – 3.13Python 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

  1. 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?

  2. 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.

  3. Q3. Which of these does a managed node genuinely require? Select all that apply.

  4. Q4. What does a successful ansible.builtin.ping against a new host prove?

Passing score: 75%. Answers are checked in this browser.