# Software Package Management

> Chapter 13 of the Courseiva REDHAT-RHCE curriculum — https://courseiva.com/learn/redhat-rhce/software-package-management

**Official objective:** 5.3 — Manage software packages and repositories using Ansible

## Introduction

The EX294 exam objective 5.3 demands you understand how to manage software packages and repositories through Ansible. This concept solves the fundamental problem of consistently installing, updating, and removing software across hundreds of servers without manual, error-prone work. For an EX294 candidate, mastering this is critical because automation is the entire point of the exam, and package management is the most common task you will automate in the real world.

## The Grocery Store Order Analogy

A grocery store is a central repository where every product has a specific shelf location. If you want to make a specific dinner dish, you first decide what ingredients you need, like tomatoes, pasta, and cheese. You then go to the store, find each ingredient on the correct aisle, pick the exact brand and quantity, and bring it home. This is precisely how a software package manager works on a Linux system. Instead of physically walking aisles, a tool like `yum` or `dnf` contacts a remote server called a repository. That repository is a catalogue of software packages, each with a name, version, and list of dependencies (ingredients that other packages need). When you tell the package manager to install 'tomatoes' (e.g., the 'httpd' web server package), it checks the repository, finds the package, and then looks at its dependency list. It sees that 'httpd' needs 'libssl' (the cheese) and 'glibc' (the pasta) to work. The package manager then fetches all those required packages from the repository at once, exactly like bringing home all the ingredients for your recipe in one trip. It also handles upgrades automatically: when a new version of a package is available in the repository, it updates it just like a store restocking fresher produce. This organised, centralised, and automated process replaces the old, chaotic method of manually downloading and compiling software from random websites, ensuring your system stays consistent, secure, and functional.

The key difference from a grocery store is that a package manager also tracks what you have installed. It knows the exact version of every 'ingredient' on your system. This means it can intelligently resolve conflicts. If one recipe (package) needs 'flour version 2.0' but another needs 'flour version 1.5', the package manager will refuse to install both unless they are compatible or will suggest a compromise. This prevents the system from becoming a broken kitchen with incompatible ingredients.

## Core explanation

Software package management is the process of installing, updating, configuring, and removing software on a computer in a controlled and automated way. Before package managers existed, system administrators had to find software on the internet, download an archive file (often a `.tar.gz` file), manually compile the source code (turn human-readable code into machine-executable binaries), and then copy the resulting files to the correct directories. This was slow, error-prone, and inconsistent. If a piece of software needed another piece of software to run (a dependency), the administrator had to find, compile, and install that dependency first. This was called 'dependency hell'.

A package manager solves this by introducing a centralised system. The core concepts are: 

- **Package:** A single, compressed file that contains all the files needed for a piece of software to run, plus metadata. Metadata includes the package's name, version, description, and its **dependencies** (a list of other packages it needs to work). For example, the `httpd` package (the Apache web server) is a collection of executable files, configuration templates, and documentation. 
- **Repository:** A server or a collection of servers that stores packages and a catalogue (an index) of those packages. This is like the grocery store's inventory system. On Red Hat Enterprise Linux (RHEL), the default repositories are provided by Red Hat and require a subscription. Organisations often create their own private repositories for internal software, which Ansible can then configure. 
- **Dependency Resolution:** The process by which the package manager automatically determines and installs all required supporting packages before installing the requested package. If you install `httpd`, the manager checks if `libssl` (a cryptographic library) is already installed. If not, it installs `libssl` automatically, then `httpd`. This happens recursively, meaning it will find dependencies of dependencies. 
- **Package Manager (Tool):** The actual software that talks to repositories and performs operations on packages. On RHEL 8 and 9, the default tool is `dnf` (Dandified YUM), which replaced the older `yum` (Yellowdog Updater, Modified). Both use the same underlying RPM (RPM Package Manager) format. 

The RPM file format (`.rpm`) is the actual container for the software. You can manually download an RPM file and install it with the `rpm` command, but then you must handle dependencies yourself. The `dnf` tool is a higher-level tool that automatically resolves dependencies by consulting repositories. You can think of `rpm` as a powerful individual screwdriver and `dnf` as a power drill that also knows which screws (dependencies) you need.

