Home DevOps Your First Ansible Playbook: Automate Server Setup Without the Pain
Beginner 5 min · July 11, 2026

Your First Ansible Playbook: Automate Server Setup Without the Pain

Write your first Ansible playbook with a real-world example.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Production
production tested
July 11, 2026
last updated
1,727
articles · all by Naren
Before you start⏱ 30 min
  • Ansible installed and working (test with ansible all -m ping). An inventory file with at least one target host.
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

An Ansible playbook is a YAML file with a list of tasks. Each task calls an Ansible module (like apt or copy). You run it with ansible-playbook playbook.yml. It connects via SSH, runs tasks in order, and reports changes.

✦ Definition~90s read
What is Your First Ansible Playbook?

An Ansible playbook is a YAML file that defines a set of tasks to run on remote servers. It's the core unit of automation in Ansible — you describe the desired state, and Ansible makes it happen, idempotently.

Think of a playbook as a recipe card for your server.
Plain-English First

Think of a playbook as a recipe card for your server. Instead of telling a chef each step in detail, you write 'install nginx, configure it, start it.' Ansible reads the recipe, checks what's already done, and only does what's missing. No overcooking, no burnt meals.

I've seen too many engineers SSH into 50 servers, running the same commands by hand, praying they don't fat-finger a config file at 2 AM. That's not DevOps — that's a disaster waiting to happen. Ansible playbooks automate that drudgery, and they do it without agents, without a database, and without a PhD in YAML.

The problem is simple: manual server setup is slow, error-prone, and impossible to audit. You need a repeatable, version-controlled way to configure servers. Ansible playbooks give you that. They describe the desired state of your infrastructure in plain YAML, and Ansible makes it so.

By the end of this article, you'll write a playbook that installs Nginx, configures a virtual host, deploys a static site, and ensures the service is running — all with a single command. You'll understand idempotency, modules, and the patterns that keep production playbooks clean.

What Is a Playbook? (And Why You Need One)

A playbook is a YAML file that tells Ansible what to do on your servers. It's a list of 'plays' — each play targets a group of hosts and runs a set of tasks. Tasks call modules, which are the actual tools that install packages, copy files, restart services, etc.

Without a playbook, you're SSHing into boxes and typing commands. That works for one server. For ten? You'll make mistakes. For a hundred? You'll cry. A playbook is your single source of truth — run it, and every server ends up in the exact same state.

Here's the kicker: playbooks are idempotent. Run them once, run them ten times — same result. That's because modules check the current state before making changes. If Nginx is already installed, the apt module skips it. No wasted time, no side effects.

first-playbook.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — DevOps tutorial

---
- name: Ensure Nginx is installed and running
  hosts: webservers
  become: yes
  tasks:
    - name: Install Nginx
      apt:
        name: nginx
        state: present
    - name: Ensure Nginx is running
      service:
        name: nginx
        state: started
        enabled: yes
Output
PLAY [Ensure Nginx is installed and running] ************************************
TASK [Gathering Facts] *********************************************************
ok: [web1.example.com]
TASK [Install Nginx] ***********************************************************
changed: [web1.example.com]
TASK [Ensure Nginx is running] *************************************************
changed: [web1.example.com]
PLAY RECAP *********************************************************************
web1.example.com : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Senior Shortcut:
Always use become: yes for tasks that need root. It's cleaner than sudo in every command. And never run playbooks as root directly — use a regular user with sudo.

Anatomy of a Playbook: Hosts, Tasks, and Modules

Every playbook starts with three things: a name (optional but recommended), a hosts line that tells Ansible which servers to run on, and a list of tasks. Tasks are the workhorses — each one calls a module with specific parameters.

Modules are the secret sauce. Ansible ships with hundreds — apt, yum, copy, template, service, user, file, command, and more. Each module is a self-contained unit that does one thing: install a package, copy a file, start a service. You don't write shell scripts; you declare the desired state.

For example, instead of writing apt-get install -y nginx, you write:

``yaml - name: Install Nginx apt: name: nginx state: present ``

Ansible checks if Nginx is already installed. If yes, it does nothing. If no, it installs it. That's idempotency in action.

playbook-anatomy.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// io.thecodeforge — DevOps tutorial

