The Shape of a Playbook

Scenario: A brand-new playbook refuses to run: 'ERROR! Syntax Error while loading YAML ... did not find expected key'. The file looks fine at a glance.

New words, in plain English

A playbook is a list of plays. Each play is a dictionary with keys such as name, hosts, gather_facts, vars, tasks and handlers. tasks is itself a list, so each task starts with - .

Almost every beginner syntax error is one of five things:

  1. Tabs. YAML forbids them outright. Configure your editor to insert spaces.
  2. A missing - before a task, which turns the list into a dictionary.
  3. Inconsistent indentation between sibling keys.
  4. An unquoted value starting with {{, which YAML reads as the start of a dictionary.
  5. A colon-space inside an unquoted string, which YAML reads as a key/value split.

The fix for the last two is the same: quote the value.

ansible-playbook site.yml --syntax-check parses without executing anything, and is instant. Run it before every real run until it becomes reflex.

Analogy: YAML indentation is not decoration, it is the punctuation. Moving a task two spaces to the left is like moving a full stop - the words are identical and the sentence now means something else entirely.

A worked example

- name: Publish the maintenance notice   # play 1
  hosts: webservers
  gather_facts: false
  vars:
    notice_dir: "~/srv/notice"

  tasks:                                  # a LIST
    - name: Create the notice directory   # each item starts with '- '
      ansible.builtin.file:
        path: "{{ notice_dir }}"          # quoted: starts with {{
        state: directory
        mode: "0755"                      # quoted: keeps the leading zero

    - name: Write the notice
      ansible.builtin.copy:
        dest: "{{ notice_dir }}/hello.txt"
        content: "hello from {{ inventory_hostname }}"
        mode: "0644"

Two conventions save real time later. Always give every task a name - it is what appears in the output, and an unnamed task shows up as the raw module call, which is unreadable in a fifty-task run. And always write file modes as quoted strings: unquoted 0644 is parsed by YAML as a number and can be applied as a surprising octal value.

Use the fully-qualified module name (ansible.builtin.copy rather than copy). The short name still works, but the qualified form is unambiguous once collections are in play, and it is what every current example uses.

Warning: mode: 0644 without quotes is a YAML integer, not a permission string. Always quote modes.
Goal: Put this to work in the ansible-playbook-syntax lab. Open /labs/ansible, pick ansible-playbook-syntax, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.