Safe Repeatable Ansible Runs with Check Mode, Handlers and Error Handling
Design Ansible runs for safe repeatability with idempotent modules, check mode dry-runs, handlers and block rescue always error handling. Includes a worked nginx role example and verification steps.
01 Sept 2025, 19:15 UTC

Production changes that look small in a playbook can cause unexpected restarts, partial updates, or silent drift when re-run. The useful takeaway is to design Ansible runs around idempotent modules, a dry-run preview, deferred side effects, and explicit error handling so the same playbook can be executed repeatedly without surprise.
The cost of noisy changes
Ansible reports changed, ok, or skipped per task. That reporting only helps if modules are declarative and if side effects like service restarts are deferred. Without that, a re-run can repeatedly touch files, restart services, or fail halfway through a group of related tasks.
Idempotency is a module property, not a guarantee of the playbook. Using shell or command bypasses it and can cause repeated changes. Module documentation should be inspected for idempotency guarantees and supported parameters before adoption.
Dry-run preview with check mode and diff
Check mode provides a best-effort dry run. Diff mode shows the before and after for file or template changes. Together they let a reviewer or CI gate see what would change before any state is modified.
Run from a control node with read access to inventory and SSH access to targets:
ansible-playbook --check --diff -i inventory.ini site.yml --limit testRequired permissions are read-only SSH to the target and read access to playbooks. Meaningful placeholders: inventory.ini for inventory path, site.yml for playbook, test for a limited host group. Expected check: tasks marked ok, changed, or skipped in the preview. Risk: check mode is best effort. Some modules cannot accurately predict changes and may report no change when a change would occur.
Verify preview accuracy by comparing the changed tasks reported in check mode to an actual run on a non-production host. Use ansible-playbook --list-tags and --list-hosts to confirm role and tag scoping before execution.
Deferring side effects with handlers
Handlers run at most once per host per play and only when notified. That keeps normal tasks idempotent and avoids multiple restarts when several tasks modify the same service.
Handlers can surprise users who expect immediate execution after a notifying task. Execution is deferred until the end of the play.
Structured error handling with block rescue always
Block, rescue and always give per-task-group error handling. Rescue runs only if a task in the block fails. Always runs regardless of success or failure, which is useful for cleanup reporting.
Test block rescue always by forcing a failing task in a sandbox inventory and observe rescue and always execution. Do not test failure paths on production.
Worked example: nginx config role with safe reload
This example shows a role layout with defaults, vars and tasks, a handler for reload, and block rescue always for safe reporting.
# roles/nginx/tasks/main.yml
- block:
- name: Ensure nginx package is present
ansible.builtin.package:
name: nginx
state: present
- name: Deploy nginx site config
ansible.builtin.template:
src: site.conf.j2
dest: /etc/nginx/conf.d/site.conf
owner: root
group: root
mode: '0644'
notify: reload nginx
rescue:
- name: Record failure for alerting
ansible.builtin.debug:
msg: \"nginx config deployment failed on {{ inventory_hostname }}\"
always:
- name: Summarize run
ansible.builtin.debug:
msg: \"nginx role completed with status {{ ansible_failed_task | default('ok') }}\"
# roles/nginx/handlers/main.yml
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
Tags can be added to tasks or roles for targeted execution across large inventories, e.g., tags: [nginx, config]. The role uses defaults for tunable values and vars for host-specific overrides.
Trade-offs and limitations
Idempotency depends on module quality. Check mode is best effort. Handlers run only when notified and only once per play. Vault encrypted secrets require secure key management; loss of the vault password makes secrets unrecoverable.
Avoid shell/command for state that has a dedicated module. Prefer declarative modules that report changed accurately.
Actionable closing
Start with --check --diff on a test host, confirm tag and host scoping with --list-tags and --list-hosts, keep side effects in handlers, and wrap related tasks in block rescue always. Inspect module docs for idempotency before use and never assume check mode is perfect.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.