# Variables and Facts in Ansible

> Chapter 6 of the Courseiva REDHAT-RHCE curriculum — https://courseiva.com/learn/redhat-rhce/variables-and-facts

**Official objective:** 3.3 — Use variables and facts to manage system-specific data

## Introduction

Variables and facts are what stop Ansible from being a blind robot that repeats the same command on every machine, regardless of whether that machine is a tiny virtual server running Ubuntu on a developer's laptop or a massive database server running Red Hat Enterprise Linux in a data centre. They allow a single playbook to adapt automatically to the specific characteristics of each target system. For anyone studying for the EX294 exam, understanding variables and facts is the difference between writing brittle, one-off playbooks and writing reusable, professional automation.

## The Family Recipe Card Analogy

When you first start cooking a new dish, you follow a recipe exactly. As you make it more often, you realise the cooking time depends on your particular oven, and the amount of salt depends on who will eat it. That's where variables come in. Because your oven runs hotter than the recipe assumes, you reduce the cooking time by 10 minutes. Because your family hates salty food, you halve the salt. In Ansible, a playbook is like that base recipe: it tells you what steps to perform. But each server (your oven, your family, your kitchen) has its own facts: its operating system, its available memory, its hostname. A variable is like the note you write on the recipe card: "reduce time by 10 minutes if using the fan oven". Facts are the measurements you discover by looking at the specific kitchen before you start cooking: "this oven is a fan oven, so adjust the time".

The first cause is that you write a generic playbook. The effect is that Ansible runs it on many different servers. Without variables and facts, you would have to write a separate recipe for every single kitchen. With them, you write one recipe that automatically changes its behaviour based on the kitchen it finds itself in. That's the whole point: saving you from repetitive work while keeping your systems correctly configured.

## Core explanation

Let's start with the big picture. When you use Ansible to manage servers, you write a file called a playbook. A playbook is a list of tasks that Ansible should perform, like "install this package" or "start that service". Without variables and facts, every playbook would be rigid: you'd have to hard-code every detail, such as the exact IP address of a database server, the exact path to a log file, or the exact number of CPU cores you expect. That works if you are managing exactly one server exactly once. But what if you have ten servers, or fifty, or a thousand?

Variables are like adjustable settings. They let you store a piece of data (a value) and refer to it by a name. Instead of writing "install httpd" in your playbook, you could write "install {{ webserver_package }}" and define webserver_package as "httpd" in one place. If you later move to a different Linux distribution that uses a different package name, you change the variable value once, not every single task.

Facts are a special kind of variable. They are not written by you; they are collected automatically by Ansible before it runs any tasks. Ansible connects to each managed node (the remote server it's controlling) and runs a module called the setup module. That module gathers a huge dictionary (a collection of key-value pairs) of information about that specific machine: its IP addresses, its operating system version, the amount of RAM, the disk partitions, the network interfaces, the hostname, the CPU architecture, and hundreds of other details. This dictionary is stored in a special variable called ansible_facts.

Why do facts exist? Because when you automate, you cannot predict every detail of every machine. Imagine you write a playbook that should install a web server. On a Red Hat Enterprise Linux (RHEL) system, the package is called "httpd". On an Ubuntu system, it's called "apache2". If you hard-code "httpd", your playbook will fail on Ubuntu. With facts, you can check the value of ansible_os_family inside your playbook and use the right package name automatically. The playbook becomes smart.

Now, how do you use variables and facts in practice? There are several places you can define variables:

- In the playbook itself, under the vars keyword. This is good for small, playbook-specific values.
- In separate variable files, especially in a directory called group_vars or host_vars. Ansible automatically loads variables from these files based on the group or host name of the target machine. This is how you manage differences between groups of servers, like web servers versus database servers.
- In inventory files, either as inventory variables attached to a specific host or group.
- From the command line using the --extra-vars (or -e) option. This overrides all other definitions, which is useful for testing.

For facts, you do not define them; Ansible gathers them automatically at the start of every playbook run. You access them using the ansible_facts variable. For example, ansible_facts['os_family'] gives you the operating system family (like "RedHat" or "Debian"). You can also use a shorthand: ansible_os_family. Both work.

The order of precedence is critical for the EX294 exam. Ansible uses a specific hierarchy to decide which value to use if the same variable is defined in multiple places. The general rule is: the most specific definition wins. From highest to lowest priority:

- Extra vars from the command line (-e).
- Variables set in a playbook's vars_prompt (you'll learn that later).
- Variables set in a task block or include.
- Role variables (defaults and vars in roles).
- Variables set in the inventory file (host variables before group variables).
- Variables set in group_vars and host_vars directories.
- The default values in a role's defaults directory.
- The ansible_facts gathered from the managed node.

