Table of Contents
Understanding Ansible Playbooks
Ansible playbooks are YAML files that describe what you want done on which machines, and in what order. They are the core of how you use Ansible beyond one-off ansible ad‑hoc commands.
A playbook is made of one or more plays. Each play maps:
- a set of hosts (from the inventory)
- to a set of tasks (modules with parameters)
- possibly with variables, handlers, and roles
At a high level:
- One YAML file = one playbook.
- One playbook = one or more plays.
- One play = many tasks run on a host/group selection.
Basic Playbook Structure
The minimal structure:
- A YAML list (
-) of plays. - Each play has:
name: human-readable description (optional but recommended)hosts: target hosts or groups from the inventorybecome: whether to use privilege escalation (e.g.sudo)tasks: list of tasks
Example: simple webserver setup
- name: Install and start Nginx
hosts: webservers
become: true
tasks:
- name: Install nginx package
ansible.builtin.package:
name: nginx
state: present
- name: Ensure nginx is running and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: trueKey points:
- YAML starts with
---(convention, not mandatory). hosts: webserversrefers to a group from the inventory (explained later).ansible.builtin.packageandansible.builtin.serviceare modules.- Each task should have a
namefor readable output.
Multiple Plays in a Single Playbook
You can have multiple plays in one playbook, targeting different host groups or doing different stages.
Example: configure database and web servers in one run:
- name: Configure database servers
hosts: db
become: true
tasks:
- name: Install PostgreSQL
ansible.builtin.package:
name: postgresql
state: present
- name: Configure web servers
hosts: web
become: true
tasks:
- name: Install application dependencies
ansible.builtin.package:
name:
- python3
- python3-venv
state: presentPlays run top to bottom; each play loops over its target hosts.
Tasks and Modules
A task calls a module with parameters. Tasks are executed in order.
Common points:
nameappears in Ansible’s output.- Module name is a key (e.g.
ansible.builtin.copy). - Module arguments are an indented mapping.
Example:
- name: Copy application config
ansible.builtin.copy:
src: files/app.conf
dest: /etc/myapp/app.conf
owner: root
group: root
mode: "0644"Tasks are idempotent when modules are written correctly: running the same playbook again should not keep changing the system.
Using Variables in Playbooks
Variables make playbooks reusable and flexible.
Defining Variables in Plays
You can define variables in a play using vars:
- name: Configure app
hosts: web
become: true
vars:
app_port: 8080
app_user: myapp
tasks:
- name: Ensure app user exists
ansible.builtin.user:
name: "{{ app_user }}"
system: true
- name: Configure firewall
ansible.posix.firewalld:
port: "{{ app_port }}/tcp"
permanent: true
state: enabledNote:
- Use
{{ variable_name }}Jinja2 syntax to refer to variables. - Keep quoting around Jinja expressions to avoid YAML parsing issues.
Including Variable Files
Instead of putting many variables inside a play, you can use vars_files:
- name: Configure app from variable file
hosts: web
become: true
vars_files:
- vars/app.yml
tasks:
- name: Debug app settings
ansible.builtin.debug:
var: app_port
Where vars/app.yml might look like:
app_port: 8080
app_env: productionConditionals and When Clauses
Playbooks often need to do something only if a condition is met. Use when.
Example: run a task only on Debian-based systems:
- name: Install httpd or apache2 depending on OS
hosts: web
become: true
tasks:
- name: Install Apache on Debian/Ubuntu
ansible.builtin.package:
name: apache2
state: present
when: ansible_os_family == "Debian"
- name: Install httpd on RedHat family
ansible.builtin.package:
name: httpd
state: present
when: ansible_os_family == "RedHat"
Conditions can use facts (like ansible_os_family), variables, or registered results.
Loops in Playbooks
Loops let you repeat a task over a list of items.
Example: install several packages:
- name: Install multiple packages
ansible.builtin.package:
name: "{{ item }}"
state: present
loop:
- git
- curl
- vimOr with dictionaries:
- name: Create multiple users
ansible.builtin.user:
name: "{{ item.name }}"
shell: "{{ item.shell }}"
loop:
- { name: "alice", shell: "/bin/bash" }
- { name: "bob", shell: "/bin/zsh" }Handlers and Notifications
Handlers are tasks that run only when notified, usually to restart services after changes.
Example:
- name: Webserver configuration
hosts: web
become: true
tasks:
- name: Deploy nginx config
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart nginx
handlers:
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restartedNotes:
notifyrefers to a handler name.- A handler runs once per play at the end of all tasks, even if notified multiple times.
- Handlers run only if at least one notifying task reports
changed.
Roles in Playbooks (High-Level Usage)
The concept of roles themselves belongs in more detail elsewhere, but for playbooks you need to know how to use them.
To apply roles in a play:
- name: Apply common configuration
hosts: all
become: true
roles:
- common
- users
- nginxrolesis a simple list of role names.- Role directories must be in
roles/(or configuredroles_path).
Roles help split big playbooks into reusable components.
Understanding Ansible Inventories
The inventory tells Ansible which hosts exist and how to connect to them. Playbooks reference hostnames or groups from the inventory via the hosts field.
An inventory can be:
- A simple static text file (INI or YAML format).
- A dynamic inventory script or plugin (for cloud, containers etc.).
Here we focus on static inventories and how playbooks use them.
Basic INI-Style Inventory
The simplest static inventory format is INI-style.
Example inventory.ini:
[web]
web1.example.com
web2.example.com
[db]
db1.example.com
[allservers:children]
web
db
Group names are in []. Hosts under them are listed line-by-line.
You can then run:
ansible-playbook -i inventory.ini site.ymlAnd in your playbook:
- name: Configure web servers
hosts: web
...Host Variables and Group Variables
You can define variables in the inventory itself.
Example with host-specific variables:
[web]
web1 ansible_host=10.0.0.11 ansible_user=ubuntu
web2 ansible_host=10.0.0.12 ansible_user=ubuntu
[db]
db1 ansible_host=10.0.0.21 ansible_user=postgresHere:
web1is an Ansible hostname;ansible_hostis the real IP/hostname.ansible_useris the default SSH user for that host.
Group variables in the same file:
[web]
web1
web2
[web:vars]
http_port=80
max_clients=200
All hosts in web now have http_port and max_clients defined.
Inventory Directory Layout: `group_vars` and `host_vars`
A more scalable way to manage variables is using group_vars and host_vars directories, usually next to your playbook.
Typical layout:
project/
inventory.ini
group_vars/
all.yml
web.yml
db.yml
host_vars/
web1.yml
web2.yml
site.ymlExamples:
group_vars/all.yml (applies to every host):
ansible_user: ubuntu
timezone: UTC
group_vars/web.yml (applies to the web group):
http_port: 80
app_env: production
host_vars/web1.yml (applies only to web1):
app_env: staging
Variable precedence rules are detailed in Ansible docs, but in short, host_vars override group_vars, and closer definitions override more global ones.
YAML-Style Inventory
You can also write the inventory itself in YAML.
Example inventory.yml:
all:
children:
web:
hosts:
web1:
ansible_host: 10.0.0.11
web2:
ansible_host: 10.0.0.12
db:
hosts:
db1:
ansible_host: 10.0.0.21
vars:
ansible_user: ubuntuUse it with:
ansible-playbook -i inventory.yml site.ymlThis style is more structured, which can be helpful in complex setups.
Host Patterns in Playbooks
The hosts field in a play uses patterns to select hosts from the inventory.
Some useful patterns:
- Single group:
hosts: web - Multiple groups (union):
hosts: web:db - Exclude a group:
hosts: web:!staging - Host by name:
hosts: web1 - All hosts:
hosts: all
Example:
- name: Apply base configuration to all but database servers
hosts: all:!db
become: true
roles:
- commonPutting It Together: Playbooks + Inventories
A small complete example, referencing both playbook and inventory concepts.
inventory.ini:
[web]
web1 ansible_host=10.0.0.11 ansible_user=ubuntu
web2 ansible_host=10.0.0.12 ansible_user=ubuntu
[db]
db1 ansible_host=10.0.0.21 ansible_user=postgres
[all:vars]
ansible_ssh_private_key_file=~/.ssh/id_rsa
group_vars/web.yml:
nginx_listen_port: 8080
site.yml playbook:
- name: Configure web servers
hosts: web
become: true
tasks:
- name: Install nginx
ansible.builtin.package:
name: nginx
state: present
- name: Configure nginx port
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart nginx
handlers:
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted
- name: Configure database servers
hosts: db
become: true
tasks:
- name: Install PostgreSQL
ansible.builtin.package:
name: postgresql
state: presentRun:
ansible-playbook -i inventory.ini site.ymlHere you can see:
- Inventory defines hosts, groups, and connection details.
- Group vars define parameters like
nginx_listen_port. - Playbook targets groups (
web,db), and applies tasks and handlers.
This combination—clear inventories plus structured playbooks—is the core workflow of Ansible-based configuration management.