Introduction
Red Hat Enterprise Linux 10 ships with Python 3.12 as the system Python, updated package names, and systemd/firewalld changes that affect Ansible playbooks. If you're managing RHEL 9 hosts and planning to add RHEL 10, this guide covers what changed, what breaks, and how to update your playbooks for both versions.
What Changed in RHEL 10
| Area | RHEL 9 | RHEL 10 |
|---|---|---|
| Python | 3.9 (default), 3.11/3.12 optional | 3.12 (default) |
| Kernel | 5.14 | 6.12 |
| systemd | 252 | 256 |
| Firewalld | 1.x | 2.x |
| Podman | 4.x | 5.x |
| OpenSSL | 3.0 | 3.2 |
| GCC | 11 | 14 |
| ansible-core (AppStream) | 2.14 | 2.17 |
| Network config | NetworkManager | NetworkManager (nmstate default) |
| Default editor | vi | vi (nano available) |
| Crypto policy | DEFAULT | DEFAULT (stricter TLS defaults) |
| Support | Until 2032 | Until 2035 |
Install Ansible on RHEL 10
As Controller (Run Ansible From RHEL 10)
# ansible-core from AppStream
sudo dnf install ansible-core
# Full Ansible with collections
sudo dnf install ansible
# Verify
ansible --version
# ansible [core 2.17.x]
# python version = 3.12.x
As Managed Node (Target Host)
RHEL 10 targets need Python 3.12 (installed by default):
# Verify Python is available
python3 --version
# Python 3.12.x
# Install minimal dependencies for Ansible
sudo dnf install python3 python3-libselinux python3-dnf
Inventory Configuration
[rhel10]
rhel10-web01 ansible_host=10.0.0.11
rhel10-web02 ansible_host=10.0.0.12
[rhel10:vars]
ansible_python_interpreter=/usr/bin/python3
Python Changes
Python 3.12 as Default
RHEL 10's system Python is 3.12. Key impacts for Ansible:
# ansible.cfg or inventory — usually auto-detected, but be explicit
[defaults]
interpreter_python = auto_silent
# Or per host
# ansible_python_interpreter = /usr/bin/python3
Removed Python 2
Python 2 is completely removed. If your playbooks or custom modules use Python 2 syntax, they will fail:
# ❌ Python 2 syntax — fails on RHEL 10
print "hello"
except Exception, e:
# ✅ Python 3 syntax
print("hello")
except Exception as e:
pip and Virtual Environments
- name: Create Python virtual environment on RHEL 10
ansible.builtin.pip:
name:
- flask
- gunicorn
virtualenv: /opt/myapp/venv
virtualenv_command: python3 -m venv
Package Name Changes
Some packages have been renamed or split:
# ❌ May not work on RHEL 10
- ansible.builtin.dnf:
name: python3-pip
state: present
# ✅ Works on both RHEL 9 and 10
- ansible.builtin.package:
name: python3-pip
state: present
Use ansible.builtin.package for Portability
# Works across RHEL 8, 9, and 10
- name: Install common packages
ansible.builtin.package:
name:
- vim-enhanced
- tmux
- curl
- git
- python3-pip
state: present
RHEL 10-Specific Packages
- name: Install RHEL 10 specific packages
ansible.builtin.dnf:
name:
- python3.12-pip
- container-tools
- nmstate
state: present
when: ansible_distribution_major_version == '10'
Firewalld 2.x Changes
RHEL 10 ships firewalld 2.x with policy-based filtering:
# Works on both versions
- name: Allow HTTPS
ansible.posix.firewalld:
service: https
permanent: true
state: enabled
immediate: true
# RHEL 10 — new policy objects
- name: Configure firewall policy
ansible.posix.firewalld:
policy: allow-web-traffic
rich_rule: 'rule family="ipv4" source address="10.0.0.0/8" service name="https" accept'
permanent: true
state: enabled
when: ansible_distribution_major_version == '10'
systemd 256 Changes
# Credentials directory (new in systemd 256)
- name: Deploy service with credentials
ansible.builtin.copy:
dest: /etc/systemd/system/myapp.service
content: |
[Unit]
Description=My Application
After=network.target
[Service]
Type=notify
ExecStart=/opt/myapp/bin/server
# New: systemd credentials
LoadCredential=db-password:/etc/myapp/db-password
# New: resource control improvements
MemoryMax=2G
CPUWeight=100
[Install]
WantedBy=multi-user.target
- name: Reload and start
ansible.builtin.systemd_service:
name: myapp
state: started
enabled: true
daemon_reload: true
Crypto Policy Changes
RHEL 10 has stricter default crypto policies:
# Check current policy
- name: Get crypto policy
ansible.builtin.command: update-crypto-policies --show
register: crypto_policy
changed_when: false
# Set policy if needed (e.g., for legacy compatibility)
- name: Set crypto policy
ansible.builtin.command: update-crypto-policies --set DEFAULT:SHA1
when: crypto_policy.stdout != 'DEFAULT:SHA1'
notify: reboot
Migration Playbook: RHEL 9 → RHEL 10
---
- name: Prepare for RHEL 10 upgrade
hosts: rhel9_servers
become: true
tasks:
- name: Verify current RHEL version
ansible.builtin.assert:
that:
- ansible_distribution == 'RedHat'
- ansible_distribution_major_version == '9'
- name: Update all RHEL 9 packages
ansible.builtin.dnf:
name: '*'
state: latest
update_cache: true
- name: Install leapp upgrade tool
ansible.builtin.dnf:
name:
- leapp-upgrade
state: present
- name: Run leapp preupgrade check
ansible.builtin.command: leapp preupgrade
register: preupgrade
failed_when: false
changed_when: false
- name: Display preupgrade report
ansible.builtin.debug:
var: preupgrade.stdout_lines
- name: Check for inhibitors
ansible.builtin.assert:
that: "'inhibitor' not in preupgrade.stdout"
fail_msg: "Upgrade inhibitors found — review /var/log/leapp/leapp-report.txt"
Multi-Version Playbooks
Support both RHEL 9 and 10 in one playbook:
---
- name: Configure web server (RHEL 9 + 10)
hosts: webservers
become: true
tasks:
- name: Install packages
ansible.builtin.dnf:
name:
- nginx
- python3-pip
- firewalld
state: present
- name: Configure firewall
ansible.posix.firewalld:
service: "{{ item }}"
permanent: true
state: enabled
immediate: true
loop:
- http
- https
- name: RHEL 10 specific — configure nmstate
ansible.builtin.dnf:
name: nmstate
state: present
when: ansible_distribution_major_version == '10'
- name: Start services
ansible.builtin.service:
name: "{{ item }}"
state: started
enabled: true
loop:
- nginx
- firewalld
Testing with Molecule
# molecule/default/molecule.yml
---
driver:
name: podman
platforms:
- name: rhel9
image: registry.access.redhat.com/ubi9/ubi:latest
command: /usr/sbin/init
privileged: true
- name: rhel10
image: registry.access.redhat.com/ubi10/ubi:latest
command: /usr/sbin/init
privileged: true
Troubleshooting
"No module named 'dnf'"
# RHEL 10 uses dnf5 — ensure python3-dnf is installed
- name: Bootstrap dnf Python bindings
ansible.builtin.raw: dnf install -y python3-dnf
when: ansible_distribution_major_version == '10'
SELinux Context Errors
# Ensure SELinux Python bindings are installed
- name: Install SELinux bindings
ansible.builtin.dnf:
name: python3-libselinux
state: present
SSH Key Exchange Failures
RHEL 10's stricter crypto policy may reject older SSH key types:
# Use ed25519 keys (recommended)
- name: Deploy SSH key
ansible.posix.authorized_key:
user: ansible
key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
Related Articles
Conclusion
RHEL 10 brings Python 3.12 as default, firewalld 2.x, systemd 256, and stricter crypto policies. For Ansible, the main change is ensuring ansible_python_interpreter points to Python 3.12 and updating any Python 2 syntax in custom modules. Use ansible.builtin.package for cross-version compatibility and when: ansible_distribution_major_version conditionals for version-specific tasks. Test with Molecule against both RHEL 9 and 10 containers before rolling out.