Why does this order matter? Because if you accidentally define the same variable in two places, Ansible will use the one with higher priority. If you expect a variable to be one value but it's being overridden, your playbook might behave unexpectedly. The exam loves to test this through questions like: "Given the following inventory and playbook, what value will the variable my_var have?"

Let's walk through a real example. You have a playbook that installs Nginx. On a CentOS server, the package is called "nginx". On an Ubuntu server, the package is also called "nginx" in modern versions, but the service name might differ. Let's say you want to use a variable called nginx_service_name. You define it in group_vars/centos.yml as "nginx" and in group_vars/ubuntu.yml as "nginx" as well (they are actually the same now, but historically they differed). Using facts, you can dynamically select the correct group variable file based on the group membership of the host. Alternatively, you can use a conditional task that checks ansible_facts['os_family'] and sets the variable accordingly.

Facts also allow you to make decisions in your playbook. For example, you might want to disable transparent huge pages on a database server, but only if the total memory is greater than 16 GB. You can write a task that checks ansible_facts['memtotal_mb'] and runs a command conditionally. This is where variables and facts show their power: they turn a static script into a dynamic, adaptive automation system.

## Real-world context

Imagine you work for a mid-sized company that runs an e-commerce website. You are responsible for managing the infrastructure: a few dozen web servers running Red Hat Enterprise Linux, a handful of database servers also on RHEL, and a small number of Ubuntu-based application servers for development. Each group of servers has similar but slightly different requirements. Your task is to automate the initial setup and ongoing configuration of all these machines.

Without variables and facts, you would have to write a separate playbook for each type of server, and then a separate playbook for each environment (development, testing, production). That would be a maintenance nightmare. With variables and facts, you write one main playbook that does the following:

- It runs against all servers in your inventory.
- At the start of the run, Ansible gathers facts from each server.
- Based on the value of ansible_os_family, it uses the correct package manager. For RHEL, that's yum (or dnf); for Ubuntu, that's apt.
- Based on the group the server belongs to (web, database, app), it loads specialised variables from group_vars. For example, web servers get variables like http_port: 80 and ssl_cert_path: /etc/ssl/certs. Database servers get variables like db_port: 3306 and max_connections: 500.
- It then uses those variables in tasks. One task might be: "Copy the Nginx configuration file using the template module, which substitutes the value of {{ http_port }} into the config file."
- Finally, it restarts the appropriate service, also defined by a variable like {{ webserver_service }}.

The step-by-step reality of an IT professional's day:

1. You start by defining your inventory. In production, you have 20 RHEL web servers in the group [webservers]. In development, you have 2 Ubuntu servers in [webservers_dev]. You also have a group [databases] with 5 RHEL database servers.

2. You create the directory structure for variables: group_vars/webservers.yml, group_vars/webservers_dev.yml, group_vars/databases.yml. In each file, you define the variables specific to that group. For example, in webservers.yml you might have:
   - webserver_package: httpd
   - webserver_service: httpd
   - http_port: 80

   In webservers_dev.yml you might have:
   - webserver_package: apache2
   - webserver_service: apache2
   - http_port: 8080 (because port 80 might be taken on a shared dev server)

3. In your main playbook, you write a task that uses the template module to deploy a configuration file. The template contains lines like:
   Listen {{ http_port }}
   ServerName {{ ansible_fqdn }}
   Here, {{ ansible_fqdn }} is a fact that Ansible gathered, giving the fully qualified domain name of the server.

4. You also write a task that ensures the correct package is installed:
   yum: name={{ webserver_package }} state=present
   This task is written differently for the Ubuntu servers (using the apt module), but you can use a when condition based on ansible_os_family:
   - name: Install webserver on RHEL
     yum: name={{ webserver_package }} state=present
     when: ansible_facts['os_family'] == "RedHat"

   - name: Install webserver on Ubuntu
     apt: name={{ webserver_package }} state=present
     when: ansible_facts['os_family'] == "Debian"

