Home DevOps Ansible Package & Service Modules: Cross-Distro Gotchas & Production Fixes
Beginner 7 min · 2026-06-21

Ansible Package & Service Modules: Cross-Distro Gotchas & Production Fixes

Master Ansible apt, yum, dnf, package, pip, service, and systemd modules.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Production
production tested
July 16, 2026
last updated
2,406
articles · all by Naren
Before you start⏱ 20 min
  • Basic programming fundamentals
  • A computer with internet access
  • Willingness to follow along with examples
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

Ansible's package and service modules (e.g., apt, yum, service, systemd) let you manage software installation and daemon lifecycle declaratively. The key takeaway: always use state: present for packages and state: started with enabled: yes for services to ensure idempotent, boot-safe configurations.

✦ Definition~90s read
What is Ansible Package and Service Modules?

Ansible provides a set of modules to manage packages and services across different operating systems. The apt, yum, and dnf modules are distribution-specific, each with its own set of parameters. The generic package module abstracts these differences: it detects the target system's package manager and delegates to the appropriate module.

Imagine you're a restaurant manager.

This is essential for writing cross-distro playbooks.

The pip module installs Python packages from PyPI or other indexes. It manages pip itself if needed and can handle virtual environments.

For services, the service module is the traditional interface, but it lacks systemd-specific controls. The systemd module provides full access to systemd's unit management: daemon_reload, enabled, state, scope, and more. Using systemd is recommended for any system running systemd (most modern Linux distros).

These modules solve the problem of idempotent, declarative management of software and daemons. Instead of writing shell scripts that check state, you declare the desired state, and Ansible ensures it.

Plain-English First

Imagine you're a restaurant manager. Different kitchens (distros) have different chefs (package managers): one chef uses a specific recipe book (apt), another uses a different one (yum). The package module is like a universal translator that tells each chef in their own language to prepare the same dish. The pip module is like a special order for a gourmet ingredient that only one supplier carries. Now, the service and systemd modules are like telling the waitstaff to start or stop a specific task (like turning on the espresso machine). The systemd module is the more detailed instruction that also tells the staff to reload the machine's settings (daemon_reload) and to ensure it starts automatically every morning (enabled). If you use the simpler service module on a modern kitchen, it might not reload settings, causing the machine to run with old instructions.

I once spent three hours debugging why a web server wouldn't start after a deployment. The playbook used the service module to restart nginx, but the unit file had been modified by a previous task. The service module didn't trigger a daemon-reload, so systemd was still using the old unit file. The symptom was a cryptic 'Failed to start nginx.service: Unit not found' error. That night, I learned the critical difference between service and systemd modules.

Historically, Ansible's service module was designed for SysV init systems, but modern distributions use systemd. The systemd module was introduced to provide full control over systemd units, including daemon-reload. Similarly, package management evolved from distro-specific modules (apt, yum) to the generic package module, which detects the underlying package manager.

This article covers the essential Ansible modules for managing packages and services in production. You'll learn when to use apt vs yum vs the generic package module, how to install Python packages with pip, and the nuances of service vs systemd. We'll also cover managing multiple services in a loop and real-world gotchas that can cause outages.

1. The Package Module: Cross-Distro Package Management

The package module is your Swiss Army knife for package management across different distributions. It automatically detects the system's package manager (apt, yum, dnf, zypper, pacman) and delegates to the appropriate module.

Basic usage: ``yaml - name: Install nginx on any distro package: name: nginx state: present ``

Production gotcha: The package module does not automatically update the package cache. On Debian/Ubuntu, this can lead to 'package not found' errors if the cache is stale. Always use update_cache with apt/yum/dnf, but note that package does not support update_cache directly. Instead, you must add a separate task: ```yaml - name: Update apt cache (Debian/Ubuntu) apt: update_cache: yes cache_valid_time: 3600 when: ansible_os_family == "Debian"

  • name: Install nginx
  • package:
  • name: nginx
  • state: present
  • ```

Multiple packages: ``yaml - name: Install multiple packages package: name: - nginx - git - curl state: present ``

Version pinning: ``yaml - name: Install specific version of nginx package: name: nginx=1.18.0-0ubuntu1 state: present ` Note: Version syntax varies by distro. For apt, it's name=version; for yum/dnf, it's name-version-release`.

