Mastering RPM Package Deployment and Lifecycle Management with Ansible

The deployment of software on Red Hat-based Linux distributions relies fundamentally on the RPM (Red Hat Package Manager) format. While the modern ecosystem predominantly utilizes centralized repositories for software distribution, there remain numerous critical scenarios where standalone .rpm files are the primary vehicle for delivery. These include proprietary vendor packages, internal corporate tools developed in-house, and third-party software that does not maintain a public repository. Automating the installation, upgrading, and management of these files requires more than just a simple command execution; it necessitates a robust understanding of dependency resolution, cryptographic verification, and the principle of idempotency. Ansible provides a sophisticated toolset via the dnf and yum modules to orchestrate these processes, ensuring that software is deployed consistently across a fleet of servers without manual intervention.

Fundamental Mechanics of RPM Installation via Ansible

The primary mechanism for installing RPM packages in modern Red Hat-based systems is the dnf module. This module acts as a high-level wrapper around the DNF package manager, providing a declarative way to ensure a package exists on the system. When working with local files, the dnf module is designed to accept a direct file path as the package name.

The technical implementation typically follows this structure:

yaml - name: Install custom application from RPM ansible.builtin.dnf: name: /tmp/myapp-2.5.0-1.el9.x86_64.rpm state: present disable_gpg_check: yes

In this configuration, the name parameter is pointed toward the absolute path of the .rpm file on the remote host. The state: present directive tells Ansible to ensure the package is installed. If the package is already installed and the version matches, Ansible will report no change, maintaining the desired state of the system.

The disable_gpg_check: yes parameter is a critical administrative setting. By default, RPM-based systems verify the GPG (GNU Privacy Guard) signature of a package to ensure its authenticity and integrity. However, internal or custom-built RPMs are often unsigned. Without this parameter, the installation would fail with a signature verification error. By setting this to yes, the administrator explicitly tells the system to skip the signature check, allowing the unsigned local package to be installed.

The real-world impact of this approach is the ability to distribute proprietary software across thousands of nodes without needing to maintain a full-scale Yum repository infrastructure. This streamlines the deployment of internal tools and allows for rapid iteration of software versions.

Advanced Version Control: Upgrades and Downgrades

Managing the lifecycle of a package involves moving beyond initial installation to handle updates and, in some cases, reverting to previous stable versions.

Upgrading Packages

Upgrading an application using a local RPM file requires a two-step process: delivering the new binary to the target host and then instructing the package manager to apply the update.

```yaml
- name: Copy new version
ansible.builtin.copy:
src: packages/myapp-3.0.0-1.el9.x8664.rpm
dest: /tmp/myapp-3.0.0-1.el9.x86
64.rpm
mode: '0644'

  • name: Install/upgrade to new version
    ansible.builtin.dnf:
    name: /tmp/myapp-3.0.0-1.el9.x8664.rpm
    state: present
    disable
    gpg_check: yes
    ```

When a newer RPM file is passed to the dnf module with state: present, the package manager recognizes that a newer version of the package is available via the local file and proceeds to upgrade the existing installation. This ensures that the application is brought up to the specified version.

Handling Downgrades

There are scenarios where a new release introduces regressions, requiring a rollback to a known stable version. By default, package managers resist downgrading to prevent accidental data loss or system instability. To override this behavior in Ansible, the allow_downgrade parameter must be used.

yaml - name: Downgrade myapp to stable version ansible.builtin.dnf: name: /tmp/myapp-2.4.0-1.el9.x86_64.rpm state: present allow_downgrade: yes disable_gpg_check: yes

The technical requirement of allow_downgrade: yes explicitly permits the DNF transaction to replace a higher version number with a lower one. This is an essential capability for disaster recovery and stability management in production environments.

Orchestrating a Complete Deployment Workflow

A production-grade deployment is rarely a single-step installation. It involves a sequence of preparatory and post-installation tasks to ensure the application is healthy and configured correctly.

The following table outlines the typical components of a complete RPM deployment pipeline:

Phase Ansible Module Purpose
Security ansible.builtin.rpm_key Imports the GPG key to verify package authenticity.
Delivery ansible.builtin.get_url Downloads the RPM from a remote server to the local /tmp directory.
Preparation ansible.builtin.systemd Stops the existing service to prevent file locking or corruption during upgrade.
Installation ansible.builtin.dnf Executes the actual RPM installation or upgrade.
Configuration ansible.builtin.command Runs post-install setup scripts or configuration defaults.
Activation ansible.builtin.systemd Starts the service and enables it to launch on boot.
Cleanup ansible.builtin.file Removes the installer file to reclaim disk space.
Verification ansible.builtin.uri Performs a health check via HTTP to ensure the app is responding.