Ansible interacts with this system through the `dnf` module (and the legacy `yum` module for older systems). The `dnf` module allows you to write a playbook that says: 'On all web servers, ensure the `httpd` package is present (installed) and at the latest version.' Ansible then connects to each target machine via SSH and executes the equivalent of `dnf install httpd`, but it does so idempotently. 'Idempotence' means that running the playbook multiple times produces the same result. If `httpd` is already installed, Ansible does nothing. If a newer version is available in the repository, it updates. If it is missing, it installs. This guarantees that the state of your servers matches what you defined in code.

The `state` parameter in the Ansible `dnf` module is crucial. It can be: 

- `present`: Ensures the package is installed, but does not update it if it already exists.
- `latest`: Ensures the package is installed and at the most recent version available in the configured repositories.
- `absent`: Ensures the package is removed.
- `removed`: An alias for absent.

Ansible also allows you to manage the repositories themselves using the `yum_repository` module. This module adds, removes, or modifies repository configuration files (located in `/etc/yum.repos.d/`). For example, you can create a custom internal repository named 'internal-tools' that points to your company's private server.

Ansible's power here is that you write the desired end state once and apply it to hundreds of servers. You can also use variables to install different packages on different server types (e.g., `httpd` on web servers, `postgresql` on database servers) using the same playbook structure. This is the foundation of configuration management: defining infrastructure as code, where server configurations are version-controlled, testable, and repeatable. Without this, administrators would have to SSH into each server and run commands manually, leading to configuration drift (servers slowly becoming different over time).

## Real-world context

Consider a medium-sized e-commerce company called 'ShopFast'. They have 50 web servers running a PHP application and 10 database servers running PostgreSQL. The company is migrating to a new version of their PHP framework, which requires PHP 8.1 and several new PHP extensions.

The manual approach would be: a junior administrator logs into each of the 50 web servers, runs `dnf install php8.1 php8.1-mbstring php8.1-xml` on each one, and hopes they don't make a typo or miss a server. This takes hours, is mind-numbingly boring, and is prone to error.

With Ansible, the senior DevOps engineer writes a single playbook. The playbook contains two plays. The first play targets the group 'web_servers' and uses the `dnf` module to install packages. The second play targets 'db_servers' and uses the `dnf` module to install a different set of packages (e.g., `postgresql14-server`).

The steps the engineer takes are: 

1. **Define inventory:** She updates the Ansible inventory file (`/etc/ansible/hosts` or a custom file) to group servers logically. For example: 
   ```
   [web_servers]
   web01.example.com
   web02.example.com
   ...
   
   [db_servers]
   db01.example.com
   db02.example.com
   ```
2. **Write the Playbook:** She creates a file named `update-php.yml`. The playbook uses the `dnf` module with `state: latest` to ensure the packages are at the newest version. She also uses a `yum_repository` module to add the official Remi repository for PHP 8.1, as RHEL's default repositories may not have it. 
3. **Run the Playbook:** She executes `ansible-playbook -i inventory update-php.yml`. Ansible connects to all 60 servers simultaneously (or within a controlled parallelism), checks the current state of each package, and only acts if needed. 
4. **Verification:** Ansible returns a report showing 'changed' for servers that were updated and 'ok' for those already compliant. She can immediately see if any server failed (e.g., due to a network issue or a broken repository URL) and fix the specific problem.

This approach is not just faster. It is auditable. The playbook can be stored in a Git repository. When a change needs to be made, it goes through code review. If a bug is introduced, rolling back means reverting the Git commit and re-running the playbook. The company now has a standardised, reliable, and fast way to manage software across its entire infrastructure. This is the everyday reality for a Red Hat Certified Engineer: writing code that describes the desired state of servers and using Ansible to enforce that state.

## Exam focus

The EX294 exam tests your ability to write Ansible playbooks that manage packages. You will not be asked to manually run `dnf` commands, but you must know the Ansible modules and their parameters perfectly. The exam environment uses RHEL 9, so the `dnf` module is the primary one. However, the legacy `yum` module can also appear, and you should know both exist.

Here is what EX294 specifically tests: 