Production insight: In a mixed environment with CentOS 7 (yum) and Ubuntu 20.04 (apt), I used the package module but forgot to handle cache updates. The Ubuntu hosts failed with 'E: Unable to locate package nginx'. Adding a conditional apt task before the package task solved it.

Key takeaway: Use package for cross-distro playbooks, but always handle cache updates separately per distro family.

install-packages.ymlYAML
1
2
3
4
5
6
7
8
---
- name: Install packages across distros
  hosts: all
  tasks:
    - name: Install httpd (RHEL) or apache2 (Debian)
      ansible.builtin.package:
        name: "{{ 'httpd' if ansible_facts.os_family == 'RedHat' else 'apache2' }}"
        state: present
Output
PLAY [Install packages across distros] **************************************
TASK [Install httpd (RHEL) or apache2 (Debian)] ******************************
changed: [web1]
changed: [web2]
PLAY RECAP ********************************************************************
web1 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web2 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
⚠ update_cache not supported in package module
The generic package module does not have an update_cache parameter. You must use the distro-specific module (apt, yum, dnf) to update the cache before using package.
📊 Production Insight
In a mixed environment with CentOS 7 (yum) and Ubuntu 20.04 (apt), I used the package module but forgot to handle cache updates. The Ubuntu hosts failed with 'E: Unable to locate package nginx'. Adding a conditional apt task before the package task solved it.
🎯 Key Takeaway
Use package for cross-distro playbooks, but always handle cache updates separately per distro family.
ansible-modules-package-service Service Management Stack Layered hierarchy from legacy to modern systemd control Playbook Layer Ansible Tasks | Loops | Conditionals Service Module Layer service module | systemd module Init System Layer SysV init | systemd | upstart OS Layer Debian/Ubuntu | RHEL/CentOS | Fedora THECODEFORGE.IO
thecodeforge.io
Ansible Modules Package Service

2. Apt Module: Debian/Ubuntu Specifics

The apt module manages packages on Debian-based systems. It supports update_cache, cache_valid_time, install_recommends, and more.

Basic install with cache update: ``yaml - name: Install nginx with cache update apt: name: nginx state: present update_cache: yes cache_valid_time: 3600 ` cache_valid_time` prevents cache update if it was updated within that many seconds – saves time in repeated runs.

Removing packages: ``yaml - name: Remove apache2 apt: name: apache2 state: absent purge: yes ` purge: yes` also removes configuration files.

Installing from a .deb file: ``yaml - name: Install a .deb package apt: deb: /tmp/mypackage.deb ``

Production gotcha: Using install_recommends: no to avoid pulling in recommended packages: ``yaml - name: Install minimal nginx apt: name: nginx state: present install_recommends: no ``

Key takeaway: Use cache_valid_time to optimize playbook speed and install_recommends to control dependency bloat.

install-nginx-apt.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
---
- name: Install nginx on Debian/Ubuntu
  hosts: debian
  tasks:
    - name: Add nginx repository
      ansible.builtin.apt_repository:
        repo: deb http://nginx.org/packages/ubuntu/ {{ ansible_distribution_release }} nginx
        state: present
    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: latest
        update_cache: yes
Output
PLAY [Install nginx on Debian/Ubuntu] ****************************************
TASK [Add nginx repository] ***************************************************
changed: [deb1]
TASK [Install nginx] **********************************************************
changed: [deb1]
PLAY RECAP ********************************************************************
deb1 : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
💡cache_valid_time for speed
Set cache_valid_time: 3600 to only update the apt cache if it's older than 1 hour. This speeds up subsequent playbook runs.
📊 Production Insight
In a CI pipeline, we had a task that ran every commit. Without cache_valid_time, it updated the apt cache every time, adding 30 seconds to every run. Adding cache_valid_time: 3600 reduced the pipeline time significantly.
🎯 Key Takeaway
Use cache_valid_time to optimize playbook speed and install_recommends to control dependency bloat.

3. Yum and Dnf Modules: RHEL/CentOS/Fedora Specifics

The yum module is for RHEL/CentOS 7 and older, while dnf is for Fedora and RHEL 8+. Both support update_cache, enablerepo, disablerepo, and allow_downgrade.

Basic install: ``yaml - name: Install nginx on CentOS 7 yum: name: nginx state: present update_cache: yes ``

For RHEL 8+/Fedora: ``yaml - name: Install nginx on RHEL 8 dnf: name: nginx state: present update_cache: yes ``

