How to unify package installation tasks in ansible?
I am starting with ansible and will use it, among others, to install packages on several Linux distros.
I see in the docs that the yum
and apt
commands are separated - what would be the easiest way to unify them and use something like this:
- name: install the latest version of Apache
unified_install: name=httpd state=latest
instead of
- name: install the latest version of Apache on CentOS
yum: name=httpd state=latest
when: ansible_os_family == "RedHat"
- name: install the latest version of Apache on Debian
apt: pkg=httpd state=latest
when: ansible_os_family == "Debian"
I understand that the two package managers are different, but they still have a set of common basic usages. Other orchestators (salt for instance) have a single install command.
Solution 1:
Update: As of Ansible 2.0, there is now a generic & abstracted package
module
Usage Examples:
Now when the package name is the same across different OS families, it's as simple as:
---
- name: Install foo
package: name=foo state=latest
When the package name differs across OS families, you can handle it with distribution or OS family specific vars files:
---
# roles/apache/apache.yml: Tasks entry point for 'apache' role. Called by main.yml
# Load a variable file based on the OS type, or a default if not found.
- include_vars: "{{ item }}"
with_first_found:
- "../vars/{{ ansible_distribution }}-{{ ansible_distribution_major_version | int}}.yml"
- "../vars/{{ ansible_distribution }}.yml"
- "../vars/{{ ansible_os_family }}.yml"
- "../vars/default.yml"
when: apache_package_name is not defined or apache_service_name is not defined
- name: Install Apache
package: >
name={{ apache_package_name }}
state=latest
- name: Enable apache service
service: >
name={{ apache_service_name }}
state=started
enabled=yes
tags: packages
Then, for each OS that you must handle differently... create a vars file:
---
# roles/apache/vars/default.yml
apache_package_name: apache2
apache_service_name: apache2
---
# roles/apache/vars/RedHat.yml
apache_package_name: httpd
apache_service_name: httpd
---
# roles/apache/vars/SLES.yml
apache_package_name: apache2
apache_service_name: apache2
---
# roles/apache/vars/Debian.yml
apache_package_name: apache2
apache_service_name: apache2
---
# roles/apache/vars/Archlinux.yml
apache_package_name: apache
apache_service_name: httpd
EDIT: Since Michael DeHaan (creator of Ansible) has chosen not to abstract out the package manager modules like Chef does,
If you are still using an older version of Ansible (Ansible < 2.0), unfortunately you'll need to handle doing this in all of your playbooks and roles. IMHO this pushes a lot of unnecessary repetitive work onto playbook & role authors... but it's the way it currently is. Note that I'm not saying we should try to abstract package managers away while still trying to support all of their specific options and commands, but just have an easy way to install a package that is package manager agnostic. I'm also not saying that we should all jump on the Smart Package Manager bandwagon, but that some sort of package installation abstraction layer in your configuration management tool is very useful to simplify cross-platform playbooks/cookbooks. The Smart project looks interesting, but it is quite ambitious to unify package management across distros and platforms without much adoption yet... it'll be interesting to see whether it is successful. The real issue is just that package names sometimes tend to be different across distros, so we still have to do case statements or when:
statements to handle the differences.
The way I've been dealing with it is to follow this tasks
directory structure in a playbook or role:
roles/foo
└── tasks
├── apt_package.yml
├── foo.yml
├── homebrew_package.yml
├── main.yml
└── yum_package.yml
And then have this in my main.yml
:
---
# foo: entry point for tasks
# Generally only include other file(s) and add tags here.
- include: foo.yml tags=foo
This in foo.yml
(for package 'foo'):
---
# foo: Tasks entry point. Called by main.yml
- include: apt_package.yml
when: ansible_pkg_mgr == 'apt'
- include: yum_package.yml
when: ansible_pkg_mgr == 'yum'
- include: homebrew_package.yml
when: ansible_os_family == 'Darwin'
- name: Enable foo service
service: >
name=foo
state=started
enabled=yes
tags: packages
when: ansible_os_family != 'Darwin'
Then for the different package managers:
Apt:
---
# tasks file for installing foo on apt based distros
- name: Install foo package via apt
apt: >
name=foo{% if foo_version is defined %}={{ foo_version }}{% endif %}
state={% if foo_install_latest is defined and foo_version is not defined %}latest{% else %}present{% endif %}
tags: packages
Yum:
---
# tasks file for installing foo on yum based distros
- name: Install EPEL 6.8 repos (...because it's RedHat and foo is in EPEL for example purposes...)
yum: >
name={{ docker_yum_repo_url }}
state=present
tags: packages
when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int == 6
- name: Install foo package via yum
yum: >
name=foo{% if foo_version is defined %}-{{ foo_version }}{% endif %}
state={% if foo_install_latest is defined and foo_version is not defined %}latest{% else %}present{% endif %}
tags: packages
- name: Install RedHat/yum-based distro specific stuff...
yum: >
name=some-other-custom-dependency-on-redhat
state=latest
when: ansible_os_family == "RedHat"
tags: packages
Homebrew:
---
- name: Tap homebrew foobar/foo
homebrew_tap: >
name=foobar/foo
state=present
- homebrew: >
name=foo
state=latest
Note that this is awfully repetitive and not D.R.Y., and although some things might be different on the different platforms and will have to be handled, generally I think this is verbose and unwieldy when compared to Chef's:
package 'foo' do
version node['foo']['version']
end
case node["platform"]
when "debian", "ubuntu"
# do debian/ubuntu things
when "redhat", "centos", "fedora"
# do redhat/centos/fedora things
end
And yes, there is the argument that some package names are different across distros. And although there is currently a lack of easily accessible data, I'd venture to guess that most popular package names are common across distros and could be installed via an abstracted package manager module. Special cases would need to be handled anyway, and would already require extra work making things less D.R.Y. If in doubt, check pkgs.org.
Solution 2:
You can abstract out package managers via facts
- name: Install packages
with_items: package_list
action: "{{ ansible_pkg_mgr }} state=installed name={{ item }}"
All you need is some logic that sets ansible_pkg_mgr
to apt
or yum
etc.
Ansible are also working on doing what you want in a future module.
Solution 3:
From Ansible 2.0 there is the new Package
-modul.
http://docs.ansible.com/ansible/package_module.html
You can then use it like your proposal:
- name: install the latest version of Apache
package: name=httpd state=latest
You still have to consider name differences.
Solution 4:
Check out Ansible's documentation on Conditional Imports.
One task to ensure that apache is running even if the service names is different across each OS.
---
- hosts: all
remote_user: root
vars_files:
- "vars/common.yml"
- [ "vars/{{ ansible_os_family }}.yml", "vars/os_defaults.yml" ]
tasks:
- name: make sure apache is running
service: name={{ apache }} state=running