Playbooks are written per service : a playbook is a collection of tasks, and eventually associated handers, templates, and files, that are required to properly install and setup a service.
Each playbook has a setup.yml
entry point, which is responsible of including
various necessary tasks to get the service up and running. Typically, this
means installing the service and configuring it.
Here is an entry point example for a tmux playbook :
---
- hosts: debian
tasks:
- include: tasks/install.yml
- include: tasks/configure.yml
Install task handles package installation and adds pair programming scripts scripts in /usr/local/bin
:
---
- name: Tmux | Installs tmux
action: apt pkg=tmux state=latest
action: ping
only_if: ${tmux.install}
tags:
- tmux
- name: Tmux | Adds tmux pairing scripts
action: copy src=files/$item dest=/usr/local/bin/ owner=root group=root mode=0777
only_if: ${tmux.install}
with_items:
- tmux-attach
- tmux-start
- tmux-stop
tags:
- tmux
Here, these actions are only ran if tmux.install is set to "yes" or True somewhere in the group/host vars. More on that later.
Then the setup tasks will push configuration files to selected users :
---
- name: Tmux | Gets installed Tmux version
shell: tmux -V || echo 0.0
only_if: ${tmux.install}
register: tmux_version
- name: Tmux | Adds .tmux.conf for all accounts for version ${tmux_version.stdout}
action: template src=templates/tmux.conf.j2 dest=~${item.username}/.tmux.conf owner=${item.username} group=${item.username}
with_items: ${accounts}
only_if: ${tmux.install} and is_set('$tmux_version')
tags:
- tmux
The first task tries to get the installed tmux version (ince (not so) old tmux version do not have the -V switch, the ||
trick will set the version number to 0.0 if not set). The version is then registered and is used in the template to add certain directives depending on tmux version.
The second task installs a tmux.conf file in several home directories (accounts, defined also somewhere in the group/host vars). The template is generated only if tmux_version is set. This prevents template generation to choke if we run in check mode (which skips the previous shell: action and thus doesn't register tmux_version). Also, tmux
Again, those two tasks are ran only if tmux.install is set to "yes" or True somewhere.
I use ansible with the hash_behaviour=merge
in ansible.cfg
. This means that if you define a hash i a group, a subsequent redefinition of this hash will merge keys of both hashes. If you have identical keys in both, the keys of the last defined hash will override previously defined ones.
For instance, let's say that, by default, we don't want tmux installed. We just need to define the following in group_vars/all
:
tmux:
install: no
However, if we want to install tmux on all debian systems, we could have in group_vars/debian :
tmux:
install: yes
In the same vein, I usually have several keys under a "service namespace" (tmux here). For instance, let's say I have this in the 'all' vars file :
tmux:
install: no
prefix: C-b
I could then, at a refined level (a specific host, or a specific group), have tmux installed with a different key binding just by changing few keys :
tmux:
install: yes
prefix: C-x
As you might guess, I try to mutualize common vars/hash keys at the highest possible level, but each host ends up with some kind of "identity card" consisting in a bunch of variables, for instance :
accounts:
- username: gina
name: "Ski Instructor"
email: "[email protected]"
- username: root
name: "Rooooot"
email: "[email protected]"
- username: caroline
name: "Drop out"
email: "[email protected]"
mysql:
bind_address: "127.0.0.1"
key_buffer: "16M"
keep_backup_count: 30
root_password: "thisisthepass"
users:
- name: db_user1
password: "omg"
privs: "some_database.*:ALL"
network_interfaces_ipv4:
eth0:
address: 192.168.0.2
netmask: 255.255.255.192
gateway: 192.168.0.1
firewall: true
ruby:
install: yes
current: "1.9.3-p374"
deploy_user: "caroline"
ssh:
allow_users:
- root
- git
local_admins:
# this will pull out james' key from top level defined 'admins'
# hash and add it to root's authorized_keys
- james
For instance, DNS servers are defined in group_vars/all
, but coul easily be overriden for this specific host by adding :
# List of nameservers
nameservers:
- 10.12.13.14
- 10.11.12.13
in the hosts playbook.
In order to run specific playbooks on specific hosts, I use a "top level" playbook per host. It's a kind of "pivot table" between hosts and service playbooks. For instance, here is a playbook dedicated for a host :
---
- include: apt/setup.yml
- include: system/setup.yml
- include: ssh/setup.yml
- include: network/setup.yml
- include: bash/setup.yml
- include: git/setup.yml
- include: htop/setup.yml
- include: tmux/setup.yml
- include: postfix/setup.yml
- include: grub/setup.yml
- include: sudo/setup.yml
- include: mysql/setup.yml
- include: ruby/setup.yml
Whan I want this host to be configured according the current policies, I just :
ansible-playbook -l host.example.org host.e.o.yml
Doing it this way ensures that playbook contains absolutely no information about specific hosts or groups (besides OS information of course). This makes the service playbooks adhere to the Single Responsability Principle : they focus on the service only, not on what is required on the target machines.
Of course, this requires extensive templating where cases regarding all hosts are accounted for. However, this is a continuous process. Templates are changed when needs arise and get more and more complete as requirements increase.
However, this might have limitations. Some machines require very specific configurations. Adding more templating might not be the be-all and end-all solution to the problem. I only deployed rather simple services for now, so I don't know how extensible this approach is to more complex services.
This "pivot playbook" could be better expressed like this :
---
- hosts: all
include: apt/setup.yml
include: system/setup.yml
include: ssh/setup.yml
include: network/setup.yml
include: bash/setup.yml
include: sudo/setup.yml
- hosts: http*.example.com
include: git/setup.yml
include: htop/setup.yml
include: tmux/setup.yml
include: postfix/setup.yml
include: grub/setup.yml
- hosts: dbservers*.example.com
include: mysql/setup.yml
- hosts: webapp[1-10].example.com
include: ruby/setup.yml
...
However this is not currently possible IMHO.
Thoughts ?
I have two comments:
Wouldn't it be better not to include the tmux playbook on machines that
don't get tmux, rather than turning it into a play conditional play?
A case could be made for tmux.install==False causing tmux to be purged from
the system, but you don't do that.
And even if you did, I think there are numerous cases where parametrisation
via variables should actually affect the composition of the playbook, not
just the behaviour.
Have a look at
https://groups.google.com/forum/?fromgroups=#!topic/ansible-project/HetiQdNKOj4
and the referenced post to the salt-users list and chime in (on the ansible
list of course).