An exhaustive example of this workflow is implemented as follows:

```yaml
- name: Deploy MyApp
hosts: appservers
become: yes
vars:
app
version: "2.5.0"
apprelease: "1.el9"
rpm
baseurl: "https://releases.example.com/myapp"
tasks:
- name: Import application GPG key
ansible.builtin.rpm
key:
state: present
key: "{{ rpmbaseurl }}/GPG-KEY-myapp"

- name: Download application RPM
  ansible.builtin.get_url:
    url: "{{ rpm_base_url }}/myapp-{{ app_version }}-{{ app_release }}.x86_64.rpm"
    dest: "/tmp/myapp-{{ app_version }}-{{ app_release }}.x86_64.rpm"
    mode: '0644'

- name: Stop application before upgrade
  ansible.builtin.systemd:
    name: myapp
    state: stopped
    ignore_errors: yes

- name: Install application RPM
  ansible.builtin.dnf:
    name: "/tmp/myapp-{{ app_version }}-{{ app_release }}.x86_64.rpm"
    state: present
  register: install_result

- name: Run post-install configuration
  ansible.builtin.command:
    cmd: /opt/myapp/bin/configure --defaults
  when: install_result.changed

- name: Start application
  ansible.builtin.systemd:
    name: myapp
    state: started
    enabled: yes

- name: Clean up RPM file
  ansible.builtin.file:
    path: "/tmp/myapp-{{ app_version }}-{{ app_release }}.x86_64.rpm"
    state: absent

- name: Verify application is running
  ansible.builtin.uri:
    url: http://localhost:8080/health
    return_content: yes
  register: health_check
  retries: 5
  delay: 10
  until: health_check.status == 200

```

In this playbook, the use of variables like app_version and app_release centralizes the version control. This allows an administrator to update the entire fleet by changing a single variable rather than searching and replacing version strings across multiple tasks. The register: install_result combined with when: install_result.changed ensures that the post-install configuration command only runs if the package was actually updated or installed, avoiding unnecessary executions.

Dependency Resolution and Management

One of the most significant advantages of using the dnf module over the lower-level rpm command is automatic dependency resolution. If a local RPM file depends on other packages (e.g., libcurl or openssl), dnf will automatically attempt to resolve these dependencies from the configured system repositories.

Automatic Resolution Example

yaml - name: Install application with automatic dependency resolution ansible.builtin.dnf: name: /tmp/myapp-2.5.0-1.el9.x86_64.rpm state: present disable_gpg_check: yes

If the required dependencies are not present in the standard system repositories, the administrator must first provide a source for them. This is achieved using the yum_repository module to define a custom repository.

Custom Repository Integration

```yaml
- name: Enable custom repository for dependencies
ansible.builtin.yum_repository:
name: myapp-deps
description: MyApp Dependencies
baseurl: https://packages.example.com/el/$releasever/$basearch/
gpgcheck: no
enabled: yes

  • name: Install application RPM
    ansible.builtin.dnf:
    name: /tmp/myapp-2.5.0-1.el9.x8664.rpm
    state: present
    disable
    gpg_check: yes
    ```

By enabling the myapp-deps repository first, dnf has a searchable index of the required libraries, allowing the installation of the local RPM to proceed without manual dependency hunting.

Managing Multiple RPMs in a Single Transaction

When deploying a software suite consisting of multiple interdependent RPMs, installing them one by one can lead to "dependency hell" or temporary broken states where a package is installed but its required sibling is not yet present. To solve this, Ansible can pass a list of packages to the dnf module, which then processes them in a single transaction.

```yaml
- name: Copy all RPM files
ansible.builtin.copy:
src: "packages/{{ item }}"
dest: "/tmp/{{ item }}"
mode: '0644'
loop:
- myapp-2.5.0-1.el9.x8664.rpm
- myapp-libs-2.5.0-1.el9.x86
64.rpm
- myapp-plugin-monitoring-1.1.0-1.el9.x86_64.rpm

  • name: Install all application RPMs
    ansible.builtin.dnf:
    name:
    - /tmp/myapp-2.5.0-1.el9.x8664.rpm
    - /tmp/myapp-libs-2.5.0-1.el9.x86
    64.rpm
    - /tmp/myapp-plugin-monitoring-1.1.0-1.el9.x8664.rpm
    state: present
    disable
    gpg_check: yes
    ```

This approach ensures that the package manager evaluates all three files simultaneously, resolving inter-dependencies between them before committing the changes to the system disk.

