Steven's Knowledge

Core Concepts

Inventory, plays, tasks, modules, variables, and facts - the building blocks behind every playbook

Core Concepts

ConceptWhat it is
InventoryThe list of managed hosts, grouped
PlayA mapping of hosts to a set of tasks
TaskA single unit of work — a module invocation
ModuleThe thing that actually runs (apt, copy, service, ...)
VariableNamed data injected into tasks and templates
FactA variable Ansible discovered about the host
HandlerA 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/python3

Group 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-01

Ansible 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: true

Each 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:

ModulePurpose
apt / yum / dnf / packageInstall packages
copy / templatePush files (template renders Jinja2 first)
fileManage permissions, symlinks, directories
lineinfile / blockinfileEdit a line or block inside a file
service / systemdStart, stop, enable services
user / groupManage accounts
ufw / firewalld / iptablesFirewall rules
gitCheck out a repo
command / shellLast resort — non-idempotent
uriHit an HTTP endpoint (health checks, API calls)
docker_container / kubernetes.core.k8sManage 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):

  1. Role defaults (roles/<name>/defaults/main.yml)
  2. Inventory file vars: blocks
  3. group_vars/ and host_vars/
  4. Play vars: block
  5. Role vars/ (roles/<name>/vars/main.yml)
  6. Task vars: block
  7. -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:

FactWhat it is
ansible_distribution"Ubuntu", "Debian", "RedHat", ...
ansible_distribution_version"22.04", "11", ...
ansible_processor_vcpusCPU count
ansible_memtotal_mbTotal RAM in MB
ansible_default_ipv4.addressPrimary IPv4
ansible_hostnameShort hostname
inventory_hostnameThe 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: false

Handlers

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: restarted

Handlers 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 packages

Typical 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.

On this page