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.x8664.rpm
mode: '0644'
- name: Install/upgrade to new version
ansible.builtin.dnf:
name: /tmp/myapp-3.0.0-1.el9.x8664.rpm
state: present
disablegpg_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:
appversion: "2.5.0"
apprelease: "1.el9"
rpmbaseurl: "https://releases.example.com/myapp"
tasks:
- name: Import application GPG key
ansible.builtin.rpmkey:
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
disablegpg_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.x8664.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.x8664.rpm
- /tmp/myapp-plugin-monitoring-1.1.0-1.el9.x8664.rpm
state: present
disablegpg_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
disablegpgcheck: yes
when: >
'myapp' not in ansiblefacts.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.dnffor RHEL 8 and later systems. - Use
yumfor RHEL 7 and earlier systems. - Use
state: latestwhen dealing with packages hosted in a repository to ensure the newest version is always installed. - Use
state: presentwhen 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_urlto download it, verify the checksum, and then usednfto install. - If the RPM is located on the Ansible control node: Use the
copymodule to move the file to the target host's/tmpdirectory and then usednfto install. - If the RPM is already present on the target host: Use the direct local path in the
dnfmodule.
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.