Rocky Linux 10 uses DNF for packages. In Ansible, ansible.builtin.dnf manages install, remove, and update operations, while ansible.builtin.yum_repository adds or changes .repo files under /etc/yum.repos.d/. That split keeps repository configuration separate from package state—the pattern Rocky Linux documents for DNF and Ansible mirrors in its module design.
This guide shows tested playbooks on Rocky Linux 10.2: single and multiple package installs, removal, updates, package groups, temporary repo enablement, custom repositories with GPG check, and verification commands. It assumes you can run a basic play from your first playbook and use become for root-only changes. Deep DNF troubleshooting and AppStream module streams are out of scope—this page focuses on Ansible modules only.
Tested on: Rocky Linux 10.2 (Red Quartz); kernel 6.12.0-211.16.1.el10_2.0.1.x86_64; ansible-core 2.16.16; DNF 4.20.0.
dnf --version reports 4.20.0 on the tested host). EL10 is transitioning toward DNF 5 (dnf5) in some channels—module parameters stay the same, but CLI output and plugin behavior can differ. If your host reports dnf5, verify group names and repo IDs with dnf5 grouplist and dnf5 repolist before you copy playbook examples.
~/ansible-project, inventory group lab, and playbooks in playbooks/. Use your own host names and paths if yours differ.
| Task | Module |
|---|---|
| Install package | ansible.builtin.dnf |
| Remove package | ansible.builtin.dnf |
| Update package | ansible.builtin.dnf |
| Install package group | ansible.builtin.dnf |
| Add repo file | ansible.builtin.yum_repository |
| Enable/disable repo file | ansible.builtin.yum_repository |
Configure gpgcheck |
ansible.builtin.yum_repository |
What This Article Covers
ansible.builtin.dnffor packages and groups on Rocky Linux 10ansible.builtin.yum_repositoryfor repository files, GPG settings, and enablementenablerepo/disablerepoon package tasks when you need a temporary repo slice- Verification with
rpm,dnf repolist, and Ansible check mode - Common mistakes such as using
command: dnfor installing before the repo exists
Not covered here: full DNF CLI tutorials, private mirror construction, RPM signing theory, or collection layout beyond FQCN usage.
Why Use Ansible for Package and Repository Management?
Manual / script dnf |
ansible.builtin.dnf |
|---|---|
| Re-running may repeat work blindly | state: present / absent / latest is idempotent |
| No built-in check-mode preview | --check on the playbook when the module supports it |
| Repo file edits mixed with installs | yum_repository owns /etc/yum.repos.d/ content declaratively |
Package tasks appear in almost every baseline play—web servers, agents, admin tools. On a full Rocky baseline, pair installs with user and sudo management on the same hosts. Keeping package work in modules (not raw shell) matches idempotency practice and reads clearly in playbook structure. Use FQCN in playbooks: ansible.builtin.dnf, not bare dnf—see modules and ansible-doc.
dnf vs yum_repository in Ansible
ansible.builtin.dnf |
ansible.builtin.yum_repository |
|
|---|---|---|
| Manages | RPM packages and groups | .repo file stanzas |
Typical dest |
Installed package database | /etc/yum.repos.d/<file>.repo |
state: absent |
Removes packages | Removes the repository definition/stanza from the repo file |
| GPG | Uses repo file settings at install time | Sets gpgcheck and gpgkey in the repo file |
Official references: dnf module, yum_repository module.
Prerequisites for Rocky Linux 10
- Managed host running Rocky Linux 10 with working DNF metadata (
baseos,appstream, …) ansible-coreon the control node and Python on the target (default on Rocky)become: true(or-b) for package and repo tasks- Outbound HTTPS to mirrors when installing from remote
baseurlvalues
Quick connectivity check:
ansible lab -m ansible.builtin.pingSample output:
localhost | SUCCESS => {
"changed": false,
"ping": "pong"
}Confirm DNF works on the target before debugging Ansible:
ansible lab -m ansible.builtin.command -a "dnf --version" -bSample output:
localhost | CHANGED | rc=0 >>
4.20.0
Installed: dnf-0:4.20.0-12.el10.noarch at Mon 07 Jul 2025 10:15:22 AM ISTInstall Packages with ansible.builtin.dnf
state: present installs the package if missing and does nothing when the version already satisfies the request.
playbooks/install-jq.yml:
---
- name: Install single package with dnf
hosts: lab
become: true
gather_facts: false
tasks:
- name: Install jq
ansible.builtin.dnf:
name: jq
state: presentansible-playbook playbooks/install-jq.ymlSample output:
TASK [Install jq] **************************************************************
changed: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0A second run reports ok and changed=0—the package is already installed.
Verify:
rpm -q jqSample output:
jq-1.7.1-11.el10_2.2.x86_64Install Multiple Packages
Pass a YAML list to name:
---
- name: Install multiple packages
hosts: lab
become: true
gather_facts: false
tasks:
- name: Install admin tools
ansible.builtin.dnf:
name:
- tree
- bind-utils
state: presentansible-playbook playbooks/install-multi.ymlSample output:
TASK [Install admin tools] *****************************************************
changed: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0rpm -q tree bind-utilsSample output:
tree-2.1.0-8.el10.x86_64
bind-utils-9.18.33-15.el10_2.2.x86_64Remove Packages
state: absent removes the package:
---
- name: Remove package
hosts: lab
become: true
gather_facts: false
tasks:
- name: Remove tree package
ansible.builtin.dnf:
name: tree
state: absentansible-playbook playbooks/remove-tree.ymlSample output:
TASK [Remove tree package] *****************************************************
changed: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0rpm -q treeSample output:
package tree is not installedUpdate Packages
state: latest upgrades named packages to the newest version available from enabled repositories.
---
- name: Update single package
hosts: lab
become: true
gather_facts: false
tasks:
- name: Update jq to latest
ansible.builtin.dnf:
name: jq
state: latestWhen the package is already current, the task reports ok with changed=0.
Use state: latest deliberately. It is useful for patching a known package, but it can introduce version changes on every run as repositories publish updates. For stable application hosts, prefer state: present with a tested package version, or run updates through a maintenance workflow instead of leaving latest on critical packages indefinitely.
Update All Packages Safely
To upgrade everything DNF can reach, some playbooks use name: "*" with state: latest. That is equivalent to a broad dnf upgrade and belongs in a maintenance window—not casual reruns.
Preview first:
---
- name: Preview update all packages
hosts: lab
become: true
gather_facts: false
tasks:
- name: Preview updating all packages
ansible.builtin.dnf:
name: "*"
state: latestansible-playbook playbooks/update-all-check.yml --checkSample output:
TASK [Preview updating all packages] *******************************************
changed: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0changed under --check means DNF would upgrade something—review before dropping --check in production. Prefer named packages or security-only workflows when you need tighter control.
Install Package Groups
DNF groups install with an @ prefix in the name field:
---
- name: Install package group
hosts: lab
become: true
gather_facts: false
tasks:
- name: Install Legacy UNIX Compatibility group
ansible.builtin.dnf:
name: "@Legacy UNIX Compatibility"
state: presentList available groups on the target when you need the exact string:
dnf grouplist --availableSample output:
Available Groups:
Legacy UNIX Compatibility
Smart Card Support
...The Ansible task reports changed only when the group install actually adds packages.
name: "@Group Name") and AppStream module streams are different DNF concepts. This guide covers groups only. Module streams (dnf module enable) are not managed by ansible.builtin.dnf parameters alone—you typically need command/shell with a clear idempotency guard, or a dedicated role. List exact group strings with dnf grouplist --available (or dnf5 grouplist on DNF 5 hosts) before you hard-code them in playbooks.
Enable and Disable Repositories During Package Tasks
For a single dnf task you can enable or disable repos without editing .repo files permanently:
---
- name: Install with enablerepo
hosts: lab
become: true
gather_facts: false
tasks:
- name: Install jq with explicit baseos repo
ansible.builtin.dnf:
name: jq
state: present
enablerepo: baseos
disablerepo: epelenablerepo and disablerepo accept comma-separated repo IDs or lists. Use this when one install should ignore optional repos (for example EPEL) for that task only.
Use yum_repository with enabled: false when you want to persistently disable a repo file on disk. Use dnf enablerepo / disablerepo when you only want to change repo visibility for one transaction.
Add a Custom Repository with yum_repository
Add a .repo file before installing packages that live only in that mirror:
---
- name: Add custom repository
hosts: lab
become: true
gather_facts: false
tasks:
- name: Add demo custom repo file
ansible.builtin.yum_repository:
name: ansible-demo-custom
description: Ansible demo custom repository
baseurl: https://download.rockylinux.org/pub/rocky/10/BaseOS/x86_64/os/
gpgcheck: true
gpgkey: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rockyofficial
enabled: true
file: ansible-demo-customansible-playbook playbooks/add-repo.ymlSample output:
TASK [Add demo custom repo file] ***********************************************
changed: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Rendered file:
Sample output:
[ansible-demo-custom]
name = Ansible demo custom repository
baseurl = https://download.rockylinux.org/pub/rocky/10/BaseOS/x86_64/os/
enabled = 1
gpgcheck = 1
gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rockyofficialWhen you add or change a repository and install from it in the same play, set update_cache: true on the first package install task. This forces DNF to refresh metadata before resolving the package:
- name: Install package from refreshed metadata
ansible.builtin.dnf:
name: jq
state: present
update_cache: trueThe dnf module applies update_cache when state is present or latest—pair it with the install task rather than a standalone metadata-only call.
Add the repository task before any dnf task that needs packages from that mirror.
Configure GPG Check and GPG Key
On Rocky Linux, official mirrors ship GPG keys under /etc/pki/rpm-gpg/. In yum_repository:
| Option | Verifies |
|---|---|
gpgcheck |
RPM package signatures |
repo_gpgcheck |
Repository metadata signatures, when the repository provides signed metadata |
gpgkey |
Location of the public key used for package signature verification |
gpgcheck: true
gpgkey: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rockyofficialMost simple lab examples use gpgcheck: true with a trusted gpgkey. Enable repo_gpgcheck only when the repository publishes signed metadata and your environment expects that check.
Setting gpgcheck: false installs faster in lab sandboxes but removes an important trust check—avoid on production hosts unless you fully trust the mirror path and accept the risk.
Enable, Disable and Remove Repository Files
Disabling keeps the file but sets enabled=0. The module needs the same baseurl (or metalink / mirrorlist) as when the repo was created:
- name: Disable demo custom repo
ansible.builtin.yum_repository:
name: ansible-demo-custom
description: Ansible demo custom repository
baseurl: https://download.rockylinux.org/pub/rocky/10/BaseOS/x86_64/os/
gpgcheck: true
gpgkey: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rockyofficial
enabled: false
file: ansible-demo-customansible-playbook playbooks/disable-repo.ymlSample output:
enabled = 0Remove the repository definition with state: absent:
- name: Remove demo custom repo
ansible.builtin.yum_repository:
name: ansible-demo-custom
file: ansible-demo-custom
state: absentInstall a Local RPM or RPM URL
ansible.builtin.dnf can install from a package name, a local .rpm path, or an HTTPS URL. Use this for vendor RPMs when no repository exists, but prefer a signed repository for repeatable fleet management.
- name: Install local RPM package
ansible.builtin.dnf:
name: /tmp/vendor-agent.rpm
state: present
- name: Install RPM from URL
ansible.builtin.dnf:
name: https://example.com/packages/vendor-agent.rpm
state: presentFor production, a signed repository is easier to audit and update than scattered RPM URLs. The dnf module accepts package names, specs, filenames, and URLs in name.
Lab test with a downloaded RPM on disk:
ansible-playbook playbooks/install-local-rpm.ymlSample output:
TASK [Install jq from local RPM path] ******************************************
ok: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Verify Installed Packages and Repositories
After package tasks, confirm RPM state on the host:
rpm -q jq httpdSample output:
jq-1.7.1-11.el10_2.2.x86_64
httpd-2.4.63-13.el10_2.1.x86_64List enabled repositories:
dnf repolistSample output:
repo id repo name
appstream Rocky Linux 10 - AppStream
baseos Rocky Linux 10 - BaseOS
epel Extra Packages for Enterprise Linux 10 - x86_64
extras Rocky Linux 10 - ExtrasFor playbook verification, prefer package_facts over dnf list:—the dnf module docs treat list as a command-style, non-idempotent query better suited to ad hoc use.
---
- name: Verify packages with package_facts
hosts: lab
become: true
gather_facts: false
tasks:
- name: Gather installed package facts
ansible.builtin.package_facts:
manager: auto
- name: Confirm jq is installed
ansible.builtin.debug:
msg: jq is installed
when: "'jq' in ansible_facts.packages"ansible-playbook playbooks/package-facts.ymlSample output:
TASK [Confirm jq is installed] *************************************************
ok: [localhost] => {
"msg": "jq is installed"
}Common Examples
Install common admin tools
Use a list install for small utility bundles (tree, bind-utils, jq) with state: present—see Install Multiple Packages.
Install and start Apache
Install the package, then hand off to the service module:
---
- name: Install and start httpd
hosts: lab
become: true
gather_facts: false
tasks:
- name: Install httpd package
ansible.builtin.dnf:
name: httpd
state: present
- name: Start and enable httpd
ansible.builtin.service:
name: httpd
state: started
enabled: trueansible-playbook playbooks/install-httpd.ymlSample output:
TASK [Start and enable httpd] **************************************************
changed: [localhost]systemctl is-active httpdSample output:
activeFor reload-only-on-config-change patterns, pair file or template tasks with handlers instead of restarting every play.
Add a custom repository and install a package
Use two tasks in order: yum_repository (add/enable repo with GPG) → dnf install with update_cache: true. Skipping the repo step or metadata refresh is a common cause of “No package available” or stale-metadata errors.
- name: Add demo custom repo file
ansible.builtin.yum_repository:
name: ansible-demo-custom
description: Ansible demo custom repository
baseurl: https://download.rockylinux.org/pub/rocky/10/BaseOS/x86_64/os/
gpgcheck: true
gpgkey: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rockyofficial
enabled: true
file: ansible-demo-custom
- name: Install package from refreshed metadata
ansible.builtin.dnf:
name: jq
state: present
update_cache: trueDisable a repository temporarily
Use enabled: false in yum_repository when the file should stay on disk but DNF should ignore it—see Enable, Disable and Remove Repository Files.
Update packages with check mode and diff mode
Run targeted updates with --check before applying state: latest:
ansible-playbook playbooks/update-check.yml --checkAdd --diff when you want file-level detail for modules that support diff output. Combine with debug playbook techniques when a package task fails mid-play.
Common Mistakes
| Mistake | Fix |
|---|---|
Using command: dnf install |
Use ansible.builtin.dnf for idempotency and changed reporting |
Forgetting become: true |
Add privilege escalation for package and repo file changes |
| Adding repo without GPG check | Set gpgcheck and gpgkey for trusted mirrors |
| Installing package before adding repo | Add or enable the repository first |
| Updating all packages blindly | Use --check, a maintenance window, or named packages |
| Confusing repo enablement with package install | yum_repository for repo files; dnf for packages |
Disabling repo without baseurl |
yum_repository updates need baseurl, metalink, or mirrorlist |
Installing from new repo without update_cache on the install task |
Set update_cache: true on the first dnf install after adding the repo |
Using state: latest on all app packages |
Prefer present or a controlled patch workflow |
Package locks are common when dnf-automatic or another admin job is running. Wait for the lock instead of failing immediately:
- name: Install jq and wait for DNF lock
ansible.builtin.dnf:
name: jq
state: present
lock_timeout: 120Best Practices
| Practice | Why |
|---|---|
Use FQCN ansible.builtin.dnf |
Clear in exams and multi-collection projects |
Pin critical packages with name + state: present |
Avoid surprise upgrades on app servers |
| Separate repo tasks from package tasks | Easier to read and retry |
Run --check before name: "*" updates |
Surfaces upgrade scope early |
| Keep GPG enabled for external mirrors | Matches Rocky security defaults |
Verify with rpm -q and dnf repolist |
Confirms play outcome on the host |
Use package_facts in playbooks |
Better than dnf list: for idempotent checks |
Set lock_timeout on busy hosts |
Waits for DNF lock instead of immediate failure |
| Move repeated package sets into roles | Reuse across plays |
For minimal images, install_weak_deps: false skips weak dependencies on a single install. Use autoremove only when you intend to remove unused dependencies— it can delete packages other roles still expect.
Summary
On Rocky Linux 10, use ansible.builtin.dnf for package and group state (present, absent, latest) and ansible.builtin.yum_repository for /etc/yum.repos.d/ files—including gpgcheck, gpgkey, and enabled. Add repositories before installing from them, use enablerepo / disablerepo for one-off install context, and preview broad updates with --check. Verify with rpm and dnf repolist, and keep package logic in modules instead of raw dnf shell commands.
References
- dnf module — package install, remove, update, groups
- yum_repository module — repo files and GPG settings
- Rocky Linux package management — DNF on Rocky 10
- Ansible modules and ansible-doc — FQCN and module discovery
- Ansible idempotency — why
statematters - Debug Ansible playbooks — when package tasks fail

