Using Ansible handlers in loops
Posted 12.09.2020 · 4 min readRecently I have been dusting of my old ansible playbooks that I use to deploy personal stuff. Everything from side projects like feedhuddler.com to the unifi controller for my home network. In that setup I have a projects role that takes a list of projects and deploy them either running directly on the host with systemd services or docker containers. The systemd setup was using a task to restart the service after changing them. I wanted to change this to be done with a handler instead. However, handlers does not accept arguments. The posts describes a workaround for that problem, enabling dynamic use of handlers.
First let us look at what a handler is and how it differs from a task. The handlers are actions that can be triggered by other tasks. On a task you can define which handlers they should trigger. Very useful for things like "Configuration file was updated so we want to restart an application". A nice feature is that these handlers are triggered at the end of the playbook, and they only run once. Thus, if you have 5 tasks that triggers the same handler it will only run once at the end.
The problem at hand is that handlers does not accept any arguments. Thus, when a task have a loop going through a list of projects it is not straight forward how to be able to keep one handler for all these services. Below is an example setup, we will be looking at single file playbooks to make it easier to follow the changes that would usually be across different files in roles.
---- hosts: all vars: projects: - name: super-project port: 4000 - name: not-so-super-project port: 5000 tasks: - name: 'Create systemd config for {{ item.name }}' loop: '{{ projects }}' template: src: web.service.jinja2 dest: /etc/systemd/system/{{ item.name }}.service owner: root group: root notify: - reload systemd - 'restart {{ item.name }}'
handlers: - name: reload systemd systemd: daemon_reload: yes - name: restart super-project service: name: '{{ item }}' state: restarted - name: restart not-so-super-project service: name: '{{ item }}' state: restarted
This works, but when adding a new project it is necessary to add a new handler. This is not the end of the world. However, I do not touch this code very often and when I do it is usually because I need to change the configuration for a service or add a new service. Thus, it would be nice to be able to that without editing the role every time.
There is another feature in ansible we can use to work around the fact that handlers does not support
arguments. Each task has the ability to store the result in a variable, and if the task has a loop that
variable will be a list of these results. This is done by adding register: variable_name
to the task.
Even though handlers does not work with arguments, it has access to the global variable scope. This means
we can make a handler looping over the result and restart the services. See the highlighted changes below.
---- hosts: all vars: projects: - name: super-project port: 4000 - name: not-so-super-project port: 5000 tasks: - name: Create systemd config for projects loop: '{{ projects }}' template: src: web.service.jinja2 dest: /etc/systemd/system/{{ item.name }}.service owner: root group: root register: systemd_projects notify: - reload systemd - restart systemd projects
handlers: - name: reload systemd systemd: daemon_reload: yes - name: restart systemd projects loop: "{{ systemd_projects.results | selectattr('changed', 'equalto', true) | map(attribute='item') | map(attribute='name') | list }}" service: name: '{{ item }}' state: restarted
The playbook now have one handler for restarting the projects: restart systemd projects
. Let's have a closer look
at the loop for that handler. systemd_projects.results
is the result list from the task above creating the
systemd service files. selectattr('changed', 'equalto', true)
is a filter that will select only the items
in the list that has a property changed set to true. The two next filters map(attribute='item') | map(attribute='name')
maps the items to certain attributes. The last filter list
converts a generator into a list.
This means that this list
[ { "changed": true, "item": { "name": "super-project", "port": 4000 } }, { "changed": false, "item": { "name": "not-so-super-project", "port": 5000 } }]
would turn into
["super-project"]
which in turn would result in the handler running
service: name: super-project state: restarted
In other words, we have a dynamic handler that can restart services based on changes so that we can focus on only the configuration whenever there is a new side-project to deploy.