---
- name: Configure web server
  hosts: webservers
  become: yes
  vars:
    http_port: 80
    max_clients: 200
  tasks:
    - name: Install Nginx
      apt:
        name: nginx
        state: present
    - name: Create web root directory
      file:
        path: /var/www/myapp
        state: directory
        owner: www-data
        group: www-data
        mode: '0755'
    - name: Copy index.html
      copy:
        src: files/index.html
        dest: /var/www/myapp/index.html
        owner: www-data
        group: www-data
        mode: '0644'
    - name: Start Nginx
      service:
        name: nginx
        state: started
        enabled: yes
Output
PLAY [Configure web server] ***************************************************
TASK [Gathering Facts] *********************************************************
ok: [web1.example.com]
TASK [Install Nginx] ***********************************************************
ok: [web1.example.com]
TASK [Create web root directory] ***********************************************
changed: [web1.example.com]
TASK [Copy index.html] *********************************************************
changed: [web1.example.com]
TASK [Start Nginx] *************************************************************
ok: [web1.example.com]
PLAY RECAP *********************************************************************
web1.example.com : ok=5 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Production Trap:

Your First Playbook: Install and Configure Nginx

Let's write a real playbook. We'll install Nginx, create a custom web root, copy an index.html, and ensure the service is running. This is the pattern you'll use for 90% of your automation.

`` project/ ├── playbook.yml ├── files/ │ └── index.html └── ansible.cfg ``

The ansible.cfg file sets defaults. For a beginner, the most important setting is host_key_checking = False — otherwise Ansible will prompt you to accept SSH host keys on first connection.

Now the playbook. We'll use the apt module for package installation, file for directory creation, copy for files, and service for service management.

setup-nginx.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// io.thecodeforge — DevOps tutorial

---
- name: Setup Nginx with custom web root
  hosts: webservers
  become: yes
  vars:
    web_root: /var/www/myapp
  tasks:
    - name: Install Nginx
      apt:
        name: nginx
        state: present
        update_cache: yes
    - name: Create web root directory
      file:
        path: "{{ web_root }}"
        state: directory
        owner: www-data
        group: www-data
        mode: '0755'
    - name: Copy index.html
      copy:
        src: files/index.html
        dest: "{{ web_root }}/index.html"
        owner: www-data
        group: www-data
        mode: '0644'
    - name: Ensure Nginx is running
      service:
        name: nginx
        state: started
        enabled: yes
Output
PLAY [Setup Nginx with custom web root] **************************************
TASK [Gathering Facts] *********************************************************
ok: [web1.example.com]
TASK [Install Nginx] ***********************************************************
changed: [web1.example.com]
TASK [Create web root directory] ***********************************************
changed: [web1.example.com]
TASK [Copy index.html] *********************************************************
changed: [web1.example.com]
TASK [Ensure Nginx is running] *************************************************
ok: [web1.example.com]
PLAY RECAP *********************************************************************
web1.example.com : ok=5 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
The Classic Bug:
Forgetting update_cache: yes on the apt module means Ansible uses the local package cache. If the cache is stale, you might get 'package not found'. Always include it on the first apt task.

Running Your Playbook: The Command That Matters

``bash ansible-playbook -i inventory.ini setup-nginx.yml ``

But wait — you need an inventory file. The inventory tells Ansible which servers to connect to. For a single server, create inventory.ini:

``ini [webservers] web1.example.com ansible_user=deploy ``

Replace web1.example.com with your server's IP or hostname, and deploy with your SSH user. Make sure you can SSH into that server without a password (use SSH keys).

Now run the playbook. You'll see output like the example above. Green ok means no change, yellow changed means Ansible made a change. Red failed means something broke — check the error message.

Pro tip: Always run with --check first to see what would change without actually doing it:

``bash ansible-playbook -i inventory.ini setup-nginx.yml --check ``

run-playbook.shBASH
1
2
3
4
5
6
7
8
9
10
// io.thecodeforge — DevOps tutorial

# Run the playbook with check mode first
ansible-playbook -i inventory.ini setup-nginx.yml --check

# If that looks good, run for real
ansible-playbook -i inventory.ini setup-nginx.yml

