AnsibleXL · Patch and Reboot ManagementPatch and Reboot Management
Patching RHEL, Rocky and Alma
What you'll learn
- Apply a security-only patch set and explain what its dependency handling actually does
- Choose between ansible.builtin.dnf and ansible.builtin.dnf5 and know what use_backend selects
- Scope a transaction with exclude, enablerepo and disablerepo without hiding a security update
- Use download_only to stage packages before a window and apply them inside a shorter one
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 RHEL family gives you something the Debian family does not: package updates carry advisory metadata, so the package manager can be asked for “only the security updates” and answer meaningfully.
That capability is the reason a lot of organisations patch RHEL-family
hosts on a different cadence from their Debian-family ones, and it comes
with a set of semantics that are easy to state wrongly. security: true
does not mean “install only packages flagged as security fixes”. What it
actually means is worth getting right, because the gap between the two
readings is a set of packages that moved when you thought nothing would.
Which module is running
There are two modules, and on a modern host you may not be executing the one you named.
ansible.builtin.dnf drives the dnf-4 Python API and needs
python3-dnf. ansible.builtin.dnf5 drives dnf-5 and needs
python3-libdnf5. They are separate modules, not aliases, and the
dnf5 documentation carries an explicit warning that dnf-5 is still
under development and that not every option the dnf module provides is
implemented in dnf5.
use_backend on the dnf module selects between them:
use_backend | Effect |
|---|---|
auto (default) | select from the ansible_facts.pkg_mgr fact |
dnf, dnf4 | force ansible.builtin.dnf |
dnf5 | force ansible.builtin.dnf5 |
yum, yum4 | compatibility aliases |
Another asymmetry worth carrying: lock_timeout defaults to 30
seconds on ansible.builtin.dnf, and on ansible.builtin.dnf5 the
documentation records it as currently a no-op, because dnf-5 does
not expose an option to configure it. A play that raises lock_timeout
to survive a competing transaction gets that behaviour on dnf-4 hosts
and not on dnf-5 ones.
Security-only patching, and what it really installs
- name: Apply security updates only
ansible.builtin.dnf:
name: '*'
state: latest
security: true
lock_timeout: 300
register: dnf_security
- name: Record the transaction, for the audit trail
ansible.builtin.debug:
var: dnf_security.results
when: dnf_security.changedThree things about that task.
security: true only has an effect with state: latest. The
documentation states the condition explicitly. With state: present it
does nothing, and the task will happily report ok while applying no
security updates at all.
name: '*' is what makes it a fleet patch. The documentation notes
that with state: latest, name can be '*', which means run
dnf -y update. Without it you are filtering an empty set.
The dependency handling is upgrade-minimal semantics. This is the
part that is usually stated wrongly. From the module documentation: “If
set to true, and state=latest then only installs updates that have
been marked security related. Note that, similar to dnf upgrade-minimal, this filter applies to dependencies as well.”
upgrade-minimal means dnf resolves to the lowest version that
carries the fix, rather than the newest available. So a security-only
run does not freeze your dependency tree — it moves dependencies, just
conservatively, to the minimum versions the advisories require.
bugfix: true works identically for bugfix-flagged advisories, and the
two can be combined. Both default to false.
Verify the repositories actually publish advisories
The security filter is only as good as the updateinfo metadata in the
repositories the host is configured with. A repository that ships
packages but no advisory metadata contributes nothing to a
security-only run, and does so silently: the filter matches no
advisories in that repository, so nothing from it is upgraded, and the
task reports success.
This bites hardest with third-party and internal repositories, which frequently publish no advisory data at all.
- name: List the security advisories dnf can see
ansible.builtin.command:
argv: [dnf, '-q', 'updateinfo', 'list', '--security']
register: advisories
changed_when: false
failed_when: false
- name: Warn when a host has no visible advisories
ansible.builtin.debug:
msg: >-
{{ inventory_hostname }} reports no security advisories. Confirm the
repositories publish updateinfo metadata before treating a
security-only patch run as complete.
when: advisories.stdout | trim | length == 0On dnf-5 the command was renamed: dnf5 advisory list is the current
form, with dnf5 updateinfo list retained as a compatibility alias.
Scoping the transaction
Four parameters narrow what a run may touch, and each has a way of going wrong.
update_only: true upgrades a named package only if it is already
installed, and installs nothing new. It is the RHEL-family counterpart
of apt’s only_upgrade, and it exists for the same reason: a
state: latest on an absent package is an install.
- name: Upgrade the packages we own, where they are installed
ansible.builtin.dnf:
name:
- nginx
- openssl
- openssh-server
state: latest
update_only: trueexclude holds packages back. It is genuinely necessary — a
database host that must not take a postgresql minor bump during a
routine window is a real requirement — and it is also how a security
update quietly stops being applied.
enablerepo / disablerepo scope the transaction to specific
repositories for that operation only; the change does not persist beyond
the transaction. Both default to an empty list.
The safe pattern is to enable a repository for the one task that needs it rather than leaving it enabled on the host, because a repository enabled permanently on a host is a source of packages nobody reviewed.
allowerasing: true permits dnf to erase installed packages to
resolve dependencies. It defaults to false, and that default is the one
you want in a patch run. Turning it on is the RHEL-family equivalent of
upgrade: full on Debian: it converts “this update cannot be applied”
into “this update was applied and something was removed to make room”.
Related: nobest and its inverse best both default to null in
ansible-core 2.17 and later, meaning the value comes from the
operating system distribution rather than from Ansible. Do not assume
either state — if the behaviour matters to you, set it explicitly.
Splitting the transaction across two windows
download_only: true fetches packages without installing them. That
turns one long risky operation into two operations with very different
risk profiles.
- name: Stage the security updates without installing them
ansible.builtin.dnf:
name: '*'
state: latest
security: true
download_only: true
download_dir: /var/cache/dnf/staged
register: staged
- name: Fail the host if nothing could be staged
ansible.builtin.assert:
that: staged is not failed
fail_msg: >-
{{ inventory_hostname }} could not stage its updates. This host is
excluded from tonight's window rather than discovered mid-run.- name: Apply the staged updates
ansible.builtin.dnf:
name: '*'
state: latest
security: true
cacheonly: true
lock_timeout: 300The staging run is READ-ONLY in the sense that matters: it changes no
package, restarts no service, and can be run at 14:00 on a Tuesday
against the whole fleet. What it buys you is that the risky window
contains only the install, and the two commonest ways a patch window
overruns — a slow mirror and a full disk — have both been discovered
hours earlier, when someone was awake.
Knowledge check
Knowledge check · 4 questions
Q1. A play sets security: true with state: present on ansible.builtin.dnf and name: "*". What happens?
Q2. What does the dnf module documentation mean when it says the security filter "applies to dependencies as well", similar to dnf upgrade-minimal?
Q3. Which of these are true of ansible.builtin.dnf5 relative to ansible.builtin.dnf on ansible-core 2.21? Select all that apply.
Q4. Because RHEL-family repositories carry advisory metadata, a play using security: true is guaranteed to apply every security update available to the host.
Passing score: 75%. Answers are checked in this browser.