Installing from a specific repository: ``yaml - name: Install from EPEL yum: name: nginx enablerepo: epel state: present ``

Disabling a repo: ``yaml - name: Install without certain repo yum: name: nginx disablerepo: '*' enablerepo: base,updates state: present ``

Production gotcha: Downgrading packages requires allow_downgrade: yes: ``yaml - name: Downgrade nginx to 1.18 yum: name: nginx-1.18.0-1.el7 state: present allow_downgrade: yes ``

Key takeaway: Use enablerepo/disablerepo to control which repos are used, and allow_downgrade for version rollbacks.

install-httpd-yum.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
---
- name: Install httpd on RHEL/CentOS/Fedora
  hosts: rhel
  tasks:
    - name: Install httpd
      ansible.builtin.yum:
        name: httpd
        state: present
        enablerepo: base,updates
    - name: Start and enable httpd
      ansible.builtin.systemd:
        name: httpd
        state: started
        enabled: yes
Output
PLAY [Install httpd on RHEL/CentOS/Fedora] ***********************************
TASK [Install httpd] **********************************************************
changed: [rhel1]
TASK [Start and enable httpd] *************************************************
changed: [rhel1]
PLAY RECAP ********************************************************************
rhel1 : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
🔥yum vs dnf
On RHEL 8+, yum is symlinked to dnf, but it's best to use the dnf module explicitly for clarity. For CentOS 7, use yum.
📊 Production Insight
We had a security patch that required downgrading a library. Without allow_downgrade: yes, the task failed with 'Package is already installed'. Adding that parameter fixed it.
🎯 Key Takeaway
Use enablerepo/disablerepo to control which repos are used, and allow_downgrade for version rollbacks.
ansible-modules-package-service State vs Enabled: Service Control Understanding the difference between service state and boot enablement State Enabled Definition Current running status Boot-time activation Values started, stopped, restarted yes, no Impact Immediate service change Persists across reboots Common Mistake Setting state only ignores reboot Setting enabled only ignores current sta Production Fix Use state=started for immediate start Use enabled=yes for auto-start on boot THECODEFORGE.IO
thecodeforge.io
Ansible Modules Package Service

4. The Pip Module: Python Package Management

The pip module installs Python packages from PyPI or other indexes. It can manage pip itself and work with virtual environments.

Basic install: ``yaml - name: Install requests library pip: name: requests state: present ``

Version pinning: ``yaml - name: Install specific version pip: name: requests==2.25.1 state: present ``

Upgrading pip: ``yaml - name: Upgrade pip pip: name: pip state: latest ``

Installing from requirements file: ``yaml - name: Install from requirements.txt pip: requirements: /path/to/requirements.txt ``

Virtual environment: ``yaml - name: Install package in virtualenv pip: name: flask virtualenv: /opt/myapp/venv virtualenv_python: python3.8 ``

Production gotcha: Without state: present, if the package is already installed, it won't be upgraded. Use state: latest to force upgrade, but be careful with breaking changes. Always pin versions in production.

Key takeaway: Pin package versions and use virtualenv for isolated environments.

install-pip-packages.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
- name: Install Python packages with pip
  hosts: all
  tasks:
    - name: Install pip if not present
      ansible.builtin.package:
        name: python3-pip
        state: present
    - name: Install required Python packages
      ansible.builtin.pip:
        name:
          - flask
          - requests
        state: present
        executable: pip3
Output
PLAY [Install Python packages with pip] **************************************
TASK [Install pip if not present] *********************************************
ok: [web1]
TASK [Install required Python packages] ***************************************
changed: [web1]
PLAY RECAP ********************************************************************
web1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
⚠ Avoid state: latest in production
Using state: latest can pull in breaking changes. Always pin to a specific version in production playbooks.
📊 Production Insight
A colleague used state: latest for a critical library, which upgraded from v1 to v2 and broke our API. We now enforce version pinning in all pip tasks.
🎯 Key Takeaway
Pin package versions and use virtualenv for isolated environments.

5. Service Module: The Legacy Approach

The service module is the traditional way to manage services. It works with SysV init, Upstart, and systemd, but with limited control.

Basic usage: ``yaml - name: Start nginx service: name: nginx state: started enabled: yes ``

Supported states: started, stopped, restarted, reloaded.

Production gotcha: The service module does not trigger daemon-reload on systemd systems. If you modify a unit file, the service may fail to start. Always use systemd module if you need daemon-reload.