5. You run the playbook against all servers. Ansible connects to each one, gathers facts, matches them to the correct group variables, and executes the tasks. The same playbook works on RHEL and Ubuntu, on production and development, because the variables and facts adapt the behaviour automatically.

The next day, your company adds five new Ubuntu servers for a caching layer. You simply add them to the inventory, create a new group_vars file for them, and re-run the playbook. No new playbooks are needed. That is the power of variables and facts in real IT work.

## Exam focus

The EX294 exam tests your ability to use variables and facts in practical, scenario-based questions. You will not be asked to recite definitions; instead, you will be given a situation and asked to choose the correct variable definition, fact usage, or precedence outcome. Here is exactly what to expect.

The exam loves to test variable precedence. They will give you an inventory file that defines a variable at the host level and another at the group level, and a playbook that defines the same variable. You must know which value wins. The correct answer pattern is to recall the priority order: extra vars (highest), then playbook vars, then inventory host_vars, then inventory group_vars, then role defaults. A common trap is that they define a variable in a role's defaults directory but also in group_vars, and ask you what the final value is. The correct answer is group_vars overrides role defaults, because role defaults have the lowest priority.

Another trap involves the ansible_facts variable. Beginners often think that ansible_facts is defined within the playbook, but it is automatically gathered by the setup module. The exam may ask: "What module is used to gather facts?" The answer is the setup module. They may also ask: "Which of the following is NOT a valid way to access a fact?" The correct answer is anything that uses an incorrect syntax, like "ansible_facts.os" (missing the brackets or dot notation). You must know that you can access facts using either ansible_facts['os_family'] or ansible_os_family (the shorthand).

Key exam topics include:

- Variable definitions: Where you can define variables (playbook, inventory, group_vars, host_vars, command line, role defaults, role vars).
- Variable precedence: The exact order, from highest to lowest.
- Using facts in conditionals: For example, using when: ansible_facts['os_family'] == "RedHat".
- The setup module: What it does and how to see gathered facts (ansible hostname -m setup).
- The difference between inventory variables and playbook variables: Inventory variables are tied to hosts/groups; playbook variables are tied to the play.
- Using vars_files and vars_prompt: How to load variables from external files or prompt the user for input.
- The special variable ansible_facts: Its structure and how to use it.
- Gathered facts versus cached facts: Ansible can cache facts for performance, which is relevant but not always tested in depth.

Typical exam question patterns:

