Ansible

Install Ansible on Rocky Linux 10, RHEL and Ubuntu

The version you get when you install Ansible depends far more on the Python running your controller than on the command you type. On a stock Rocky Linux 10 or Ubuntu 24.04 box, pipx install ansible-core finishes without an error and hands you the previous release line. The current ansible-core refuses to run on Python 3.12, so pip simply picks the newest build that still fits, and nothing tells you that happened unless you read the version string.

Original content from computingforgeeks.com - post 165255

So this guide starts with what each method actually installs. Then it walks you through the pipx route on Rocky Linux 10 (AlmaLinux 10 is the same, and RHEL 10 gets a variant without EPEL), the AppStream package and the apt, PPA and uv options on Ubuntu, followed by SSH keys, ansible.cfg, an inventory, ad-hoc commands and a first playbook with a vault secret. I ran through all of it on three VMs in my homelab in November 2026 with ansible-core 2.22.0 on a Rocky Linux 10 controller, and every output below comes from those boxes.

Which ansible-core each install method gives you

Only two methods in this table give you the newest ansible-core, and both need Python 3.13 or newer on the controller. Everything else lands one or more release lines behind, which is fine as long as you chose it on purpose. We measured every row marked as tested on Rocky Linux 10.2 and Ubuntu 24.04.4 in this lab; the two rows taken from our other guides say so.

MethodController PythonWhat it installedSource
pipx install --python python3.14 ansible-core (Rocky 10)3.14.8 (AppStream)ansible-core 2.22.0Tested here
uv tool install --python 3.14 ansible-core (Ubuntu 24.04)3.14.10 (fetched by uv)ansible-core 2.22.0Tested here
pipx install ansible-core (Rocky 10 or Ubuntu 24.04)3.12 (system)ansible-core 2.21.6Tested here
pipx install --python python3.14 --include-deps ansible (Rocky 10)3.14.8ansible 14.5.0 with ansible-core 2.21.6Tested here
dnf install ansible-core (Rocky 10 AppStream)3.12.15ansible-core 2.16.16, no collectionsTested here
dnf install ansible (Rocky 10, EPEL enabled)n/aNo match for argumentTested here
apt install ansible (Ubuntu 24.04)3.12.3ansible 9.2.0 with ansible-core 2.16.3Tested here
ppa:ansible/ansible (Ubuntu 24.04)3.12.3ansible 14.5.0 with ansible-core 2.21.5Tested here (candidate version)
apt install ansible (Ubuntu 26.04)systemansible-core 2.20.1Our Ubuntu 26.04 guide
apt install ansible (Debian 13)systemansible 12.0.0 with ansible-core 2.19.11Our Debian 13 guide, October 2026

Two rows surprise most people. The full ansible community package still pins the previous ansible-core line, because the next major release of that bundle was still in alpha on PyPI when we tested, so “install the full package” and “install the newest engine” are different choices right now. And Rocky Linux 10 does ship a usable ansible-core in AppStream, it is simply six release lines behind and comes with no collections. If you need the newest engine plus a few collections, install ansible-core with pipx and add the collections with ansible-galaxy, which is what we do in Step 2.

Our lab used three VMs on Proxmox, and the names and addresses below appear in every command and output that follows:

  • homelab-ctl: Rocky Linux 10.2 control node, 10.0.1.10
  • web01: Rocky Linux 10.2 managed node, SELinux enforcing, 10.0.1.21
  • nas01: Ubuntu 24.04.4 LTS, managed node and the Python 3.12 controller test, 10.0.1.22
  • One non-root user, kibet, with passwordless sudo on every box

Managed nodes only need SSH and Python 3.9 or newer, which every current distro already has, so nothing gets installed on web01 or nas01 for Ansible itself. No firewall port has to be opened either, because the controller only makes outbound SSH connections on port 22.

Step 1: Check the controller Python before you install