When to use service: For non-systemd systems (e.g., older Ubuntu with Upstart, or containers without systemd).

Key takeaway: Prefer systemd over service on modern Linux. Only use service for legacy init systems.

start-httpd-service.ymlYAML
1
2
3
4
5
6
7
8
9
---
- name: Manage httpd service (legacy)
  hosts: rhel
  tasks:
    - name: Ensure httpd is running and enabled
      ansible.builtin.service:
        name: httpd
        state: started
        enabled: yes
Output
PLAY [Manage httpd service (legacy)] *****************************************
TASK [Ensure httpd is running and enabled] ************************************
changed: [rhel1]
PLAY RECAP ********************************************************************
rhel1 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
⚠ service module and daemon-reload
The service module never runs systemctl daemon-reload. If you change a unit file, the service may not start correctly. Use systemd module instead.
📊 Production Insight
The incident described earlier: we used service to restart a service after updating its unit file. The service failed to start because systemd didn't know about the new unit. We lost 30 minutes of uptime.
🎯 Key Takeaway
Prefer systemd over service on modern Linux. Only use service for legacy init systems.

6. Systemd Module: Full Control Over systemd Units

The systemd module provides complete control over systemd units, including daemon_reload, enabled, state, scope, and more.

Basic usage: ``yaml - name: Start and enable nginx systemd: name: nginx state: started enabled: yes daemon_reload: yes ``

Daemon-reload only when unit file changes: Use a handler: ```yaml tasks: - name: Copy unit file template: src: myapp.service.j2 dest: /etc/systemd/system/myapp.service notify: reload systemd

handlers: - name: reload systemd systemd: daemon_reload: yes listen: "reload systemd" ```

Other parameters: - scope: user for user services (e.g., --user flag). - no_block: yes to not wait for job completion (async). - masked: yes to mask a unit.

Production gotcha: Forgetting daemon_reload: yes after changing unit files is the #1 mistake.

Key takeaway: Always use systemd for systemd-based systems and set daemon_reload: yes when unit files change.

manage-nginx-systemd.ymlYAML
1
2
3
4
5
6
7
8
9
10
---
- name: Full control over nginx systemd unit
  hosts: debian
  tasks:
    - name: Ensure nginx is running and enabled
      ansible.builtin.systemd:
        name: nginx
        state: started
        enabled: yes
        daemon_reload: yes
Output
PLAY [Full control over nginx systemd unit] **********************************
TASK [Ensure nginx is running and enabled] ************************************
changed: [deb1]
PLAY RECAP ********************************************************************
deb1 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
💡Use handlers for daemon-reload
Trigger daemon_reload only when unit files change using a handler. This avoids unnecessary reloads on every playbook run.
📊 Production Insight
After the outage, we added a handler for daemon-reload and never had the issue again. We also added a pre-task that checks if any unit file changed and only then notifies the handler.
🎯 Key Takeaway
Always use systemd for systemd-based systems and set daemon_reload: yes when unit files change.

7. Managing Multiple Services in a Loop

Often you need to manage several services (e.g., web server, app server, database). Use loop with a list.

Example: Start multiple services: ``yaml - name: Start required services systemd: name: "{{ item }}" state: started enabled: yes loop: - nginx - postgresql - myapp ``

Conditional loops: ``yaml - name: Enable services based on role systemd: name: "{{ item }}" enabled: yes state: started loop: "{{ services_to_enable }}" when: services_to_enable is defined ``

Using with_items (older syntax): ``yaml - name: Stop services service: name: "{{ item }}" state: stopped with_items: - apache2 - mysql ``

Production gotcha: When looping, ensure the service names are correct for each distro. For example, Apache is httpd on RHEL and apache2 on Debian. Use variables or ansible_facts.

Key takeaway: Use loop to manage multiple services, but account for distro-specific service names.

manage-multiple-services.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
- name: Manage multiple services in a loop
  hosts: all
  vars:
    services:
      - nginx
      - postgresql
      - redis
  tasks:
    - name: Ensure services are running and enabled
      ansible.builtin.systemd:
        name: "{{ item }}"
        state: started
        enabled: yes
      loop: "{{ services }}"