- You are given a snippet of an inventory file and a playbook. You must determine the value of a variable after the playbook runs. The trap is that the inventory defines a variable at the group level, but the playbook also defines the same variable. The correct answer is the playbook variable, unless extra vars are used.
- You are asked to write a task that only runs on servers with a specific OS version. The correct answer uses a when clause with ansible_facts['distribution_version'] or ansible_distribution_version.
- You are given a list of variable definitions and asked which one will override the others. The correct answer is the one with the highest precedence, such as extra vars (-e).
- You are asked to identify the correct syntax for referencing a fact. The correct answer is either the dictionary syntax (ansible_facts['hostname']) or the shorthand (ansible_hostname). Incorrect answers often use dot notation incorrectly (ansible_facts.hostname) — note that while dot notation does work, the exam often expects the bracket syntax for clarity.
- You are asked to explain why a playbook fails when run on a server. The answer is usually because a variable is not defined for that server (it might be missing from group_vars/host_vars), or because a fact is not available (the setup module couldn't connect, or the server is unreachable).

Memorise the following definitions exactly:
- Variable: A named value defined by the user or by Ansible that can be used in tasks, templates, and conditionals.
- Fact: System information gathered by the setup module from a managed node, stored in ansible_facts.
- Precedence: The order in which Ansible evaluates conflicting variable definitions, from highest priority to lowest.

Finally, the exam will test whether you can actually read and understand variable syntax in YAML. You will see lines like:
  vars:
    my_var: "value"
  tasks:
    - name: Print variable
      debug:
        msg: "The value is {{ my_var }}"
Make sure you are comfortable with double curly braces ({{ }}) for variable interpolation.

## Step by step

1. **Write a generic playbook using variables** — Instead of hard-coding values like package names or service names, use placeholders with double curly braces. For example, write 'package: {{ webserver_pkg }}' instead of 'package: httpd'. This makes the playbook reusable.
2. **Define variables in the inventory or group_vars** — Create a directory called group_vars in your project. Inside it, create a YAML file named after a group (e.g., webservers.yml). Define the variable webserver_pkg: httpd for RHEL machines. For a different group, define the same variable with a different value.
3. **Run the playbook against a host and observe fact gathering** — Execute 'ansible-playbook -i inventory myplaybook.yml'. Ansible will automatically connect to each host, run the setup module, and store the gathered facts in memory. You can see all facts by running 'ansible hostname -m setup'.
4. **Use a fact in a conditional task** — Write a task that only runs when a specific fact is true. For example: 'when: ansible_facts['os_family'] == "Debian"'. This allows the same playbook to handle different operating systems by executing different commands or using different modules.
5. **Deploy a config file with a template that uses variables and facts** — Create a Jinja2 template file (e.g., nginx.conf.j2) that includes variable references like 'listen {{ http_port }}' and fact references like 'server_name {{ ansible_fqdn }}'. Use the template module in a task to deploy the rendered file to the target host. The final file will be customised per host.

## Comparisons

### Variable vs Fact

**Variable:**
- Defined by the user in playbooks, inventory, or variable files
- Has a scope determined by where it is defined
- Can be overridden by higher-precedence definitions

**Fact:**
- Automatically collected by Ansible from the managed node
- Per-host, not global; each host has its own set of facts
- Cannot be overridden by user-defined variables of the same name (user vars win)

### Inventory variable vs Playbook variable

**Inventory variable:**
- Defined in the inventory file or group_vars/host_vars directories
- Has lower precedence than playbook vars (except extra vars)
- Applies to all tasks for hosts in that group or host

**Playbook variable:**
- Defined directly under the 'vars' keyword in a play
- Has higher precedence than inventory variables
- Applies only to the play (or role) where it is defined

### ansible_facts['os_family'] vs ansible_distribution

**ansible_facts['os_family']:**
- Returns a high-level category: RedHat, Debian, Suse, Windows
- Useful for broad package manager decisions
- Less granular; does not distinguish between CentOS and Fedora

**ansible_distribution:**
- Returns the specific distribution name: CentOS, Ubuntu, Fedora, etc.
- Useful for distribution-specific configuration files or package names
- More granular than os_family

### group_vars/ vs host_vars/

**group_vars/:**
- Variables defined here apply to all hosts in a specific inventory group
- The directory name matches the group name (e.g., webservers.yml)
- Lower precedence than host_vars for the same host

**host_vars/:**
- Variables defined here apply only to a specific host
- The file name matches the host name exactly (e.g., web01.yml)
- Higher precedence than group_vars variables for that host

## Diagram

_This flow chart shows how Ansible starts with a generic playbook, gathers facts from each host, loads group and host variables, resolves any conflicts using precedence, and then uses the final values to run tasks that are tailored to that specific host._

```mermaid
flowchart TD
    A[Playbook uses variable placeholders] --> B[Ansible connects to host]
    B --> C[Gathers facts via setup module]
    C --> D[ansible_facts created for that host]
    D --> E[Variables loaded from group_vars/host_vars]
    E --> F[Precedence rules applied]
    F --> G[Tasks run using final variable values and facts]
    G --> H[Host-specific configuration deployed]
```

## Common misconceptions

- **Misconception:** Facts are just variables that you define in a file called facts.yml. **Reality:** Facts are automatically gathered by Ansible's setup module and are not defined by the user. You can define custom facts, but the term 'facts' in Ansible primarily refers to the system information collected automatically. (The word 'facts' sounds like 'variables you set', so beginners assume they need to write them manually. The exam specifically tests that facts are gathered, not defined.)
- **Misconception:** All variables in Ansible are global and can be accessed anywhere in the playbook regardless of where they are defined. **Reality:** Variables have a defined scope. A variable defined in a playbook is available only within that play. A variable defined in a role's defaults directory is available to that role and any task that includes the role. Host and group variables are available to tasks targeting those hosts. Scope matters. (It is intuitive to think that once you define a variable, it is 'global' like a constant in a script. Ansible's scoping rules are more nuanced, and the exam tests this by defining variables in nested structures.)
- **Misconception:** If I define a variable in my inventory file, it will override any variable with the same name defined in my playbook. **Reality:** Playbook variables have a higher precedence than inventory variables (with the exception of extra vars). So a variable defined in the playbook's 'vars' section will override the same variable defined in the host or group variables in the inventory. (Many beginners think the inventory is the 'top level' because it defines the hosts. The precedence hierarchy is unintuitive: the playbook is more specific than the inventory, so its variables win.)
- **Misconception:** The ansible_facts variable contains only the facts from the first host in the play, so I cannot use facts from different hosts within the same play. **Reality:** ansible_facts is per-host. During a play that targets multiple hosts, Ansible gathers facts for each host independently. When a task runs on a specific host, ansible_facts contains the facts for that specific host. You can also access facts of other hosts using the hostvars dictionary. (New learners often think of ansible_facts as a single global variable. The per-host nature becomes clear when you think about the fact that each server is a separate machine with its own data.)

