Skip to main content

Command Palette

Search for a command to run...

Ansible Playbooks Explained: From First YAML File to Managing Real Servers

If you have already run a few Ansible ad hoc commands and seen how they work, you already understand the core idea: one command, many servers. But ad hoc commands only take you so far. When you need to install software, start services, create users, copy files, and print confirmation messages, all in one automated run across multiple servers, that is when you move to playbooks. Playbooks are where Ansible truly earns its place in a DevOps workflow. Everything you do in Ansible at scale, you do through playbooks.

Updated
13 min read
Ansible Playbooks Explained: From First YAML File to Managing Real Servers
S
Passionate about Cloud and DevOps engineering. I write structured technical notes and beginner-friendly articles on AWS, Linux, CI/CD, networking, system architecture and modern software delivery workflows.

What Is a Playbook?

A playbook is a YAML file where you define what Ansible should do on your remote servers. It is a structured list of tasks, and each task calls an Ansible module to do something specific. One playbook can have as many tasks as you need, and every task runs in order, on every server you target.

The playbook structure always follows the same pattern:

---
- name: <playbook description>
  hosts: <target group or all>
  become: yes
  tasks:
    - name: <task description>
      <module_name>:
        <argument>: <value>

Let us break down each part of this structure before writing any actual playbooks.


The Structure of a Playbook

The Three Dashes at the Top

Every Ansible playbook starts with ---. This is a YAML convention that marks the beginning of a YAML document. Always include it.

name

This is just a description. It tells you and anyone else reading the playbook what this playbook is about. It appears in the terminal output when the playbook runs, so write something meaningful.

hosts

This tells Ansible where to run the playbook. You can put all to target every server in your inventory, or you can put a group name like prod or dev to target only those servers. This maps directly to the groups you defined in /etc/ansible/hosts.

become: yes

This is the equivalent of running commands with sudo. Whenever you are installing software, starting or stopping services, or doing anything that requires elevated permissions, you must include become: yes. Without it, your tasks will fail on protected operations.

A good rule to follow: if you are installing or managing services, always add become: yes. If you are only checking connectivity or reading information, you can skip it.

tasks

This is the list of things Ansible will do. Each task has a name (description) and a module with its arguments.


Checking Playbook Syntax Before Running

Before you run any playbook, especially one you just wrote, always check the syntax first. One wrong indentation in YAML can break everything.

The command is:

ansible-playbook pb1.yml --syntax-check

If there are no issues, Ansible will say the playbook is fine. If there is a problem, it will point to exactly where the error is.

YAML is incredibly sensitive to indentation. A single extra space or a missing space causes syntax errors. When Ansible shows you a syntax error, it tells you the approximate line where the problem is. Fix it, run the check again, and then proceed.


Your First Playbook: Testing Connectivity

Before installing anything on remote servers, it is a good habit to first confirm that Ansible can reach all of them. A simple ping playbook does exactly that.

---
- name: Ping playbook
  hosts: all
  tasks:
    - name: Connectivity check
      ping:

Save this as pb1.yml. To run it:

ansible-playbook pb1.yml

When you run this, you will notice Ansible does two things before executing your task. First, it runs Gathering Facts.


Understanding Gathering Facts

By default, every time a playbook runs, Ansible first connects to each target server and collects system information. This is called Gathering Facts. It checks things like the operating system, available memory, IP addresses, and other system details.

You can disable it if you want to speed things up on large inventories, but by default it runs automatically. Think of it as Ansible doing a quick health check before getting to work.

After Gathering Facts completes, your tasks execute.


Reading the Ansible Output: Color Codes

One thing that immediately stands out when you run Ansible is the colors in the terminal output. They are not decorative. Each color carries meaning:

Green means the task ran successfully and nothing changed. The system was already in the desired state.

Yellow means the task ran successfully and something changed. A package was installed, a service started, a file was created.

Red means the task failed.

