Ansible OEL 8 DevOps · OEL 8 · Fundamentals

AnsiblePlaybook Structure — Plays, Tasks, Handlers

The skeleton of every Ansible playbook explained from the outside in — a YAML file holds plays, plays hold tasks, tasks call modules, and handlers wait until the end of the play. Once you can see this shape you can read any playbook.

A playbook is a YAML file. Inside it lives a list of plays. Each play maps a set of hosts to a list of tasks that should be executed on them. A task is one invocation of a module. Handlers are special tasks that only run if some other task notifies them.

site.yml a playbook is a YAML file PLAY 1 · hosts: databases - name: Install MySQL module: dnf - name: Configure my.cnf module: template - name: Start mysqld module: systemd PLAY 2 · hosts: webservers - name: Install nginx module: dnf - name: Push site config module: copy - name: Reload nginx module: systemd
YAML — annotated play
---
- name: Configure database servers       # human-readable label for the PLAY
  hosts: databases                       # which hosts from inventory to target
  remote_user: deploy                    # SSH connection user
  become: true                           # escalate to root via sudo
  gather_facts: true                     # run setup module first (default)
  vars:                                  # play-scoped variables
    mysql_root_password: "secret"
    mysql_port: 3306
  vars_files:                            # additional vars from external files
    - vars/db_creds.yml
  pre_tasks:                             # run BEFORE roles
    - name: Update package cache
      ansible.builtin.dnf:
        update_cache: true
  roles:                                 # roles to apply
    - common
    - mysql
  tasks:                                 # explicit tasks (run AFTER roles)
    - name: Open MySQL port in firewalld
      ansible.posix.firewalld:
        port: "{{{{ mysql_port }}}}/tcp"
        permanent: true
        immediate: true
        state: enabled
  post_tasks:                            # run AFTER tasks
    - name: Verify MySQL is listening
      ansible.builtin.wait_for:
        port: "{{{{ mysql_port }}}}"
        timeout: 30
  handlers:                              # only run if notified
    - name: restart mysql
      ansible.builtin.systemd:
        name: mysqld
        state: restarted
#SectionWhen it runs
1gather_facts (setup)First, on every host (unless disabled)
2pre_tasksBefore any role
3rolesIn the order listed
4tasksAfter all roles complete
5post_tasksAfter explicit tasks
6handlersAt the very end, only if notified by a changed task
💡 Tip: If you put validation logic in post_tasks instead of regular tasks, handlers fire before validation runs — your service is restarted, then the post-task verifies it actually came back up. This is the right ordering.

A single playbook file can hold many plays — each targeting different hosts:

YAML — multi-play playbook
---
- name: Database tier
  hosts: databases
  become: true
  roles:
    - mysql

- name: Web tier
  hosts: webservers
  become: true
  roles:
    - nginx
    - app

- name: Wire web tier to db tier
  hosts: webservers
  become: true
  tasks:
    - name: Set MySQL host in app config
      ansible.builtin.lineinfile:
        path: /etc/myapp/config.ini
        regexp: "^db_host"
        line: "db_host = {{{{ groups['databases'][0] }}}}"

Plays run sequentially. Play 2 doesn't start until play 1 finishes on every host in its target group. This makes orchestration straightforward: the database tier finishes provisioning before the web tier starts.

Every play and every task supports name. It's optional but skipping it makes output unreadable: tasks without names show as TASK [ansible.builtin.dnf] instead of TASK [Install MySQL server package].

⚠ Warning: ansible-lint flags un-named tasks as a violation. Treat name as mandatory in shared code — your future self reading a 200-line playbook will thank you.

Page 11 (next) goes deep on variables — including the precedence rules that determine which value wins when the same variable is set in five different places.