ansible.builtin.package looks like the answer to running one patching
role across a mixed fleet. It selects apt on Debian-family hosts and
dnf on RHEL-family ones, so the role stops needing a when: ansible_facts.os_family == 'Debian' on every task.
It does exactly that, and it does only that. The documentation says
so in a sentence that is easy to skim past:
Package names also vary with package manager; this module will not
“translate” them per distribution. For example libyaml-dev,
libyaml-devel.
The manager is abstracted. The names are not. Half a problem solved is
still half a problem, and the half that remains is the half that fails
in production rather than at parse time.
What it actually abstracts
Read-only / Safethe module's complete parameter list— Read-only. Three parameters. That brevity is the point: the module documents only the minimum intersection of arguments that every packaging module supports.
$ ansible-doc ansible.builtin.package
name Package name, or package specifier with version.
Syntax varies with package manager. For example `name-1.0'
or `name=1.0'.
Package names also vary with package manager; this module
will not "translate" them per distribution.
state Whether to install (`present'), or remove (`absent') a
package.
You can use other states like `latest' ONLY if they are
supported by the underlying package module(s) executed.
use The required package manager module to use (`dnf', `apt',
and so on). The default `auto' will use existing facts or
try to auto-detect it.
default: auto
Three parameters, and the documentation is explicit about why: “This
documentation only covers the minimum intersection of module arguments
that all packaging modules support.”
Everything from the previous two lessons is outside that intersection.
cache_valid_time, dpkg_options, lock_timeout, only_upgrade,
security, update_only, download_only, allowerasing — none of
them are package parameters.
The documentation does say “all arguments will be passed to the
underlying module”, so an extra argument is not silently dropped. It is
passed through, and it fails on any host whose selected manager does not
accept it. A package task carrying security: true works on your
RHEL-family hosts and fails on your Debian-family ones — which is to
say, the abstraction stops abstracting at exactly the point where you
needed it to.
The name problem, and the shape that solves it
libyaml-dev on Debian and Ubuntu. libyaml-devel on RHEL, Rocky and
Alma. The module will not bridge that, so you bridge it in variables.
Configuration changea role vars/ split keyed on os_family— Installs packages. The names come from a per-family vars file loaded by fact, so the task itself stays free of conditionals.
# roles/appdeps/vars/Debian.yml
app_packages:
- libyaml-dev
- libssl-dev
- python3-dev
# roles/appdeps/vars/RedHat.yml
app_packages:
- libyaml-devel
- openssl-devel
- python3-devel
# roles/appdeps/tasks/main.yml
- name: Load the package names for this distribution family
ansible.builtin.include_vars: '{{ ansible_facts.os_family }}.yml'
- name: Install the application build dependencies
ansible.builtin.package:
name: '{{ app_packages }}'
state: present
This is the shape worth internalising, because it puts the variability
where it belongs. The task expresses the intent once. The vars files
carry the per-family facts, and a reviewer can see the whole
distribution difference by opening two short files rather than reading
every task in the role.
ansible_facts.os_family is the right key for this, not
ansible_facts.distribution. os_family groups Rocky, Alma, RHEL,
CentOS and Fedora as RedHat, and Debian and Ubuntu as Debian, which
is the granularity package names actually vary at. Keying on
distribution gives you five near-identical files that drift apart.
use and ansible_package_use
The default auto selects the manager from facts — ansible_facts.pkg_mgr
is the fact that carries it. Two overrides exist.
use is the task parameter. ansible_package_use is a variable,
available since ansible-core 2.17, which can be set in inventory or
group vars. The documentation states the precedence directly: the
variable is available “but this option still takes precedence” — so
use on the task wins over ansible_package_use.
Read-only / Safesetting the manager where the exception lives— Read-only: a variable assignment, not a task. Putting the override in inventory keeps the exception visible next to the hosts it applies to.
# host_vars/build07.example.com.yml
# This host runs a dnf-4 stack that auto-detection gets wrong after the
# in-place upgrade. Owner: platform. Remove once the host is rebuilt.
ansible_package_use: dnf
The order to reach for these: fix the facts, then set the variable in
inventory, and only put use on a task when the task genuinely must
pin a manager regardless of the host. A use: hard-coded in a shared
role is an assumption about every host the role will ever run against.
Check mode is where the abstraction leaks most
Every other module in this part declares a definite check-mode
capability. apt is check_mode: full. dnf and dnf5 are
check_mode: full.
package declares check_mode: N/A, with the detail “support depends
on the underlying plugin invoked”.
The same caveat applies to diff_mode, documented identically as
depending on the dispatched plugin.
When package is genuinely the right call
The abstraction earns its place when three things are true at once:
The package name is the same everywhere.curl, git, rsync,
jq, tmux, chrony. Development headers and library packages
almost never qualify.
You need only present or absent. These are the two states the
module documents; anything else is conditional on the dispatched
plugin.
No manager-specific option is required. No cache handling, no
lock timeout, no security filter, no repository scoping.
Configuration changethe case package was written for— Installs a small set of universally-named tools. Runs identically on Debian-family and RHEL-family hosts with no conditional and no vars file.
- name: Ensure the standard operator toolset is present
ansible.builtin.package:
name:
- curl
- git
- rsync
- tmux
state: present
That task is better than the two-conditional version it replaces. It is
shorter, it has one failure mode instead of two, and it does not go
stale when someone adds a third distribution family.
The judgement is not “abstract or do not abstract”. It is knowing that
this particular abstraction covers the manager and not the vocabulary,
and putting it where that is enough.
Knowledge check
Knowledge check · 4 questions
Q1. A role uses ansible.builtin.package with name: libyaml-dev against a fleet of Ubuntu and Rocky hosts. What happens on the Rocky hosts?
Q2. Why is ansible.builtin.package a poor choice for the task that performs a fleet patch run?
Q3. Which statements about check mode and ansible.builtin.package are correct? Select all that apply.
Q4. If both are set, the use parameter on a package task overrides the ansible_package_use variable set in inventory.
Passing score: 75%. Answers are checked in this browser.