At the end of every playbook run, Ansible prints a recap summarizing what happened across all servers. It shows how many tasks were OK, how many had changes, how many were unreachable, and how many failed. Get comfortable reading this recap. It tells you everything you need to know at a glance.


Installing Apache with a Playbook

Now let us write a playbook that actually does something useful: install Apache on all remote servers.

---
- name: Installing Apache
  hosts: all
  become: yes
  tasks:
    - name: Install Apache
      yum:
        name: httpd
        state: present

    - name: Print message
      debug:
        msg: "Apache installed successfully"

Save this as pb2-apache.yml. Run it with:

ansible-playbook pb2-apache.yml

A few things to notice here.

The yum module handles package management on Red Hat based systems like Amazon Linux 2. You give it the package name and a state. state: present means "make sure this package is installed." If it is already installed, Ansible skips it and shows green. If it is not installed, Ansible installs it and shows yellow.

The debug module prints a message to the terminal. You use it to confirm that a step completed or to show any information you want visible in the output. The parameter is msg, and you set it to whatever string you want printed.

When this playbook runs, you will see yellow for the Apache installation (because a change happened) and green or yellow for the debug message. The recap will show changed=1, confirming that Apache was installed.


Installing Apache and Starting the Service

Installing Apache is only half the job. You also need to start the service. Let us expand the playbook to handle both.

---
- name: Install and start Apache
  hosts: all
  become: yes
  tasks:
    - name: Install Apache
      yum:
        name: httpd
        state: present

    - name: Print install message
      debug:
        msg: "Apache installed"

    - name: Start Apache service
      service:
        name: httpd
        state: started

The service module manages system services. You give it the service name and the desired state. state: started means "make sure this service is running." If it is already running, Ansible does nothing and shows green. If it is stopped, Ansible starts it and shows yellow.

After running this playbook, you can quickly verify the service status using an ad hoc command without writing another playbook:

ansible all -a "systemctl status httpd"

That is an important habit to develop. Not everything needs a playbook. Use ad hoc commands for quick one-time checks. Use playbooks for repeatable, structured work.


Installing Git and Docker

Let us write a more complete playbook that installs multiple packages and starts a service.

---
- name: Install Git and Docker
  hosts: all
  become: yes
  tasks:
    - name: Install Git
      yum:
        name: git
        state: present

    - name: Install Docker
      yum:
        name: docker
        state: present

    - name: Start Docker service
      service:
        name: docker
        state: started

Save this as pb4-git-docker.yml and run it:

ansible-playbook pb4-git-docker.yml

Ansible will work through the tasks in order. It installs Git, installs Docker, then starts the Docker service. Each task that causes a change will appear in yellow. The recap at the end tells you how many changes happened across how many servers.

This is where the real value of Ansible becomes obvious. A single playbook file handles all three tasks across every server in your inventory simultaneously. Imagine doing this manually across forty servers.


Two Ways to Write Module Arguments

You may have noticed there are two ways to write module arguments in YAML. Both are valid.

Inline style:

- name: Install Git
  yum: name=git state=present

Block style (recommended):

- name: Install Git
  yum:
    name: git
    state: present

The block style is more readable and is the standard convention. Use it consistently, especially when a module takes multiple arguments.


Creating Users and Copying Files

Ansible is not limited to package management. You can also create system users and copy files to remote machines.

First, create a file locally that you want to copy:

touch file2

Now write a playbook:

---
- name: Create users and copy files
  hosts: all
  tasks:
    - name: Create user
      user:
        name: alex
        state: present

    - name: Copy file to remote servers
      copy:
        src: file2
        dest: /home/ec2-user/

The user module creates system users. Give it a name and state: present to create the user.

The copy module copies a file from the Ansible master to the remote servers. src is the path to the file on the master. dest is where it should land on the remote machine.

After running this playbook, you can verify the file was copied by checking the remote machine directly or using an ad hoc command.


Removing Software with state: absent

The same playbook you used to install software can be used to remove it. The only change is the state value.

state: present installs a package. state: absent removes a package.

