Skip to content

Intermediate · Configuration management with Ansible, and YAML · topic 36 of 53 · 35 min

Ansible playbooks

Explanation

A playbook is a YAML file with one or more plays. Each play picks hosts and runs a list of tasks, and each task calls one module. Variables can live in the play, in group_vars and host_vars, or come from facts that Ansible gathers about each server. Handlers run once at the end, only when a task that notified them changed something. when adds conditions, loop repeats a task, and --check --diff shows changes without making them.

From ad-hoc to playbooks

Ad-hoc commands are one-off. A playbook is a file you keep in Git and run again and again. It is written in YAML. A playbook contains plays; a play says which hosts to target and lists tasks; a task calls one module with arguments and has a name that shows up in the output.

yaml
---
- name: Set up web servers for the college website
  hosts: web
  become: true
  vars:
    site_name: college.edu
    packages:
      - nginx
      - ufw
  tasks:
    - name: Install packages
      ansible.builtin.apt:
        name: "{{ packages }}"
        state: present
        update_cache: true

    - name: Deploy the nginx site config
      ansible.builtin.template:
        src: templates/site.conf.j2
        dest: /etc/nginx/sites-available/{{ site_name }}
        mode: "0644"
      notify: Reload nginx

    - name: Enable the site
      ansible.builtin.file:
        src: /etc/nginx/sites-available/{{ site_name }}
        dest: /etc/nginx/sites-enabled/{{ site_name }}
        state: link
      notify: Reload nginx

    - name: Copy the home page
      ansible.builtin.copy:
        src: files/index.html
        dest: /var/www/html/index.html
        owner: www-data
        mode: "0644"

    - name: Make sure nginx is running and starts at boot
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded
site.yml: a complete play for Ubuntu web servers. Run it with ansible-playbook -i inventory.ini site.yml.

Useful modules

ModuleWhat it does
ansible.builtin.apt / ansible.builtin.dnfInstall, upgrade or remove packages (Debian and Ubuntu / Fedora and RHEL family)
ansible.builtin.copyCopy a file from the control node, or write given content
ansible.builtin.templateRender a Jinja2 template with variables, then copy it
ansible.builtin.service / ansible.builtin.systemd_serviceStart, stop, restart, reload and enable services
ansible.builtin.userCreate or remove users, set groups and shells
ansible.builtin.fileCreate folders, links, set owners and permissions, delete files
ansible.builtin.lineinfileMake sure one line is present (or absent) in a file

Variables

A variable is a named value used as {{ name }} (Jinja2 syntax). You can set variables in a play under vars, in files group_vars/web.yml (for every host in the web group), group_vars/all.yml (for every host), and host_vars/web1.college.edu.yml (for one host). Ansible has a long precedence order; the short version is: more specific wins (host over group, group over all), and extra vars on the command line (-e) win over everything. Role defaults have the lowest priority.

