Manage Packages and Repositories on Rocky Linux 10 with Ansible

Learn Ansible dnf and yum_repository on Rocky Linux 10—install, remove, update packages, package groups, custom repo files, GPG check, enable or disable repos, and verification with tested playbooks.

Published

Updated

Read time 13 min read

Reviewed byDeepak Prasad

Ansible dnf and yum_repository modules for Rocky Linux 10 package and repository management

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.

NOTE
Rocky Linux 10 currently ships DNF 4.x (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.
NOTE
This chapter is part of the GoLinuxCloud Ansible tutorial (RHCE EX294). Follow along from ~/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.dnf for packages and groups on Rocky Linux 10
  • ansible.builtin.yum_repository for repository files, GPG settings, and enablement
  • enablerepo / disablerepo on package tasks when you need a temporary repo slice
  • Verification with rpm, dnf repolist, and Ansible check mode
  • Common mistakes such as using command: dnf or 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-core on 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 baseurl values

Quick connectivity check:

bash
ansible lab -m ansible.builtin.ping

Sample output:

output
localhost | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

Confirm DNF works on the target before debugging Ansible:

bash
ansible lab -m ansible.builtin.command -a "dnf --version" -b

Sample output:

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 IST

Install 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:

yaml
---
- name: Install single package with dnf
  hosts: lab
  become: true
  gather_facts: false
  tasks:
    - name: Install jq
      ansible.builtin.dnf:
        name: jq
        state: present
bash
ansible-playbook playbooks/install-jq.yml

Sample output:

output
TASK [Install jq] **************************************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

A second run reports ok and changed=0—the package is already installed.

Verify:

bash
rpm -q jq

Sample output:

output
jq-1.7.1-11.el10_2.2.x86_64

Install Multiple Packages

Pass a YAML list to name:

yaml
---
- name: Install multiple packages
  hosts: lab
  become: true
  gather_facts: false
  tasks:
    - name: Install admin tools
      ansible.builtin.dnf:
        name:
          - tree
          - bind-utils
        state: present
bash
ansible-playbook playbooks/install-multi.yml

Sample output:

output
TASK [Install admin tools] *****************************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
bash
rpm -q tree bind-utils

Sample output:

output
tree-2.1.0-8.el10.x86_64
bind-utils-9.18.33-15.el10_2.2.x86_64

Remove Packages

state: absent removes the package:

yaml
---
- name: Remove package
  hosts: lab
  become: true
  gather_facts: false
  tasks:
    - name: Remove tree package
      ansible.builtin.dnf:
        name: tree
        state: absent
bash
ansible-playbook playbooks/remove-tree.yml

Sample output:

output
TASK [Remove tree package] *****************************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
bash
rpm -q tree

Sample output:

output
package tree is not installed

Update Packages

state: latest upgrades named packages to the newest version available from enabled repositories.

yaml
---
- name: Update single package
  hosts: lab
  become: true
  gather_facts: false
  tasks:
    - name: Update jq to latest
      ansible.builtin.dnf:
        name: jq
        state: latest

When 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:

yaml
---
- name: Preview update all packages
  hosts: lab
  become: true
  gather_facts: false
  tasks:
    - name: Preview updating all packages
      ansible.builtin.dnf:
        name: "*"
        state: latest
bash
ansible-playbook playbooks/update-all-check.yml --check

Sample output:

output
TASK [Preview updating all packages] *******************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

changed 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:

yaml
---
- 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: present

List available groups on the target when you need the exact string:

bash
dnf grouplist --available

Sample output:

output
Available Groups:
   Legacy UNIX Compatibility
   Smart Card Support
   ...

The Ansible task reports changed only when the group install actually adds packages.

NOTE
Package groups (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:

yaml
---
- 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: epel

enablerepo 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:

yaml
---
- 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-custom
bash
ansible-playbook playbooks/add-repo.yml

Sample output:

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=0

Rendered file:

Sample output:

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-rockyofficial

When 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:

yaml
- name: Install package from refreshed metadata
  ansible.builtin.dnf:
    name: jq
    state: present
    update_cache: true

The 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
yaml
gpgcheck: true
gpgkey: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rockyofficial

Most 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:

yaml
- 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-custom
bash
ansible-playbook playbooks/disable-repo.yml

Sample output:

output
enabled = 0

Remove the repository definition with state: absent:

yaml
- name: Remove demo custom repo
  ansible.builtin.yum_repository:
    name: ansible-demo-custom
    file: ansible-demo-custom
    state: absent

Install 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.

yaml
- 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: present

For 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:

bash
ansible-playbook playbooks/install-local-rpm.yml

Sample output:

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=0

Verify Installed Packages and Repositories

After package tasks, confirm RPM state on the host:

bash
rpm -q jq httpd

Sample output:

output
jq-1.7.1-11.el10_2.2.x86_64
httpd-2.4.63-13.el10_2.1.x86_64

List enabled repositories:

bash
dnf repolist

Sample output:

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 - Extras

For 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.

yaml
---
- 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"
bash
ansible-playbook playbooks/package-facts.yml

Sample output:

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:

yaml
---
- 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: true
bash
ansible-playbook playbooks/install-httpd.yml

Sample output:

output
TASK [Start and enable httpd] **************************************************
changed: [localhost]
bash
systemctl is-active httpd

Sample output:

output
active

For 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.

yaml
- 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: true

Disable 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:

bash
ansible-playbook playbooks/update-check.yml --check

Add --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:

yaml
- name: Install jq and wait for DNF lock
  ansible.builtin.dnf:
    name: jq
    state: present
    lock_timeout: 120

Best 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 state matters
  • Debug Ansible playbooks — when package tasks fail

Frequently Asked Questions

1. Should I use ansible.builtin.dnf or command dnf in playbooks?

Use ansible.builtin.dnf. It is idempotent, reports changed correctly, and supports check mode where the underlying DNF stack allows it. command dnf always runs the shell command and is harder to make safe for reruns.

2. What is the difference between dnf and yum_repository?

dnf installs, removes, and updates packages. yum_repository manages .repo files under /etc/yum.repos.d/—baseurl, gpgcheck, gpgkey, and enabled state—not individual RPM transactions.

3. Do I need become for package tasks on Rocky Linux?

Yes for most package and repository changes. Installing RPMs and writing repo files under /etc/yum.repos.d/ requires root privileges on the managed host.

4. Can I add a repository and install a package in one play?

Yes, but order matters. Add or enable the repository with yum_repository first, then install the package with dnf in a later task so metadata and GPG settings exist before DNF resolves the package.

5. How do I preview package updates safely?

Run ansible-playbook with --check (and --diff when supported) before applying state latest on many packages. Target specific package names instead of name "*" unless you intend a full update during a maintenance window.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …