The problem
You change nginx's config. nginx has to reload to pick it up (2.10: a daemon reads its config at start or on reload). But you do not want a reload on every run - a reload or restart on every run means every run is a small outage, and "every run" might be every 30 minutes from a scheduler. You want: reload if, and only if, the config changed. Ansible's answer is handlers. The same "only when it matters" idea runs through the other tools of this lesson: tags (run only part of a playbook) and check mode (run without changing anything).
What you need to know already: the playbook and template lessons (changed vs ok, template), 2.10 (reload vs restart), 2.9 (reading systemctl status).
Handlers
A handler is a task that only runs when another task notifies it, and only if that task reported changed:
- name: Web servers
hosts: web
become: true
tasks:
- name: Site config
ansible.builtin.template:
src: site.conf.j2
dest: /etc/nginx/sites-available/default
validate: nginx -t -c /etc/nginx/nginx.conf
notify: Reload nginx # the handler's name, exactly
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
The rules:
- A handler runs at the end of the play (after all tasks), not right after the task that notified it. Ten tasks notifying it means one reload.
- Handlers run in the order they are defined in
handlers:, not the order they were notified. - A handler's name must match the
notify:text exactly, or the run stops withThe requested handler 'Reload ngnix' was not found in either the main handlers list nor in the listening handlers list. listen: web config changedon handlers lets several of them answer one topic:notify: web config changed.ansible.builtin.meta: flush_handlersas a task runs the notified handlers now · useful when a later task needs the reloaded service (a health check, say).
The output marks them RUNNING HANDLER:
$ cd ~/oncall-lab/labs/1a-ansible/try/handlers
$ ansible-playbook web.yml
PLAY [Web servers] *************************************************************
TASK [Gathering Facts] *********************************************************
ok: [web-1]
ok: [web-2]
TASK [Install nginx] ***********************************************************
changed: [web-1]
changed: [web-2]
TASK [Site config] *************************************************************
changed: [web-1]
changed: [web-2]
TASK [nginx runs] **************************************************************
ok: [web-1]
ok: [web-2]
RUNNING HANDLER [Reload nginx] *************************************************
changed: [web-1]
changed: [web-2]
PLAY RECAP *********************************************************************
web-1 : ok=5 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web-2 : ok=5 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
When a handler does not run
The trap that bites every team once: a task changed the config and notified the handler, then a later task failed on that host. A failed host stops - and its pending handlers are dropped. The config file is new, nginx still runs the old one. Worse, on the next run the template task is ok (the file is already right), so nothing notifies the handler ever again. The change is stuck.
Three tools:
force_handlers: trueon the play (or--force-handlers): run notified handlers even on hosts that failed.meta: flush_handlersright after the config tasks, so the reload happens before the risky part.- A one-off fix: reload by hand (
ansible web -b -m service -a 'name=nginx state=reloaded').
An incident later in the chapter is exactly this.
changed_when and failed_when
Every task can decide for itself what "changed" and "failed" mean, from its registered result:
- name: Apply the migrations
ansible.builtin.command: /opt/app/bin/migrate
register: mig
changed_when: "'Applied' in mig.stdout" # changed only if it applied something
failed_when: mig.rc not in [0, 3] # rc 3 = "nothing to do" for this tool
notify: Restart app
changed_when: false is the standard line for commands that only read (nginx -t, cat, systemctl is-active). Without it, every run reports changed - and if such a task notifies a handler, every run restarts the service.
Tags
Tags label tasks so you can run part of a playbook:
- name: Install nginx
ansible.builtin.apt:
name: nginx
tags: [packages]
- name: Site config
ansible.builtin.template: { src: site.conf.j2, dest: /etc/nginx/sites-available/default }
tags: [config]
--tags config only tasks tagged config (plus those tagged always)
--skip-tags packages everything except packages
--list-tags what tags exist
--tags never,debug tasks tagged never only run when asked for by name
Two special tags: always runs unless skipped by name (good for gathering facts or a sanity check), never runs only when its other tag is asked for (good for a dangerous "reset" task). Tags on a play, a block or a role apply to everything inside.
$ ansible-playbook web.yml --list-tags
playbook: web.yml
play #1 (web): Web servers TAGS: []
TASK TAGS: [always, config, packages]
$ ansible-playbook web.yml --tags config
PLAY [Web servers] *************************************************************
TASK [Gathering Facts] *********************************************************
ok: [web-1]
ok: [web-2]
TASK [Site config] *************************************************************
ok: [web-1]
ok: [web-2]
TASK [nginx runs] **************************************************************
ok: [web-1]
ok: [web-2]
PLAY RECAP *********************************************************************
web-1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web-2 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Check mode and diff mode
Check mode (--check, -C) runs every task in "what would you do?" mode: modules compare the desired state with the host and report changed without changing anything. Diff mode (--diff, -D) prints what changed (or would change) in files. Together they are the review step before a risky change:
$ sed -i 's/keepalive_timeout 65/keepalive_timeout 30/' templates/site.conf.j2
$ ansible-playbook web.yml --check --diff -l web-1
PLAY [Web servers] *************************************************************
TASK [Gathering Facts] *********************************************************
ok: [web-1]
TASK [Install nginx] ***********************************************************
ok: [web-1]
TASK [Site config] *************************************************************
changed: [web-1]
--- before: /etc/nginx/sites-available/default
+++ after: /home/learner/oncall-lab/labs/1a-ansible/try/handlers/templates/site.conf.j2
@@ -4,7 +4,7 @@
server_name web-1;
root /var/www/html;
index index.html;
- keepalive_timeout 65;
+ keepalive_timeout 30;
location / {
try_files $uri $uri/ =404;
TASK [nginx runs] **************************************************************
ok: [web-1]
RUNNING HANDLER [Reload nginx] *************************************************
changed: [web-1]
PLAY RECAP *********************************************************************
web-1 : ok=5 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
The diff shows the exact lines, the task says changed, the handler shows as notified
- and nothing on web-1 changed. Things to know about check mode:
- command and shell are skipped (
skipping:): Ansible cannot know what they would do. A task that only reads can opt in withcheck_mode: false(run it even in check mode). - A registered result from a skipped task has no
stdout; a later task usingresult.stdoutfails in check mode. Guard it withwhen: result.stdout is defined. - A task with
check_mode: truealways runs in check mode, even in a normal run. - Sensitive files:
diff: falseon the task keeps their content out of the output.
Two more ways to run part of a playbook, for long ones:
--start-at-task "Site config" skip everything before that task
--step ask before every task: (N)o/(y)es/(c)ontinue
What you can now do
- Notify handlers from tasks, and know when they run, in which order, and when they do not run at all (
force_handlers,flush_handlers). - Make tasks honest with
changed_when/failed_when. - Tag tasks and run or skip parts of a playbook, including
alwaysandnever. - Review a change with
--check --diffbefore running it for real, knowing what check mode cannot predict.