Playbook Fundamentals
Basic Structure
---
- name: Playbook name
hosts: webservers
become: yes
vars:
http_port: 80
tasks:
- name: First task
module_name:
param1: value1
param2: value2
Multiple Plays
---
- name: First play
hosts: webservers
tasks:
- name: Task for webservers
debug:
msg: "This runs on webservers"
- name: Second play
hosts: dbservers
tasks:
- name: Task for dbservers
debug:
msg: "This runs on dbservers"
Executing Playbooks
# Basic execution
$ ansible-playbook playbook.yml
# Check mode (dry run)
$ ansible-playbook --check playbook.yml
# Limit to specific hosts
$ ansible-playbook playbook.yml --limit web1.example.com
# Start at a specific task
$ ansible-playbook playbook.yml --start-at-task="Install packages"
# Step-by-step execution
$ ansible-playbook playbook.yml --step
# Show skipped hosts
$ ansible-playbook playbook.yml --list-skipped-hosts
# List tasks that would run
$ ansible-playbook playbook.yml --list-tasks
# List tags in playbook
$ ansible-playbook playbook.yml --list-tags
Variables in Playbooks
Defining Variables
# In playbook
vars:
http_port: 80
max_clients: 200
# From external files
vars_files:
- vars/common.yml
- vars/app_vars.yml
# Prompting for variables
vars_prompt:
- name: username
prompt: "Enter username"
private: no
- name: password
prompt: "Enter password"
private: yes
Using Variables
tasks:
- name: Create directory
file:
path: "/opt/{{ app_name }}/data"
state: directory
- name: Template config
template:
src: "templates/{{ app_name }}.conf.j2"
dest: "/etc/{{ app_name }}.conf"
Special Variables
# Host facts
- debug:
msg: "OS: {{ ansible_distribution }} {{ ansible_distribution_version }}"
# Inventory variables
- debug:
msg: "Database: {{ hostvars['db1.example.com']['ansible_host'] }}"
# Group information
- debug:
msg: "Webservers: {{ groups['webservers'] | join(', ') }}"
Task Control
Conditionals
# Basic condition
- name: Install Apache on RedHat systems
yum:
name: httpd
state: present
when: ansible_os_family == "RedHat"
# Multiple conditions (AND)
- name: Install app if requirements met
apt:
name: my-app
state: present
when:
- ansible_distribution == "Ubuntu"
- ansible_memtotal_mb > 1024
# Multiple conditions (OR)
- name: Restart if config changed
service:
name: httpd
state: restarted
when: config_file_1.changed or config_file_2.changed
# Checking variable existence
- name: Do something if variable exists
debug:
msg: "Variable exists"
when: my_variable is defined
# Checking strings
- name: Check if string contains substring
debug:
msg: "String contains 'foo'"
when: my_string is search('foo')
Loops
# Basic loop
- name: Create multiple users
user:
name: "{{ item }}"
state: present
loop:
- alice
- bob
- charlie
# Loop with dictionary
- name: Create users with properties
user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
state: present
loop:
- { name: alice, groups: admin }
- { name: bob, groups: developers }
- { name: charlie, groups: developers }
# Loop with index
- name: Indexed loop example
debug:
msg: "{{ item.0 }} - {{ item.1 }}"
loop: "{{ ['a', 'b', 'c'] | zip_longest(range(3), fillvalue='') | list }}"
# Nested loops
- name: Nested loop demo
debug:
msg: "{{ item[0] }}: {{ item[1] }}"
loop: "{{ lookup('subelements', users, 'phones') }}"
vars:
users:
- name: alice
phones:
- 123-456-7890
- 234-567-8901
- name: bob
phones:
- 345-678-9012
Loop Controls
# Loop with label
- name: Create users
user:
name: "{{ item }}"
state: present
loop:
- alice
- bob
loop_control:
label: "user {{ item }}" # Cleaner output
# Pausing between loop iterations
- name: Restart servers with pause
command: "/scripts/restart_server.sh {{ item }}"
loop:
- server1
- server2
- server3
loop_control:
pause: 5 # 5 second pause
# Index tracking
- name: Items with index
debug:
msg: "Item {{ loop_index }} is {{ item }}"
loop:
- apple
- banana
- cherry
loop_control:
index_var: loop_index
Handlers
Basic Handlers
tasks:
- name: Template configuration file
template:
src: template.j2
dest: /etc/foo.conf
notify: Restart service
- name: Some other task
command: echo "Something else"
handlers:
- name: Restart service
service:
name: foo
state: restarted
Multiple Notifications
tasks:
- name: Template configuration file
template:
src: template.j2
dest: /etc/foo.conf
notify:
- Restart service
- Log configuration change
handlers:
- name: Restart service
service:
name: foo
state: restarted
- name: Log configuration change
command: echo "Config changed" >> /var/log/config_changes.log
Handler Execution
# Force handlers to run
- name: Example playbook
hosts: all
tasks:
- name: Task 1
debug:
msg: "Task 1"
notify: Handler 1
- name: Run handlers mid-play
meta: flush_handlers
- name: Task 2
debug:
msg: "Task 2"
handlers:
- name: Handler 1
debug:
msg: "Handler 1 executed"
Tags
Tagging Tasks
tasks:
- name: Install packages
apt:
name: "{{ item }}"
state: present
loop:
- nginx
- postgresql
tags:
- packages
- name: Configure nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
tags:
- configuration
- nginx
Tagging Plays and Roles
# Tag entire play
- name: Webserver configuration
hosts: webservers
tags:
- webservers
tasks:
- name: Some task
# ...
# Tag a role
- name: Complete system setup
hosts: all
roles:
- role: common
tags: [base, common]
- role: webserver
tags: [web, apache]
Using Tags
# Run only tasks with specific tags
$ ansible-playbook playbook.yml --tags "configuration,packages"
# Skip tasks with specific tags
$ ansible-playbook playbook.yml --skip-tags "notification"
# Run tasks with a specific tag tree
$ ansible-playbook playbook.yml --tags "webservers"
Error Handling
Ignoring Errors
- name: This command may fail but playbook will continue
command: /opt/might-fail.sh
ignore_errors: yes
# Continue on skipped
- name: Run only on Ubuntu
apt:
name: some-package
state: present
when: ansible_distribution == "Ubuntu"
any_errors_fatal: no
Block Error Handling
- block:
- name: Update application
command: /opt/app/update.sh
- name: Restart application
service:
name: myapp
state: restarted
rescue:
- name: Revert to previous version
command: /opt/app/rollback.sh
- name: Alert team
mail:
to: team@example.com
subject: "Update failed"
body: "Update failed on {{ inventory_hostname }}"
always:
- name: Always run this
debug:
msg: "Attempted update on {{ inventory_hostname }}"
Failing on Purpose
- name: Check if service is running
command: systemctl status myservice
register: service_status
ignore_errors: yes
- name: Fail if service is not running
fail:
msg: "The service is not running!"
when: service_status.rc != 0
Including and Importing
Tasks
# Static import (processed during playbook parsing)
- name: Import tasks
import_tasks: tasks/setup.yml
vars:
package: nginx
# Dynamic include (processed during playbook execution)
- name: Include tasks
include_tasks: "tasks/{{ ansible_distribution }}.yml"
Playbooks
# Import entire playbook
- name: Import playbook
import_playbook: webserver.yml
# Conditional import
- name: Import database playbook if needed
import_playbook: database.yml
when: setup_database | bool
Variables with Includes
# Pass variables to included files
- name: Include with vars
include_tasks: wordpress.yml
vars:
wp_version: 5.5.3
wp_install_dir: /var/www/html
Roles
Using Roles in Playbooks
# Basic role usage
- hosts: webservers
roles:
- common
- nginx
- php-fpm
# Role with variables
- hosts: webservers
roles:
- role: nginx
vars:
http_port: 8080
max_clients: 200
# Conditional role
- hosts: all
roles:
- role: monitoring
when: enable_monitoring | bool
Including Roles in Tasks
# Include roles in tasks (dynamic)
tasks:
- name: Run common tasks first
debug:
msg: "Begin setup"
- name: Include nginx role
include_role:
name: nginx
vars:
http_port: 8080
- name: Setup after nginx
debug:
msg: "Nginx setup complete"
# Import role in tasks (static)
tasks:
- name: Import database role
import_role:
name: database
vars:
db_name: myapp
Advanced Playbook Features
Delegation
# Run task on a different host
- name: Add to load balancer
command: /usr/bin/add-to-lb.sh {{ inventory_hostname }}
delegate_to: lb.example.com
# Run locally
- name: Local task
command: echo "Running locally"
delegate_to: localhost
# Run once
- name: Send notification
mail:
to: admin@example.com
subject: "Deployment started"
run_once: true
delegate_to: localhost
Task Control
# Serial execution (batches)
- hosts: webservers
serial: 2 # 2 hosts at a time
tasks:
- name: Update application
command: /opt/app/update.sh
# Percentage-based batches
- hosts: webservers
serial:
- "10%"
- "30%"
- "100%"
# Failure handling
- hosts: webservers
serial: 2
max_fail_percentage: 25 # Allow 25% failure
Async Tasks
# Fire and forget
- name: Long running operation
command: /opt/long_operation.sh
async: 3600 # 1 hour timeout
poll: 0 # Don't wait
# Run with polling
- name: Update packages
apt:
update_cache: yes
upgrade: dist
async: 600 # 10 minute timeout
poll: 15 # Check every 15 seconds
# Check status later
- name: Start backup
command: /usr/local/bin/backup.sh
async: 3600
poll: 0
register: backup_job
- name: Do other tasks
debug:
msg: "Doing other things"
- name: Check backup status
async_status:
jid: "{{ backup_job.ansible_job_id }}"
register: job_result
until: job_result.finished
retries: 30
delay: 60
Throttling
- name: Restart app servers
command: /usr/bin/restart_app.sh
throttle: 3 # Maximum 3 hosts in parallel
Playbook Organization
Directory Structure
project/
├── ansible.cfg
├── inventory.ini
├── playbooks/
│ ├── site.yml # Main playbook
│ ├── webservers.yml # Web server playbook
│ └── dbservers.yml # Database playbook
├── roles/
│ ├── common/
│ ├── nginx/
│ └── mysql/
└── group_vars/
├── all.yml # Variables for all hosts
├── webservers.yml # Variables for web servers
└── dbservers.yml # Variables for database servers
Main Playbook (site.yml)
---
- name: Include webserver playbook
import_playbook: webservers.yml
- name: Include database playbook
import_playbook: dbservers.yml
Webservers Playbook
---
- name: Configure webservers
hosts: webservers
roles:
- common
- nginx
- php-fpm
Best Practices
YAML Syntax
- Use 2 spaces for indentation
- Keep lines under 80 characters
- Use consistent quoting style
- Use
>
for multi-line strings - Use
|
for multi-line commands or scripts
Naming Conventions
- Use descriptive play and task names
- Task names should describe what they do and why
- Start task names with verbs (Install, Configure, Start)
- Use snake_case for variable names
- Prefix role variables with role name (e.g., nginx_port)
Task Design
- Make tasks idempotent (can run multiple times safely)
- Use state parameters explicitly (present, absent, started)
- Prefer modules over shell/command where possible
- Use check_mode: yes for tasks that should run in check mode
- Use changed_when/failed_when to control task status
Playbook Structure
- Group related tasks together
- Use includes and roles for modularity
- Separate environment-specific variables
- Keep playbooks focused on a specific purpose
- Use tags for selective execution
Security
- Use Ansible Vault for sensitive data
- Avoid hardcoding credentials in playbooks
- Use become only when necessary
- Limit visibility of sensitive task output
Performance
- Set gather_facts: no when facts aren’t needed
- Use async for long-running tasks
- Consider using serial for controlled deployment
- Increase forks in ansible.cfg for faster execution
Debugging Playbooks
Debug Module
- name: Print variable value
debug:
var: my_variable
- name: Print custom message
debug:
msg: "The value is {{ my_variable }}"
# Set verbosity level
- name: Debug at level 1
debug:
msg: "Debug info"
verbosity: 1 # Only with -v or higher
Verbose Output
# Increasing levels of verbosity
$ ansible-playbook playbook.yml -v
$ ansible-playbook playbook.yml -vv
$ ansible-playbook playbook.yml -vvv
$ ansible-playbook playbook.yml -vvvv
Registered Variables
- name: Run command
command: whoami
register: result
- name: Show command result
debug:
var: result
- name: Show specific properties
debug:
msg: "stdout: {{ result.stdout }}, rc: {{ result.rc }}"
Single-Task Execution
# Run just one task
$ ansible-playbook playbook.yml --start-at-task="Task name"
# Step through execution
$ ansible-playbook playbook.yml --step
Example Playbooks
Simple Web Server Setup
---
- name: Configure web server
hosts: webservers
become: yes
vars:
http_port: 80
doc_root: /var/www/html
tasks:
- name: Install nginx
apt:
name: nginx
state: present
update_cache: yes
- name: Create document root
file:
path: "{{ doc_root }}"
state: directory
mode: '0755'
- name: Copy website files
copy:
src: files/website/
dest: "{{ doc_root }}"
mode: '0644'
- name: Configure nginx
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/sites-available/default
notify: Restart nginx
handlers:
- name: Restart nginx
service:
name: nginx
state: restarted
Application Deployment
---
- name: Deploy application
hosts: app_servers
become: yes
vars:
app_version: "1.2.3"
app_root: "/opt/myapp"
app_user: "appuser"
app_repo: "https://github.com/example/myapp.git"
tasks:
- name: Ensure app user exists
user:
name: "{{ app_user }}"
state: present
- name: Create app directories
file:
path: "{{ item }}"
state: directory
owner: "{{ app_user }}"
mode: '0755'
loop:
- "{{ app_root }}"
- "{{ app_root }}/releases"
- "{{ app_root }}/shared/logs"
- "{{ app_root }}/shared/config"
- name: Clone application repository
git:
repo: "{{ app_repo }}"
dest: "{{ app_root }}/releases/{{ app_version }}"
version: "v{{ app_version }}"
become_user: "{{ app_user }}"
- name: Install dependencies
pip:
requirements: "{{ app_root }}/releases/{{ app_version }}/requirements.txt"
virtualenv: "{{ app_root }}/venv"
virtualenv_command: python3 -m venv
become_user: "{{ app_user }}"
- name: Configure application
template:
src: templates/app_config.j2
dest: "{{ app_root }}/shared/config/config.yml"
owner: "{{ app_user }}"
notify: Restart application
- name: Create symlink to current release
file:
src: "{{ app_root }}/releases/{{ app_version }}"
dest: "{{ app_root }}/current"
state: link
owner: "{{ app_user }}"
notify: Restart application
handlers:
- name: Restart application
systemd:
name: myapp
state: restarted
Multi-Environment Deployment
---
- name: Deploy to {{ environment }} environment
hosts: "{{ environment }}"
become: yes
vars_files:
- "vars/common.yml"
- "vars/{{ environment }}.yml"
pre_tasks:
- name: Verify environment
fail:
msg: "Environment must be 'development', 'staging', or 'production'"
when: environment not in ['development', 'staging', 'production']
- name: Notify start of deployment
slack:
token: "{{ slack_token }}"
msg: "Starting deployment to {{ environment }}"
channel: "#deployments"
delegate_to: localhost
run_once: true
when: slack_notifications | bool
roles:
- role: common
- role: database
when: deploy_database | bool
- role: application
app_version: "{{ app_version }}"
- role: monitoring
when: environment == 'production'
post_tasks:
- name: Run smoke tests
command: "/opt/tests/smoke_test.sh"
register: smoke_test
ignore_errors: "{{ environment != 'production' }}"
- name: Notify deployment status
slack:
token: "{{ slack_token }}"
msg: >
Deployment to {{ environment }}
{% if smoke_test.rc == 0 %}succeeded{% else %}failed{% endif %}
channel: "#deployments"
delegate_to: localhost
run_once: true
when: slack_notifications | bool