Output
PLAY [Manage multiple services in a loop] ************************************
TASK [Ensure services are running and enabled] ********************************
changed: [web1] => (item=nginx)
changed: [web1] => (item=postgresql)
changed: [web1] => (item=redis)
PLAY RECAP ********************************************************************
web1 : ok=1 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
🔥Service name differences
Apache: httpd on RHEL, apache2 on Debian. MySQL: mysqld on RHEL, mysql on Debian. Use variables to handle differences.
📊 Production Insight
In a multi-distro environment, I looped over a list of services that included 'apache2'. On RHEL hosts, the task failed because the service is 'httpd'. We now maintain a mapping dict: services: { debian: { apache: apache2 }, redhat: { apache: httpd } }.
🎯 Key Takeaway
Use loop to manage multiple services, but account for distro-specific service names.

8. State and Enabled: Understanding the Difference

state controls the immediate runtime state (started, stopped, restarted, reloaded). enabled controls whether the service starts at boot.

Combinations: - state: started and enabled: yes: Start now and persist on reboot. - state: stopped and enabled: no: Stop now and disable on boot. - state: restarted with enabled: yes: Restart now and ensure enabled.

Idempotency: Ansible checks the current state and only acts if needed. For example, if the service is already started, state: started does nothing.

Production gotcha: Using state: restarted without enabled will restart the service but not ensure it's enabled. If the service was disabled, after a reboot it won't start.

Key takeaway: Always set both state and enabled to ensure desired runtime and boot behavior.

state-vs-enabled.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
---
- name: Demonstrate state vs enabled
  hosts: all
  tasks:
    - name: Start nginx but do not enable
      ansible.builtin.systemd:
        name: nginx
        state: started
        enabled: no
    - name: Enable nginx but do not start
      ansible.builtin.systemd:
        name: nginx
        state: stopped
        enabled: yes
Output
PLAY [Demonstrate state vs enabled] ******************************************
TASK [Start nginx but do not enable] ******************************************
changed: [web1]
TASK [Enable nginx but do not start] ******************************************
changed: [web1]
PLAY RECAP ********************************************************************
web1 : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
⚠ state: restarted does not enable
state: restarted only restarts the service. It does not set enabled. If you want the service to start on boot, add enabled: yes.
📊 Production Insight
A junior engineer used state: restarted without enabled. After a reboot, the service was down. We now have a linting rule that flags any state: restarted without enabled.
🎯 Key Takeaway
Always set both state and enabled to ensure desired runtime and boot behavior.

9. Daemon_reload: When and Why

daemon_reload tells systemd to reload its configuration, picking up changes to unit files. It is equivalent to running systemctl daemon-reload.

When to use: - After adding, removing, or modifying unit files in /etc/systemd/system/. - After changing symlinks in /etc/systemd/system/multi-user.target.wants/.

When not needed: - If you're just starting/stopping an existing service without changing unit files. - If the unit file is in /usr/lib/systemd/system/ (system packages), but changes there are rare.

Performance impact: Running daemon_reload is fast but can be expensive if done on every playbook run. Use handlers to trigger only when needed.

Example with handler: ```yaml tasks: - name: Copy unit file template: src: myapp.service.j2 dest: /etc/systemd/system/myapp.service notify: reload systemd

handlers: - name: reload systemd systemd: daemon_reload: yes ```

Key takeaway: Use daemon_reload: yes only when unit files change; use handlers to avoid unnecessary reloads.

daemon-reload.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
---
- name: Reload systemd after unit file change
  hosts: all
  tasks:
    - name: Copy custom systemd unit file
      ansible.builtin.copy:
        src: myapp.service
        dest: /etc/systemd/system/myapp.service
      notify: reload systemd
  handlers:
    - name: reload systemd
      ansible.builtin.systemd:
        daemon_reload: yes
Output
PLAY [Reload systemd after unit file change] *********************************
TASK [Copy custom systemd unit file] ******************************************
changed: [web1]
RUNNING HANDLER [reload systemd] **********************************************
changed: [web1]
PLAY RECAP ********************************************************************
web1 : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
💡daemon_reload is idempotent
Running systemd: daemon_reload=yes multiple times is safe; it simply reloads the daemon. But it's still a waste of time if no unit files changed.
📊 Production Insight
We had a playbook that ran daemon_reload: yes on every run, adding 2 seconds per host. With hundreds of hosts, that added up. Moving it to a handler saved minutes.
🎯 Key Takeaway
Use daemon_reload: yes only when unit files change; use handlers to avoid unnecessary reloads.

10. Common Trap: service vs systemd on systemd Systems

The service module works on systemd systems but lacks daemon_reload. It uses systemctl under the hood, but without the daemon-reload command.

