Plays, Tasks, and Modules
The playbook is the central artifact in Ansible. It is a YAML file that describes automation work: which servers to target, what permissions to use, and what actions to perform.
A playbook contains one or more plays. Each play targets a group of hosts from your inventory and defines a list of tasks. Each task calls a module — a small, purpose-built program that performs one specific action (install a package, create a user, copy a file, call an API, etc.). Ansible ships with thousands of built-in modules, and the community has published thousands more.
Here is a complete, annotated playbook:
# setup_web.yml
---
- name: Configure Web Servers # Human-readable name for this play
hosts: webservers # Target group from inventory
become: true # Escalate to root (equivalent to sudo)
become_method: sudo
vars:
web_package: nginx
web_root: /var/www/html
tasks:
- name: Ensure Nginx is installed
apt: # Module: manages apt packages (Debian/Ubuntu)
name: "{{ web_package }}" # Variable reference using Jinja2 syntax
state: present # Desired state: package must be installed
update_cache: yes # Run apt-get update first
- name: Deploy index.html
template: # Module: renders a Jinja2 template
src: templates/index.html.j2
dest: "{{ web_root }}/index.html"
owner: www-data
mode: '0644'
- name: Ensure Nginx is running and enabled on boot
service: # Module: manages system services
name: nginx
state: started # Service must be running
enabled: yes # Service must start on boot
The state Parameter
Most Ansible modules accept a state parameter that defines the desired outcome. Common values:
| Module | state: present |
state: absent |
state: started |
|---|---|---|---|
apt / yum |
Package installed | Package removed | — |
user |
User exists | User deleted | — |
file |
File/dir exists | File/dir deleted | — |
service |
— | — | Service running |
This is the declarative model in practice: you specify the end state, not the steps to get there.
Running a Playbook
A playbook run can be scoped and inspected several ways: a dry-run mode that reports what would change without touching anything, a verbose mode that shows each task's result in detail, and a limit flag that restricts execution to a single host or group instead of the whole inventory.
The dry-run mode is one of Ansible's most valuable operational features. Before applying changes to production, you can see exactly what would change — which tasks would report changed vs. ok — without touching anything.
A common interview scenario: "How would you safely test an Ansible change before applying it to production?" The layered answer: "First, run with --check to see what would change. Second, run with --diff alongside --check to see the exact line-by-line differences in files that would be modified. Third, run against a staging environment that mirrors production. Finally, use --limit to apply to one production host first and verify before rolling out to the rest."