- **Module names:** You must use the correct module name. For RHEL 9, use `dnf`. For RHEL 8, both `dnf` and `yum` are valid. The exam typically specifies the OS version, so you adapt. Using `yum` when `dnf` is expected may still work (Ansible often handles aliases) but relying on that is risky.
- **State parameter values:** The exam loves to test the difference between `present`, `latest`, and `absent`. A common trap is using `present` when the requirement says 'ensure the package is the latest version'. Another trap is using `absent` when the question says 'remove the package if it exists', which is correct, but they might use `removed` as a synonym.
- **Multiple packages:** You will need to install or remove lists of packages. The `name` parameter accepts either a single string (`'httpd'`) or a YAML list. The correct syntax for a list in a playbook is:
   ```yaml
   - name: Install packages
     ansible.builtin.dnf:
       name:
         - httpd
         - php
         - mariadb-server
       state: present
   ```
- **Managing repositories:** The `yum_repository` module is tested. You must know how to add a repo. Key parameters: `name` (the repo ID, e.g., 'my-custom-repo'), `description`, `baseurl` (the URL of the repo), `gpgcheck` (set to `yes` to verify package signatures), `gpgkey` (URL of the GPG key), and `enabled` (set to `no` to disable the repo without deleting it).
- **Using variables for package names:** They will often ask you to use a variable (defined in `vars` or a `vars_file`) for the package name. This tests your understanding of variable substitution within a module parameter.
- **Idempotence awareness:** Multiple-choice questions may ask what happens if you run a playbook twice. The correct answer is that Ansible only changes what is out of state. If the package is already installed, `dnf` reports no change.

Common traps: 
- Forgetting to use the Fully Qualified Collection Name (FQCN) for modules. While plain `dnf` works in many contexts, the exam expects `ansible.builtin.dnf`.
- Using the `rpm` module incorrectly. The `rpm` module does exist but it is low-level and does not handle dependencies. The exam will almost never test `rpm` directly because it is not the Idempotent, dependency-aware way.
- Confusing `name` and `pkg` as parameter names. The `dnf` module uses `name`. The older `yum` module also used `name`.
- Missing the `state: absent` requirement but failing to also handle dependencies. The exam may ask you to remove a package and all its dependencies. The `dnf` module has an `autoremove` parameter (set to `yes`) that removes unused dependencies, but they rarely test this deeply. Usually, just `state: absent` is enough.

Key definitions to memorise: 
- **Package:** A file (`.rpm`) containing software and metadata.
- **Repository:** A collection of packages and their metadata, accessible via a URL.
- **Dependency:** A package required by another package to function.
- **Idempotence:** Running the operation multiple times results in the same final state.
- **FQCN:** Fully Qualified Collection Name, e.g., `ansible.builtin.dnf`.

## Step by step

1. **Define the desired state in a variable** — Before writing the playbook, decide what software you want on which machines. Create a variable, e.g., `web_packages: ['httpd', 'mod_ssl', 'php']`, stored in a file like `group_vars/web_servers.yml`. This separates configuration from code, making your playbook reusable for different server types.
2. **Write the Ansible task using the dnf module** — In your playbook, create a task that calls `ansible.builtin.dnf`. Set the `name` parameter to the variable (e.g., `{{ web_packages }}`) and `state: present` or `state: latest`. This is where you translate the business requirement (install PHP) into an Ansible instruction. The module handles the underlying `dnf` command and dependency resolution.
3. **Add a handler to restart the service (if needed)** — After installing a service like `httpd`, you often need to start it and enable it to start on boot. Use the `service` module in a separate task, or use a 'notify' directive to call a handler. A handler runs only when a change occurs (e.g., a package was installed or updated), preventing unnecessary restarts.
4. **Optionally configure the repository** — If the package you need is not in the default RHEL repositories (e.g., a custom internal tool), use the `yum_repository` module in a task before the package installation task. Set the `name`, `baseurl`, `description`, `gpgcheck`, and `enabled` parameters. This ensures that when the `dnf` module runs, it can find the package in the newly added repository.
5. **Execute the playbook and verify the output** — Run `ansible-playbook -i inventory your_playbook.yml`. Review the output: 'changed' means the dnf module actually performed an installation or update; 'ok' means the target was already compliant. You can also use `--check` mode to do a dry run without making changes, which is crucial for testing before a production deployment.

## Comparisons

### ansible.builtin.dnf vs ansible.builtin.package

**ansible.builtin.dnf:**
- Specific to Red Hat family (RHEL, CentOS, Fedora).
- Uses the dnf (or yum) command-line tool under the hood.
- Can use RHEL-specific options like `autoremove` and `disablerepo`.