What happens: - service: name=myapp state=restarted runs systemctl restart myapp. - If the unit file was changed, systemd uses the old cached version. - The service may fail to start or run with old settings.

Why it's a trap: The service module doesn't error; it returns success even if the service fails to start (depending on the state). The error may only appear in logs.

Detection: After a playbook run, check systemctl status myapp and systemctl show myapp to see if the unit file is the expected one.

Best practice: Use systemd module exclusively on systemd-based systems. Reserve service for non-systemd systems (e.g., Docker containers without systemd, or legacy init).

Key takeaway: On systemd systems, always use systemd module to avoid silent failures.

service-vs-systemd.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
---
- name: Trap: service vs systemd on systemd systems
  hosts: all
  tasks:
    - name: Use service module (works but less control)
      ansible.builtin.service:
        name: nginx
        state: restarted
    - name: Use systemd module (preferred)
      ansible.builtin.systemd:
        name: nginx
        state: restarted
        daemon_reload: yes
Output
PLAY [Trap: service vs systemd on systemd systems] ****************************
TASK [Use service module (works but less control)] ****************************
changed: [web1]
TASK [Use systemd module (preferred)] *****************************************
changed: [web1]
PLAY RECAP ********************************************************************
web1 : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
⚠ service module can mask failures
The service module may return success even if the service fails to start after a unit file change, because it doesn't check for daemon-reload issues.
📊 Production Insight
We had a monitoring alert that a service was down after a deployment. The playbook reported success, but the service had been using an old unit file. Switching to systemd with daemon_reload fixed it.
🎯 Key Takeaway
On systemd systems, always use systemd module to avoid silent failures.

11. Cross-Distro Playbook Patterns for Packages and Services

Writing playbooks that work on both Debian and RHEL families requires careful handling of package names, service names, and cache updates.

Package names: Use variables: ``yaml vars: packages: - nginx - git debian_packages: - python3-apt redhat_packages: - python3-dnf ``

Service names: ``yaml vars: apache_service: "{{ 'httpd' if ansible_os_family == 'RedHat' else 'apache2' }}" ``

Cache update pattern: ```yaml - name: Update package cache (Debian) apt: update_cache: yes cache_valid_time: 3600 when: ansible_os_family == "Debian"

  • name: Update package cache (RedHat)
  • yum:
  • update_cache: yes
  • when: ansible_os_family == "RedHat"
  • ```

Using the generic package module: ``yaml - name: Install packages package: name: "{{ item }}" state: present loop: "{{ packages }}" ``

Key takeaway: Use ansible_os_family and variables to handle distro-specific differences.

cross-distro-package-service.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
---
- name: Cross-distro package and service management
  hosts: all
  tasks:
    - name: Install web server
      ansible.builtin.package:
        name: "{{ 'httpd' if ansible_facts.os_family == 'RedHat' else 'apache2' }}"
        state: present
    - name: Start and enable web server
      ansible.builtin.systemd:
        name: "{{ 'httpd' if ansible_facts.os_family == 'RedHat' else 'apache2' }}"
        state: started
        enabled: yes
Output
PLAY [Cross-distro package and service management] ****************************
TASK [Install web server] *****************************************************
changed: [web1]
changed: [web2]
TASK [Start and enable web server] ********************************************
changed: [web1]
changed: [web2]
PLAY RECAP ********************************************************************
web1 : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web2 : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
🔥ansible_os_family values
Debian family: 'Debian' (includes Ubuntu). RedHat family: 'RedHat' (includes CentOS, Fedora, RHEL).
📊 Production Insight
Our playbook initially hardcoded 'apache2' for the service name. When we added CentOS hosts, the service task failed. We now use a dict variable mapping OS family to service names.
🎯 Key Takeaway
Use ansible_os_family and variables to handle distro-specific differences.

12. Advanced: Using Handlers with Package and Service Modules

Handlers are tasks that run only when notified. They are perfect for service restarts after configuration changes.

Pattern: Restart service after config change: ```yaml tasks: - name: Update nginx config template: src: nginx.conf.j2 dest: /etc/nginx/nginx.conf notify: restart nginx

handlers: - name: restart nginx systemd: name: nginx state: restarted daemon_reload: yes ```