# To see verbose output (useful for debugging)
ansible-playbook -i inventory.ini setup-nginx.yml -v
Output
PLAY [Setup Nginx with custom web root] **************************************
TASK [Gathering Facts] *********************************************************
ok: [web1.example.com]
TASK [Install Nginx] ***********************************************************
ok: [web1.example.com]
TASK [Create web root directory] ***********************************************
ok: [web1.example.com]
TASK [Copy index.html] *********************************************************
ok: [web1.example.com]
TASK [Ensure Nginx is running] *************************************************
ok: [web1.example.com]
PLAY RECAP *********************************************************************
web1.example.com : ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Senior Shortcut:
Use ansible-playbook --syntax-check playbook.yml to validate YAML before running. Catches indentation errors fast.

Idempotency: The Superpower You Didn't Know You Needed

Idempotency means running the same playbook multiple times produces the same result. No side effects, no duplicate work. Ansible achieves this because modules check the current state before making changes.

For example, the apt module checks if a package is already installed. If it is, it skips the install. The service module checks if a service is running. If it is, it skips the start. The copy module checks if the file content matches. If it does, it skips the copy.

This is huge. You can run a playbook daily as a cron job to enforce state. If someone manually changes a config file, the next run will fix it. No more drift.

But idempotency only works if you use the right modules. The command and shell modules are NOT idempotent — they run every time. Use them only when no module exists, and add creates or changed_when to control behavior.

idempotent-vs-non.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge — DevOps tutorial

---
- name: Compare idempotent vs non-idempotent
  hosts: localhost
  connection: local
  tasks:
    # Idempotent: file module
    - name: Create a file (idempotent)
      file:
        path: /tmp/test.txt
        state: touch
    # Non-idempotent: command module
    - name: Create a file (non-idempotent)
      command: touch /tmp/test2.txt
      # This runs every time, even if file exists
    # Fix: add creates to make it idempotent
    - name: Create a file (now idempotent)
      command: touch /tmp/test3.txt
      args:
        creates: /tmp/test3.txt
Output
PLAY [Compare idempotent vs non-idempotent] ***********************************
TASK [Gathering Facts] *********************************************************
ok: [localhost]
TASK [Create a file (idempotent)] **********************************************
changed: [localhost]
TASK [Create a file (non-idempotent)] ******************************************
changed: [localhost]
TASK [Create a file (now idempotent)] ******************************************
ok: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Never Do This:
Using shell or command for package installation or service management. Use the dedicated modules (apt, service, etc.) — they handle idempotency, error handling, and cross-platform differences.

Variables and Templates: Making Playbooks Dynamic

Hardcoding values in playbooks is a rookie move. Use variables to make your playbooks reusable across environments. Define variables in the playbook itself, in separate files, or in the inventory.

For example, you might have different web roots for staging and production. Define a variable web_root and reference it with {{ web_root }}.

Templates take this further. Ansible uses Jinja2 templating to generate config files dynamically. You create a template file with .j2 extension, and the template module fills in variables.

Here's a real pattern: deploy an Nginx virtual host config using a template. The template contains placeholders like {{ server_name }} and {{ web_root }}. Ansible fills them in based on variables you define.

with-template.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// io.thecodeforge — DevOps tutorial

---
- name: Deploy Nginx config with template
  hosts: webservers
  become: yes
  vars:
    server_name: example.com
    web_root: /var/www/example
  tasks:
    - name: Install Nginx
      apt:
        name: nginx
        state: present
        update_cache: yes
    - name: Create web root
      file:
        path: "{{ web_root }}"
        state: directory
        owner: www-data
        group: www-data
        mode: '0755'
    - name: Deploy virtual host config
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/{{ server_name }}
        owner: root
        group: root
        mode: '0644'
      notify: restart nginx
    - name: Enable site
      file:
        src: /etc/nginx/sites-available/{{ server_name }}
        dest: /etc/nginx/sites-enabled/{{ server_name }}
        state: link
      notify: restart nginx
  handlers:
    - name: restart nginx
      service:
        name: nginx
        state: restarted
Output
PLAY [Deploy Nginx config with template] *************************************
TASK [Gathering Facts] *********************************************************
ok: [web1.example.com]
TASK [Install Nginx] ***********************************************************
ok: [web1.example.com]
TASK [Create web root] *********************************************************
changed: [web1.example.com]
TASK [Deploy virtual host config] **********************************************
changed: [web1.example.com]
TASK [Enable site] *************************************************************
changed: [web1.example.com]
RUNNING HANDLER [restart nginx] ************************************************
changed: [web1.example.com]
PLAY RECAP *********************************************************************
web1.example.com : ok=6 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Production Trap:
Handlers run only at the end of the play, not immediately after the task that notifies them. If you need a service restarted before the next task, use meta: flush_handlers or restart directly in the task.