**ansible.builtin.package:**
- Generic across all Linux distributions (uses apt, yum, zypper, etc.).
- Automatically detects the package manager of the target OS.
- May not support all distribution-specific features or options.

### state: present vs state: latest

**state: present:**
- Ensures the package is installed, but does NOT upgrade it.
- If the package is present but a newer version exists, it remains at the current version.
- Useful for pinning a specific version to avoid unexpected upgrades.

**state: latest:**
- Ensures the package is installed AND at the newest version available in the repository.
- Will upgrade the package if a newer version exists.
- Cannot be used with a specific version string in the `name` parameter because it will attempt to upgrade past it.

### yum_repository module vs dnf module

**yum_repository module:**
- Used to add, remove, or modify repository configuration files on target hosts.
- Operates on files in `/etc/yum.repos.d/`.
- Does not install software; it provides the source for the dnf module to use.

**dnf module:**
- Used to install, update, and remove software packages.
- Consults repositories configured by yum_repository or system defaults.
- Resolves dependencies automatically.

### remove a package with state: absent vs remove a package with state: absent and autoremove: yes

**remove a package with state: absent:**
- Only removes the specified package.
- Leaves any dependencies that were installed with it on the system.
- Dependencies become orphaned and take up disk space.

**remove a package with state: absent and autoremove: yes:**
- Removes the specified package and also removes any dependencies no longer needed by other packages.
- Cleans up the system more thoroughly.
- Safer when you want to completely remove a software stack.

### dnf module with a list of packages vs dnf module with a single package name in a loop

**dnf module with a list of packages:**
- More efficient: one task, one connection, one transaction.
- Uses the YAML list syntax for the `name` parameter.
- Dependency resolution is done once for all packages in the list.

**dnf module with a single package name in a loop:**
- Less efficient: each package is installed in a separate task and transaction.
- Requires a `loop` or `with_items` directive.
- Can be more readable if you need to install different packages conditionally.

## Diagram

_Flowchart showing how an Ansible playbook uses the dnf module to install or update packages on a remote target host, including dependency resolution and idempotent behaviour._

```mermaid
flowchart TD
    A[Ansible Playbook] --> B[Task: ansible.builtin.dnf]
    B --> C[Connect via SSH to Target Host]
    C --> D{Check current state?}
    D -- Package not installed --> E[dnf install package]
    D -- Package outdated --> F[dnf update package]
    D -- Package correct --> G[Do nothing - Idempotent]
    E --> H[Resolve dependencies]
    F --> H
    H --> I[Fetch packages from Repository]
    I --> J[Install on Target Host]
    G --> K[Report 'ok']
    J --> K
```

## Common misconceptions

- **Misconception:** Ansible's 'dnf' module can install packages from any URL by directly specifying the .rpm file location. **Reality:** The `dnf` module installs from configured repositories only. To install a local .rpm file, you must use the `rpm` module or the `command` module with `dnf localinstall`, which is not idempotent. (Beginners often think a package manager works like a simple downloader, not understanding it relies on a repository index and metadata to resolve dependencies.)
- **Misconception:** If I set `state: present`, Ansible will automatically update the package to the latest version every time I run the playbook. **Reality:** `state: present` only ensures the package is installed. It will NOT update it if it is already present. To get the latest version, you must use `state: latest`. (This mistake happens because 'present' sounds like 'currently available', but in Ansible terminology it means 'exists, regardless of version'. The nuance between 'present' and 'latest' is one of the most tested distinctions on the exam.)
- **Misconception:** Removing a package with `state: absent` automatically removes all its dependencies that were installed with it. **Reality:** By default, `state: absent` only removes the specified package. Dependencies remain on the system as orphaned packages. You must explicitly use the `autoremove: yes` option with `state: absent` to clean up unused dependencies. (This matches real-world experience: people assume removal is a clean 'undo', but package managers are conservative to avoid breaking other software that might rely on the same dependency.)
- **Misconception:** I can use the `yum_repository` module to create a new repository on the control node itself. **Reality:** The `yum_repository` module manages repository files (`/etc/yum.repos.d/`) on the target nodes, not on the control node (the machine running `ansible-playbook`). Ansible modules always act on the remote target, not on localhost unless explicitly told to run locally. (Newcomers often confuse the 'control node' with the 'target node'. They think the playbook modifies the machine they are typing on, but Ansible is fundamentally a remote automation tool.)
- **Misconception:** The `dnf` module and the `package` module are completely interchangeable and equally good for automating package management. **Reality:** The `package` module is a generic abstraction that works across different OS families (Red Hat, Debian, etc.) but it uses the OS's native package manager (e.g., `dnf` on RHEL, `apt` on Ubuntu). For the EX294 exam, you should use the OS-specific module (`dnf` or `yum`) because it is more reliable and you are specifically tested on Red Hat systems. The `package` module may be slower or have inconsistent behaviour. (This confusion arises from Ansible's versatility. Beginners think 'one module to rule them all' is always better, but for a specific exam targeting RHEL, the specific module is safer and more direct.)

