Skip to main content
RunBook Academy

AnsibleIII · Installing and Designing the ControllerInstalling the controller

Installing: pipx, virtualenv, pip and the distro package

Foundation⏱ ~20 minbashpython3pip

What you'll learn

  • Compare the four install paths on binding, upgradability, coexistence and reproducibility
  • Install ansible-core into an explicit virtualenv and confirm which one is in use
  • State honestly what goes wrong with a system-wide pip install
  • Choose an install method for a workstation and for a shared or CI controller, and justify each

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.

There are four ways to put Ansible on a machine, and the tutorials that list them usually rank them by convenience. Convenience is the wrong axis. What decides whether an install is a problem in eighteen months is:

  1. Which Python does it bind to — and can that Python change under you?
  2. Can it be upgraded without touching the system — or does an Ansible upgrade become an operating-system event?
  3. Can two versions coexist — because upgrading safely means running both for a while.
  4. Is the result reproducible on another machine — from a file in the repository, not from memory.

The four paths

Distro packageSystem-wide pippipxExplicit venv
Binds toSystem PythonSystem PythonAn interpreter pipx picks, isolatedThe interpreter you name
Version you getWhatever the release frozeNewest that installsAny, you chooseAny, you pin
Upgrade touches the systemYesYesNoNo
Two versions coexistNoNoWith --suffixYes, trivially
Reproducible from a fileOnly with the same OS releasePoorlyReasonablyYes
Sensible forA throwaway boxNothingA workstationA shared or CI controller

The rest of this lesson is that table, argued.

The distribution package

Configuration changethe distribution's Ansible
sudo apt install ansible-core        # Debian/Ubuntu
sudo dnf install ansible-core        # RHEL/Rocky/Alma

It works, it is signed, it is patched by the distribution’s security process, and it needs no Python knowledge at all. For a throwaway machine, or for a host where Ansible is a convenience rather than the tool that changes production, this is fine.

Its problem is the previous lesson’s problem: the version is a property of your operating system release, not of what you chose. You cannot pin it to what your repository declares, you cannot install two versions to test an upgrade, and upgrading it means upgrading the distribution.

System-wide pip install ansible — what actually goes wrong

This is the instruction in every old blog post, and it deserves a straight answer rather than a dismissal.

Configuration changedo not do this on a machine you care about
sudo pip install ansible                        # refused on a PEP 668 system
sudo pip install --break-system-packages ansible  # and this is why

Three concrete things go wrong.

It shares a dependency tree with the operating system. Distribution tooling is written in Python and imports the same site-packages directory. Ansible pulls in jinja2, cryptography, pyyaml, packaging and resolvelib, and pip will upgrade whichever of those it needs. When the version pip installs is not the version the distribution built its own tools against, the breakage lands somewhere unrelated — the package manager’s Python bindings, a cloud-init hook, a backup agent. The error message will not mention Ansible.

PEP 668 exists precisely because this happened often enough to be worth a standard. On a distribution that implements it, the system Python is marked externally managed and pip refuses. --break-system-packages is not a workaround; it is a switch whose name tells you the outcome.

There is no per-project version. One machine, one Ansible. If two repositories on that controller need different versions — and during any upgrade, they do — you cannot have both.

Upgrades happen to you. pip install --upgrade of anything else on that machine can move a shared dependency underneath Ansible. So can a distribution update that replaces the system Python minor version, which orphans everything pip installed into the old site-packages path. The controller stops working, and nothing in your change log mentions Ansible.

The honest summary: a system-wide pip install ansible produces a controller whose version and dependency set are decided by the interaction of two package managers that do not know about each other. It is not that it never works. It is that when it stops working, the cause is somewhere you would not think to look.

pipx — the workstation answer

pipx installs a Python application into its own private virtualenv and puts the entry-point commands on your PATH. You get ansible, ansible-playbook and the rest as commands, with none of their dependencies visible to anything else.

Configuration changea workstation controller
sudo apt install pipx
pipx ensurepath
pipx install 'ansible-core==2.21.3'

Collections are a separate matter — pipx installs the application, not the collections, so ansible-galaxy collection install still runs afterwards. If you need a Python library inside the Ansible environment (a cloud SDK a collection depends on, for instance), pipx inject is the mechanism:

Configuration changea library the collection needs, inside Ansible's environment
pipx inject ansible-core boto3

Two versions can coexist with --suffix, which is what makes pipx usable for an upgrade rehearsal:

Configuration changethe new version, alongside the old
pipx install --suffix=-next 'ansible-core==2.22.0'
ansible-playbook-next --syntax-check site.yml

pipx is the right default for a laptop or a personal jump box. It is weaker for a shared controller because the environment is defined by the commands you ran rather than by a file in the repository, and pipx list is a description of a machine’s history, not a specification.

The explicit virtualenv — the controller answer

A virtualenv is a directory containing an interpreter and a site-packages. You name the Python, you name the versions, and both facts live in your repository.

Configuration changea controller environment you can rebuild
python3.12 -m venv /opt/estate/venv
/opt/estate/venv/bin/pip install --upgrade pip
/opt/estate/venv/bin/pip install 'ansible-core==2.21.3'
/opt/estate/venv/bin/ansible --version

Confirm what you built rather than assuming it:

Read-only / Safeproving which install is in effect
$ ansible --version
ansible [core 2.21.3]
config file = None
configured module search path = ['/home/ops/.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/ops/.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)

executable location under /opt/estate/venv is the proof. If it says /usr/bin/ansible, you are running the distribution package regardless of what you installed five minutes ago — the shell resolved ansible from PATH and PATH did not have your venv on it.

The environment is then reproducible from one file:

# requirements.txt
ansible-core==2.21.3

A second machine runs the same three commands, gets the same ansible --version, and the estate has two controllers that agree. That test is the subject of the next lesson.

Choosing

For a workstation: pipx. You want commands on your PATH, you want them isolated from the system Python, and you want pipx install --suffix available when it is time to try a new version.

For a shared or CI controller: an explicit virtualenv at a known path, built from a requirements.txt in the repository, invoked by absolute path. Nothing about which version is running should depend on who is logged in.

For neither: a system-wide pip install. There is no situation where it is the best available option, because pipx does the same job without the shared dependency tree.

The blast-radius framing matters here as much as anywhere else in the course. The controller is the machine allowed to change every host you own. An install method that leaves its version ambiguous leaves the behaviour of every scheduled change ambiguous too.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A scheduled cron job on the controller behaves differently from the same command run interactively. Both invoke `ansible-playbook site.yml`. What should you check first?

  2. Q2. What specifically goes wrong with `sudo pip install ansible` into the system Python? Select all that apply.

  3. Q3. Why does this course prefer calling `/opt/estate/venv/bin/ansible-playbook` by absolute path rather than sourcing `activate` first?

  4. Q4. Upgrading the base Python interpreter from 3.12 to 3.13 can break a virtualenv built from it.

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