Careful: If a value starts with {{, quote the whole value: name: "{{ packages }}". Without quotes, YAML reads the { as the start of a map and the playbook fails to parse.

Facts

At the start of each play, Ansible gathers facts: information about every host, such as the OS family, distribution, IP addresses, memory and CPUs. They are available as ansible_facts, for example ansible_facts['os_family'] is "Debian" on Ubuntu. Run ansible web1.college.edu -m ansible.builtin.setup to see them all. Set gather_facts: false to skip this step when you do not need facts and want speed.

Handlers and notify

You want to reload nginx only when its config actually changed, not on every run. A handler is a task that runs only when notified. A task notifies a handler with notify, and the notification counts only if the task reported changed. Handlers run once, at the end of the play (after all tasks), even if several tasks notified them, and they run in the order they are listed in the handlers section.

Conditionals and loops

yaml
- name: Install nginx on Debian-family hosts
  ansible.builtin.apt:
    name: nginx
    state: present
  when: ansible_facts['os_family'] == "Debian"

- name: Create staff accounts
  ansible.builtin.user:
    name: "{{ item }}"
    groups: www-data
    append: true
  loop:
    - asha
    - ravi
    - meena
when takes a plain expression (no {{ }}); loop repeats the task with each value as item.

Check mode and diff

bash
ansible-playbook -i inventory.ini site.yml --check --diff   # dry run: what would change, and how
ansible-playbook -i inventory.ini site.yml --limit web1.college.edu   # run on one host only
ansible-playbook -i inventory.ini site.yml --syntax-check
--check makes no changes. --diff shows file differences. Some tasks (like command) cannot predict their result in check mode.

Common mistakes

  • Restarting a service in a normal task on every run. Use a handler with notify so it happens only after a change.
  • Writing when: "{{ x }} == 1". when is already an expression: write when: x == 1.
  • Forgetting become: true when tasks need root.
  • Wrong indentation under a module: its arguments must be indented one level below the module name.
  • Unquoted file modes such as mode: 0644. Quote them as "0644" so they are not read as a different number.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Ansible playbooks · 1/7

Ansible playbooks

In this lesson you will write Ansible playbooks, files you keep in Git and run again and again.

Prefer a video by a teacher? Find free videos on “ansible playbook tutorial handlers variables loops nginx” on YouTube ↗

In real life: Rolling out a college website to two web servers

The IT cell of an engineering college in Coimbatore runs its public website on two Ubuntu VMs behind a load balancer. The playbook above installs nginx, deploys the site config from a template, copies the home page and keeps nginx running. When the web team changes the server_name in the template, only the template task reports changed, and the Reload nginx handler runs once per server at the end. Before each run on production they use --check --diff to see the exact lines that will change.

output
TASK [Deploy the nginx site config] ****
changed: [web1.college.edu]
changed: [web2.college.edu]
RUNNING HANDLER [Reload nginx] ****
changed: [web1.college.edu]
changed: [web2.college.edu]
PLAY RECAP ****
web1.college.edu : ok=6  changed=2  unreachable=0  failed=0
Part of the ansible-playbook output.

Practice (0/3 done)

Task 1 · Question

Which keyword in a play runs its tasks with sudo?

Task 2 · Code · Python

Simulate handlers. handlers is the list of handler names in the order they are defined. results is a list of task results, each a dict like {"changed": True, "notify": ["Reload nginx"]} (notify may be missing). Write handlers_to_run(handlers, results) that returns the handlers that will run at the end: each one only once, only if notified by a task that changed, in the order of the handlers list.

Task 3 · Practice

Write a playbook users.yml for the hosts group "lab" that: installs git and htop with apt, creates the users asha and ravi with a loop, adds the line "PermitRootLogin no" to /etc/ssh/sshd_config with lineinfile, and restarts ssh through a handler only when that line changed. Run it with --check --diff first (in a VM or with Vagrant/Multipass if you have one).

Quick quiz

Question 1

1.Two tasks notify "Reload nginx" and both report changed. How many times does the handler run on that host?

Question 2

2.Which line is a correct condition?

Question 3

3.What does ansible-playbook site.yml --check --diff do?

Question 4

4.Where do you put variables that apply to every host in the group "web"?

Question 5

5.name: {{ packages }} fails to parse. Why?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Two tasks notify "Reload nginx" and both report changed. How many times does the handler run on that host?

Answer: Once, at the end of the play

Handlers are deduplicated and run once, after the tasks of the play.

Which line is a correct condition?

Answer: when: ansible_facts['os_family'] == "Debian"

when is already a Jinja2 expression, so no {{ }} is needed.

What does ansible-playbook site.yml --check --diff do?

Answer: Shows what would change, with file differences, without changing anything

Check mode is a dry run; diff shows the before and after of files.

Where do you put variables that apply to every host in the group "web"?

Answer: group_vars/web.yml

group_vars/GROUPNAME.yml applies to all hosts in that group.

name: {{ packages }} fails to parse. Why?

Answer: A value starting with { must be quoted in YAML

YAML reads an unquoted { as the start of a map. Write "{{ packages }}".

Which module gathers facts manually?Related

Answer: ansible.builtin.setup

setup is what Gathering Facts runs.

From Variables, facts and registered results

What does -C do with ansible-playbook?Related

Answer: Check mode: predict changes without making them

It is a dry run.

From Writing and running playbooks

Three tasks notify the same handler and all change. How many times does it run?Related

Answer: 1

Once, at the end of the play.

From Everyday modules and handlers

What does -b mean in an ad-hoc command?Related

Answer: Become (privilege escalation)

-b = --become.

From Ad-hoc commands and ansible-doc

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Configuration management with Ansible, and YAML” before its exam: Roles, templates, Galaxy and Vault.
  2. Module 8 exam: Configuration management with Ansible, and YAML · 13 questions · 20 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Monitoring and logging. 53 topics left in the course before the final exam.

← Previous: Ansible: inventory and ad-hoc commands