Real automation must cope with expected non-zero exit codes and partial failures without lying about state.
By default a task fails when the module says so (for command/shell, a non-zero return code). You can redefine both outcomes:
failed_when: result.rc not in [0, 2]or"'ERROR' in result.stderr".changed_when: falsefor read-only commands;changed_when: "'created' in result.stdout"for commands that sometimes change things. Accurate change reporting keeps handlers and recaps honest.ignore_errors: truecontinues after a failure (sparingly; it hides problems).ignore_unreachablefor flaky hosts.
Blocks group tasks to share when, become and other keywords, and add exception handling:
block:the main tasks,rescue:runs if any task in the block fails (likeexcept); a successful rescue marks the host as recovered (rescued=1in the recap),always:runs regardless (likefinally), for cleanup or notifications.
Controlling the blast radius across hosts:
any_errors_fatal: truestops the whole play on all hosts when one fails, useful for clusters that must stay consistent.max_fail_percentage: 20aborts once too many hosts have failed.serial: 2(or"25%") runs the play in batches: a rolling update; a failing batch stops the rollout before it reaches every host.
retries, delay and until retry a task until a condition holds, perfect for waiting on a health endpoint.
Example
- name: Rolling deploy
hosts: app
serial: "25%"
max_fail_percentage: 0
tasks:
- name: Deploy with rollback
block:
- name: Switch symlink to the new release
ansible.builtin.file: {src: "/opt/app/{{ release }}", dest: /opt/app/current, state: link}
- name: Wait for health check
ansible.builtin.uri: {url: "http://localhost:8080/healthz", status_code: 200}
register: health
retries: 10
delay: 3
until: health is succeeded
rescue:
- name: Roll back to previous release
ansible.builtin.file: {src: "/opt/app/{{ previous_release }}", dest: /opt/app/current, state: link}
- name: Mark host failed after rollback
ansible.builtin.fail: {msg: "Release {{ release }} failed health checks"}
always:
- name: Record the attempt
ansible.builtin.lineinfile: {path: /var/log/deploys.log, line: "{{ release }} {{ health.status | default('n/a') }}", create: true}Remember
- failed_when and changed_when make results truthful
- block/rescue/always = try/except/finally
- serial + max_fail_percentage give safe rolling updates; until/retries wait for health
Watch out for
- Using ignore_errors: true to make a playbook green, hiding real failures.