Best Practices: Running Multiple Ansible Playbooks in Sequential Order for Server Provisioning


2 views

When provisioning complex server environments, we often need to execute multiple Ansible playbooks in a specific sequence. While a shell script with sequential ansible-playbook commands works, it lacks the native Ansible benefits of error handling, variable sharing, and dependency management.

Ansible provides several built-in approaches for playbook orchestration:

# Method 1: Importing playbooks
- name: First playbook tasks
  import_playbook: playbook1.yml

- name: Second playbook with different user
  import_playbook: playbook2.yml
  vars:
    ansible_user: deploy_user
    ansible_become: yes

For more complex scenarios with mixed privilege requirements:

# master_playbook.yml
- hosts: all
  tasks:
    - name: Include first phase as root
      include_tasks: setup_phase1.yml
      vars:
        ansible_become: yes

- hosts: all
  tasks:
    - name: Include second phase as app_user
      include_tasks: setup_phase2.yml
      vars:
        ansible_user: app_user

Add robustness with blocks and error handling:

- block:
    - import_playbook: db_setup.yml
    - import_playbook: app_deploy.yml
  rescue:
    - debug:
        msg: "Playbook sequence failed, rolling back"
    - import_playbook: rollback.yml

For selective execution within the sequence:

# Execute only security-related playbooks
ansible-playbook master_playbook.yml --tags "security"

When dealing with numerous playbooks:

  • Use strategy: free for independent tasks
  • Leverage serial for rolling updates
  • Consider asynchronous tasks where appropriate

When provisioning complex server environments, we often need to chain multiple Ansible playbooks that have specific execution orders and privilege requirements. While a shell script approach works, it lacks native Ansible features like error handling, variable sharing, and proper privilege escalation management.

Ansible offers two primary mechanisms for combining playbooks:


# Static inclusion (pre-Ansible 2.4 style)
- include: playbook1.yml
- include: playbook2.yml

# Dynamic imports (modern approach)
- import_playbook: playbook1.yml
- import_playbook: playbook2.yml

Here's a robust implementation that handles ordering and privilege separation:


---
# master_playbook.yml
- name: Phase 1 - Root-level configuration
  hosts: all
  become: yes
  vars:
    phase: initial_setup
  tasks: []
  
- import_playbook: hardware_config.yml
- import_playbook: os_baseline.yml

- name: Phase 2 - Application deployment
  hosts: app_servers
  become: no
  become_user: "{{ deployment_user }}"
  vars:
    phase: app_deployment
  tasks: []

- import_playbook: db_setup.yml
- import_playbook: webapp_deploy.yml

For more complex scenarios, consider these patterns:


# Conditional execution
- name: Check if DB needs provisioning
  hosts: db_servers
  tasks:
    - stat:
        path: /var/lib/mysql
      register: mysql_installed

- import_playbook: mysql_install.yml
  when: not mysql_installed.stat.exists

# Error handling with blocks
- block:
    - import_playbook: critical_setup.yml
  rescue:
    - debug:
        msg: "Critical setup failed, aborting!"
    - meta: end_play
  • Use serial directive for rolling updates
  • Leverage strategy: free when order doesn't matter
  • Consider asynchronous execution for long-running tasks
  • Cache facts between playbooks with fact_caching