## Key takeaways

- Variables store user-defined values and adapt your playbook to different environments without rewriting tasks.
- Facts are system information automatically gathered by the setup module from every managed node before tasks run.
- Variable precedence determines which value wins when the same variable is defined in multiple places; extra vars override everything.
- You can access facts using either ansible_facts['key'] or the shorthand ansible_<key> (like ansible_os_family).
- Group variables defined in group_vars/ are the primary way to manage differences between groups of servers.
- Variable scope is not global; a variable defined in one play is not automatically available in another play unless explicitly passed.

## FAQ

**How do I see all facts about a managed node in Ansible?**

Run the command: ansible hostname -m setup. Replace hostname with the actual host name from your inventory. This will output a huge JSON dictionary of all gathered facts.

**What is the difference between 'vars' and 'vars_files' in a playbook?**

The 'vars' keyword defines variables directly inside the playbook YAML. 'vars_files' loads variables from an external file. Both have the same precedence. Use 'vars_files' to keep your playbook clean when you have many variables.

**Can I define a variable that is the same on every host without using group_vars?**

Yes, you can define a variable in the playbook under the 'vars' section, or define it in a file loaded via 'vars_files'. Alternatively, you can use the 'defaults' directory in a role for role-specific defaults.

**Why does my playbook say 'VARIABLE IS NOT DEFINED'?**

This error means the variable you are referencing has not been defined anywhere that Ansible can see. Check that you have defined it in the correct scope (playbook vars, group_vars, host_vars, inventory, or role defaults) and that the variable name is spelled exactly the same.

**What is the ansible_facts key for the operating system?**

Use 'ansible_facts['os_family']' or the shorthand 'ansible_os_family'. This returns values like 'RedHat', 'Debian', 'Suse', or 'Windows'.

**How do I override a variable for a single playbook run without editing files?**

Use the --extra-vars (or -e) flag on the command line. For example: ansible-playbook myplaybook.yml -e "my_var=new_value". This has the highest precedence.

## Check your understanding

1. **What module does Ansible run automatically to gather facts about a managed node at the start of a play?**

   Answer: The setup module. It collects detailed system information and stores it in the ansible_facts variable.

2. **Your playbook defines a variable 'app_port' as 8080. Your inventory defines the same variable for the host as 9090. Which value will be used when running the playbook?**

   Answer: The value from the playbook (8080) will be used, because playbook variables have higher precedence than inventory variables.

3. **How do you conditionally run a task only on hosts running an operating system from the Debian family?**

   Answer: Add a 'when' condition: when: ansible_facts['os_family'] == "Debian". You can also use the shorthand 'ansible_os_family'.

4. **What is the difference between a variable defined in 'group_vars/all.yml' and one defined in 'group_vars/webservers.yml'?**

   Answer: The variable in 'group_vars/all.yml' applies to every host in the inventory. The variable in 'group_vars/webservers.yml' applies only to hosts that belong to the 'webservers' group.

5. **Which command allows you to see all gathered facts for a host named 'node1' from the command line?**

   Answer: The command is: ansible node1 -m setup. This outputs the full dictionary of facts for that node.

---

Interactive version with quiz and diagrams: https://courseiva.com/learn/redhat-rhce/variables-and-facts