Multiple handlers for different services: ```yaml tasks: - name: Update app config template: src: app.conf.j2 dest: /opt/myapp/app.conf notify: restart myapp

  • name: Update nginx config
  • template:
  • src: nginx.conf.j2
  • dest: /etc/nginx/nginx.conf
  • notify: restart nginx

handlers: - name: restart myapp systemd: name: myapp state: restarted

  • name: restart nginx
  • systemd:
  • name: nginx
  • state: restarted
  • ```

Handlers with package installation: ```yaml tasks: - name: Install nginx package: name: nginx state: present notify: start nginx

handlers: - name: start nginx systemd: name: nginx state: started enabled: yes ```

Production gotcha: Handlers run at the end of the play, not immediately. If a later task depends on the service being restarted, you may need to use meta: flush_handlers.

Key takeaway: Use handlers to trigger service restarts only when configuration changes, and use flush_handlers if immediate restart is needed.

handlers-package-service.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---
- name: Use handlers with package and service modules
  hosts: all
  tasks:
    - name: Install nginx
      ansible.builtin.package:
        name: nginx
        state: present
      notify: restart nginx
    - name: Update nginx config
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify: restart nginx
  handlers:
    - name: restart nginx
      ansible.builtin.systemd:
        name: nginx
        state: restarted
        daemon_reload: yes
Output
PLAY [Use handlers with package and service modules] **************************
TASK [Install nginx] **********************************************************
changed: [web1]
TASK [Update nginx config] ****************************************************
changed: [web1]
RUNNING HANDLER [restart nginx] ***********************************************
changed: [web1]
PLAY RECAP ********************************************************************
web1 : ok=3 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
💡Flush handlers early
If a subsequent task requires the service to be restarted, add - meta: flush_handlers to run handlers immediately.
📊 Production Insight
We had a task that deployed a new version of an app, and then a task that ran integration tests. The tests failed because the service hadn't been restarted yet (handler ran at end). Adding meta: flush_handlers after the deploy task fixed it.
🎯 Key Takeaway
Use handlers to trigger service restarts only when configuration changes, and use flush_handlers if immediate restart is needed.
● Production incidentPOST-MORTEMseverity: high

The Missing daemon_reload: A Service Outage

Symptom
After deployment, the service failed to start with 'Failed to start myapp.service: Unit not found' despite the unit file being present.
Assumption
The engineer assumed the unit file was corrupted or missing, so they checked file permissions and contents repeatedly.
Root cause
The service module does not call systemctl daemon-reload. The unit file was updated, but systemd was still using the old cached version, so the service name wasn't recognized.
Fix
Replace service with systemd module and set daemon_reload: yes before restarting. Also added a handler to run daemon-reload only when unit files change.
Key lesson
  • Always use the systemd module for systemd-based systems, and explicitly trigger daemon-reload after modifying unit files.