Instead of rewriting the whole playbook manually, you can use sed to do a global find-and-replace directly in the file:

sed -i 's/present/absent/g' pb4-git-docker.yml

This command replaces every occurrence of present with absent throughout the file. Verify the change with:

cat pb4-git-docker.yml

Now run the playbook again:

ansible-playbook pb4-git-docker.yml

Ansible will uninstall Git and Docker. However, notice what happens with the Docker service task. Once Docker is removed, Ansible tries to stop a service that no longer exists. That task will fail with an error. This brings us to an important concept.


Handling Expected Errors with ignore_errors

When you uninstall a package and then have a task that manages its service, that service task will naturally fail because the service no longer exists. In this case, the failure is expected and you do not want it to stop the playbook or alarm anyone reading the output.

Add ignore_errors: true to the task that you expect might fail:

- name: Start Docker service
  service:
    name: docker
    state: started
  ignore_errors: true

Now when you run the playbook, instead of showing a red failure, Ansible will show the word ignoring next to that task. The playbook continues running, and the recap reflects the ignored error clearly.

This is useful in patching workflows and uninstall scripts where certain follow-up tasks are not applicable once a package is gone.


Using the command Module

Sometimes you need to run a Linux command that does not have a dedicated Ansible module. For those situations, the command module lets you pass any shell command directly.

- name: Run a custom command
  command: your-linux-command-here

This is particularly useful when you need to do something specific to your Linux distribution, like enabling a package repository that is not available by default.


Installing Nginx on Amazon Linux 2

Here is where things get a little more interesting. If you try to install Nginx on Amazon Linux 2 using just yum install nginx, it will fail. The reason is that the Nginx package is not included in the default Amazon Linux 2 repositories.

Every Linux system has a local repository that the package manager checks before downloading and installing anything. If a package is not registered in that repository, the package manager cannot find it and the installation fails.

For Amazon Linux 2, Nginx is available through Amazon Linux Extras, which is a separate package management layer that provides additional software not in the default repos. You must enable the Nginx repository through Amazon Linux Extras before you can install it.

Here is the playbook:

---
- name: Install Nginx and start service
  hosts: all
  become: yes
  tasks:
    - name: Enable Nginx repository
      command: amazon-linux-extras enable nginx1

    - name: Install Nginx
      yum:
        name: nginx
        state: present

    - name: Start Nginx service
      service:
        name: nginx
        state: started

The first task uses the command module to run amazon-linux-extras enable nginx1, which registers the Nginx repository on the machine. Once that runs, the next yum task can find and install Nginx successfully.

After Nginx is installed and started, you can drop an index.html file into the Nginx web root and access your server's public IP in a browser to see the page load.


A Full Reference: Modules Covered

Here is a quick summary of every module used in this article:

Module What It Does
ping Tests connectivity to remote hosts
yum Installs or removes packages on RHEL/Amazon Linux
service Starts, stops, or restarts system services
debug Prints a message to the terminal output
user Creates or removes system users
copy Copies files from master to remote servers
command Runs any Linux shell command on remote servers

Ad Hoc Commands vs Playbooks: When to Use Which

A natural question comes up once you start working with playbooks: when should you use an ad hoc command and when should you write a playbook?

The answer is straightforward.

Use ad hoc commands for quick, one-time actions. Checking a service status, verifying a package version, reading a file. Anything you would type once and not need again.

Use playbooks for anything repeatable. Installing software stacks, configuring servers, deploying applications, patching systems. Any workflow you need to run more than once, or across multiple environments, belongs in a playbook.

Playbooks are also self-documenting. The name fields on every play and task describe exactly what is happening. When someone else (or future you) reads the playbook six months later, it is clear what it does without reading the actual module arguments.


Summary

Ansible playbooks are structured YAML files that define exactly what should happen on your remote servers. A playbook has a name, a target host group, an optional privilege escalation setting, and a list of tasks. Each task calls a module with specific arguments.