Handlers: The Elegant Way to Restart Services

Handlers are special tasks that run only when notified by another task. They're perfect for restarting services after a config change. You define handlers in the handlers section of a play, and tasks notify them using the notify keyword.

In the previous example, the template task notifies restart nginx. But here's the magic: if the template task doesn't change anything (because the file is already up-to-date), the handler is NOT called. No unnecessary restarts.

This is a huge win in production. You don't want to restart Nginx every time you run the playbook — only when the config actually changes. Handlers give you that automatically.

handlers-example.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// io.thecodeforge — DevOps tutorial

---
- name: Demonstrate handlers
  hosts: webservers
  become: yes
  tasks:
    - name: Update Nginx config
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify: restart nginx
    - name: Update site config
      template:
        src: templates/site.conf.j2
        dest: /etc/nginx/sites-available/default
      notify: restart nginx
  handlers:
    - name: restart nginx
      service:
        name: nginx
        state: restarted
    - name: reload nginx
      service:
        name: nginx
        state: reloaded
Output
PLAY [Demonstrate handlers] ***************************************************
TASK [Gathering Facts] *********************************************************
ok: [web1.example.com]
TASK [Update Nginx config] *****************************************************
changed: [web1.example.com]
TASK [Update site config] ******************************************************
ok: [web1.example.com]
RUNNING HANDLER [restart nginx] ************************************************
changed: [web1.example.com]
PLAY RECAP *********************************************************************
web1.example.com : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Senior Shortcut:
Use reload instead of restart for services that support it (like Nginx). Reload doesn't drop connections — it gracefully applies config changes. Define both handlers and choose based on the change.

Debugging Playbooks: What to Do When Things Go Wrong

Playbooks fail. It's a fact of life. The most common failures: SSH connection issues, incorrect module parameters, missing files, and permission errors.

First, increase verbosity with -v, -vv, or -vvv. The more v's, the more output. -vvv shows SSH commands and module arguments — invaluable for debugging.

``yaml - name: Print web_root debug: var: web_root ``

Third, use --step to run tasks one by one, confirming each before execution. This lets you see exactly where it fails.

Fourth, check the error message. Ansible errors are usually clear: "file not found", "permission denied", "module not found". Read them carefully.

``bash ansible webservers -m ping ansible webservers -m setup ``

The setup module gathers facts about the server — useful for debugging variables.

debug-playbook.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — DevOps tutorial

---
- name: Debug example
  hosts: webservers
  become: yes
  vars:
    web_root: /var/www/myapp
  tasks:
    - name: Check if web_root exists
      stat:
        path: "{{ web_root }}"
      register: dir_status
    - name: Print directory status
      debug:
        var: dir_status.stat.exists
    - name: Fail if directory doesn't exist
      fail:
        msg: "Directory {{ web_root }} does not exist!"
      when: not dir_status.stat.exists
