Core Concepts
Inventory, plays, tasks, modules, variables, and facts - the building blocks behind every playbook
Core Concepts
| Concept | What it is |
|---|---|
| Inventory | The list of managed hosts, grouped |
| Play | A mapping of hosts to a set of tasks |
| Task | A single unit of work — a module invocation |
| Module | The thing that actually runs (apt, copy, service, ...) |
| Variable | Named data injected into tasks and templates |
| Fact | A variable Ansible discovered about the host |
| Handler | A task triggered by a notify |
Inventory
Inventories organize hosts into groups, and groups into super-groups. Variables can attach at host, group, or global scope.
# inventory/production.yml
all:
children:
webservers:
hosts:
web-01:
ansible_host: 10.0.1.10
web-02:
ansible_host: 10.0.1.11
vars:
app_port: 3000
databases:
hosts:
db-01:
ansible_host: 10.0.2.10
monitoring:
hosts:
monitor-01:
ansible_host: 10.0.3.10
vars: # applies to every host
ansible_user: deploy
ansible_ssh_private_key_file: ~/.ssh/deploy_key
ansible_python_interpreter: /usr/bin/python3Group variables get unwieldy inline. The convention is to split them out:
inventory/
├── production.yml
├── group_vars/
│ ├── all.yml # applies to every host
│ ├── webservers.yml # applies to the webservers group
│ └── databases.yml
└── host_vars/
└── web-01.yml # applies only to web-01Ansible auto-loads group_vars/<group>.yml and host_vars/<host>.yml adjacent to the inventory.
Plays and Tasks
A play maps a group of hosts to a set of tasks. A playbook is one or more plays in a YAML file:
# site.yml
---
- name: Configure all servers
hosts: all
become: true
tasks:
- name: Set timezone
timezone:
name: UTC
- name: Configure web servers
hosts: webservers
become: true
tasks:
- name: Install nginx
apt:
name: nginx
state: present
- name: Start nginx
service:
name: nginx
state: started
enabled: trueEach task has a name (shown in output), a module (apt, service, ...), and the module's arguments. Tasks run top-to-bottom on each host; hosts run in parallel.
Modules
Modules are the verbs of Ansible. A small set covers most config work:
| Module | Purpose |
|---|---|
apt / yum / dnf / package | Install packages |
copy / template | Push files (template renders Jinja2 first) |
file | Manage permissions, symlinks, directories |
lineinfile / blockinfile | Edit a line or block inside a file |
service / systemd | Start, stop, enable services |
user / group | Manage accounts |
ufw / firewalld / iptables | Firewall rules |
git | Check out a repo |
command / shell | Last resort — non-idempotent |
uri | Hit an HTTP endpoint (health checks, API calls) |
docker_container / kubernetes.core.k8s | Manage containers / K8s objects |
command and shell run every time and Ansible has no way to know if anything changed. Reach for a real module first. If you must use shell, add a creates: or changed_when: so it stops lying about idempotency.
Variables
Variables can be declared in many places. Common ones (in roughly increasing precedence):
- Role defaults (
roles/<name>/defaults/main.yml) - Inventory file
vars:blocks group_vars/andhost_vars/- Play
vars:block - Role
vars/(roles/<name>/vars/main.yml) - Task
vars:block -e "key=value"on the CLI (wins all)
Reference them with {{ name }} in YAML strings and Jinja2 templates:
- name: Configure nginx with a tuned worker count
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
vars:
worker_processes: "{{ ansible_processor_vcpus }}"Facts
Before running tasks, Ansible gathers facts — it SSHes in and dumps everything it can learn about the host into variables prefixed with ansible_:
- debug:
msg: "{{ inventory_hostname }} is {{ ansible_distribution }} {{ ansible_distribution_version }} with {{ ansible_processor_vcpus }} vCPUs"Useful facts:
| Fact | What it is |
|---|---|
ansible_distribution | "Ubuntu", "Debian", "RedHat", ... |
ansible_distribution_version | "22.04", "11", ... |
ansible_processor_vcpus | CPU count |
ansible_memtotal_mb | Total RAM in MB |
ansible_default_ipv4.address | Primary IPv4 |
ansible_hostname | Short hostname |
inventory_hostname | The name in your inventory (set by you, not the host) |
Fact gathering takes a few seconds. If a play doesn't need facts, disable it:
- hosts: webservers
gather_facts: falseHandlers
A handler is a task that only runs if something notifies it. Use them for service restarts triggered by config changes:
tasks:
- name: Push nginx config
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx # fires only if file changed
handlers:
- name: restart nginx
service:
name: nginx
state: restartedHandlers run once at the end of the play, even if notified multiple times — so a series of config tweaks doesn't restart nginx five times.
Tags
Tags let you run a subset of tasks:
tasks:
- name: Install nginx
apt: { name: nginx, state: present }
tags: [packages]
- name: Push nginx config
template: { src: nginx.conf.j2, dest: /etc/nginx/nginx.conf }
tags: [config]
- name: Deploy app
git: { repo: ..., dest: ... }
tags: [deploy]ansible-playbook site.yml --tags deploy # only deploy tasks
ansible-playbook site.yml --skip-tags packages # everything except packagesTypical Project Layout
ansible/
├── ansible.cfg
├── inventory/
│ ├── production.yml
│ ├── staging.yml
│ ├── group_vars/
│ └── host_vars/
├── playbooks/
│ ├── site.yml
│ ├── setup.yml
│ └── deploy.yml
└── roles/
├── common/
├── nginx/
└── app/What's Next
You can run idempotent playbooks against your fleet. Next, stop repeating yourself by packaging logic into roles → Roles & Templates.