Handlers¶
Handlers are tasks that run only when notified by another task, and only once per play even if notified multiple times. The classic use case: "restart nginx if any of these configuration changes happen".
The basic pattern¶
- hosts: lab
become: true
tasks:
- name: copy main nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: reload nginx
- name: copy site config
ansible.builtin.template:
src: site.conf.j2
dest: /etc/nginx/sites-available/site.conf
notify: reload nginx
- name: enable site
ansible.builtin.file:
src: /etc/nginx/sites-available/site.conf
dest: /etc/nginx/sites-enabled/site.conf
state: link
notify: reload nginx
handlers:
- name: reload nginx
ansible.builtin.systemd:
name: nginx
state: reloaded
If any one of the three tasks reports changed, reload nginx runs once at the end of the play.
When handlers run¶
By default: at the end of the play, after all tasks complete. Notifications queue up; the handler section runs them in the order they appear in the handlers: block, regardless of which task triggered them.
You can force handlers earlier with flush_handlers::
tasks:
- name: copy config
ansible.builtin.template: ...
notify: reload nginx
- name: force the reload right now
ansible.builtin.meta: flush_handlers
- name: now check that the service responds
ansible.builtin.uri:
url: http://localhost
status_code: 200
flush_handlers is rare in practice. It's mostly useful in roles where you need a service restarted before subsequent tasks in the same role.
Multiple notifies, single run¶
- name: a
ansible.builtin.copy: ...
notify:
- reload nginx
- restart varnish
- name: b
ansible.builtin.copy: ...
notify: reload nginx
If both a and b are changed, reload nginx runs once, then restart varnish runs once. The handler runs at most once per play even when notified by N tasks.
Listen — many notifiers to one handler¶
When you have several tasks that each ought to trigger "restart everything", give them a shared listen topic:
tasks:
- name: a
ansible.builtin.copy: ...
notify: app reconfig
- name: b
ansible.builtin.copy: ...
notify: app reconfig
handlers:
- name: restart nginx
ansible.builtin.systemd:
name: nginx
state: restarted
listen: app reconfig
- name: restart varnish
ansible.builtin.systemd:
name: varnish
state: restarted
listen: app reconfig
notify: app reconfig triggers both handlers (because both listen on that topic).
When handlers DON'T run¶
- The triggering task wasn't
changed. Ifapt: name=nginx state=presentruns and nginx is already installed, nochanged, no notification. - Earlier tasks failed. A play that fails on a host typically skips the handlers for that host. Override with
force_handlers: true:
--list-tasks/--check. Dry runs go through the motions but don't actually trigger handler runs in some cases.--check --diffruns handlers in check mode (which most handlers tolerate fine:systemd: state: reloadedreports "would reload" without doing it).
Ordering guarantees¶
Handlers run in the order they appear in handlers:, NOT in the order they were notified. If A is defined before B in handlers, and B was notified first, A still runs first.
That's almost always what you want — it means you can put daemon-reload before restart nginx in handler definitions and trust it'll run in that order even if restart nginx got notified first.
handlers:
- name: reload systemd # runs first if both notified
ansible.builtin.systemd:
daemon_reload: true
- name: restart nginx
ansible.builtin.systemd:
name: nginx
state: restarted
Service-restart pattern for config files¶
The canonical pattern for any service whose config you manage:
tasks:
- name: install service
ansible.builtin.apt:
name: foo
state: present
notify: enable foo
- name: configure service
ansible.builtin.template:
src: foo.conf.j2
dest: /etc/foo/foo.conf
validate: '/usr/bin/foo --check-config %s'
notify: restart foo
handlers:
- name: enable foo
ansible.builtin.systemd:
name: foo
enabled: true
state: started
- name: restart foo
ansible.builtin.systemd:
name: foo
state: restarted
The enable foo handler runs once after install. The restart foo runs once if config changed. If both fire in the same play, enable foo runs first (handler-order, not notify-order), which guarantees the service is enabled before we try to restart it.
Reload vs restart¶
state: reloaded— send SIGHUP (or service-specific reload); typically zero downtime. Use for config-file changes.state: restarted— full stop+start; brief downtime. Use when reloading isn't enough (binary upgrade, port change, kernel module reload).
For nginx, sshd, postgres, almost everything: reloaded is the right answer for config changes. Use restarted only when the docs say you have to (e.g. logrotate creating a new file that requires the service to re-open).
Examples from this build¶
# scripts/lab/ansible/playbooks/ssh-hardening.yml
- name: sshd_config — drop a hardened override
ansible.builtin.copy:
dest: /etc/ssh/sshd_config.d/00-hardening.conf
...
notify: reload sshd
handlers:
- name: reload sshd
ansible.builtin.systemd:
name: ssh
state: reloaded
# scripts/lab/ansible/playbooks/bootstrap.yml
- name: journald — keep logs persistent across reboots
ansible.builtin.lineinfile:
path: /etc/systemd/journald.conf
regexp: '^#?\s*Storage='
line: 'Storage=persistent'
notify: restart journald
handlers:
- name: restart journald
ansible.builtin.systemd:
name: systemd-journald
state: restarted
Tips¶
- Always name your handlers descriptively.
notify: reload nginxis better thannotify: handler1. - Define handlers in
handlers:, never intasks:. A handler-style task intasks:runs every time, defeating the point. - Test handlers in check mode.
ansible-playbook play.yml --check --diffwalks through handler runs without actually restarting services. Good sanity check before applying.
Where to go next¶
- Modules -> systemd — the module handlers typically invoke.
- Playbooks — where
notify:fits in task syntax.