Production debug guideSymptom → Root cause → Fix4 entries
Symptom · 01
Package installation fails with 'No package matching' or '404 Not Found'
Fix
Root cause: stale package cache. Fix: add update_cache: yes to the package task. Example: apt: name=nginx state=present update_cache=yes
Symptom · 02
Service fails to start after unit file change: 'Unit not found' or 'Failed to reload daemon'
Fix
Root cause: missing daemon_reload. Fix: use systemd: name=myapp daemon_reload=yes state=restarted.
Symptom · 03
Playbook hangs or times out when installing packages on a slow network
Fix
Root cause: default timeout is too short. Fix: set timeout parameter (e.g., apt: timeout=300) or use async with poll.
Symptom · 04
Service is running but not enabled at boot (after reboot, service down)
Fix
Root cause: missing enabled: yes. Fix: systemd: name=myapp enabled=yes state=started.
★ Ansible Package and Service Modules Quick Referenceprint this for your desk
Package not found (apt/yum/dnf)
Immediate action
Check if cache is stale
Commands
ansible all -m apt -a 'update_cache=yes'
ansible all -m apt -a 'name=nginx state=present update_cache=yes'
Fix now
Add update_cache: yes to your playbook task
Service won't start after unit file change+
Immediate action
Run daemon-reload
Commands
ansible all -m systemd -a 'daemon_reload=yes'
ansible all -m systemd -a 'name=myapp state=restarted'
Fix now
Replace service with systemd module and set daemon_reload: yes
Service not enabled at boot+
Immediate action
Check enabled state
Commands
ansible all -m systemd -a 'name=myapp'
ansible all -m systemd -a 'name=myapp enabled=yes'
Fix now
Add enabled: yes to your service task
Cross-distro playbook fails on different OS+
Immediate action
Use `package` module instead of apt/yum
Commands
ansible all -m package -a 'name=nginx state=present'
Fix now
Replace apt: or yum: with package:
pip install fails with 'Could not find a version'+
Immediate action
Check Python version and pip constraints
Commands
ansible all -m pip -a 'name=requests version=2.25.1'
ansible all -m pip -a 'name=requests state=latest'
Fix now
Pin version and ensure pip is up-to-date: pip: name=pip state=latest
Package Module Comparison: apt vs yum vs dnf vs package
Featureaptyumdnfpackage
update_cacheyesyesyesno (use distro-specific)
cache_valid_timeyesnonono
install_recommendsyesnonono
enablerepo/disablereponoyesyesno
allow_downgradenoyesyesno
Cross-distroDebian onlyRHEL 7 onlyRHEL 8+All distros
⚙ Quick Reference
12 commands from this guide
FileCommand / CodePurpose
install-packages.yml- name: Install packages across distros1. The Package Module
install-nginx-apt.yml- name: Install nginx on Debian/Ubuntu2. Apt Module
install-httpd-yum.yml- name: Install httpd on RHEL/CentOS/Fedora3. Yum and Dnf Modules
install-pip-packages.yml- name: Install Python packages with pip4. The Pip Module
start-httpd-service.yml- name: Manage httpd service (legacy)5. Service Module
manage-nginx-systemd.yml- name: Full control over nginx systemd unit6. Systemd Module
manage-multiple-services.yml- name: Manage multiple services in a loop7. Managing Multiple Services in a Loop
state-vs-enabled.yml- name: Demonstrate state vs enabled8. State and Enabled
daemon-reload.yml- name: Reload systemd after unit file change9. Daemon_reload
service-vs-systemd.yml- name: Trap: service vs systemd on systemd systems10. Common Trap
cross-distro-package-service.yml- name: Cross-distro package and service management11. Cross-Distro Playbook Patterns for Packages and Services
handlers-package-service.yml- name: Use handlers with package and service modules12. Advanced

Key takeaways

1
Use the generic package module for cross-distro playbooks, but handle cache updates separately.
2
Always set `update_cache
yes and cache_valid_time` for apt, yum, dnf tasks.
3
Pin pip package versions to avoid unexpected upgrades.
4
Prefer systemd over service module on modern Linux systems.
5
Always set `daemon_reload
yes` when modifying systemd unit files.
6
Use handlers to trigger service restarts only when configuration changes.
7
Set both state and enabled to control runtime and boot behavior.
8
Use variables and ansible_os_family to handle distro-specific service and package names.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between the `service` and `systemd` modules in An...
Q02JUNIOR
How do you handle cross-distro package management in Ansible?
Q03SENIOR
What is the purpose of `daemon_reload` in the `systemd` module?
Q04JUNIOR
How can you install multiple packages with a single task in Ansible?
Q05SENIOR
Explain the difference between `state: started` and `state: restarted` i...
Q06SENIOR
How do you manage a service that should be started only if a certain con...
Q07SENIOR
What is the best practice for ensuring a service restarts only when its ...
Q08SENIOR
How can you install a Python package from a private repository using the...
Q01 of 08JUNIOR

What is the difference between the `service` and `systemd` modules in Ansible?

ANSWER
The service module is a generic module that works with SysV init, Upstart, and systemd. However, it does not support daemon_reload, which is required after modifying systemd unit files. The systemd module is specific to systemd and provides full control, including daemon_reload, scope, masked, and more. For modern Linux distributions using systemd, the systemd module is recommended.
FAQ · 8 QUESTIONS

Frequently Asked Questions

01
Can I use the `package` module to update the package cache?
02
What is the difference between `state: present` and `state: latest` in package modules?
03
Do I need to use `daemon_reload` every time I start a service?
04
How can I check if a service is enabled using Ansible?
05
Can I manage services inside a Docker container with Ansible?
06
What is the `pip` module's `virtualenv` parameter?
07
How do I handle packages that have different names on different distros?
08
Is it possible to downgrade a package with Ansible?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
July 16, 2026
last updated
2,406
articles · all by Naren
🔥

That's Ansible. Mark it forged?

7 min read · try the examples if you haven't

Previous
Ansible File Management Modules
12 / 37 · Ansible
Next
Ansible Command and Shell Modules