The current ansible-core needs Python 3.13 or newer on the controller. PyPI lists it with Requires-Python >=3.13, and the ansible-core changelog says it plainly: “Drop support for Python 3.12 on the controller.” Rocky Linux 10, RHEL 10 and Ubuntu 24.04 all ship Python 3.12 as python3, so this affects the most common controller distros today. Managed nodes are a different story, because modules still run on Python 3.9 or newer there.

Check what python3 is on your controller before anything else. On homelab-ctl it is the Rocky default:

python3 --version

The answer was Python 3.12.15, which is below the floor. To show what that costs you, we ran pipx (installed as in Step 2) against the system Python. You do not need to repeat this, and if you do, remove the venv with pipx uninstall ansible-core before Step 2:

pipx install ansible-core

Read the last line of the output, not the “done!” above it. pip quietly resolved to the newest release that still supports Python 3.12:

creating virtual environment...
creating shared libraries...
upgrading shared libraries...
WARNING: Skipping setuptools as it is not installed.
installing ansible-core...
done! ✨ 🌟 ✨
  installed package ansible-core 2.21.6, installed using Python 3.12.15

Asking for the new line explicitly turns the silent downgrade into a clear error, which is the quickest way to confirm the controller is the problem:

pipx install --force 'ansible-core>=2.22'

pip lists every build of the new line it skipped and why.

Fatal error from pip prevented installation. Full pip output in file:
    /home/kibet/.local/state/pipx/log/cmd_2026-11-03_04.23.19_pip_errors.log

Some possibly relevant errors from pip install:
    ERROR: Ignored the following yanked versions: 2.14.0rc1.post0
    ERROR: Ignored the following versions that require a different python version: 2.22.0 Requires-Python >=3.13; 2.22.0b1 Requires-Python >=3.13; 2.22.0b2 Requires-Python >=3.13; 2.22.0rc1 Requires-Python >=3.13; 2.22.0rc2 Requires-Python >=3.13
    ERROR: Could not find a version that satisfies the requirement ansible-core>=2.22 (from versions: 0.0.1a1, 2.11.0b1, ... , 2.21.6rc1, 2.21.6)
    ERROR: No matching distribution found for ansible-core>=2.22

The Ubuntu box behaves the same way. Its pipx (1.4.3 from apt) installed the same older core on Python 3.12.3 and printed the identical Requires-Python >=3.13 error for the pinned request. The terminal below is the Rocky run:

pipx install ansible-core on Python 3.12 resolves to ansible-core 2.21.6 and refuses 2.22 because it requires Python 3.13

Staying on the previous line is a valid choice if your playbooks are not ready for the changes in Step 9. If you want the current release, you need a newer interpreter on the controller, and the next step gets you one without touching the system python3, which dnf and other system tools depend on.

Step 2: Install Ansible on Rocky Linux 10 with pipx

Rocky Linux 10 carries Python 3.14 in AppStream as a separate python3.14 package, and pipx comes from EPEL. Installing both leaves /usr/bin/python3 on 3.12, so dnf keeps working, while Ansible gets its own virtual environment on 3.14. Keep in your plan that the extra AppStream Python streams have a shorter support life than the system 3.12, so expect to rebuild the venv on a newer stream later in the RHEL 10 cycle. Enable EPEL and install the two packages:

sudo dnf install -y epel-release
sudo dnf install -y python3.14 pipx

Now point pipx at the 3.14 interpreter. Without --python it falls back to the system 3.12 and you are back to the result from Step 1. If an older pipx install of ansible-core already exists, pipx prints “already seems to be installed. Not modifying existing installation” and exits cleanly while leaving the old version in place, so run pipx uninstall ansible-core first or use the pipx reinstall command from Step 9:

pipx install --python python3.14 ansible-core

This time the summary line names the version you came for.

creating virtual environment...
installing ansible-core...
done! ✨ 🌟 ✨
  installed package ansible-core 2.22.0, installed using Python 3.14.8

