Playbooks and modules

A playbook is YAML that states the desired end result: plays, tasks, module arguments, variables and handlers.

A first playbook

A playbook is a list of plays. Each play names a host pattern and a list of tasks; tasks run in order, one host batch at a time.

# site.yml
- name: Configure web servers
  hosts: web
  become: true
  vars:
    app_port: 8080

  tasks:
    - name: Install nginx
      apt:
        name: nginx
        state: present

    - name: Deploy the site config
      template:
        src: templates/app.conf.j2
        dest: /etc/nginx/conf.d/app.conf
      notify: Reload nginx

    - name: Ensure nginx is running
      service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Reload nginx
      service:
        name: nginx
        state: reloaded
ansible-playbook -i inventory.ini site.yml
ansible-playbook -i inventory.ini site.yml --check --diff   # rehearse
ansible-playbook -i inventory.ini site.yml --limit web1     # a single host

Modules worth knowing

ModulePurposeIdempotent?
apt / dnf / packageInstall, remove or upgrade packagesYes, through state
copy / templatePush a file, optionally rendered from Jinja2Yes, compared by checksum
fileDirectories, symlinks, permissions, ownershipYes
lineinfile / blockinfileEdit one line or one block of a config fileYes, matched by pattern
service / systemdStart, stop and enable a unitYes
user / groupAccounts and groupsYes
command / shellRun a program; shell adds pipes and redirectionNo — use only when nothing else fits
get_url / uriDownload a file or call an HTTP endpointPartly; creates or checksum help
    - name: Create the release directory
      file:
        path: /srv/app/releases/{{ app_version }}
        state: directory
        owner: deploy
        mode: "0755"

    - name: Fetch the artifact
      get_url:
        url: "https://artifacts.example.com/app-{{ app_version }}.tgz"
        dest: /tmp/app.tgz
        checksum: "sha256:{{ artifact_sha256 }}"

    - name: Unpack it only once
      unarchive:
        src: /tmp/app.tgz
        dest: "/srv/app/releases/{{ app_version }}"
        remote_src: true
        creates: "/srv/app/releases/{{ app_version }}/bin/app"
💡
creates: is the simplest idempotency guard for a task that cannot check itself: the task is skipped when that path already exists. removes: is the mirror image.

Variables, facts and handlers

Variables arrive from several places with a fixed precedence: extra vars beat task vars, which beat play vars, which beat inventory vars, which beat role defaults. Facts are variables Ansible discovers on each host by running setup.

  vars_files:
    - vars/common.yml

  tasks:
    - name: Show what we discovered
      debug:
        msg: "{{ inventory_hostname }} runs {{ ansible_facts.distribution }} {{ ansible_facts.distribution_version }}"

    - name: Loop over a list
      user:
        name: "{{ item.name }}"
        groups: "{{ item.groups }}"
      loop:
        - { name: alice, groups: sudo }
        - { name: bob, groups: docker }
      when: ansible_facts.os_family == "Debian"

    - name: Register a result and act on it
      command: /usr/bin/check-migration
      register: migration
      changed_when: false
      failed_when: migration.rc not in [0, 1]
  • Handler: a task that runs only when a task notifying it reported changed, exactly once, at the end of the play.
  • tag: --tags deploy runs only the tasks you tagged, which keeps a long playbook usable.
  • block / rescue / always: groups tasks with error handling, like try, catch and finally.
  • changed_when / failed_when: overrides Ansible's guess about what a command's result means.
  • check_mode: modules that support it report whether they would change anything.

FAQ

When is a module not idempotent?
Practically, when it cannot tell whether the work is already done. command and shell always run; wrap them with creates, a registered result and when, or use changed_when: false for a command that only reads.
Why did my handler not run?
A handler fires only if a notifying task reported changed. If that task was skipped, already in the desired state, or ran in check mode, there was nothing to notify.

Inventories and ad-hoc commands Roles, Vault and idempotency

Last refreshed 2026-09-18.