Defining and Using Variables

Scenario: The same port number appears in nine places across the repository. A change request asks to move it, and the eighth occurrence is missed.

New words, in plain English

Variables can be declared in many places: inline in a play's vars:, in files listed under vars_files:, in group_vars/ and host_vars/, inside role defaults/ and vars/, registered from a task result, set at run time with set_fact, and passed on the command line with -e.

Referencing one is always {{ name }}, and the YAML rule from Phase 1 applies: if a value starts with {{, the whole value must be quoted, because YAML would otherwise read the brace as the start of a dictionary.

Jinja2 expressions are not limited to bare names. Filters transform values with a pipe - {{ name | upper }}, {{ items | length }}, {{ path | basename }} - and | default(x) supplies a fallback when a variable is undefined, which is the single most useful filter in the language.

One rule that prevents a class of confusing bugs: variable names must be valid Python identifiers - letters, digits and underscores, not starting with a digit. Hyphens are not allowed. app-port is not a variable name; app_port is.

Analogy: A variable is a labelled dial rather than a number scribbled in nine places. Move the dial once and everything wired to it follows. Scribbled numbers require you to find every scrap of paper, and you will miss one.

A worked example

- name: Configure the application
  hosts: webservers
  vars:
    app_port: 8081
    app_root: /srv/checkout
  vars_files:
    - vars/common.yml

  tasks:
    - name: Render the config
      ansible.builtin.template:
        src: app.conf.j2
        dest: "{{ app_root }}/app.conf"     # quoted - starts with {{
        mode: "0644"

    - name: Show what was actually resolved
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }} will listen on {{ app_port }}"

# Filters do the transforming
#   {{ app_port | int }}            -> a number
#   {{ tls_ciphers | join(':') }}   -> a list becomes HIGH:!aNULL
#   {{ retries | default(3) }}      -> a fallback when undefined

set_fact sets a variable at run time, on a per-host basis, and it persists for the rest of the play. It is the right tool for a value you computed from a registered result. It is the wrong tool for configuration, because a value that only exists mid-run is invisible to anyone reading the repository.

A related discipline: prefix variables with the thing they belong to. webapp_port rather than port. Ansible has one flat variable namespace per host, so a role that declares port and another that declares port will collide silently, and the resulting bug is genuinely unpleasant to find.

Warning: Variable names cannot contain hyphens. app-port is parsed as a subtraction, not a name, and the error message will not make that obvious.
Goal: Put this to work in the ansible-variable-precedence lab. Open /labs/ansible, pick ansible-variable-precedence, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.