Devops

How to Create an Ansible Playbook That Actually Works

April 7, 2026
Published
#Ansible#Automation#DevOps#Infrastructure as Code#YAML

You don’t really understand Ansible until you write your first playbook. Running ad-hoc commands is useful, but playbooks are where automation becomes repeatable, version-controlled, and actually scalable.

Let’s build one from scratch and talk through the decisions along the way.

What is an Ansible Playbook (in practical terms)?

An Ansible playbook is just a YAML file that defines what should happen on which machines. Think of it as a script, but declarative — you describe the desired state, not the exact steps.

At its simplest, a playbook answers three questions:

  • Which hosts are we targeting?
  • What tasks should run?
  • With what configuration?

A minimal working playbook

Here’s the smallest useful playbook you can write:

YAML
1- name: Install nginx on web servers
2  hosts: web
3  become: true
4
5  tasks:
6    - name: Install nginx package
7      apt:
8        name: nginx
9        state: present
10

Save this as install-nginx.yml and run:

TEXT
1ansible-playbook -i inventory install-nginx.yml

That’s it. You’ve just automated package installation across multiple machines.

Breaking it down (without overcomplicating it)

hosts

This tells Ansible where to run:

TEXT
1hosts: web

“web” maps to a group in your inventory file.

become

This enables privilege escalation (sudo):

TEXT
1become: true

Without it, tasks like installing packages will fail on most systems.

tasks

This is where the actual work happens. Each task uses a module:

YAML
1- name: Install nginx package
2  apt:
3    name: nginx
4    state: present
5

A common mistake developers make is treating tasks like shell scripts. Avoid that when possible — prefer built-in modules like apt, yum, copy, and service.

Adding real-world usefulness

A playbook that only installs a package isn’t very helpful. Let’s extend it:

YAML
1- name: Configure web server
2  hosts: web
3  become: true
4
5  vars:
6    nginx_port: 8080
7
8  tasks:
9    - name: Install nginx
10      apt:
11        name: nginx
12        state: present
13        update_cache: true
14
15    - name: Copy custom config
16      copy:
17        src: nginx.conf
18        dest: /etc/nginx/nginx.conf
19
20    - name: Ensure nginx is running
21      service:
22        name: nginx
23        state: started
24        enabled: true
25

Now we’re doing three important things:

  • Installing the package
  • Applying configuration
  • Ensuring the service is running

This is the core pattern of most Ansible playbooks.

Inventory matters more than you think

Your playbook depends on a clean inventory. A simple one looks like:

TEXT
1[web]
2192.168.1.10
3192.168.1.11
4

You can also use hostnames, cloud inventories, or dynamic sources. As your infrastructure grows, keeping inventory organized becomes just as important as writing the playbook itself.

Variables: small feature, big impact

Hardcoding values is fine for demos, but not for real systems. Variables make playbooks reusable.

Example:

YAML
1vars:
2  app_port: 3000
3

Used inside a task:

JSON
1port: {{ app_port }}

Here’s where things get interesting — variables can come from multiple places:

  • Inline in the playbook
  • Inventory files
  • Group vars / host vars
  • Command line overrides

This flexibility is powerful, but can also get messy if you don’t stay consistent.

Handlers: avoid unnecessary restarts

If you restart services every time, your playbooks become disruptive. Handlers fix that.

YAML
1tasks:
2  - name: Update nginx config
3    copy:
4      src: nginx.conf
5      dest: /etc/nginx/nginx.conf
6    notify: Restart nginx
7
8handlers:
9  - name: Restart nginx
10    service:
11      name: nginx
12      state: restarted
13

The handler only runs if the file actually changes. That’s efficient and safer.

Idempotency (the quiet superpower)

Ansible playbooks are designed to be idempotent — running them multiple times shouldn’t break anything or repeat work unnecessarily.

For example:

  • Installing a package that already exists → no change
  • Copying an identical file → no change
  • Starting a running service → no change

If your playbook isn’t idempotent, it’s a sign something’s off.

Common pitfalls when creating playbooks

  • Overusing shell commands: Prefer modules whenever possible
  • Ignoring errors: Don’t silence failures without reason
  • Hardcoding values: Use variables early
  • Mixing concerns: Separate roles as complexity grows

When to move beyond a single playbook

Once your playbook grows past ~100 lines, consider splitting it into roles:

  • web server role
  • database role
  • app deployment role

This keeps things maintainable and reusable across projects.

A quick mental model

An Ansible playbook is not a script. It’s a declaration of system state.

That mindset shift makes everything else easier — from debugging to scaling automation.

Wrapping up

Creating an Ansible playbook isn’t complicated, but writing a good one takes a bit of discipline. Start simple, rely on modules, keep things idempotent, and introduce structure as your use cases grow.

If your playbook can be run twice without surprises and still leaves the system exactly how you expect, you’re on the right track.

Comments

Leave a comment on this article with your name, email, and message.

Loading comments...

Similar Articles

More posts from the same category you may want to read next.

Share: