The Problem
Your playbook uses pre_tasks for setup before roles, but issues occur:
fatal: [server]: FAILED! => {"msg": "pre_tasks failed before roles could execute"}Or handlers notified in pre_tasks don't run until after roles:
pre_tasks:
- name: Configure
template: ...
notify: restart service
# Handler doesn't run until after all roles completeOr pre_tasks don't run at all when expected.
Why This Happens
pre_tasks errors stem from:
Handler timing - Handlers from pre_tasks wait until roles complete.
Variable scope - Variables not available when pre_tasks need them.
pre_tasks failure stops roles - If pre_tasks fail, roles never execute.
Serial interaction - pre_tasks run for each serial batch.
Condition confusion - Conditions on pre_tasks vs play.
Understanding pre_tasks
pre_tasks run before roles in a play:
```yaml - hosts: webservers pre_tasks: - name: Setup before roles debug: msg: "Pre-task running"
roles: - webserver
tasks: - name: After roles debug: msg: "Regular task" ```
Execution order: pre_tasks -> roles -> tasks -> handlers
Diagnosing the Issue
Run with verbose output:
ansible-playbook playbook.yml -vvCheck execution order:
ansible-playbook playbook.yml --list-tasks
# Shows pre_tasks first, then role tasksDebug handler notifications:
```yaml pre_tasks: - name: Notify handler command: /opt/setup.sh notify: my_handler register: notify_check
- debug:
- msg: "Handler notified: {{ notify_check }}"
`
The Fix
Fix 1: Flush Handlers in pre_tasks
Handlers from pre_tasks wait for roles - flush explicitly:
```yaml - hosts: webservers pre_tasks: - name: Update config template: src: app.conf.j2 dest: /etc/app.conf notify: restart app
- name: Flush handlers before roles
- meta: flush_handlers
- # Handler runs now, before roles
roles: - deploy_app # App already restarted, roles proceed with running service ```
Fix 2: Handle pre_tasks Failure Gracefully
pre_tasks failure stops entire play - use block/rescue:
```yaml - hosts: webservers pre_tasks: - name: Setup with error handling block: - name: Critical setup command: /opt/critical-setup.sh
- name: Optional setup
- command: /opt/optional-setup.sh
- ignore_errors: yes
rescue: - name: Handle setup failure debug: msg: "Setup failed, continuing with degraded state"
roles: - app # Roles run even if optional setup failed ```
Fix 3: Define Variables for pre_tasks
Variables must exist before pre_tasks:
```yaml # WRONG - variable undefined for pre_tasks - hosts: webservers pre_tasks: - name: Use undefined variable debug: msg: "{{ app_name }}"
vars: app_name: myapp # Defined after pre_tasks block
# CORRECT - vars before pre_tasks - hosts: webservers vars: app_name: myapp
pre_tasks: - name: Use defined variable debug: msg: "{{ app_name }}" ```
Fix 4: pre_tasks with Serial
pre_tasks run for EACH serial batch:
- hosts: webservers
serial: 3
pre_tasks:
- name: Setup
debug:
msg: "Pre-task for {{ inventory_hostname }}"
# Runs 4 times if 10 hosts (4 batches)For one-time pre_task with serial:
- hosts: webservers
serial: 3
pre_tasks:
- name: One-time setup
command: /opt/global-setup.sh
run_once: yes
# Runs once, not per batchFix 5: pre_tasks Conditions
Conditions apply to individual pre_tasks:
```yaml - hosts: webservers pre_tasks: - name: Conditional pre-task debug: msg: "Only for Debian" when: ansible_os_family == "Debian"
- name: Always runs
- debug:
- msg: "No condition"
`
Play-level conditions don't affect pre_tasks:
- hosts: webservers
when: some_condition # This doesn't affect pre_tasks
pre_tasks:
- name: Still runs
debug:
msg: "pre_tasks ignore play-level when"Fix 6: Gather Facts in pre_tasks
Disable default fact gathering, gather in pre_tasks:
```yaml - hosts: webservers gather_facts: no pre_tasks: - name: Gather facts selectively setup: filter: ansible_os_family # Faster than full fact gathering
- name: Use minimal facts
- debug:
- msg: "{{ ansible_os_family }}"
`
Fix 7: pre_tasks Common Patterns
Load balancer removal before deployment:
```yaml - hosts: webservers pre_tasks: - name: Disable in load balancer command: lb-cli disable {{ inventory_hostname }} delegate_to: lb-server
- name: Wait for connections drain
- wait_for:
- port: 80
- state: drained
- timeout: 30
roles: - deploy
post_tasks: - name: Enable in load balancer command: lb-cli enable {{ inventory_hostname }} delegate_to: lb-server ```
Database migration before app update:
```yaml - hosts: appservers pre_tasks: - name: Run migrations command: /opt/app/migrate.sh delegate_to: db-server run_once: yes
roles: - app-update ```
Fix 8: Verify Facts for pre_tasks
Check facts exist before using:
```yaml - hosts: webservers pre_tasks: - name: Verify required facts assert: that: - ansible_os_family is defined - ansible_default_ipv4.address is defined fail_msg: "Required facts not available" success_msg: "Facts verified"
roles: - configure ```
Verifying the Fix
Test pre_tasks execution order:
```yaml # test_pre.yml - hosts: localhost gather_facts: no pre_tasks: - name: Pre-task 1 debug: msg: "First pre-task"
- name: Pre-task 2
- debug:
- msg: "Second pre-task"
roles: - role: test_role # Create a simple test role
tasks: - name: Regular task debug: msg: "After roles" ```
Run:
ansible-playbook test_pre.yml -vExpected order:
``
TASK [Pre-task 1] ***********************************************************
TASK [Pre-task 2] ***********************************************************
TASK [test_role : main] ***********************************************************
TASK [Regular task] ***********************************************************
Prevention
Document pre_tasks purpose:
- hosts: webservers
# pre_tasks: Remove from load balancer before deployment
pre_tasks:
- name: Disable in LB
...Add pre-flight checks:
pre_tasks:
- name: Verify prerequisites
assert:
that:
- required_var is defined
fail_msg: "Prerequisites not met"