pipx drops the binaries in ~/.local/bin. The default ~/.bashrc on Rocky already puts that directory on your PATH (pipx ensurepath reported exactly that on our box), so a new shell is enough. Did it really land on the new core? Check before you go further.

ansible --version

Check two lines here. ansible [core ...] is the engine version, and python version must point at the pipx venv on 3.14, not at /usr/bin/python3:

ansible [core 2.22.0]
  config file = None
  configured module search path = ['/home/kibet/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/kibet/.local/share/pipx/venvs/ansible-core/lib64/python3.14/site-packages/ansible
  ansible collection location = /home/kibet/.ansible/collections:/usr/share/ansible/collections
  executable location = /home/kibet/.local/bin/ansible
  python version = 3.14.8 (main, Oct  1 2026, 00:00:00) [GCC 14.3.1 20251022 (Red Hat 14.3.1-4)] (/home/kibet/.local/share/pipx/venvs/ansible-core/bin/python)
  jinja version = 3.1.6
  pyyaml version = 6.0.3 (with libyaml v0.2.5)

The same run as it looked in the terminal on homelab-ctl:

pipx install with Python 3.14 installs ansible-core 2.22.0 on Rocky Linux 10

On RHEL 10 the epel-release package is not in any Red Hat repository, so the first command above fails there. You can enable EPEL the RHEL way (CodeReady Builder plus the EPEL release RPM, covered in our EPEL on RHEL 10 guide), or skip pipx and use a plain virtual environment, which needs nothing outside AppStream. We ran this variant on the Rocky box, not on a subscribed RHEL host, and it installed the same current core on Python 3.14.8:

sudo dnf install -y python3.14
python3.14 -m venv ~/ansible-venv
~/ansible-venv/bin/pip install ansible-core

With the venv route you either call ~/ansible-venv/bin/ansible directly or add ~/ansible-venv/bin to your PATH, since nothing links the binaries into ~/.local/bin for you.

Add only the collections you use

ansible-core ships the ansible.builtin modules and nothing else, so anything under community.general or ansible.posix has to come from Galaxy. We prefer that to the full bundle: on our controller, pipx install --python python3.14 --include-deps ansible took 25 seconds, filled 476 MB with about 90 collections and still ran the previous core line. Pull the two collections most server playbooks need instead:

ansible-galaxy collection install community.general ansible.posix

Galaxy resolves dependencies on its own, which is why a third collection shows up in the list afterwards. List what is installed now.

ansible-galaxy collection list

Everything lands under your home directory, outside the pipx venv, so a later reinstall of ansible-core keeps them:

# /home/kibet/.ansible/collections/ansible_collections
Collection                               Version
---------------------------------------- -------
ansible.posix                            2.2.2
community.general                        14.0.0
community.library_inventory_filtering_v1 1.1.5

If you would rather have the full bundle and accept the older engine, the --include-deps ansible command above is the one to run. Just do not install both into pipx at once, because both packages provide the same ansible binary.

Step 3: Use the AppStream package if you need the vendor build

The ansible-core RPM in Rocky Linux 10 AppStream works, it is just old, six release lines behind on Python 3.12 with no collections. The full ansible package is not in AppStream or in EPEL 10, so dnf install ansible fails outright. We ran both on web01:

sudo dnf install -y ansible-core

The version check shows the RPM paths under /usr/lib/python3.12 and the system config file at /etc/ansible/ansible.cfg:

dnf install ansible-core on Rocky Linux 10 installs ansible-core 2.16.16 from AppStream while the full ansible package is not found

This package makes sense when your support contract or a compliance baseline requires vendor RPMs, since it updates through the same dnf upgrade stream as the rest of the OS. For a homelab or anything where you write new playbooks against current docs, the pipx install from Step 2 is the better default, because every example on docs.ansible.com assumes a recent core. Do not mix the two on one controller: whichever ansible comes first in PATH wins, and ~/.local/bin is ahead of /usr/bin on Rocky.

Step 4: Install Ansible on Ubuntu 24.04

Ubuntu 24.04 gives you three sources and none of them reaches the current core on their own. We checked each one on nas01, and the table above has the exact versions:

  • sudo apt install ansible: full bundle, same old core line as Rocky AppStream
  • ppa:ansible/ansible: full bundle on the previous core line
  • pipx install ansible-core: previous core line on Python 3.12.3

Ubuntu 24.04 has no python3.13 or python3.14 package in its archive (apt-cache policy finds neither), so the cleanest route to the current core is uv, which downloads a standalone Python build into your home directory and leaves the system interpreter alone. Install pipx from apt, use it to install uv, then let uv build the Ansible environment. If an earlier pipx install ansible-core left an ansible binary in ~/.local/bin, remove it with pipx uninstall ansible-core first, as we did on nas01, so the two tools do not fight over the same file:

sudo apt update
sudo apt install -y pipx
pipx install uv
~/.local/bin/uv tool install --python 3.14 ansible-core

uv fetched Python 3.14.10 on its own, prepared the nine packages in 1.97 seconds and installed the current ansible-core, then warned that ~/.local/bin is not on PATH yet:

uv tool install with Python 3.14 installs ansible-core 2.22.0 on Ubuntu 24.04

Fix the path once, then open a new shell so ansible and uv resolve without the full path:

pipx ensurepath

Pick apt instead if you want Ansible to update with the rest of the system and you are happy on that older line, which is also what the Rocky AppStream RPM gives you. Note that apt-cache policy shows the package coming from noble/universe, which does not get standard Ubuntu security maintenance without Ubuntu Pro. The PPA is the middle ground with the full bundle on the previous line. If you are building a fresh controller on Ubuntu 26.04, its apt package is newer (see the table); our Ubuntu 26.04 Ansible walkthrough covers that release end to end, and the Debian 13 Ansible guide does the same for Debian.

Step 5: Set up SSH keys from the controller

Ansible connects to every managed node over SSH as a normal user and escalates with sudo, so the controller needs a key that each node trusts. Generate one on homelab-ctl:

ssh-keygen -t ed25519 -C "kibet@homelab-ctl"

Copy the public key to each node. ssh-copy-id logs in with your existing method (a password on a fresh server) and appends the key to ~/.ssh/authorized_keys:

ssh-copy-id [email protected]
ssh-copy-id [email protected]

Each run asks for that user’s password once and ends with Number of key(s) added: 1. Test with BatchMode, which forbids password prompts. If the hostname prints, Ansible will get in the same way:

ssh -o BatchMode=yes [email protected] hostname
ssh -o BatchMode=yes [email protected] hostname

Both nodes answered with web01 and nas01. This first login also writes each host key into ~/.ssh/known_hosts, which matters in the next step because we keep host key checking switched on. If your key has a passphrase, load it into the agent once per session with eval "$(ssh-agent -s)" and ssh-add so Ansible does not stop to ask for it.

Step 6: Write ansible.cfg and the inventory

A project directory with its own ansible.cfg keeps settings next to the playbooks that need them. Ansible reads the first config it finds in this order: ANSIBLE_CONFIG, ./ansible.cfg, ~/.ansible.cfg, then /etc/ansible/ansible.cfg. A pipx install has no system file, which is why ansible --version showed config file = None earlier. Create the project:

mkdir -p ~/homelab-ansible/group_vars/all
cd ~/homelab-ansible

Open the config file in your editor.

vim ~/homelab-ansible/ansible.cfg

This is the exact file we ran Steps 6 and 7 with (Step 8 adds one line to it):

[defaults]
inventory = ./inventory.ini
remote_user = kibet
host_key_checking = True
retry_files_enabled = False
timeout = 30

[privilege_escalation]
become = True
become_method = sudo
become_ask_pass = False

[ssh_connection]
pipelining = True

We leave host_key_checking on because the keys are already in known_hosts from Step 5, and turning it off only hides a changed host key, which is exactly the event you want to hear about. pipelining cuts the number of SSH round trips per task, and every playbook in this guide ran with it on against both distros. Now the inventory:

vim ~/homelab-ansible/inventory.ini

Two groups, one host each, and a pinned interpreter for all of them.

[webservers]
web01 ansible_host=10.0.1.21

[storage]
nas01 ansible_host=10.0.1.22

[all:vars]
ansible_python_interpreter=/usr/bin/python3

The [all:vars] line is there because of a warning we hit on the first ping without it. Ansible discovered /usr/bin/python3.12 on both nodes and warned that “future installation of another Python interpreter could cause a different interpreter to be discovered”. Pinning /usr/bin/python3 silences that and survives a Python point upgrade on the node.

Check that Ansible reads your file. ansible-config now defaults to -t all, so the dump includes plugin settings such as the SSH connection options:

ansible-config dump --only-changed

Every line carries the file it came from in brackets. The new CONNECTION block is where pipelining shows up, and the deprecation notices at the top come from ansible-core inspecting the oneline and tree callbacks, not from anything in your config:

[WARNING]: Deprecation warnings can be disabled by setting `deprecation_warnings=False` in ansible.cfg.
[DEPRECATION WARNING]: oneline has been deprecated. Use another callback plugin, or vendor and/or move the oneline callback to a collection. This feature will be removed from ansible-core version 2.23.
Origin: <unknown>

oneline

[DEPRECATION WARNING]: tree has been deprecated. Use another callback plugin, or vendor and/or move the tree callback to a collection. This feature will be removed from ansible-core version 2.23.
Origin: <unknown>

tree

CONFIG_FILE() = /home/kibet/homelab-ansible/ansible.cfg
DEFAULT_BECOME(/home/kibet/homelab-ansible/ansible.cfg) = True
DEFAULT_BECOME_ASK_PASS(/home/kibet/homelab-ansible/ansible.cfg) = False
DEFAULT_BECOME_METHOD(/home/kibet/homelab-ansible/ansible.cfg) = sudo
DEFAULT_HOST_LIST(/home/kibet/homelab-ansible/ansible.cfg) = ['/home/kibet/homelab-ansible/inventory.ini']
DEFAULT_REMOTE_USER(/home/kibet/homelab-ansible/ansible.cfg) = kibet
DEFAULT_TIMEOUT(/home/kibet/homelab-ansible/ansible.cfg) = 30
HOST_KEY_CHECKING(/home/kibet/homelab-ansible/ansible.cfg) = True
RETRY_FILES_ENABLED(/home/kibet/homelab-ansible/ansible.cfg) = False

GALAXY_SERVERS:


CONNECTION:
==========

ssh:
___
host_key_checking(/home/kibet/homelab-ansible/ansible.cfg) = True
pipelining(/home/kibet/homelab-ansible/ansible.cfg) = True
remote_user(/home/kibet/homelab-ansible/ansible.cfg) = kibet
timeout(/home/kibet/homelab-ansible/ansible.cfg) = 30

Adding -t base gives you the old core-only view with no plugin sections and no callback warnings, which is handy in scripts that parse the output:

ansible-config dump --only-changed showing plugin settings and deprecation warnings compared with -t base output

The -t all default also means sections owned by plugins, like [ssh_connection], are now validated by ansible-config instead of being reported as unknown, so running an ansible-config action after editing the file catches a typo in a key there.

Step 7: Run your first ad-hoc commands

The ping module is the real connectivity test: it logs in over SSH, runs Python on the node and returns pong, so it proves the key, the user and the interpreter in one go. Run it from inside the project directory so ansible.cfg is picked up:

cd ~/homelab-ansible
ansible all -m ansible.builtin.ping

Both nodes answer, and with the interpreter pinned there is no discovered_interpreter_python noise any more:

nas01 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
web01 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

Ad-hoc commands default to the command module, so any read-only check works without naming a module. Uptime is a harmless first one.

ansible all -a "uptime"

Each host reports separately. The CHANGED label only means the command ran, since Ansible cannot tell whether an arbitrary command changed anything:

nas01 | CHANGED | rc=0 >>
 04:28:13 up 8 min,  1 user,  load average: 0.03, 0.12, 0.06
web01 | CHANGED | rc=0 >>
 04:28:13 up 9 min,  1 user,  load average: 0.00, 0.22, 0.19

Facts are what your playbooks branch on, so look at the distribution facts for a mixed fleet:

ansible all -m ansible.builtin.setup -a "filter=ansible_distribution*"

The new ansible_distribution_cpe_name fact is the line to notice. It reads CPE_NAME from /etc/os-release, so Rocky reports its CPE string while Ubuntu, which does not publish one, returns null:

Ansible ping and distribution facts for a Rocky Linux 10 and an Ubuntu 24.04 host including ansible_distribution_cpe_name

Do not build conditions on cpe_name if Ubuntu or Debian hosts are in scope, because a null there will break string comparisons. ansible_distribution and ansible_os_family are still the safe keys, and our list of os_family and distribution values shows what each distro reports. More one-liners like these are in the Ansible ad-hoc commands guide.

Step 8: Run a first playbook with a vault secret

A playbook that installs a package and writes a secret into a config file shows the two things new installs notice first: idempotent tasks, and secrets that now print as $REDACTED$. Start with a vault password file that only you can read:

echo "S9rv-Lab-2026" > ~/.vault_pass
chmod 600 ~/.vault_pass

The password lands in your shell history this way, so on a shared box type it into vim ~/.vault_pass instead. Now tell Ansible where the file is. Add the setting only after the file exists, because every ansible and ansible-playbook run fails with “The vault password file … was not found” while ansible.cfg points at a missing file. This sed line is the one we ran; it inserts the setting under [defaults] right after timeout:

sed -i "/^timeout = 30/a vault_password_file = ~/.vault_pass" ansible.cfg

Write the secret into a variables file for every host, then encrypt the file in place. ansible-vault picks up the password file from ansible.cfg, so no prompt appears:

echo "db_pass: Tr0ub4dor-Homelab" > group_vars/all/vault.yml
ansible-vault encrypt group_vars/all/vault.yml
head -2 group_vars/all/vault.yml

The file now starts with the vault header instead of YAML, so nobody reading the repository sees the password.

$ANSIBLE_VAULT;1.1;AES256
39386639376631366132613730326636646464613363363938343437356330373363396335363830

Create the playbook next.

vim ~/homelab-ansible/site.yml

It installs chrony with the distro-neutral package module, prints the password through debug, writes it to /etc/app.env and shows the registered result:

---
- name: First playbook against the homelab
  hosts: all
  tasks:
    - name: Install chrony
      ansible.builtin.package:
        name: chrony
        state: present

    - name: Show which password the app will use
      ansible.builtin.debug:
        msg: "Connecting to the database with {{ db_pass }}"

    - name: Write the password into the app config
      ansible.builtin.copy:
        dest: /etc/app.env
        content: "DB_PASS={{ db_pass }}\n"
        mode: "0600"
      register: env_file

    - name: Show the registered copy result
      ansible.builtin.debug:
        var: env_file

Check the syntax first, then run it.

ansible-playbook site.yml --syntax-check
ansible-playbook site.yml

chrony was already on the Rocky image, so only nas01 reports a change for that task. The debug task is the interesting one: ansible-core registers decrypted vault values as secrets and masks them in everything it prints:

ansible-playbook run where a vault encrypted password prints as REDACTED in task output

The last task printed the registered result. Look at what is missing from it as much as what is there.

ok: [web01] => {
    "env_file": {
        "ansible_parsed": true,
        "changed": true,
        "checksum": "9ec34752266f36693a6380801a6dfb0156803ae6",
        "dest": "/etc/app.env",
        "diff": [],
        "failed": false,
        "gid": 0,
        "group": "root",
        "md5sum": "f351c41165f3b9312fbc1423900095fe",
        "mode": "0600",
        "owner": "root",
        "secontext": "system_u:object_r:etc_t:s0",
        "size": 26,
        "src": "/home/kibet/.ansible/tmp/ansible-tmp-1793680111.4514003-52386-10982625494549/.source.env",
        "state": "file",
        "uid": 0
    }
}

There is no invocation key, because Python modules no longer add it to task results by default (that change arrived one release line earlier, so it only surprises you if you upgrade from two or more lines back). If an older playbook reads result.invocation.module_args, set ANSIBLE_INJECT_INVOCATION=true (or inject_invocation = True under [defaults]) and the key comes back; we confirmed that on a second run. The secontext line also shows the file got the normal etc_t label on web01, so SELinux in enforcing mode needed no extra steps.

Masking is only cosmetic, and that is the part worth testing yourself. Ask Ansible to read the file back, then read it over plain SSH:

ansible web01 -a "cat /etc/app.env"
ssh [email protected] sudo cat /etc/app.env

Ansible masks the value even inside command output, while the file on disk holds the real password:

web01 | CHANGED | rc=0 >>
DB_PASS=$REDACTED$
DB_PASS=Tr0ub4dor-Homelab

So the template and copy tasks still receive the real secret, and only logs, callbacks and your terminal see $REDACTED$. Vault values used to print in clear text, and module options marked no_log used to come back as a literal VALUE_SPECIFIED_IN_NO_LOG_PARAMETER placeholder. Tests that look for that placeholder need updating, because the module result now carries the real value and masking happens at display time. The Ansible Vault tutorial goes further into vault IDs and rekeying, and your first Ansible playbook is the next stop for handlers and variables.

Step 9: Upgrade an existing install to the new core

If you installed Ansible with pipx on the system Python months ago, pipx upgrade will not move you to the new release line. The venv is still on Python 3.12, so pip finds nothing newer that fits and reports success:

pipx upgrade ansible-core

“Already at latest” here means latest for this interpreter, not latest on PyPI.

upgrading shared libraries...
upgrading ansible-core...
ansible-core is already at latest version 2.21.6 (location:
/home/kibet/.local/share/pipx/venvs/ansible-core)

Rebuild the venv on the newer interpreter instead. pipx reinstall keeps the package name and swaps the Python underneath (install python3.14 first, as in Step 2):

pipx reinstall --python python3.14 ansible-core

The terminal shows the before and after on homelab-ctl, ending on the new core and Python 3.14.8:

pipx upgrade stays on ansible-core 2.21.6 until pipx reinstall moves the venv to Python 3.14 and ansible-core 2.22.0

On Ubuntu with uv, uv tool upgrade ansible-core is the routine command once the tool was installed with --python 3.14; on our box it answered “Nothing to upgrade” because the box already ran the newest release. Before you upgrade a controller that runs real playbooks, check them against the behaviour changes we hit in this lab:

  • No invocation key in registered results (only if you come from two lines back or older)
  • no_log values and vault secrets print as $REDACTED$
  • follow_redirects: yes on uri now fails; write all for yes and none for no
  • ansible-config commands default to -t all

The first two only matter if your playbooks inspect results, while the uri one breaks a task outright, so grep your roles for follow_redirects before anything else. The changelog linked in Step 1 lists the rest, and the oneline and tree callback warnings from Step 6 tell you those two callbacks are due to leave ansible-core in the next release.

What broke in our lab

Every error below came up while building this guide, with the fix we used.

“No matching distribution found for ansible-core”

The controller is on Python 3.12, and the line above it in the pip output names the reason: Requires-Python >=3.13. Install python3.14 from AppStream on Rocky or RHEL 10 and pass --python python3.14 to pipx, or use uv tool install --python 3.14 on Ubuntu 24.04. Without a version pin you get no error at all, just the older core, so always read the “installed package” line.

“Attempting to decrypt but no vault secrets found”

You only meet this one if you skip or later remove the vault_password_file line. In our first pass the line was not there yet, and once group_vars/all/vault.yml existed, even a plain ansible web01 -m ansible.builtin.ping stopped with this error, because Ansible loads group variables for every host it touches, ad-hoc or not. Passing --vault-password-file ~/.vault_pass on each command works, but the vault_password_file line added to ansible.cfg in Step 8 fixes it for the whole project. Keep that password file at mode 600 and out of Git.

“value of follow_redirects must be one of: all, none, safe, urllib2, got: yes”

We ran an ad-hoc uri call with follow_redirects=yes, the way many older roles write it, against web01, and the task failed with exactly this message. The yes and no values were removed from the uri module. The old yes meant all and the old no meant none, so map them one to one; safe (the module default, which follows redirects for GET and HEAD only) would quietly change a task that expects a 30x status. With follow_redirects=safe the same request returned status 200.

“Permission denied (publickey,gssapi-keyex,gssapi-with-mic)”

Our first ssh-copy-id pass printed “All keys were skipped because they already exist on the remote system” and then the login failed with this error on web01. ssh-copy-id decides whether a key is present by trying to log in, and our test login succeeded through a different key, so it skipped the copy. If you see the skip message on a host where the key cannot be there yet, run ssh-copy-id -f [email protected] to force it, then repeat the BatchMode test from Step 5.

“Ansible requires Python 3.9 or newer on the target”

This is the managed-node side of the Python question, and the message comes straight from the ansible-core module code. Rocky 10, Ubuntu 24.04 and Debian 13 are all well above the floor, so you may only meet it on old hosts such as a RHEL 8 box still on its platform Python 3.6. Install a newer Python there and set ansible_python_interpreter for that host in the inventory.

Roll back or remove Ansible

If a playbook breaks on the new core and you need the old one back today, pin the previous line inside the same pipx venv. --force reuses the existing environment, so it stays on Python 3.14:

pipx install --force "ansible-core<2.22"

On our controller that put the previous core back in place, and running the same command with plain ansible-core returned it to the current release. To remove Ansible completely, use the command that matches how you installed it:

  • pipx: pipx uninstall ansible-core
  • uv: uv tool uninstall ansible-core
  • Rocky or RHEL RPM: sudo dnf remove ansible-core
  • Ubuntu apt: sudo apt purge ansible ansible-core

None of these touch ~/.ansible/collections or your project directory, so collections and playbooks survive a reinstall. Delete ~/.ansible only if you want a clean slate. For a printable list of the commands used here and in the rest of the series, keep the Ansible cheat sheet open, and the Ansible automation guide maps out where to go after your first playbook.

Keep reading

OpenCode CLI Cheat Sheet: Commands, Sessions and Fixes (PDF) AI OpenCode CLI Cheat Sheet: Commands, Sessions and Fixes (PDF) Best Mini PC for Home Assistant: 4 Picks for HAOS and Proxmox Automation Best Mini PC for Home Assistant: 4 Picks for HAOS and Proxmox Ansible + Proxmox: Automated VM Management Ansible Ansible + Proxmox: Automated VM Management Best Weather Stations for Home Assistant: Ecowitt, Tempest, Davis Automation Best Weather Stations for Home Assistant: Ecowitt, Tempest, Davis Best Water Leak Sensors and Auto Shutoff Valves for Home Assistant Automation Best Water Leak Sensors and Auto Shutoff Valves for Home Assistant Install Apache Tomcat Using Ansible on Ubuntu / Rocky Linux / Debian Ansible Install Apache Tomcat Using Ansible on Ubuntu / Rocky Linux / Debian

Leave a Comment

Press ESC to close