Error handling, blocks and changed_when

Ansible · Advanced · 10 min

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: false for 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: true continues after a failure (sparingly; it hides problems). ignore_unreachable for 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 (like except); a successful rescue marks the host as recovered (rescued=1 in the recap),
  • always: runs regardless (like finally), for cleanup or notifications.

Controlling the blast radius across hosts:

  • any_errors_fatal: true stops the whole play on all hosts when one fails, useful for clusters that must stay consistent.
  • max_fail_percentage: 20 aborts 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.

Check yourself

  1. A task inside block fails and its rescue section succeeds. How does the host end up?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.ansible.com/ansible/latest/playbook_guide/playbooks_blocks.html