Ensuring Idempotency and Precision

Idempotency is the core tenet of Ansible, ensuring that running a playbook multiple times does not change the system after the first successful run. The dnf module is natively idempotent; it checks if the version specified in the RPM is already installed. However, for high-precision environments, an explicit version check using package_facts is recommended.

Explicit Version Verification

By using the package_facts module, an administrator can gather the current state of all installed RPMs and use that data to make a conditional decision.

```yaml
- name: Gather package facts
ansible.builtin.package_facts:
manager: rpm

  • name: Install myapp only if needed
    ansible.builtin.dnf:
    name: /tmp/myapp-2.5.0-1.el9.x8664.rpm
    state: present
    disable
    gpgcheck: yes
    when: >
    'myapp' not in ansible
    facts.packages or
    ansible_facts.packages['myapp'][0].version is version('2.5.0', '<')
    ```

In this logic, the installation only occurs if the package myapp is missing entirely or if the currently installed version is less than 2.5.0. This prevents the dnf module from even attempting to process the local file if the requirement is already met, further optimizing the execution time of the playbook.

Security Best Practices for RPM Deployment

Security is paramount when installing software from local files, as these files can potentially be tampered with during transit.

GPG Key Management

The use of disable_gpg_check: yes should be limited to internal, trusted environments. In production, the best practice is to import the vendor's GPG key.

```yaml
- name: Import vendor GPG key
ansible.builtin.rpm_key:
state: present
key: https://packages.example.com/GPG-KEY-myapp

  • name: Install signed RPM with GPG check
    ansible.builtin.dnf:
    name: /tmp/myapp-2.5.0-1.el9.x86_64.rpm
    state: present
    ```

When the GPG key is imported, the dnf module verifies the digital signature of the RPM against the imported key. If the signature does not match, the installation is aborted, protecting the system from installing corrupted or malicious packages.

File Integrity and Cleanup

When downloading files via ansible.builtin.get_url, it is critical to verify the checksum of the file. This ensures that the file was not corrupted during the download process. Once the installation is complete, the .rpm file should be removed from the /tmp directory.

yaml - name: Clean up RPM file ansible.builtin.file: path: "/tmp/myapp-{{ app_version }}-{{ app_release }}.x86_64.rpm" state: absent

Leaving installer files in /tmp is a poor practice that wastes disk space and can lead to confusion during future audits or deployments.

Comparing Yum and DNF Approaches

In older versions of Ansible and on older RHEL/CentOS systems, the yum module was the standard. While very similar to dnf, there are subtle differences in how they are handled in modern Ansible versions.

The following list highlights the key strategies for using these modules:

  • Use ansible.builtin.dnf for RHEL 8 and later systems.
  • Use yum for RHEL 7 and earlier systems.
  • Use state: latest when dealing with packages hosted in a repository to ensure the newest version is always installed.
  • Use state: present when dealing with specific local RPM files to ensure a specific version is installed.

For those who need to install a list of repository-based packages and ensure they are at the latest version, the with_items (or loop) pattern is most efficient:

yaml - name: Ensure packages are installed and the latest available version yum: name: "{{ item }}" state: latest with_items: - some_package - some_other_package - a_third_package

Decision Logic for RPM Sourcing

The path to installation depends entirely on where the RPM file currently resides. The following logic defines the operational flow:

  • If the RPM is located on a remote URL: Use get_url to download it, verify the checksum, and then use dnf to install.
  • If the RPM is located on the Ansible control node: Use the copy module to move the file to the target host's /tmp directory and then use dnf to install.
  • If the RPM is already present on the target host: Use the direct local path in the dnf module.

To validate these workflows without making actual changes to the system, administrators should use the check mode:

bash ansible-playbook deploy.yml --check

This command allows the user to see which tasks would result in a "changed" status, providing a safe way to dry-run complex RPM deployments.

Conclusion

The automation of RPM installations via Ansible transforms a manual, error-prone process into a reliable, repeatable pipeline. By leveraging the dnf module, administrators can handle the complexities of dependency resolution, version upgrades, and forced downgrades with a few lines of YAML. The integration of GPG key management and package_facts ensures that the deployment is not only efficient but also secure and idempotent. Whether deploying a single standalone package or a complex suite of interdependent RPMs, the combination of strategic file delivery, cryptographic verification, and post-installation health checks provides a comprehensive framework for software lifecycle management on Red Hat-based systems.

Sources

  1. OneUptime Blog: How to Use Ansible to Install Packages from Local RPM Files
  2. Ansible Forum: Help Install/Upgrade a List of RPMs

Related Posts