Skip to content

Your First Playbook


Back in the ad hoc chapter, we ran three separate commands by hand — a connectivity check, a command, a debug message — and I promised we’d come back once the repetition started to hurt. This is that chapter. A playbook takes exactly that kind of sequence, writes it down once, and lets you run it as a single, repeatable unit, as many times as you need.

What Is A Playbook?

A playbook is a YAML file describing one or more plays. Each play targets a set of hosts and runs an ordered list of tasks against them — the saved, reusable alternative to typing out ansible commands one at a time.

Anatomy Of A Playbook

At the level we need for now, a play has just two things worth knowing:

  • hosts — the target pattern, exactly like the pattern you’d pass to an ad hoc command (a host, a group, or all).
  • tasks — an ordered list of steps to run, each one a module invocation.

Each task gets a name (a human-readable description, shown while the playbook runs) and a module key with whatever arguments that module needs.

Writing Your First Playbook

Here’s the exact sequence from the ad hoc chapter, as a playbook instead:

first_playbook.yaml
- name: My First Playbook
  hosts: servers
  tasks:
    - name: Check connectivity
      ansible.builtin.ping:

    - name: Show system uptime
      ansible.builtin.command: uptime

    - name: Print a message
      ansible.builtin.debug:
        msg: "Hello from a playbook"

Three tasks, matching the three ad hoc commands you already ran by hand — ping, command, debug — just written down once instead of retyped every time.

Running It

Playbooks run through a different command than ad hoc tasks — ansible-playbook (against the same inventory.yaml from previous chapters), not ansible:

ansible-playbook -i inventory.yaml first_playbook.yaml --private-key=alice
PLAY [My First Playbook] ******************************************

TASK [Gathering Facts] ********************************************
ok: [ubuntu]
ok: [fedora]

TASK [Check connectivity] *****************************************
ok: [ubuntu]
ok: [fedora]

TASK [Show system uptime] *****************************************
changed: [ubuntu]
changed: [fedora]

TASK [Print a message] ********************************************
ok: [ubuntu] => {
    "msg": "Hello from a playbook"
}
ok: [fedora] => {
    "msg": "Hello from a playbook"
}

PLAY RECAP *********************************************************
ubuntu   : ok=4  changed=1  unreachable=0  failed=0
fedora   : ok=4  changed=1  unreachable=0  failed=0

About That Private Key Flag

You’ll have noticed --private-key=alice in that command — worth explaining briefly here, even though the full picture waits for this section’s final chapter. We generated SSH keys for alice and bob back in the installation chapter, but never told Ansible where to find them by default, and there’s no SSH agent running to hand them over automatically either. Without one or the other, Ansible has no private key to authenticate with at all, so it has to be told explicitly, every time, with --private-key (you may also see this written as --key-file — they’re the same option).

Strictly speaking, this applies to every ad hoc command back in Chapter 4 too — they were missing this same flag, and would need it added to actually authenticate successfully. The configuration chapter at the end of this section covers exactly how to set a default private key (and a default inventory path) once, so you stop needing to repeat either flag on every single command — that’s precisely why that chapter comes last.

Task Execution Order

Tasks run top to bottom, in the exact order written — nothing about a playbook reorders or parallelizes your task list on its own. With more than one host, the default behavior is to run each task across every targeted host before moving on to the next task — not all of a host’s tasks first, then the next host. In the output above, notice Check connectivity completed on both ubuntu and fedora before Show system uptime started on either one.

Idempotency

This is one of Ansible’s core ideas: a well-written task should be safe to run again and again, only making a change when one is actually needed — reporting changed when it did something, and ok when it checked and found nothing to do.

Look back at the output above: Check connectivity reported ok, not changed — the ping module doesn’t change anything, ever. Show system uptime reported changed on both hosts, every single time you run it, regardless of whether anything on the system actually changed — because command and shell have no way to know whether they changed something; they just ran a program and report changed unconditionally as a result.

This is a real limitation of command and shell specifically, not of Ansible in general — and it’s exactly what purpose-built modules are designed to avoid, as the next section shows directly.

Introducing The copy Module

ansible.builtin.copy copies a file to a remote host — and, unlike command, it’s genuinely idempotent:

first_playbook.yaml
    - name: Copy a file to the remote host
      ansible.builtin.copy:
        src: hello.txt
        dest: /home/alice/hello.txt

Create a small local hello.txt and run the playbook once — you’ll see changed, since the file didn’t exist there yet. Run it again, completely unchanged, and this time it reports ok: copy checked the destination first, found content already identical to the source, and correctly did nothing. That’s the difference idempotency actually makes in practice, not just in theory.

--check And --diff: Preview Before You Commit

--check runs a playbook in dry-run mode — reporting what would happen, without actually making any changes:

ansible-playbook -i inventory.yaml first_playbook.yaml --private-key=alice --check

Add --diff alongside it to see the actual content difference for modules that support showing one, like copy:

ansible-playbook -i inventory.yaml first_playbook.yaml --private-key=alice --check --diff

Together, these let you preview exactly what a playbook intends to do before letting it touch anything for real — genuinely useful the first time you run an unfamiliar playbook against a host you actually care about.

Note

Not every module cooperates fully with --checkcommand and shell, in particular, generally just skip themselves under check mode rather than meaningfully predicting what they’d do, since Ansible has no way to know a raw command’s effect without actually running it. This is another point in favor of reaching for a purpose-built module over command/shell whenever one exists.

Best Practices

  • Give every task a clear name — it’s what shows up in the output while the playbook runs, and it’s the first thing you’ll read when trying to figure out what failed later.
  • Prefer a purpose-built module over command/shell whenever one exists. Beyond being generally safer, you get real idempotency and genuine --check support for free — copy here versus command/shell is the clearest possible illustration of that gap.
  • Run --check --diff before running an unfamiliar or risky playbook for real, especially against anything beyond a lab.
  • Remember task order is sequential, and runs per-task-across-all-hosts by default — don’t assume one host finishes its entire task list before another host starts.
Last updated on