Introduction

Ansible has platform-specific package modules (apt, yum, dnf) and a generic package module. Choosing the right one depends on whether your playbooks target a single OS family or multiple distributions. This guide compares all four with cross-platform patterns.

Quick Comparison

ModuleOS FamilyPackage ManagerSpecific Features
aptDebian/Ubuntuapt/dpkgupdate_cache, deb, dpkg_options, PPA
yumRHEL/CentOS 7yumenablerepo, disablerepo, security
dnfRHEL 8+/Fedoradnfallowerasing, nobest, modules
packageAnyAuto-detectOnly name + state (lowest common denominator)

apt — Debian/Ubuntu

- name: Update cache and install packages
  ansible.builtin.apt:
    name:
      - nginx
      - python3
      - redis-server
    state: present
    update_cache: true
    cache_valid_time: 3600  # Don't update if < 1 hour old

- name: Install specific version
  ansible.builtin.apt:
    name: nginx=1.24.0-1ubuntu1
    state: present

- name: Install from .deb file
  ansible.builtin.apt:
    deb: /tmp/custom-package.deb

- name: Remove package and purge config
  ansible.builtin.apt:
    name: apache2
    state: absent
    purge: true

- name: Full system upgrade
  ansible.builtin.apt:
    upgrade: dist
    update_cache: true

yum / dnf — RHEL/CentOS/Fedora

# yum (RHEL 7 / CentOS 7)
- name: Install packages
  ansible.builtin.yum:
    name:
      - nginx
      - python3
      - redis
    state: present

- name: Install from specific repo
  ansible.builtin.yum:
    name: nginx
    enablerepo: epel
    state: present

# dnf (RHEL 8+, Fedora, CentOS Stream)
- name: Install packages
  ansible.builtin.dnf:
    name:
      - nginx
      - python3
      - redis
    state: present

- name: Enable module stream
  ansible.builtin.dnf:
    name: "@postgresql:16/server"
    state: present

- name: Install from local RPM
  ansible.builtin.dnf:
    name: /tmp/custom-package.rpm
    state: present
    disable_gpg_check: true

package — Generic Cross-Platform

# Works on any distro — detects package manager automatically
- name: Install packages
  ansible.builtin.package:
    name:
      - git
      - curl
      - wget
    state: present

# ⚠️ Package names must be the same across distros
# This works for git, curl, vim — but NOT for everything

package Limitations

# ❌ package doesn't support distro-specific features
- ansible.builtin.package:
    name: nginx
    update_cache: true    # ERROR — not supported
    enablerepo: epel      # ERROR — not supported

# ❌ Package names differ across distros
# Debian: python3-dev    RHEL: python3-devel
# Debian: libssl-dev     RHEL: openssl-devel
# Debian: redis-server   RHEL: redis

Cross-Platform Patterns

Pattern 1: OS-Specific Variables

# vars/Debian.yml
packages:
  - nginx
  - python3-dev
  - libssl-dev
  - redis-server

# vars/RedHat.yml
packages:
  - nginx
  - python3-devel
  - openssl-devel
  - redis

# tasks/main.yml
- name: Load OS-specific variables
  ansible.builtin.include_vars: "{{ ansible_os_family }}.yml"

- name: Install packages
  ansible.builtin.package:
    name: "{{ packages }}"
    state: present

Pattern 2: Conditional Tasks

- name: Install on Debian/Ubuntu
  ansible.builtin.apt:
    name: "{{ debian_packages }}"
    state: present
    update_cache: true
    cache_valid_time: 3600
  when: ansible_os_family == "Debian"

- name: Install on RHEL/CentOS
  ansible.builtin.dnf:
    name: "{{ redhat_packages }}"
    state: present
  when: ansible_os_family == "RedHat"

Pattern 3: Role with OS Support

# roles/webserver/tasks/main.yml
- name: Include OS-specific tasks
  ansible.builtin.include_tasks: "{{ ansible_os_family | lower }}.yml"

# roles/webserver/tasks/debian.yml
- ansible.builtin.apt:
    name: [nginx, python3-certbot-nginx]
    update_cache: true

# roles/webserver/tasks/redhat.yml
- ansible.builtin.dnf:
    name: [nginx, python3-certbot-nginx]
    enablerepo: epel

Performance Tips

# ✅ Pass a list (single transaction)
- ansible.builtin.apt:
    name: [nginx, redis, python3]
    state: present
# 1 apt call

# ❌ Loop (separate transaction per package — slow!)
- ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop: [nginx, redis, python3]
# 3 apt calls

Common Mistakes

# ❌ Using package for everything (loses features)
- ansible.builtin.package:
    name: nginx
# Can't update cache, enable repos, or use distro features

# ✅ Use package for simple cases, apt/dnf for advanced
- ansible.builtin.apt:
    name: nginx
    update_cache: true  # Only with apt

# ❌ Forgetting update_cache on first run
- ansible.builtin.apt:
    name: some-new-package
# May fail on fresh server with empty cache!

# ✅ Always update cache (with time limit)
- ansible.builtin.apt:
    name: some-new-package
    update_cache: true
    cache_valid_time: 3600

# ❌ Using yum on RHEL 8+ (it's a dnf alias but less correct)
# ✅ Use dnf for RHEL 8+, Fedora, CentOS Stream

Conclusion

apt for Debian/Ubuntu (richest feature set: cache control, PPA, purge). dnf for RHEL 8+/Fedora (module streams, repo control). yum for RHEL 7 only. package for truly cross-platform playbooks where you only need name + state. For production: use OS-specific variables (include_vars) with the native package module — you get full features without duplicating task logic.