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.