Table of Contents
Understanding Playbooks and Inventories
Configuration management tools like Ansible revolve around two central ideas. One is what you want to do, the other is where you want to do it. In Ansible, playbooks describe what to do, and inventories describe where to do it. This chapter focuses on how these two pieces fit together and how to write simple but reliable playbooks and inventories.
The Role of Playbooks
A playbook is a text file written in YAML that defines one or more plays. Each play connects a group of hosts from the inventory with a set of tasks. You can think of a play as a story that says which actors are involved and which actions they perform.
A minimal playbook defines at least which hosts it targets and what tasks it should run. A very small example looks like this:
- name: Ensure Nginx is installed
hosts: webservers
become: yes
tasks:
- name: Install Nginx
ansible.builtin.package:
name: nginx
state: present
In this example, webservers refers to a group in the inventory, not a fixed list of IP addresses. The playbook itself only knows that it should run on that group; which machines belong to that group is determined by the inventory.
Playbooks are idempotent when they are well written, which means you can run them repeatedly and nothing changes if the desired state is already reached.
Playbooks should always describe the desired state, not step-by-step manual procedures. If you write tasks that depend on previous runs or on hidden state, your automation becomes fragile.
The Structure of a Play
Each play in a playbook is a YAML dictionary. A single playbook can contain multiple plays, which are defined as a YAML list at the top level. Every play shares the same basic structure, although many fields are optional.
Common elements of a play include a name, a host selection, privilege escalation settings, variables, and a list of tasks. A typical play might look like this:
- name: Configure web servers
hosts: webservers
become: yes
vars:
app_user: "webapp"
pre_tasks:
- name: Update package cache
ansible.builtin.package:
name: "*"
state: latest
when: ansible_os_family == "Debian"
tasks:
- name: Ensure application user exists
ansible.builtin.user:
name: "{{ app_user }}"
state: present
- name: Deploy configuration file
ansible.builtin.template:
src: "templates/app.conf.j2"
dest: "/etc/app.conf"
owner: "{{ app_user }}"
mode: "0644"
post_tasks:
- name: Restart app service
ansible.builtin.service:
name: app
state: restarted
The hosts field is the most important connection to the inventory. It accepts a group name, a single host name, or a host pattern. You can use host patterns to select multiple groups or to exclude some hosts. For example, you can write webservers:!webservers_test to select all hosts in webservers except those in webservers_test.
The pre_tasks and post_tasks sections run before and after the main tasks list in the same play. These sections are useful for one-time setup or cleanup around the main configuration logic.
Tasks and Modules in Playbooks
Within a play, a task describes a single operation that Ansible should perform on each targeted host. Tasks are executed in order from top to bottom, and each task uses an Ansible module.
Although this chapter does not cover specific modules in depth, you should understand how tasks are expressed in the playbook. A typical task contains a human readable name and calls a module with some arguments:
- name: Ensure latest version of Git is installed
ansible.builtin.package:
name: git
state: latest
The module name, such as ansible.builtin.package, indicates which function is being called. The arguments are specified as YAML key value pairs under that module name. In many real playbooks, you will see shorter module names like package or user. These are shorthand forms that Ansible resolves to their fully qualified names.
Tasks can include conditionals and can register results if you want to use the outcome later in the same play. While the detailed logic features belong in other chapters, it is important to know that playbooks are more than flat lists. They can react to the environment and act on information gathered during the run.
Organizing Playbooks
As a playbook grows, it can become difficult to manage if everything is in a single file. One of the strengths of Ansible is that you can split and organize logic in several ways while still keeping a clear relationship between playbooks and the inventory.
A basic approach is to separate playbooks by purpose. You might have one playbook for base system configuration, another for web servers, and another for databases. For example, you could create files such as site.yml, web.yml, and db.yml, and then include or import one playbook into another.
An example of a top level playbook that includes other playbooks is:
- import_playbook: web.yml
- import_playbook: db.ymlThis method lets each team or role focus on its own playbooks while preserving a single entry point for full system configuration.
Another common pattern is to use roles, which encapsulate tasks, templates, and variables. Roles connect very naturally with playbooks and inventories but are covered separately in another chapter. For now, it is enough to know that a playbook can include roles in a simple list:
- name: Apply common configuration
hosts: all
roles:
- common
- monitoringThe main benefit of good playbook organization is that you can reuse logic across multiple groups of hosts without copying and pasting tasks.
What Is an Inventory?
The inventory defines the set of hosts that Ansible manages. It describes which machines exist, how to connect to them, and how they are grouped. Without an inventory, the playbook has no context for where to apply its tasks.
The simplest inventory is a static list of hosts in a text file. A minimal static inventory might be:
web1.example.com
web2.example.com
db1.example.comFor real projects, you usually group hosts and assign variables to them. Each group has a name and a list of member hosts:
[webservers]
web1.example.com
web2.example.com
[dbservers]
db1.example.com
db2.example.com
Here, webservers and dbservers are group names that you can use directly in playbooks in the hosts field. A play that targets webservers runs on web1.example.com and web2.example.com. A play that targets dbservers runs on db1.example.com and db2.example.com.
Ansible supports several inventory formats, but the two most common for static inventories are INI style and YAML. The examples above use an INI style. YAML inventories express the same information in a structured YAML file.
Defining Groups and Hierarchies
Groups are central to how inventories map real infrastructure into manageable categories. Groups let you express roles, environments, regions, or any other dimension that fits your systems.
In an INI style inventory, you define groups with a section name in square brackets. In YAML, you create mappings where a group name contains a list of hosts.
A YAML version of the earlier inventory looks like this:
all:
children:
webservers:
hosts:
web1.example.com:
web2.example.com:
dbservers:
hosts:
db1.example.com:
db2.example.com:
The all group is special and always exists. It contains every host in the inventory. The children key defines subgroups, which can themselves contain hosts or further groups.
Group hierarchies help you express common settings. For example, you might use groups such as prod, staging, and dev and place other groups inside them. A structured view might look like this:
all:
children:
prod:
children:
webservers:
hosts:
web1.example.com:
web2.example.com:
dbservers:
hosts:
db1.example.com:
staging:
children:
webservers:
hosts:
stg-web1.example.com:
In this example, the name webservers appears under both prod and staging. This allows you to target all web servers across environments with the pattern webservers, or only the production ones with prod:&webservers. Host patterns combine groups with set like operations. The expression : means union, & means intersection, and ! means exclusion.
Group design has a strong impact on how maintainable your automation is. Use clear, consistent group names that reflect stable roles or environments, not temporary projects or short lived experiments.
Host Variables and Group Variables
Variables make inventories more expressive. They let you attach configuration values directly to hosts or groups. Playbooks then use these variables to customize behavior without duplicating task logic.
In INI style inventories, you can attach variables to a host on the same line:
[webservers]
web1.example.com ansible_host=192.168.1.10 env=prod
web2.example.com ansible_host=192.168.1.11 env=prod
Here, ansible_host tells Ansible which IP address to use for SSH, and env is a custom variable that playbooks can use in templates or conditionals.
You can also define group variables in a dedicated section:
[webservers]
web1.example.com
web2.example.com
[webservers:vars]
nginx_port=80
env=prodIn YAML inventories, host and group variables are structured as nested mappings:
all:
children:
webservers:
hosts:
web1.example.com:
ansible_host: 192.168.1.10
web2.example.com:
ansible_host: 192.168.1.11
vars:
nginx_port: 80
env: prod
Playbooks reference these variables by name, just like any other Ansible variable. For example, a playbook can use {{ nginx_port }} in a template or when: env == "prod" to apply tasks only in a given environment.
Ansible also supports separate group_vars and host_vars directories to store variables in individual files, but the detailed layout of these directories is part of a broader Ansible structure that is covered elsewhere.
Matching Playbooks to Inventories
The power of configuration management appears when playbooks and inventories are designed together. The playbook expresses logic in terms of roles or environments. The inventory maps real machines into those roles and environments.
As an example, consider a playbook that configures production web servers:
- name: Configure production web servers
hosts: prod:&webservers
become: yes
tasks:
- name: Ensure Nginx is installed
ansible.builtin.package:
name: nginx
state: present
- name: Ensure Nginx is running
ansible.builtin.service:
name: nginx
state: started
enabled: yes
The host pattern prod:&webservers selects all hosts that belong to both prod and webservers groups. This lets you keep a single definition of what a web server is in the playbook, and decide in the inventory which machines are considered web servers in production.
By contrast, a playbook that configures base settings across all hosts might simply target all:
- name: Apply base configuration to all hosts
hosts: all
become: yes
tasks:
- name: Ensure vim is installed
ansible.builtin.package:
name: vim
state: presentWith this approach, adding a new server to the system only requires updating the inventory. The base configuration and any role based configuration already described in your playbooks will apply automatically the next time you run them.
Static vs Dynamic Inventories
Static inventories are stored in files and edited manually. They work well for small environments or simple labs, and they help you understand the basic concepts. However, in many real situations, machines change frequently. Servers come and go in cloud environments. For this reason, Ansible supports dynamic inventories.
A dynamic inventory is a script or plugin that outputs host and group information to Ansible when requested. The script usually pulls data from external systems such as a cloud provider, a container orchestrator, or a configuration database.
From the perspective of a playbook, it does not matter whether the inventory is static or dynamic. The playbook still uses group names and host patterns. What changes is how those groups are populated.
The detail of writing custom dynamic inventories or using provider-specific plugins belongs in a more advanced discussion. For now, you should remember that inventories are not limited to static files. The same concepts of groups, host variables, and group variables still apply, but the inventory content is generated automatically.
Running Playbooks Against Inventories
Once you have a playbook and an inventory, you use the ansible-playbook command to execute the play. This command takes the playbook file and optionally a path to the inventory. For example:
ansible-playbook -i inventory.ini site.yml
If you use YAML inventories or more complex directory structures, you still pass the inventory through the -i option, but the file name or directory path might differ:
ansible-playbook -i inventories/prod/ inventory.yml site.yml
Ansible loads the inventory, resolves the host patterns in your plays, and then runs each play on the selected hosts. If you organize your inventory and playbooks carefully, you can also limit runs to particular subsets using the --limit option without changing the playbook itself.
Always test playbooks against a limited set of hosts or a non production inventory before running them across all systems. Misapplied changes at scale can be very difficult to undo.
By keeping playbooks declarative and inventories accurate, you create a repeatable, testable, and well structured method of managing Linux systems in any environment.