Courseiva
EX294Chapter 5 of 18Objective 3.2

Writing Playbooks: Basics and Structure

If you cannot write a playbook, you cannot automate anything with Ansible, and you will fail the EX294 exam on the first hands-on question. This chapter teaches you the exact structure and syntax Ansible expects, so you can write playbooks that run without errors and pass the exam tasks that ask you to create or modify them.

12 min read
Beginner
Updated Jul 23, 2026
Editorial oversight: Johnson Ajibi· Security Systems Architect · MSc IT Security · IEEE Senior Member

A simple way to picture Writing Playbooks: Basics and Structure

The Recipe Card Analogy

Your kitchen counter is covered in ingredients: flour, eggs, sugar, butter, and a bag of chocolate chips. You also have a well-stocked pantry with measuring cups, a mixing bowl, and a whisk. Standing next to you is a very good friend who will follow your instructions exactly but will not make any decisions on their own.

You pull out a recipe card. The card does not list every possible action the friend could take—it does not say "breathe" or "stand up." Instead, it gives clear, sequential instructions: "Preheat the oven to 180°C. Mix the dry ingredients in a bowl. Cream the butter and sugar together. Add the eggs one at a time. Fold in the chocolate chips." Your friend reads a line, completes that exact step, then moves to the next line. If they encounter the instruction "add two cups of flour," they do not ask if you meant wholemeal or self-raising—they use the flour you have put out, because the recipe assumes the ingredients are ready.

This is exactly how Ansible playbooks work. The playbook is your recipe—a YAML file that lists a sequence of tasks for Ansible to execute on managed servers. The managed servers are your friend. They follow the playbook's instructions precisely, using the inventory (the list of ingredients) and variables (the specifics, like which port number to use) that you have defined beforehand. The playbook does not need to describe the operating system or the network connection because Ansible already handles that—just like you do not write "the friend must have hands" on your recipe card. The structure of a playbook—its name, hosts, tasks, and modules—is the equivalent of the recipe's sections: title, ingredients list, and step-by-step method. Each task calls a module, which is a pre-written set of commands for a specific action (like installing software or copying a file). When the playbook runs, Ansible reads each task in order, executes it on the target servers, and reports back whether it succeeded or failed—just as your friend says "I've whisked the eggs" after completing that step.

How It Actually Works

