Playbook: Structure and Tasks

K
Kai··4 min read

Ad-hoc commands (Article 2) are good for one-off jobs. But Ansible's real power lies in the playbook — a YAML file describing the system's desired state, re-runnable, and committable to Git. This article writes and runs the first playbook for real on EC2.

What a playbook is

A playbook is a YAML file made of one or more plays. Each play binds a set of hosts to a list of tasks. Each task calls a module (Article 1) with specific parameters.

   Playbook (.yml file)
   └── Play: "apply to the web group, as root"
        ├── Task 1: use the dnf module to install nginx
        ├── Task 2: use the service module to enable nginx
        └── Task 3: use the copy module to place index.html

Your first playbook

Create site.yml — install and configure a web server:

---
- name: Install and configure web server   # play name
  hosts: web                            # apply to the "web" group (Article 3)
  become: true                          # run as root (Article 2)
  tasks:
    - name: Install nginx
      ansible.builtin.dnf:
        name: nginx
        state: present

    - name: Ensure nginx is running and enabled on boot
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

    - name: Deploy the index page
      ansible.builtin.copy:
        content: "<h1>Trien khai bang Ansible</h1>\n"
        dest: /usr/share/nginx/html/index.html

Reading the structure:

  • A play starts with - name: (a description), hosts: (who to apply to), become: (privileges), then tasks:.
  • Each task has a name: (a description, shown when it runs) and a module with parameters. For example the first task calls the ansible.builtin.dnf module with name: nginx, state: present (meaning "nginx must be installed").
  • Notice we declare state (state: present, state: started), not commands — this is Ansible's declarative spirit.

About ansible.builtin.dnf — that's the FQCN (Fully Qualified Collection Name): namespace.collection.module. ansible.builtin is the built-in core collection. The shorthand dnf also works, but FQCN is best practice (explicit, avoids name clashes) — Article 9 covers collections.

Run the playbook

ansible-playbook site.yml
TASK [Gathering Facts] *********************
ok: [lab]
TASK [Install nginx] **********************
changed: [lab]
TASK [Ensure nginx is running and enabled on boot] **
changed: [lab]
TASK [Deploy the index page] **************
changed: [lab]

PLAY RECAP ********************************
lab : ok=4  changed=3  unreachable=0  failed=0  skipped=0

After running, curl http://<ip>/ returns <h1>Trien khai bang Ansible</h1> — the web server has been stood up fully automatically.

Reading the result: task states and PLAY RECAP

Each task prints a state for each host:

  • ok — already in the desired state, nothing to do.
  • changed (yellow) — Ansible changed something to reach the state.
  • failed (red) — the task errored.
  • skipped — skipped (due to a condition — Article 7).

The PLAY RECAP at the end summarizes each host: ok=4 changed=3 failed=0. Two numbers to look at immediately: failed (must be 0) and changed (how many things Ansible changed).

What Gathering Facts is

Notice the first task Gathering Facts, which we never wrote — Ansible automatically runs the setup module (Article 2) at the start of each play to gather host information (OS, IP...) for the tasks to use (Article 6). You can turn it off with gather_facts: false if you don't need it (runs faster).

Idempotency: safe to re-run

Run ansible-playbook site.yml a second time:

PLAY RECAP ********************************
lab : ok=4  changed=0  unreachable=0  failed=0

changed=0! The first time Ansible changed 3 things (install nginx, enable the service, place the file); the second time everything is already correct so it does nothing — only checks. This is idempotency: safe to run any number of times, fixing only what isn't right. Article 5 digs into how the module pulls this off.

A few useful ansible-playbook flags

ansible-playbook site.yml --check        # dry-run: see what WOULD change, change NOTHING
ansible-playbook site.yml --diff         # show the content differences of changed files
ansible-playbook site.yml --limit lab    # run only on this host/group
ansible-playbook site.yml --list-tasks   # list tasks without running
ansible-playbook site.yml -v             # verbose (add -vvv to see the mechanism — Article 1)

--check (check mode / dry-run) is extremely important in practice: see what a playbook will change before applying it for real — avoiding surprises in production. Combine with --diff to see how file contents will change. (Article 13 covers check mode in detail.)

YAML: a few common gotchas

A playbook is YAML, and YAML is sensitive to indentation (use spaces, not tabs) and structure. Common beginner mistakes: wrong indentation, forgetting the - before a list element, or missing a colon. When a playbook reports a syntax error, check indentation first. The command ansible-playbook --syntax-check site.yml catches syntax errors without running.

Wrap-up

A playbook is a YAML file of plays; each play binds hosts to a list of tasks, each task calling a module with parameters (using the FQCN like ansible.builtin.dnf). We declare state (state: present/started), not commands. Run it with ansible-playbook, read the PLAY RECAP (ok/changed/failed). Ansible automatically does Gathering Facts at the start of a play. And a playbook is idempotent — running it a second time gives changed=0. Use --check to dry-run before applying to production.

The idempotency we just touched on is a foundational property. Article 5 dissects it: how a module works internally to achieve it — which is also the deep dive that prepares you for writing your own module later.