Ansible playbooks are the foundation of automation in Ansible, allowing you to define complex IT infrastructure configurations and orchestrations in simple YAML files. This comprehensive guide covers everything from basic playbook structure to advanced concepts like handlers, variables, and multi-OS support with practical, production-ready examples.
📑 Table of Contents
- What is an Ansible Playbook?
- Basic Playbook Structure
- Key Components Explained
- Complete Example: Web Server Setup
- Understanding Key Concepts
- 1. Variables
- 2. Ansible Facts
- 3. Conditionals (when)
- 4. Handlers
- Running the Playbook
- Create an Inventory File
- Execute the Playbook
- Best Practices
- 1. Always Name Your Tasks
- 2. Use Idempotent Modules
- 3. Use Variables for Reusability
- 4. Use become Wisely
- 5. Organize with Comments
- Common Patterns and Examples
- Installing Multiple Packages
- Creating Directories with Proper Permissions
- Deploying Configuration Files
- Conditional Service Management
- Troubleshooting Common Issues
- Issue: “Permission denied”
- Issue: “Module not found”
- Issue: Tasks not idempotent
- Issue: Slow playbook execution
- Advanced Tips
- Using ansible.cfg for Defaults
- Tags for Selective Execution
- Next Steps
- Summary
What is an Ansible Playbook?
An Ansible playbook is a YAML file that defines a series of tasks to be executed on remote hosts. Think of it as a recipe or instruction manual that tells Ansible exactly what to do, where to do it, and how to do it. Playbooks are:
- Declarative: You describe the desired state, not the steps to get there
- Idempotent: Running the same playbook multiple times produces the same result
- Human-readable: Written in YAML, easy to understand and maintain
- Version-controlled: Can be stored in Git for collaboration and history
Basic Playbook Structure
Every Ansible playbook consists of one or more “plays”. Each play maps a group of hosts to a set of tasks. Here’s the anatomy of a playbook:
---
- name: Play Name
hosts: target_hosts
become: yes
gather_facts: yes
vars:
variable_name: value
tasks:
- name: Task description
module_name:
parameter: value
handlers:
- name: Handler name
module_name:
parameter: value
Key Components Explained
- name: Descriptive name for the play (optional but recommended)
- hosts: Target hosts or groups from inventory
- become: Execute tasks with elevated privileges (sudo)
- gather_facts: Collect system information before running tasks
- vars: Variables used throughout the playbook
- tasks: List of actions to perform
- handlers: Tasks triggered by other tasks (usually for service restarts)
Complete Example: Web Server Setup
Let’s build a complete playbook that sets up Apache web server across different Linux distributions. This example demonstrates variables, conditionals, handlers, and best practices:
---
# =============================================================================
# Basic Web Server Setup - Complete Ansible Playbook Example
# =============================================================================
# This playbook installs and configures Apache web server on Ubuntu/Debian
# and RHEL/CentOS systems, demonstrating cross-platform automation
- name: Basic Web Server Setup
hosts: webservers
become: yes
gather_facts: yes
vars:
# Configuration variables
http_port: 80
https_port: 443
max_clients: 200
server_admin: admin@example.com
tasks:
# ==========================================================================
# TASK 1: Update Package Cache
# ==========================================================================
# Before installing packages, we need to update the package cache
# This is OS-specific, so we use 'when' conditionals
- name: Update apt package cache (Debian/Ubuntu)
apt:
update_cache: yes
cache_valid_time: 3600 # Cache valid for 1 hour
when: ansible_os_family == "Debian"
- name: Update yum package cache (RHEL/CentOS)
yum:
update_cache: yes
when: ansible_os_family == "RedHat"
# ==========================================================================
# TASK 2: Install Apache Web Server
# ==========================================================================
# Apache package name differs between distributions:
# - Debian/Ubuntu: apache2
# - RHEL/CentOS: httpd
- name: Install Apache on Debian/Ubuntu
apt:
name: apache2
state: present # Ensure package is installed
when: ansible_os_family == "Debian"
notify: Restart Apache Debian # Trigger handler if package installed
- name: Install Apache on RHEL/CentOS
yum:
name: httpd
state: present
when: ansible_os_family == "RedHat"
notify: Restart Apache RedHat
# ==========================================================================
# TASK 3: Ensure Apache is Running and Enabled
# ==========================================================================
# Service module ensures Apache starts on boot and is currently running
- name: Ensure Apache is running (Debian)
service:
name: apache2
state: started # Start service if not running
enabled: yes # Enable service to start on boot
when: ansible_os_family == "Debian"
- name: Ensure Apache is running (RHEL)
service:
name: httpd
state: started
enabled: yes
when: ansible_os_family == "RedHat"
# ==========================================================================
# TASK 4: Display Server Information
# ==========================================================================
# Debug module displays information during playbook execution
# Using multi-line format with Jinja2 variables
- name: Display server information
debug:
msg: |
Server: {{ ansible_hostname }}
OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
IP Address: {{ ansible_default_ipv4.address }}
HTTP Port: {{ http_port }}
HTTPS Port: {{ https_port }}
Admin Email: {{ server_admin }}
# ============================================================================
# HANDLERS - Execute only when notified by tasks
# ============================================================================
# Handlers run only once at the end of the play, even if notified multiple times
# They're typically used for service restarts after configuration changes
handlers:
- name: Restart Apache Debian
service:
name: apache2
state: restarted
- name: Restart Apache RedHat
service:
name: httpd
state: restarted
Understanding Key Concepts
1. Variables
Variables make playbooks reusable and flexible. In our example, we define configuration values like ports and admin email:
vars:
http_port: 80
https_port: 443
max_clients: 200
server_admin: admin@example.com
Variables are referenced using Jinja2 syntax: {{ variable_name }}
2. Ansible Facts
When gather_facts: yes
is set, Ansible automatically collects system information. Common facts include:
ansible_hostname
– Server hostnameansible_distribution
– OS distribution (Ubuntu, CentOS, etc.)ansible_distribution_version
– OS version numberansible_os_family
– OS family (Debian, RedHat, etc.)ansible_default_ipv4.address
– Primary IP addressansible_memtotal_mb
– Total memory in MBansible_processor_vcpus
– Number of CPU cores
3. Conditionals (when)
The when
clause allows conditional task execution. This is crucial for cross-platform playbooks:
- name: Install Apache on Debian/Ubuntu
apt:
name: apache2
state: present
when: ansible_os_family == "Debian"
This task only runs on Debian-based systems. Common conditional patterns:
# Equality check
when: environment == "production"
# Multiple conditions (AND)
when:
- ansible_distribution == "Ubuntu"
- ansible_distribution_version >= "20.04"
# OR conditions
when: environment == "dev" or environment == "staging"
# Variable defined check
when: database_host is defined
# String matching
when: ansible_hostname is match('web.*')
4. Handlers
Handlers are special tasks that run only when notified and only once per play, regardless of how many times they’re notified. They’re perfect for service restarts:
tasks:
- name: Update Apache configuration
template:
src: apache.conf.j2
dest: /etc/apache2/apache2.conf
notify: Restart Apache # Triggers handler
handlers:
- name: Restart Apache
service:
name: apache2
state: restarted
Key points about handlers:
- Only run if the task that notifies them reports a “changed” status
- Run at the end of the play, not immediately
- Run only once even if notified multiple times
- Run in the order they’re defined, not the order they’re notified
Running the Playbook
Create an Inventory File
First, create an inventory file that defines your target servers:
# inventory
[webservers]
web01 ansible_host=192.168.1.10 ansible_user=ubuntu
web02 ansible_host=192.168.1.11 ansible_user=ubuntu
web03 ansible_host=192.168.1.12 ansible_user=centos
[webservers:vars]
ansible_ssh_private_key_file=~/.ssh/id_rsa
Execute the Playbook
# Basic execution
ansible-playbook -i inventory basic-webserver.yml
# Check mode (dry run - doesn't make changes)
ansible-playbook -i inventory basic-webserver.yml --check
# Verbose output (helpful for debugging)
ansible-playbook -i inventory basic-webserver.yml -v # Level 1
ansible-playbook -i inventory basic-webserver.yml -vv # Level 2
ansible-playbook -i inventory basic-webserver.yml -vvv # Level 3
# Limit to specific hosts
ansible-playbook -i inventory basic-webserver.yml --limit web01
# With extra variables (override defaults)
ansible-playbook -i inventory basic-webserver.yml -e "http_port=8080"
# Step-by-step execution (confirm each task)
ansible-playbook -i inventory basic-webserver.yml --step
Best Practices
1. Always Name Your Tasks
Descriptive task names make playbook output readable and help with debugging:
# Good
- name: Install Nginx web server on Ubuntu 20.04
apt:
name: nginx
state: present
# Bad (no name)
- apt:
name: nginx
state: present
2. Use Idempotent Modules
Most Ansible modules are idempotent – running them multiple times produces the same result:
# Idempotent - safe to run multiple times
- name: Ensure user exists
user:
name: webadmin
state: present
# NOT idempotent - appends every time
- name: Add line to file
shell: echo "config=value" >> /etc/config.conf # WRONG
# Idempotent alternative
- name: Add line to file
lineinfile:
path: /etc/config.conf
line: "config=value"
state: present
3. Use Variables for Reusability
Extract configuration values into variables:
# Good - easy to change
vars:
app_port: 8080
app_user: webapp
tasks:
- name: Create app user
user:
name: "{{ app_user }}"
# Bad - hardcoded values
tasks:
- name: Create app user
user:
name: webapp
4. Use become Wisely
Apply privilege escalation only where needed:
# At play level (all tasks run as root)
- name: System configuration
hosts: servers
become: yes
tasks: [...]
# At task level (only specific tasks run as root)
- name: Mixed privilege tasks
hosts: servers
tasks:
- name: Read user file (no sudo needed)
command: cat ~/file.txt
- name: Install package (needs sudo)
apt:
name: nginx
state: present
become: yes
5. Organize with Comments
Use comments to explain complex logic:
tasks:
# First, we need to stop the service to prevent lock conflicts
- name: Stop application service
service:
name: myapp
state: stopped
# Update configuration files while service is stopped
- name: Deploy new configuration
template:
src: app.conf.j2
dest: /etc/myapp/app.conf
# Finally, restart with new configuration
- name: Start application service
service:
name: myapp
state: started
Common Patterns and Examples
Installing Multiple Packages
- name: Install required packages
apt:
name:
- vim
- git
- htop
- curl
- wget
- python3-pip
state: present
update_cache: yes
Creating Directories with Proper Permissions
- name: Create application directories
file:
path: "{{ item }}"
state: directory
mode: '0755'
owner: www-data
group: www-data
loop:
- /var/www/myapp
- /var/www/myapp/uploads
- /var/www/myapp/logs
Deploying Configuration Files
- name: Deploy Apache virtual host configuration
copy:
content: |
ServerName {{ ansible_hostname }}
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
dest: /etc/apache2/sites-available/mysite.conf
mode: '0644'
notify: Reload Apache
Conditional Service Management
- name: Check if service exists
stat:
path: /etc/systemd/system/myapp.service
register: service_status
- name: Start service if exists
service:
name: myapp
state: started
when: service_status.stat.exists
Troubleshooting Common Issues
Issue: “Permission denied”
Solution: Add become: yes
to tasks that need root privileges:
- name: Install package
apt:
name: nginx
state: present
become: yes
Issue: “Module not found”
Solution: Update Ansible or install required collections:
# Update Ansible
pip install --upgrade ansible
# Install specific collection
ansible-galaxy collection install community.general
Issue: Tasks not idempotent
Solution: Use changed_when
to control change reporting:
- name: Check service status
command: systemctl status nginx
register: result
changed_when: false # Never report as changed
failed_when: result.rc not in [0, 3] # 0=running, 3=stopped
Issue: Slow playbook execution
Solution: Disable fact gathering if not needed:
- name: Quick tasks
hosts: servers
gather_facts: no # Skip fact gathering
tasks: [...]
Advanced Tips
Using ansible.cfg for Defaults
Create ansible.cfg
in your project directory:
[defaults]
inventory = ./inventory
host_key_checking = False
retry_files_enabled = False
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
[privilege_escalation]
become = True
become_method = sudo
become_user = root
Tags for Selective Execution
tasks:
- name: Install packages
apt:
name: nginx
state: present
tags: ['install', 'packages']
- name: Configure service
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
tags: ['config', 'nginx']
# Run only tagged tasks
# ansible-playbook playbook.yml --tags "install"
# ansible-playbook playbook.yml --skip-tags "config"
Next Steps
Now that you understand basic playbooks, you can explore:
- Jinja2 Templates: Create dynamic configuration files
- Loops and Iteration: Process multiple items efficiently
- Roles: Organize playbooks into reusable components
- Ansible Vault: Encrypt sensitive data
- Dynamic Inventory: Pull inventory from cloud providers
Summary
Ansible playbooks provide a powerful yet simple way to automate IT infrastructure. Key takeaways:
- Playbooks are YAML files defining desired system state
- Tasks execute in order on specified hosts
- Variables make playbooks flexible and reusable
- Conditionals enable cross-platform automation
- Handlers manage service restarts efficiently
- Facts provide system information for decision-making
- Idempotency ensures safe repeated execution
Start simple, test thoroughly, and gradually build more complex automation as you gain confidence with Ansible playbooks.
Was this article helpful?