Chapter 4: Managing Variables and Facts

Variables in playbooks

So far every value in your playbooks has been typed in directly: the package name, the service, the path. Variables let you name those values once and reuse them everywhere, so a change happens in one place instead of ten, and one playbook can serve different hosts.

Why variables

A variable stores a value that you can reuse throughout the files of an Ansible project. That makes projects easier to write and maintain, and reduces mistakes, because each value lives in exactly one place. Typical things to put in variables:

Package names, versions, archive URLs to download.

Users to create, services to restart, firewall services to open.

Configuration file paths, document roots, files to remove.

Naming variables

A variable name must start with a letter, and may contain only letters, numbers and underscores. Spaces, dots, dashes and symbols are not allowed.

InvalidValid
web serverweb_server
remote.fileremote_file
1st filefile_1 or file1
remoteserver$1remote_server_1 or remote_server1

Try some names of your own:

Defining variables in a play

The most common place to define variables is a vars block at the start of a play. Every task in that play can then use them.

- name: Create the standard user
  hosts: all
  vars:
    user: joe
    home: /home/joe
  tasks:
    ...

When the list grows, or you want to share variables between playbooks, move them into a separate YAML file and load it with vars_files. Paths are relative to the playbook.

- name: Create the standard user
  hosts: all
  vars_files:
    - vars/users.yml
  tasks:
    ...
user: joe
home: /home/joe

The file holds plain key-value pairs, in the same format as a vars block.

Using a variable

To use a variable, put its name inside double curly braces: {{ user }}. When the task runs, Ansible replaces the reference with the variable's value. This syntax comes from Jinja2, the template language Ansible is built on.

vars:
  user: joe

tasks:
  # The task name will read "Create the user joe"
  - name: Create the user {{ user }}
    ansible.builtin.user:
      # This creates the user named joe
      name: "{{ user }}"

The quoting rule

Notice that name: "{{ user }}" is in quotes, but the task name is not. The rule is simple:

Edit the values below, then untick the quotes to see the error you will get:

The same rule applies to list items: write - "{{ package }}", not - {{ package }}.

Dictionaries as variables

When several values belong to the same thing, a dictionary keeps them together. Compare six loose variables with one structured one:

user1_first_name: Bob
user1_last_name: Jones
user1_home_dir: /users/bjones
user2_first_name: Anne
user2_last_name: Cook
user2_home_dir: /users/acook
users:
  bjones:
    first_name: Bob
    last_name: Jones
    home_dir: /users/bjones
  acook:
    first_name: Anne
    last_name: Cook
    home_dir: /users/acook

To read a value inside a dictionary you can use dot notation, users.bjones.first_name, or bracket notation, users['bjones']['first_name']. Both return Bob. Click around this dictionary to see both forms for any value:

Capturing results with register

Every module returns information about what it did, as JSON. The register keyword saves that result in a variable, so later tasks can print it, make decisions with it, or reuse part of it.

Capturing a task result with register: Every module returns a JSON result. register saves it as a variable, so later tasks can print it, test it or reuse it.

- name: Installs a package and prints the result
  hosts: all
  tasks:
    - name: Install the package
      ansible.builtin.dnf:
        name: httpd
        state: installed
      register: install_result

    - name: Show what dnf reported
      ansible.builtin.debug:
        var: install_result

The ansible.builtin.debug module prints the variable during the run:

TASK [Show what dnf reported] **************************************************
ok: [demo.example.com] => {
    "install_result": {
        "changed": false,
        "msg": "",
        "rc": 0,
        "results": [
            "httpd-2.4.51-7.el9_0.x86_64 providing httpd is already installed"
        ]
    }
}

register is placed at the task level, at the same indentation as the module name, not inside the module's arguments. You can then read any part of it, for example install_result['changed'].

Takeaway

Name variables correctly. Use the chapter lab to check this on a real host.