What Happens When a Task Fails
Scenario: A task fails on web02. The remaining tasks skip on web02 but continue on web01, and a handler that both hosts had notified never runs anywhere.
New words, in plain English
- Per-host execution - Ansible runs the play for each host independently; one host failing does not stop the others.
any_errors_fatal- abort the whole play the moment any host fails.max_fail_percentage- abort once more than this percentage of hosts have failed.force_handlers- run notified handlers even if the play later fails.
By default a failure removes only that host from the rest of the play. Other hosts carry on. This is usually right for large fleets - one bad machine should not block a fleet-wide fix - and occasionally catastrophic, because a partial rollout can leave an estate in a state nobody designed.
Two controls change that. any_errors_fatal: true stops the entire play at the first failure on any host. max_fail_percentage: 30 allows a proportion of hosts to fail before aborting, which pairs naturally with batched rolling updates.
The handler behaviour surprises people: handlers notified during a play do not run if the play fails before reaching the handler-flush point. That is a deliberate safety default - you usually do not want to restart a service after a half-finished configuration change - but it means a genuinely needed restart can be silently skipped. force_handlers: true overrides it when the restart is the safe option.
Analogy: A failed host is a delivery driver who breaks down. By default the rest of the fleet continues.any_errors_fatalrecalls everyone the moment one van fails.max_fail_percentagesays 'carry on unless we lose more than a third of the fleet'.
A worked example
- name: Deploy to the web tier
hosts: webservers
serial: 1 # one host at a time
max_fail_percentage: 0 # any failure aborts the rollout
force_handlers: true # but still flush pending handlers
tasks:
- name: Push the config
ansible.builtin.template:
src: app.conf.j2
dest: /etc/app/app.conf
notify: restart app
handlers:
- name: restart app
ansible.builtin.service:
name: app
state: restarted
Choose deliberately rather than by default. For a config push across 500 hosts, letting a few fail and reporting them is correct. For a coordinated release where hosts must agree on a schema version, a partial rollout is worse than no rollout, and any_errors_fatal is the honest choice.
Phase 4 covers the finer-grained tool - block/rescue/always - which lets one section of a play fail, be handled, and clean up after itself, without taking the host out of the rest of the run.
Warning: Handlers do not run if the play fails first. A configuration change that landed but never triggered its restart is a genuinely dangerous half-state: the file says one thing, the running process another.
Goal: Put this to work in the ansible-idempotent-modules lab. Open/labs/ansible, pickansible-idempotent-modules, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.