## Key takeaways

- The `dnf` module in Ansible is the primary tool for managing packages on RHEL 9, and it requires the `name` parameter for package names and the `state` parameter for desired status.
- Using `state: present` installs a package but does not update it; only `state: latest` ensures the package is at the most recent version available in the repository.
- Idempotence means running the same playbook multiple times will not change the system once it is in the desired state, and the `dnf` module reports 'ok' instead of 'changed' for packages already compliant.
- To remove a package and its unused dependencies in one step, use both `state: absent` and `autoremove: yes` in the same task.
- Managing repositories is done with the `yum_repository` module, which writes configuration files in `/etc/yum.repos.d/` on target hosts, not on the control node.
- Always use the Fully Qualified Collection Name `ansible.builtin.dnf` for the dnf module to ensure clarity and compatibility with the exam environment.

## FAQ

**What is the exact difference between the `dnf` module and the `package` module in Ansible?**

The `package` module is a generic abstraction that works across multiple operating systems (like Debian, Red Hat, etc.), automatically choosing the right package manager. The `dnf` module is specific to RHEL/CentOS/Fedora systems. For EX294, you must use the `dnf` module because it is the Red Hat standard and more predictable.

**How do I install a specific version of a package with Ansible's dnf module?**

Specify the version in the `name` parameter: `name: httpd-2.4.37`. With `state: present`, it will install that exact version. If `state: latest`, it will upgrade to the latest version regardless of what you specify, so avoid combining `latest` with a version string.

**Why does my playbook fail with 'No package matching' error when I use the dnf module?**

This usually means the package name is misspelled, or the repository containing the package is not enabled on the target system. Check the `yum_repository` tasks or ensure the target machine has a valid subscription to the required Red Hat repositories.

**Can I use the `dnf` module to install a package from a local file like an RPM?**

No. The `dnf` module only works with packages from configured repositories. To install a local .rpm file, you must use the `rpm` module or the `command` module with `dnf localinstall /path/to/file.rpm`. However, this is not recommended because it bypasses dependency resolution.

**What does `autoremove: yes` do in the context of removing a package?**

When you remove a package with `state: absent` and set `autoremove: yes`, Ansible will also remove any packages that were automatically installed as dependencies of that package and are no longer needed by any other installed software. This keeps the system clean.

**How do I update all packages on a system using Ansible?**

Use the `dnf` module with `name: '*'` and `state: latest`. This tells Ansible to update every installed package to the latest version available in the repositories. Be careful in production as this can cause unexpected changes.

## Check your understanding

1. **What Ansible module and state parameter should you use to ensure a package is installed and updated to the newest version, but only if it is already present?**

   Answer: Use the `dnf` module with `state: latest`. This ensures the package is present and at the highest version available in the repositories.

2. **How do you remove a package called 'obsolete-app' and all of its unused dependencies in a single Ansible task?**

   Answer: Use the `dnf` module with `name: obsolete-app`, `state: absent`, and `autoremove: yes`.

3. **What is the parameter name used to specify the URL of a GPG key in the `yum_repository` module?**

   Answer: The parameter is `gpgkey`. It is typically a URL pointing to the key file, used when `gpgcheck` is set to `yes`.

4. **If you use `state: present` on a package that is already installed, will Ansible report 'ok' or 'changed'?**

   Answer: It will report 'ok' because no change was required. This is the essence of idempotence: the system is already in the desired state.

5. **What is the difference between the `name` parameter in the `dnf` module when it is a single string versus a YAML list?**

   Answer: A single string like `httpd` installs one package. A YAML list, like `[httpd, php, mariadb]`, installs all of them in a single transaction, which is more efficient.

---

Interactive version with quiz and diagrams: https://courseiva.com/learn/redhat-rhce/software-package-management
