Ansible fail Module — Abort Playbooks with Custom Messages
Introduction
ansible.builtin.fail intentionally stops playbook execution with a custom error message. Use it to enforce preconditions, validate inputs, and prevent playbooks from running in unsafe conditions. It's the "abort" button for Ansible — a clear way to say "something is wrong, stop here."
Basic Usage
---
- name: Fail module examples
hosts: all
tasks:
- name: Fail unconditionally
ansible.builtin.fail:
msg: "This playbook is deprecated. Use deploy-v2.yml instead."
- name: Fail with condition
ansible.builtin.fail:
msg: "This playbook only runs on Debian/Ubuntu systems"
when: ansible_os_family != "Debian"
Input Validation
- name: Require version variable
ansible.builtin.fail:
msg: "Variable 'app_version' is required. Use -e app_version=x.y.z"
when: app_version is not defined
- name: Validate version format
ansible.builtin.fail:
msg: "Invalid version '{{ app_version }}'. Expected format: x.y.z"
when: app_version is not regex('^[0-9]+\.[0-9]+\.[0-9]+$')
- name: Validate environment
ansible.builtin.fail:
msg: "Invalid environment '{{ env }}'. Must be: dev, staging, or production"
when: env not in ['dev', 'staging', 'production']
Precondition Checks
- name: Check minimum disk space
ansible.builtin.command:
cmd: df --output=avail / | tail -1
register: disk_avail
changed_when: false
- name: Fail if disk space too low
ansible.builtin.fail:
msg: "Only {{ (disk_avail.stdout | int / 1024) | int }}MB available. Need at least 2048MB."
when: (disk_avail.stdout | int / 1024) < 2048
- name: Check minimum memory
ansible.builtin.fail:
msg: "Only {{ ansible_memtotal_mb }}MB RAM. Minimum 4096MB required."
when: ansible_memtotal_mb < 4096
- name: Check Python version
ansible.builtin.fail:
msg: "Python 3.9+ required. Found: {{ ansible_python_version }}"
when: ansible_python_version is version('3.9', '<')
Production Guardrails
- name: Prevent accidental production runs
ansible.builtin.fail:
msg: |
⚠️ PRODUCTION DEPLOYMENT BLOCKED
You must set confirm_production=yes to deploy to production.
Run: ansible-playbook deploy.yml -e confirm_production=yes -e env=production
when:
- env == 'production'
- confirm_production | default('no') != 'yes'
- name: Block deployments during maintenance window
ansible.builtin.fail:
msg: "Deployments blocked during maintenance window ({{ maintenance_start }} - {{ maintenance_end }})"
when:
- maintenance_mode | default(false)
fail vs assert
# fail — single condition, custom message
- name: Check with fail
ansible.builtin.fail:
msg: "Port 8080 is already in use"
when: port_check.rc == 0
# assert — multiple conditions, cleaner for validation
- name: Check with assert
ansible.builtin.assert:
that:
- app_version is defined
- env in ['dev', 'staging', 'production']
- ansible_memtotal_mb >= 4096
- ansible_os_family == 'Debian'
fail_msg: "Precondition check failed. See conditions above."
success_msg: "All preconditions met."
| Feature | fail | assert |
|---|---|---|
| Multiple conditions | Needs multiple tasks | Single task with list |
| Custom per-condition message | ✅ Each task has its own | One message for all |
| Readability | Better for complex messages | Better for checklists |
| Success message | ❌ | ✅ success_msg |
In block/rescue
- name: Deployment with validation
block:
- name: Deploy application
ansible.builtin.command:
cmd: /opt/deploy.sh {{ app_version }}
- name: Verify health
ansible.builtin.uri:
url: "http://localhost:8080/health"
status_code: 200
rescue:
- name: Rollback
ansible.builtin.command:
cmd: /opt/deploy.sh rollback
- name: Fail with context
ansible.builtin.fail:
msg: |
Deployment of v{{ app_version }} FAILED on {{ inventory_hostname }}.
Rollback completed. Check application logs at /var/log/myapp/.
Common Patterns
Cluster Quorum Check
- name: Count available nodes
ansible.builtin.set_fact:
available_nodes: "{{ groups['db_cluster'] | map('extract', hostvars) | selectattr('ansible_host', 'defined') | list | length }}"
- name: Ensure quorum before maintenance
ansible.builtin.fail:
msg: "Only {{ available_nodes }}/{{ groups['db_cluster'] | length }} nodes available. Need quorum ({{ (groups['db_cluster'] | length // 2) + 1 }})."
when: available_nodes | int < (groups['db_cluster'] | length // 2) + 1
Incompatible Versions
- name: Check Ansible version
ansible.builtin.fail:
msg: "This playbook requires Ansible 2.15+. You have {{ ansible_version.full }}."
when: ansible_version.full is version('2.15', '<')
Troubleshooting
| Issue | Solution |
|---|---|
| Fail runs when it shouldn't | Check when condition logic (and/or/not) |
| Want to fail but continue other hosts | Use ignore_errors on the fail task (unusual) |
| Need to fail ALL hosts at once | Use any_errors_fatal: true on the play |
| Multi-line message not formatting | Use ` |
Best Practices
- Validate inputs early — fail at the start, not after 20 tasks
- Use descriptive messages — include what's wrong AND how to fix it
- Include variable values —
"Expected 4096MB, got {{ ansible_memtotal_mb }}MB" - Use
assertfor checklists — multiple conditions in one task - Add production guardrails — require explicit confirmation for dangerous operations
- Combine with
block/rescue— rollback then fail with context
Conclusion
ansible.builtin.fail is your playbook's safety net. Use it early to validate inputs, check preconditions, and prevent automation from running in unsafe conditions. A clear error message like "Only 1024MB RAM available, need 4096MB" saves hours of debugging cryptic failures halfway through a deployment.