When automating infrastructure across heterogeneous environments, one of the most common pain points in Ansible is handling package managers across different Linux distributions. Unlike some configuration management tools that abstract package operations behind a unified interface (like SaltStack's pkg
state), Ansible requires explicit module selection:
# Traditional approach with conditional statements
- name: Install NGINX on RedHat
yum:
name: nginx
state: latest
when: ansible_os_family == "RedHat"
- name: Install NGINX on Debian
apt:
name: nginx
state: latest
when: ansible_os_family == "Debian"
We can solve this by creating a custom module that automatically selects the appropriate package manager based on the OS family. Here's how to implement it:
# In library/unified_package.py
from ansible.module_utils.basic import AnsibleModule
def main():
module = AnsibleModule(
argument_spec=dict(
name=dict(type='str', required=True),
state=dict(type='str', default='present')
)
)
pkg_name = module.params['name']
pkg_state = module.params['state']
os_family = module.get_best_parsable_fact(['os_family', 'system'])[0]
if os_family == 'RedHat':
module.run_command(['yum', 'install', '-y', pkg_name], check_rc=True)
elif os_family == 'Debian':
module.run_command(['apt-get', 'install', '-y', pkg_name], check_rc=True)
else:
module.fail_json(msg="Unsupported OS family: {}".format(os_family))
module.exit_json(changed=True, msg="Package {} installed".format(pkg_name))
if __name__ == '__main__':
main()
Once you've created the custom module (saved in a library
directory relative to your playbook), you can use it like this:
- name: Install packages cross-platform
hosts: all
tasks:
- name: Install Apache
unified_package:
name: httpd
state: latest
- name: Install Vim
unified_package:
name: vim-enhanced # RedHat
name: vim-nox # Debian
Some packages have different names across distributions. We can extend our module to handle this:
# In library/smart_package.py
def get_package_name(os_family, base_name):
package_map = {
'httpd': {
'RedHat': 'httpd',
'Debian': 'apache2'
},
'vim': {
'RedHat': 'vim-enhanced',
'Debian': 'vim-nox'
}
}
return package_map.get(base_name, {}).get(os_family, base_name)
# In your playbook
- name: Install web server (httpd on CentOS, apache2 on Ubuntu)
smart_package:
name: httpd
If you prefer not to maintain custom modules, consider using community-maintained roles like geerlingguy.package
from Ansible Galaxy that provide similar functionality:
- hosts: all
roles:
- role: geerlingguy.package
vars:
package_name: httpd
When working with heterogeneous Linux environments in Ansible, package management quickly becomes complex. While RedHat-based systems use yum
or dnf
, Debian-based systems rely on apt
, and Arch Linux uses pacman
. This fragmentation leads to repetitive conditional logic in playbooks.
Ansible actually provides a solution through its generic package
module:
- name: Install httpd using generic package module
package:
name: httpd
state: present
The package
module automatically uses the appropriate package manager based on the ansible_pkg_mgr
fact. However, it has limitations with advanced package operations.
For more control, we can create a custom module. Save this as library/unified_pkg.py
:
from ansible.module_utils.basic import AnsibleModule
def run_module():
module_args = dict(
name=dict(type='str', required=True),
state=dict(type='str', default='present')
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)
result = dict(
changed=False,
original_message='',
message=''
)
pkg_mgr = module.params['name']
state = module.params['state']
os_family = module.params['ansible_os_family']
if os_family == 'RedHat':
module.run_command(['yum', '-y', 'install' if state == 'present' else 'remove', pkg_mgr])
elif os_family == 'Debian':
module.run_command(['apt-get', '-y', 'install' if state == 'present' else 'remove', pkg_mgr])
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()
For a more robust solution, we can use Ansible's facts and include additional package managers:
- name: Unified package installation
block:
- name: Install package (RedHat)
yum:
name: "{{ package_name }}"
state: "{{ package_state }}"
when: ansible_pkg_mgr == "yum" or ansible_pkg_mgr == "dnf"
- name: Install package (Debian)
apt:
name: "{{ package_name }}"
state: "{{ package_state }}"
when: ansible_pkg_mgr == "apt"
- name: Install package (Arch)
pacman:
name: "{{ package_name }}"
state: "{{ package_state }}"
when: ansible_pkg_mgr == "pacman"
vars:
package_name: httpd
package_state: latest
Different distros sometimes use different package names. We can handle this with a mapping dictionary:
- name: Install web server across distros
vars:
pkg_map:
RedHat: httpd
Debian: apache2
Suse: apache2
Arch: apache
package:
name: "{{ pkg_map[ansible_os_family] | default(pkg_map['RedHat']) }}"
state: present
- Always test package operations in check mode first
- Include timeout parameters for package operations
- Consider using roles to organize package installation logic
- Document all package name variations in your team's playbook standards