The key concepts covered here:

  • Always start playbooks with --- and validate syntax with --syntax-check before running

  • Ansible gathers system facts by default at the start of every playbook run

  • Green output means no change happened. Yellow means something changed. Red means something failed.

  • become: yes is required whenever your tasks need elevated permissions

  • state: present installs a package. state: absent removes it.

  • ignore_errors: true lets a playbook continue past a task that is expected to fail

  • The command module lets you run any Linux command when no dedicated module exists

  • Amazon Linux 2 requires enabling the Nginx repository through amazon-linux-extras before installing Nginx

One well-written playbook can manage hundreds of servers simultaneously. That is the point of Ansible, and playbooks are how you get there.

More from this blog

Stop Writing Repetitive Playbooks: Ansible Tags, Variables, Loops, Handlers and Conditions Decoded

Once you have written your first few Ansible playbooks, installed some packages, and started a couple of services, a natural question comes up: what else can Ansible do? The answer is quite a lot. This article walks through six features that turn a basic Ansible setup into something genuinely powerful: the Setup module, Tags, Variables, Loops, Handlers, and Conditional tasks. Each one builds on what you already know, and together they give you the tools to manage complex, real-world server environments cleanly and efficiently. Before going further, the environment being used here has one Ansible master and multiple worker nodes. Two are grouped as prod and two as dev inside /etc/ansible/hosts. SSH key-based authentication is already configured between the master and all worker nodes. If any of that is not set up yet, revisit passwordless SSH setup and inventory configuration before continuing.

Jun 21, 202615 min read
Stop Writing Repetitive Playbooks: Ansible Tags, Variables, Loops, Handlers and Conditions Decoded

You Have 400 Servers to Configure. Now What? Let Ansible Do the Work.

Picture this. You have four EC2 instances running in your AWS account, and someone asks you to install Apache on all four of them. What do you do? The obvious answer most people go with is SSH into each machine, run the install command, repeat. Simple enough when it is four servers. But what happens when it is forty? Or four hundred? In a real enterprise environment, that number is not exaggerated at all. That is exactly the problem that Ansible was built to solve. And once you understand what it does and how it thinks, you will wonder how anyone managed large infrastructure without it.

Jun 18, 202612 min read
You Have 400 Servers to Configure. Now What? Let Ansible Do the Work.

Building Production-Ready Docker Deployments with Secrets, Stacks, and Distroless Images

This post wraps up the core Docker Swarm curriculum by covering four important topics that complete the picture of production-ready containerized deployments. We start with Docker Secrets, which solves the real-world problem of passing sensitive credentials into containers without hardcoding them. We then look at Docker Stack, which is how you run multi-service Docker Compose files across a Swarm cluster instead of a single host. After that we cover the distinction between replicated and global services, which is a concept that appears in Kubernetes as well. We close with a look at Portainer for those who prefer a visual interface, and a brief introduction to distroless images. Each of these topics builds on everything covered so far. If you have your Swarm cluster running, you can follow along with every command shown here.

Jun 17, 202614 min read
Building Production-Ready Docker Deployments with Secrets, Stacks, and Distroless Images

Beyond One Server: Solving Docker Scaling with Swarm and Container Networks

This post covers three separate but deeply connected topics. We start by finishing what was started with Docker Hub, pushing all four bank service images to a remote registry so they survive beyond any single machine. From there, we identify a real architectural problem with single-host Docker deployments and introduce Docker Swarm as the solution. Finally, we close with Docker networking, explaining how containers communicate with each other both on the same host and across different hosts. By the end of this article, you will understand how to push and pull images from Docker Hub, how to set up a multi-node Docker Swarm cluster, how to create and scale services across that cluster, what self-healing means in practice, and how Docker networking works under the hood.

Jun 17, 202618 min read
Beyond One Server: Solving Docker Scaling with Swarm and Container Networks
S

Sai Praneeth's Blogs

38 posts

From SDLC and Agile to DevOps and CI/CD, this blog is where I share structured technical notes, concepts and practical insights in Cloud and DevOps engineering.