Output
PLAY [Debug example] **********************************************************
TASK [Gathering Facts] *********************************************************
ok: [web1.example.com]
TASK [Check if web_root exists] ************************************************
ok: [web1.example.com]
TASK [Print directory status] **************************************************
ok: [web1.example.com] => {
"dir_status.stat.exists": true
}
TASK [Fail if directory doesn't exist] *****************************************
skipping: [web1.example.com]
PLAY RECAP *********************************************************************
web1.example.com : ok=3 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
The Classic Bug:
Using register with a module that doesn't return the expected output. Always check the module documentation for the return values. Use debug to inspect the registered variable.

Organizing Playbooks: Roles and Directory Structure

As your automation grows, a single playbook file becomes unwieldy. Enter roles. A role is a self-contained unit of automation — it has its own tasks, handlers, templates, files, and variables. Roles let you reuse and share code.

`` roles/ └── nginx/ ├── tasks/ │ └── main.yml ├── handlers/ │ └── main.yml ├── templates/ │ └── nginx.conf.j2 ├── files/ │ └── index.html ├── vars/ │ └── main.yml └── defaults/ └── main.yml ``

``yaml - name: Apply nginx role hosts: webservers become: yes roles: - nginx ``

Roles are the standard way to organize production playbooks. They make your code modular, testable, and shareable via Ansible Galaxy.

site.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge — DevOps tutorial

---
- name: Apply common configuration to all servers
  hosts: all
  become: yes
  roles:
    - common

- name: Configure web servers
  hosts: webservers
  become: yes
  roles:
    - nginx
    - php

- name: Configure database servers
  hosts: databases
  become: yes
  roles:
    - mysql
Output
PLAY [Apply common configuration to all servers] ******************************
TASK [Gathering Facts] *********************************************************
ok: [web1.example.com]
...
PLAY [Configure web servers] ***************************************************
TASK [Gathering Facts] *********************************************************
ok: [web1.example.com]
...
PLAY [Configure database servers] **********************************************
TASK [Gathering Facts] *********************************************************
ok: [db1.example.com]
...
PLAY RECAP *********************************************************************
web1.example.com : ok=15 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
db1.example.com : ok=10 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Senior Shortcut:
Use ansible-galaxy init role_name to scaffold a new role directory structure. It creates all the subdirectories and main.yml files for you.

When Not to Use Ansible Playbooks

Ansible is great for configuration management and application deployment. But it's not a silver bullet. Here's when you should reach for something else:

  • Orchestration of complex, multi-step workflows: If you need to coordinate a series of actions across multiple services with rollback logic, consider a dedicated workflow engine like Airflow or a CI/CD pipeline.
  • Real-time, event-driven automation: Ansible is pull-based (you run it). For event-driven automation (e.g., auto-scaling), use tools like Terraform with cloud provider APIs or Kubernetes operators.
  • Immutable infrastructure: If you're all-in on containers and immutable servers, you might not need Ansible at all. Use Dockerfiles and Kubernetes manifests instead.
  • Windows environments: Ansible works with Windows via WinRM, but it's not as mature as Linux support. For Windows-heavy shops, consider PowerShell DSC.

My rule of thumb: if you're SSHing into a server to change a config file, use Ansible. If you're provisioning cloud resources, use Terraform. If you're deploying containers, use Kubernetes.

Interview Gold:
"When would you choose Ansible over Terraform?" Answer: Ansible for configuration management (installing packages, managing services, deploying apps). Terraform for infrastructure provisioning (VMs, networks, load balancers). They complement each other — use both.
● Production incidentPOST-MORTEMseverity: high

The Playbook That Deleted Logs

Symptom
After running a 'cleanup' playbook, all application logs on 20 servers vanished. No backups.
Assumption
Someone thought the playbook was only removing old rotated logs.
Root cause
A shell module task with rm -rf /var/log/app/* — no creates or when condition. The glob expanded to nothing on one server, and rm -rf with no arguments does nothing, but a typo in the path caused deletion of the entire log directory.
Fix
Replace shell with the file module using state=absent on specific files. Add check_mode: yes before running. Use --check flag.
Key lesson
  • Never use shell or command for destructive operations if a module exists.
  • Always run playbooks with --check first.
Production debug guideSystematic recovery paths for the failure modes engineers actually hit.3 entries
Symptom · 01
SSH connection timeout: 'Failed to connect to the host via ssh: Connection timed out'
Fix
1. Verify the host is reachable: ping host or ssh user@host manually. 2. Check firewall rules (port 22 open). 3. Ensure SSH key is added to agent: ssh-add -l. 4. In ansible.cfg, set timeout = 30.
Symptom · 02
Permission denied: 'Failed to change ownership: chown failed: Operation not permitted'
Fix
1. Check if become: yes is set on the play or task. 2. Verify the remote user has sudo privileges. 3. Test with ansible host -m shell -a 'whoami' -b.
Symptom · 03
Module not found: 'ERROR! no action detected in task'
Fix
1. Check module name spelling (case-sensitive). 2. Ensure the module is installed (e.g., community.general for some modules). 3. Run ansible-doc -l | grep module_name to verify.
★ Your First Ansible Playbook Triage Cheat SheetFirst-response commands for when things go wrong — copy-paste ready.
Playbook fails with 'ERROR! no action detected in task'
Immediate action
Check YAML indentation — most common cause.
Commands
ansible-playbook --syntax-check playbook.yml
python3 -c "import yaml; yaml.safe_load(open('playbook.yml'))"
Fix now
Fix indentation: tasks must be a list under each play, each task is a dict with 'name' and module key.
Task reports 'ok' but you expected a change+
Immediate action
Check if the module is idempotent and the desired state is already met.
Commands
ansible host -m setup | grep ansible_os_family
ansible host -m apt -a 'name=nginx state=present' --check
Fix now
If using command module, add creates or changed_when to force change detection.
SSH connection timeout on first run+
Immediate action
Ensure host key is accepted or disable host key checking.
Commands
ssh-keyscan host >> ~/.ssh/known_hosts
echo 'host_key_checking = False' >> ansible.cfg
Fix now
Set host_key_checking = False in ansible.cfg for initial setup, then revert after first connection.
Variable not defined error: 'The task includes an option with an undefined variable'+
Immediate action
Check variable name spelling and scope.
Commands
ansible host -m debug -a 'var=variable_name'
grep -r 'variable_name' .
Fix now
Define the variable in vars, vars_files, group_vars, or host_vars.
Feature / AspectAnsible PlaybookShell Script
IdempotentYes (by default with modules)No (must implement manually)
State managementDeclarative (desired state)Imperative (step-by-step)
Error handlingBuilt-in (fail on error, rescue blocks)Manual (exit codes, traps)
Cross-platformYes (modules abstract OS differences)No (OS-specific commands)
Learning curveModerate (YAML + modules)Low (bash basics)
ReusabilityHigh (roles, variables, templates)Low (copy-paste)
⚙ Quick Reference
9 commands from this guide
FileCommand / CodePurpose
first-playbook.yml- name: Ensure Nginx is installed and runningWhat Is a Playbook? (And Why You Need One)
playbook-anatomy.yml- name: Configure web serverAnatomy of a Playbook
setup-nginx.yml- name: Setup Nginx with custom web rootYour First Playbook
run-playbook.shansible-playbook -i inventory.ini setup-nginx.yml --checkRunning Your Playbook
idempotent-vs-non.yml- name: Compare idempotent vs non-idempotentIdempotency
with-template.yml- name: Deploy Nginx config with templateVariables and Templates
handlers-example.yml- name: Demonstrate handlersHandlers
debug-playbook.yml- name: Debug exampleDebugging Playbooks
site.yml- name: Apply common configuration to all serversOrganizing Playbooks

Key takeaways

1
A playbook is a YAML file with hosts, tasks, and optionally vars and handlers.
2
Use --syntax-check, --check --diff, and -v/-vvv to debug playbooks.
3
Handlers run only when notified by a task that reports 'changed'.
4
Use --limit to run against a subset of hosts for testing.

Common mistakes to avoid

3 patterns
×

Handlers that never run

Fix
The handler name in notify: must exactly match the handler's name: — including capitalization. Verify with --list-tasks.
×

YAML indentation errors

Fix
Ansible uses strict YAML indentation. Use 2-space indentation consistently. Run --syntax-check before every execution.
×

Not using --check before first run

Fix
Always run ansible-playbook playbook.yml --check --diff before the first real execution to verify what will change.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does Ansible ensure idempotency when using the `command` module?
Q02SENIOR
When would you choose Ansible over Terraform for infrastructure automati...
Q03SENIOR
What happens if a handler is notified by multiple tasks? Does it run mul...
Q04JUNIOR
What is the difference between `vars` and `defaults` in an Ansible role?
Q05SENIOR
A playbook fails with 'Failed to connect to the host via ssh: Connection...
Q06SENIOR
How would you design a playbook to deploy a microservice across 100 serv...
Q01 of 06SENIOR

How does Ansible ensure idempotency when using the `command` module?

ANSWER
The command module is not idempotent by default — it runs every time. To make it idempotent, use the creates parameter to specify a file that, if exists, skips the command. Alternatively, use changed_when to define when the task should report 'changed'.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
What is the minimal viable Ansible playbook?
02
How do I pass variables to a playbook?
03
What's a handler and when should I use one?
04
How do I debug a playbook that isn't working?
05
What's the difference between import_playbook and include_tasks?
06
How do I run a playbook against a single host?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Verified
production tested
July 11, 2026
last updated
1,727
articles · all by Naren
🔥

That's Ansible. Mark it forged?

5 min read · try the examples if you haven't

Previous
Ansible Ad-Hoc Commands Guide
30 / 37 · Ansible
Next
Ansible Installation Guide