Restart a service only when its config actually changed, using handlers and notify — the pattern that makes Ansible runs both correct and minimal.
Why: when you change a service’s config you must restart it — but only if something actually changed. A handler is a task that runs only when notified, and only once at the end of the play no matter how many tasks notify it. This avoids needless restarts on a run where nothing changed.
task changes nginx.conf ──notify──▶ "restart nginx" handler queued
task changes a second file ─notify──▶ (same handler — still queued once)
end of play ──────────▶ handler runs ONCE
(if no task changed anything, the handler never runs)Why: a task uses notify to name a handler; the handler runs at the end of the play if that task reported changed. The notify string must match the handler’s name exactly. This is the canonical "update config, then restart" pattern.
- name: Configure and run nginx
hosts: local
become: true
tasks:
- name: Deploy nginx config
ansible.builtin.template:
src: ./nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart nginx # only fires if the file changed
handlers:
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restartedWhy: a command/shell task always reports "changed", which can wrongly trigger handlers and muddy your runs. changed_when tells Ansible when a task should count as a change (based on its output); failed_when redefines failure. Use them to make raw commands behave like proper modules.
- name: Tame a raw command
hosts: local
tasks:
- name: Check app health
ansible.builtin.command: /usr/local/bin/healthcheck
register: health
changed_when: false # a check never "changes" anything
failed_when: "'OK' not in health.stdout" # define what failure meansNote: aim for playbooks you can run any number of times with the same result. Prefer state-based modules over command/shell. Guard the unavoidable raw commands with creates, when, or changed_when. After writing a play, run it twice — the second run should report changed=0. If it does not, a task is not idempotent, and that is the bug to fix.
✓ use modules that express end state (state: present / started)
✓ guard command/shell with creates / removes / when / changed_when
✓ notify handlers instead of always-restarting
✓ run twice → second run must be changed=0