A playbook is a file written in YAML (YAML Ain't Markup Language) that tells Ansible what to do on one or more managed servers. "Managed servers" are the computers (physical or virtual) that Ansible controls. "Automation" means having a machine perform repetitive tasks without a human typing commands every time.

YAML uses indentation (spaces, not tabs) to show structure—like the way you outline an essay with bullet points that sit under headings. Every playbook starts with three dashes "---" at the top. This signals to Ansible that the file is a YAML document. Three dots "..." can optionally mark the end.

The core components of a playbook are:

Play: A top-level list item that targets a group of hosts and contains a list of tasks. A playbook can have one play or multiple plays.

Hosts: The group of servers this play runs on. Defined in the inventory file, which is a list of server names or IP addresses.

Tasks: The ordered list of actions the play will execute. Each task calls a single module.

Module: A pre-built piece of code that performs a specific job—like installing a package, copying a file, or starting a service. Ansible has hundreds of built-in modules.

Becoming: Using "become: yes" tells Ansible to run the tasks with elevated privileges (like "sudo" in Linux). Many administrative tasks require this.

Here is a minimal playbook example:

--- - name: Install and start Nginx hosts: webservers become: yes tasks: - name: Install nginx package ansible.builtin.yum: name: nginx state: present - name: Start nginx service ansible.builtin.service: name: nginx state: started

Let us break this down line by line. "---" starts the playbook. "- name: Install and start Nginx" is the first play. The dash "-" shows it is an item in a list (the list of plays). "hosts: webservers" tells Ansible to run this play only on servers that belong to the group "webservers" in the inventory. "become: yes" means Ansible will run tasks with root privileges. "tasks:" starts the list of tasks. Each task starts with a dash and a name. "ansible.builtin.yum" is the module name—this module manages packages on Red Hat-based systems using the YUM package manager. The module parameters follow: "name: nginx" (the package to install) and "state: present" (meaning ensure it is installed). The second task uses the "service" module to start the Nginx service.

Why does this structure matter for EX294? The exam asks you to write playbooks from scratch, fix broken playbooks, and add tasks to existing playbooks. You must remember that:

Every play must have a "hosts" line.

Tasks are under "tasks:" at the same indentation level as "hosts".

Module names are fully qualified (like "ansible.builtin.yum") or short (like "yum"). The exam accepts either, but using the full name is safer.

YAML is whitespace-sensitive—use exactly two spaces for each indentation level. A single extra space or a tab will cause the playbook to fail.

The order of tasks matters. Ansible runs tasks from top to bottom. So you must install the package before you start the service.

Playbooks replace manual work. Before automation, a sysadmin would SSH (Secure Shell) into each server, type "sudo yum install -y nginx", type the password, then type "sudo systemctl start nginx". That is time-consuming, error-prone, and impossible to repeat exactly across hundreds of servers. A playbooks defines the desired state once, and Ansible enforces it on every target server identically.

Structure of an Ansible playbook showing how plays, hosts, become, tasks, and modules connect.

Walk-Through

1

Define the Play

Start the playbook with '---' and then write a line that begins with a dash, a space, and 'name:' followed by a description of the play. This defines the scope of work for a set of servers.

2

Specify the Target Hosts

On the next line, indent two spaces and write 'hosts:' followed by the group name from your inventory (e.g., 'webservers'). This tells Ansible which machines to connect to. If the group name is misspelt, the playbook will not work.

3

Add Privilege Escalation (if needed)

Write 'become: yes' on its own line at the same indentation as 'hosts'. This is required for any task that needs root permissions, like installing software or modifying system files.

4

List the Tasks

Write 'tasks:' on its own line at the same indentation as 'hosts' and 'become'. Then list each task starting with a dash, a space, 'name:', and the module call indented underneath. Each module has its own parameters, which you must look up from the documentation.

5

Run the Playbook

Save the file with a .yml extension (e.g., 'webserver.yml'). In the terminal, run 'ansible-playbook webserver.yml' to execute it. Review the output for 'ok' (no change), 'changed' (task executed and changed state), or 'failed' (error).

What This Looks Like on the Job

You join a company that runs a web application across 20 Linux servers. The application needs a specific configuration file that must exist on every server, with precise content. Without Ansible, you would have to log into each server one by one, open a text editor, paste the file contents, save, and exit. If you make a typo on server 12, the application might crash, but you might not notice until a user complains.

With an Ansible playbook, you write the desired configuration once and run it against all 20 servers. Here is how that works step by step:

You create a playbook file called "deploy-config.yml".

You define the play with "hosts: all" to target every server in the inventory.

You add a task using the "copy" module: "ansible.builtin.copy:" with parameters for the source file path (on your control node) and the destination path (on each managed server).

You run the playbook with the command "ansible-playbook deploy-config.yml".

Ansible connects to each server over SSH, checks whether the file already exists with the exact content you specified, and if not, copies it over. If the file already matches, Ansible reports "ok" and does nothing—this is called idempotency.

The real-world benefit is that you eliminate human error. Every server gets the same file with the same content, every time. If you need to update the file later, you edit the playbook source file once and rerun the playbook. Ansible copies the updated file only to servers where the content differs.

Common settings you might manage with playbooks include:

Installing security patches across all servers.

Restarting a service after a configuration change.

Creating user accounts on multiple servers.

Deploying application code from a version control repository.

In a business context, playbooks are often stored in a version control system like Git. This means every change to the playbook is tracked—who changed what and when. If a playbook breaks something, you can roll back to a previous version quickly. Teams collaborate by reviewing each other's playbook changes before they run against production servers.

How EX294 Actually Tests This

The EX294 exam tests your ability to write and execute basic Ansible playbooks. This is not a multiple-choice section—you are given a task description and a blank file, and you must write a working playbook that meets the requirements. The exam environment uses a terminal with a text editor (usually vim), the Ansible command line, and a set of pre-configured servers.

Key exam concepts to master:

Hosts specification: You must know how to target a specific host group (e.g., "hosts: webservers") or all hosts ("hosts: all"). The group name must match exactly what is in the inventory file. The exam will not tell you the inventory contents—you have to read it or remember it from earlier tasks.

Module selection: The exam expects you to choose the correct module for the task. Common modules include "yum" or "dnf" for package management, "copy" for files, "service" or "systemd" for services, "user" for user accounts, and "template" for generating files from templates. Using the wrong module will cause the task to fail.

Become/escalation: Many tasks require root privileges. You must add "become: yes" at the play level or at the task level. Forgetting this is a common trap—the task runs but fails with a permission error.

YAML syntax: The exam checks that your playbook is valid YAML. Indentation must be consistent (two spaces per level). You cannot use tabs. A single indentation error will cause the playbook to fail before any task runs.

Order of execution: Tasks run top to bottom. If you try to start a service before installing the package, the service task will fail. The exam may give you a sequence of required states (e.g., install package, configure service, start service), and you must order them correctly.

Task naming: Every task must have a name. This is not optional. The name should describe what the task does (e.g., "Install Apache web server"). The exam may check that your task names match what the question asks for.

Idempotency: Ansible tasks should be written so that running the playbook multiple times produces the same result. For example, use "state: present" (not "state: latest") unless you specifically want the latest version every time.

Common traps the exam sets:

The inventory file might list a host with an alias, and you must use the alias in the playbook, not the IP address.

The question may ask you to use a specific module that differs from what you are used to (e.g., "ansible.builtin.systemd" instead of "service"). Read the module name carefully.

You might be asked to include a variable in the playbook (e.g., using "{{ var_name }}"). If you forget the double curly braces, the variable is treated as a literal string.

The playbook must be saved with a ".yml" extension. Ansible will not read ".yaml" or any other extension in the exam environment.

Memorise this check pattern before you run a playbook:

Does the playbook start with "---"?

Does every play have a "hosts" line?

Is "become: yes" present if the task requires root?

Are all indents two spaces?

Does each task have a name and a module name?

Are parameters indented under the module name?

Are strings quoted if they contain special characters?

Is the file saved with ".yml" extension?

Key Takeaways

A playbook is a YAML file containing one or more plays, where each play targets a specific group of hosts and lists a sequence of tasks.

Every play must have a 'hosts' line that tells Ansible which servers to target, matching a group name from the inventory file.

Tasks run top to bottom in the exact order you write them, so install a package before you try to start its service.

Use 'become: yes' at the play or task level to run commands with root privileges, which is required for most administrative actions.

YAML uses two spaces per indentation level and does not allow tabs—one wrong indent will break the entire playbook.

Each task must have a name and call a specific module (like ansible.builtin.yum) with the correct parameters to achieve the desired state.

Easy to Mix Up

These come up on the exam all the time. Here's how to tell them apart.

Playbook

A YAML file with multiple tasks and configuration options.

Used for complex, repeatable automation across multiple servers.

Written once and executed many times with 'ansible-playbook'.

Ad-hoc Command

A single command executed in the terminal with 'ansible'.

Used for one-off quick tasks like checking server uptime.

Not saved or repeatable—must be retyped each time.

Task Name

A human-readable description of what the task does.

Optional but strongly recommended for readability and exam points.

Appears in the output to show what action is being performed.

Task Module

The Ansible module that performs the actual action (e.g., 'yum', 'copy').

Required—without a module, the task has no action to execute.

Determines what parameters you can use (e.g., 'name' and 'state' for the yum module).

hosts: all

Targets every server in the inventory file.

Useful for running a playbook across all machines (e.g., patch all).

Risky if you only intended to target a subset—can cause unintended changes.

hosts: specific_group

Targets only servers belonging to a defined group (e.g., 'webservers').

Allows fine-grained control over which servers run which tasks.

Requires the group to exist in the inventory, or the playbook fails.

Watch Out for These

Mistake

Playbooks are written in a special Ansible programming language that I need to learn first.

Correct

Playbooks are written in YAML, which is a plain-text format using indentation and key-value pairs. Anyone who can write a bullet list can learn YAML in minutes.

Beginners often think they must learn a complex scripting language before they can write a playbook, which stops them from even trying.

Mistake

I need to install Ansible on every server I want to manage.

Correct

Ansible only needs to be installed on the control node (the machine you run the playbook from). The managed servers only need SSH access and Python. Ansible pushes modules to them temporarily.

People confuse Ansible with agent-based tools like Puppet or Chef, where an agent must run on each managed server.

Mistake

The order of tasks in a playbook does not matter because Ansible figures out dependencies automatically.

Correct

Ansible executes tasks strictly in the order they are written from top to bottom. If you need software installed before you configure it, the install task must come first.

New learners see that Ansible is declarative (you describe the desired state) and mistakenly think it is also dependency-aware like a package manager.

Mistake

If my playbook runs without errors, it means every server reached the desired state perfectly.

Correct

A playbook can run with 'ok' and 'changed' results but still not achieve the desired state if the module parameters are incorrect. For example, copying a file to the wrong path will report success but the file will be in the wrong location.

Beginners trust the output colours (green = ok, yellow = changed) and do not verify the actual outcome on the servers. The exam expects you to check the module documentation for what each parameter truly does.

Do You Actually Know This?

Reveal each answer, then mark whether you got it right. Score 60%+ to unlock the next chapter.

Frequently Asked Questions

What is the difference between a playbook and a role in Ansible?

A playbook is a single YAML file that defines what to do. A role is a structured directory that bundles tasks, variables, files, and templates together for reuse. For EX294, you only need to write playbooks at the basic level.

Do I need to use the full module name like 'ansible.builtin.yum' or can I just write 'yum'?

Both work in the exam, but using the full name (ansible.builtin.yum) is more explicit and avoids ambiguity if you later add custom modules. The exam does not penalise either form.

How do I know which module to use for a given task?

Read the exam task description carefully—it often hints at the module name. For example, 'install a package' suggests 'yum' or 'dnf'. 'Copy a file' suggests 'copy'. The Ansible documentation is not available in the exam, but the module names are intuitive.

What happens if I use a tab instead of spaces in the YAML file?

Ansible will fail to parse the playbook and throw an error like 'ERROR! Syntax Error while loading YAML'. The fix is to convert tabs to spaces (usually two spaces per indent).

Can I have multiple plays in one playbook?

Yes. Each play starts with its own dash and 'name'. They run in order. For example, you might have a play that installs software on database servers and a second play that configures web servers in the same playbook file.

What does the 'state: present' parameter do?

It tells the module to ensure something exists. For package modules, 'state: present' means install the package if it is not already installed. 'state: absent' would remove it. 'state: started' on a service module ensures the service is running.

Terms Worth Knowing

Keep going

You've finished Writing Playbooks: Basics and Structure. Continue through the EX294 study guide to build a complete picture of the exam.

Done with this chapter?