Ansible Handlers and Jinja2 Templates for Safe Configuration Changes

A Green Playbook Can Leave the Old Process Running

Configuration deployment is not complete when the file changes. The service must validate and load the new state, but it should not restart when nothing changed. Ansible handlers connect those facts.

- name: Render catalog configuration
  ansible.builtin.template:
    src: catalog.conf.j2
    dest: /etc/catalog/catalog.conf
    owner: root
    group: catalog
    mode: "0640"
    validate: '/usr/local/bin/catalog-check %s'
  notify: restart catalog

handlers:
  - name: Restart catalog service
    listen: restart catalog
    ansible.builtin.service:
      name: catalog
      state: restarted

The temporary rendered file must pass validate before Ansible replaces the live file. The handler runs once at the end of the play, even if several tasks notify the same event.

---

Make Notification Names Stable

Handler names are strings. A case or wording mismatch can leave a notification with no matching handler. Use listen topics as a small public interface so a readable handler name can change without breaking every task.

A handler only runs when its notifying task reports changed. If a command hides change with changed_when: false, or a bad template contains a volatile timestamp and changes every time, restart behavior becomes wrong in opposite directions.

Templates Need Data Contracts

Use explicit defaults only where absence is genuinely valid:

timeout = {{ upstream_timeout | default(15) }}
retries = {{ retry_count | default(3) }}
ciphers = {{ tls_ciphers | join(':') }}

The default filter cannot repair a wrong type. If tls_ciphers is one string instead of a list, join operates on characters. Assert required types and ranges before rendering.

Keep templates deterministic. Avoid current timestamps, unordered input, and host-specific values unless they are part of intended configuration. Use ansible_managed or a fixed provenance comment rather than a changing clock value.

Control Handler Timing

Normally handlers run after all tasks in the play. Use meta: flush_handlers only when later tasks require the new service state. Flushing early creates another failure boundary, so follow it with readiness checks and keep rollback evidence.

The Handler That Never Fired, Templates That Survive Missing Variables, and One Line, One Block, Every Time exercise these exact contracts.