Ansible OEL 8 DevOps · OEL 8 · Fundamentals

AnsibleHandlers and notify

Tasks that only run when changes happen — service restarts, cache invalidations, config reloads. The notify pattern, listeners, when handlers actually fire, and how to force them to run mid-play.

A handler is a task defined under handlers: instead of tasks:. It only runs when a regular task notifies it — and even then, it doesn't run immediately. Handlers queue up and execute together at the end of the play, deduplicated.

Tasks (run in order) - name: Tweak my.cnf notify: restart mysql - name: Update sysctl notify: restart mysql - name: Open firewall no notify (no change needed) - name: Add user appuser notify: restart mysql 3 tasks notify the same handler Handler queue restart mysql deduplicated · runs once restart mysqld at end of play 3 changed tasks → 1 mysql restart at the end of the play

Without handlers, every config change forces a service restart even if nothing actually changed. That's noisy, slow, and risky. With handlers:

  • Five tasks edit five files in /etc/mysql/conf.d/ — service restarts once.
  • Nothing changes during a re-run — zero handlers fire.
  • Restart happens at the end, so config validation can run between change and restart.
YAML — handler basics
---
- hosts: databases
  become: true

  tasks:
    - name: Configure my.cnf
      ansible.builtin.template:
        src: my.cnf.j2
        dest: /etc/my.cnf
      notify: restart mysql

    - name: Configure logrotate
      ansible.builtin.copy:
        src: mysql-logrotate
        dest: /etc/logrotate.d/mysql
      notify: restart mysql

  handlers:
    - name: restart mysql
      ansible.builtin.systemd:
        name: mysqld
        state: restarted
YAML — notify list
- name: Update TLS cert
  ansible.builtin.copy:
    src: certs/server.crt
    dest: /etc/ssl/server.crt
  notify:
    - restart mysql
    - restart proxysql
    - reload haproxy

Handlers can subscribe to abstract events using listen — multiple handlers can subscribe to the same event:

YAML — handler listen
  tasks:
    - name: Update OS packages
      ansible.builtin.dnf:
        name: "*"
        state: latest
      notify: kernel updated

  handlers:
    - name: reload sysctl
      ansible.builtin.command: sysctl -p
      listen: kernel updated

    - name: trigger reboot
      ansible.builtin.reboot:
      listen: kernel updated

# notify: kernel updated   →   both handlers run
StageAre handlers run?
End of tasks:Yes — implicit flush
End of roles:Yes — between roles
End of the playYes — final flush
If a task fails after notifyNO — handlers DON'T run by default
You explicitly call meta: flush_handlersYes — runs queued handlers right now

Use meta: flush_handlers to drain the queue before a critical step:

YAML — flush_handlers
  tasks:
    - name: Update my.cnf
      ansible.builtin.template:
        src: my.cnf.j2
        dest: /etc/my.cnf
      notify: restart mysql

    - name: Force restart now (so the next task sees the new config)
      ansible.builtin.meta: flush_handlers

    - name: Verify MySQL is using the new max_connections
      community.mysql.mysql_query:
        query: "SHOW VARIABLES LIKE 'max_connections'"
      register: mc

By default a failed task skips handlers. To still flush queued handlers when a later task fails, run with --force-handlers or set force_handlers: true in ansible.cfg:

BASH / INI — force handlers
# CLI
ansible-playbook site.yml --force-handlers

# ansible.cfg (always-on)
[defaults]
force_handlers = True
YAML — handler library
---
handlers:
  # Service restarts
  - name: restart mysql
    ansible.builtin.systemd:
      name: mysqld
      state: restarted

  - name: reload nginx
    ansible.builtin.systemd:
      name: nginx
      state: reloaded     # graceful — keeps workers running

  # Config reloads via signal
  - name: reload sysctl
    ansible.builtin.command: sysctl -p

  # Cache busts
  - name: clear opcache
    ansible.builtin.command: php -r "opcache_reset();"

  # Notify external systems
  - name: page on-call
    ansible.builtin.uri:
      url: "https://api.pagerduty.com/incidents"
      method: POST
      body_format: json
      body: {{ event: "config-rotated" }}
⚠ Warning: If you want a service restart to happen even when no task changed (e.g. force re-roll on every run), don't use a handler — handlers only fire on changed. Instead use a regular task with changed_when: false and force=true in the systemd module.