Ansible Package & Service Modules: Cross-Distro Gotchas & Production Fixes
Master Ansible apt, yum, dnf, package, pip, service, and systemd modules.
20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.
Use the generic package module for cross-distro playbooks; it delegates to apt/yum/dnf automatically.
Always set update_cache: yes for apt/yum/dnf to avoid stale package indexes.
Use pip module with state: present and version pinning to avoid unexpected upgrades.
For systemd services, prefer systemd over service module to control daemon-reload and unit files.
Set daemon_reload: yes when modifying unit files; otherwise changes won't take effect until next reload.
Use enabled: yes to enable service at boot, and state: started to start it immediately.
Loop over multiple services with loop and {{ item }}; pass a list of service names.
Avoid using service module for systemd-based systems; it may not trigger daemon-reload correctly.
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.
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.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.package for cross-distro playbooks, but always handle cache updates separately per distro family.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.
cache_valid_time: 3600 to only update the apt cache if it's older than 1 hour. This speeds up subsequent playbook runs.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.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.
yum is symlinked to dnf, but it's best to use the dnf module explicitly for clarity. For CentOS 7, use yum.allow_downgrade: yes, the task failed with 'Package is already installed'. Adding that parameter fixed it.enablerepo/disablerepo to control which repos are used, and allow_downgrade for version rollbacks.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.
state: latest can pull in breaking changes. Always pin to a specific version in production playbooks.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.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.
service module never runs systemctl daemon-reload. If you change a unit file, the service may not start correctly. Use systemd module instead.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.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.
daemon_reload only when unit files change using a handler. This avoids unnecessary reloads on every playbook run.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.
httpd on RHEL, apache2 on Debian. MySQL: mysqld on RHEL, mysql on Debian. Use variables to handle differences.services: { debian: { apache: apache2 }, redhat: { apache: httpd } }.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: restarted only restarts the service. It does not set enabled. If you want the service to start on boot, add enabled: yes.state: restarted without enabled. After a reboot, the service was down. We now have a linting rule that flags any state: restarted without enabled.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.
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.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.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 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.systemd with daemon_reload fixed it.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.
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.
- meta: flush_handlers to run handlers immediately.meta: flush_handlers after the deploy task fixed it.flush_handlers if immediate restart is needed.The Missing daemon_reload: A Service Outage
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.service with systemd module and set daemon_reload: yes before restarting. Also added a handler to run daemon-reload only when unit files change.- Always use the
systemdmodule for systemd-based systems, and explicitly trigger daemon-reload after modifying unit files.
update_cache: yes to the package task. Example: apt: name=nginx state=present update_cache=yesdaemon_reload. Fix: use systemd: name=myapp daemon_reload=yes state=restarted.timeout parameter (e.g., apt: timeout=300) or use async with poll.enabled: yes. Fix: systemd: name=myapp enabled=yes state=started.ansible all -m apt -a 'update_cache=yes'ansible all -m apt -a 'name=nginx state=present update_cache=yes'update_cache: yes to your playbook taskKey takeaways
package module for cross-distro playbooks, but handle cache updates separately. and cache_valid_time` for apt, yum, dnf tasks.systemd over service module on modern Linux systems.state and enabled to control runtime and boot behavior.ansible_os_family to handle distro-specific service and package names.Common mistakes to avoid
6 patternsUsing `service` module on systemd systems and not triggering daemon-reload after unit file changes
systemd module with daemon_reload: yesForgetting `update_cache: yes` in apt/yum/dnf tasks
update_cache: yes to the taskNot pinning pip package versions
pip: name=requests==2.25.1Using `state: latest` for packages in production
state: present with a specific versionNot setting `enabled: yes` when starting a service
enabled: yes to the service taskHardcoding service names without considering distro differences
ansible_os_familyInterview Questions on This Topic
What is the difference between the `service` and `systemd` modules in Ansible?
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.Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.
That's Ansible. Mark it forged?
9 min read · try the examples if you haven't