Ansible Facts and Registered Variables: Building Dynamic Playbooks
Objective
This guide covers Ansible facts and registered variables, two of the most important tools for writing playbooks that adapt to each managed host rather than assuming every host is identical. By the end, you will know how to:
- Access and display automatically gathered Ansible facts
- Capture task output using the
registerkeyword - Use facts and registered variables in debug output, conditionals, and templates
- Implement conditional task execution with the
whenstatement - Control fact gathering behaviour including custom facts and selective gathering
- Apply these patterns to RHCE exam tasks involving dynamic playbook logic
A static playbook that hardcodes values works on the server you wrote it for. A dynamic playbook that reads values from the host itself works everywhere. Facts and registered variables are what make the difference. They let your playbook ask "what is this host?" and "what just happened?" before deciding what to do next.
Ansible Facts: Automatically Collected Host Information
When Ansible connects to a managed host, it runs a gather_facts step by default. This collects detailed information about the host and stores it as variables you can use throughout the playbook. You do not define these variables. Ansible gathers them automatically from the remote system.
| Category | Key Facts | Example Value |
|---|---|---|
| Identity | ansible_hostname, ansible_fqdn |
web01, web01.example.com |
| Operating system | ansible_distribution, ansible_distribution_version, ansible_os_family |
RedHat, 10.0, RedHat |
| Hardware | ansible_processor_vcpus, ansible_memtotal_mb |
8, 16384 |
| Network | ansible_default_ipv4.address, ansible_interfaces |
192.168.1.10, ['lo', 'eth0'] |
| Storage | ansible_devices, ansible_mounts |
Dict of devices and mount points |
| Time | ansible_date_time.iso8601, ansible_date_time.date |
2026-06-15T14:30:00Z, 2026-06-15 |
Viewing Available Facts
# View all facts gathered from a host
ansible webservers -m ansible.builtin.setup
# Filter to facts containing a specific keyword
ansible webservers -m ansible.builtin.setup -a "filter=ansible_memory*"
# View network facts only
ansible webservers -m ansible.builtin.setup -a "filter=ansible_*ipv4*"
# View facts in a playbook using the debug module
- name: Display all gathered facts
ansible.builtin.debug:
var: ansible_facts
Running ansible hostname -m setup directly from the command line shows the complete fact tree for a host. This is the fastest way to find the exact variable name for a piece of information you want to use in a playbook.
Using Facts in Playbooks
Displaying Facts with the debug Module
---
- name: Display system facts
hosts: all
tasks:
- name: Show hostname and OS information
ansible.builtin.debug:
msg:
- "Hostname: {{ ansible_hostname }}"
- "FQDN: {{ ansible_fqdn }}"
- "OS: {{ ansible_distribution }} {{ ansible_distribution_version }}"
- "OS Family: {{ ansible_os_family }}"
- "vCPUs: {{ ansible_processor_vcpus }}"
- "RAM: {{ ansible_memtotal_mb }} MB"
- "IP Address: {{ ansible_default_ipv4.address }}"
- name: Show current date and time on the managed host
ansible.builtin.debug:
msg: "Current time on {{ ansible_hostname }}: {{ ansible_date_time.iso8601 }}"
Using Facts in Conditionals
---
- name: OS-aware package installation
hosts: all
tasks:
- name: Install Apache on Red Hat family systems
ansible.builtin.dnf:
name: httpd
state: present
when: ansible_os_family == "RedHat"
- name: Install Apache on Debian family systems
ansible.builtin.apt:
name: apache2
state: present
when: ansible_os_family == "Debian"
- name: Configure high-performance settings for large servers
ansible.builtin.template:
src: nginx-performance.conf.j2
dest: /etc/nginx/conf.d/performance.conf
when: ansible_memtotal_mb >= 16384
- name: Display a warning for low-memory hosts
ansible.builtin.debug:
msg: "Warning: {{ ansible_hostname }} has only {{ ansible_memtotal_mb }}MB RAM"
when: ansible_memtotal_mb < 4096
Registered Variables: Capturing Task Output
The register keyword stores the complete output of a task in a variable. This output includes the return code, stdout, stderr, and a changed flag, everything Ansible knows about what the task did.
| Key in Registered Output | What It Contains | Common Use |
|---|---|---|
.stdout |
Standard output from the command as a string | Reading command output, checking values |
.stdout_lines |
Standard output split into a list by line | Looping over multi-line output |
.stderr |
Standard error output from the command | Debugging failed commands |
.rc |
Return code (0 = success, non-zero = failure) | Checking if a command succeeded |
.changed |
Boolean, true if the task made a change | Conditional logic based on whether something changed |
.failed |
Boolean, true if the task failed | Handling failures with ignore_errors |
.skipped |
Boolean, true if the task was skipped | Checking whether a when condition prevented a task |
Using register: Practical Examples
Capturing Command Output
---
- name: Capture and use command output
hosts: all
tasks:
- name: Check current disk usage
ansible.builtin.command: df -h /
register: disk_usage_result
- name: Display disk usage output
ansible.builtin.debug:
var: disk_usage_result.stdout
- name: Show just the lines of output
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ disk_usage_result.stdout_lines }}"
- name: Check if a process is running
ansible.builtin.command: pgrep nginx
register: nginx_process
ignore_errors: true
- name: Start nginx if it is not running
ansible.builtin.systemd:
name: nginx
state: started
when: nginx_process.rc != 0
Checking Service Status Before Acting
---
- name: Conditional service management
hosts: webservers
tasks:
- name: Check if Apache is installed
ansible.builtin.command: rpm -q httpd
register: apache_installed
ignore_errors: true
- name: Display Apache installation status
ansible.builtin.debug:
msg: "Apache is {{ 'installed' if apache_installed.rc == 0 else 'not installed' }}"
- name: Install Apache only if not already installed
ansible.builtin.dnf:
name: httpd
state: present
when: apache_installed.rc != 0
- name: Check Apache configuration syntax
ansible.builtin.command: httpd -t
register: apache_config_check
ignore_errors: true
- name: Fail if Apache configuration is invalid
ansible.builtin.fail:
msg: "Apache configuration has errors: {{ apache_config_check.stderr }}"
when: apache_config_check.rc != 0
Using register with stat for File Checks
---
- name: Conditional file operations
hosts: all
tasks:
- name: Check if the configuration file exists
ansible.builtin.stat:
path: /etc/myapp/myapp.conf
register: config_file
- name: Display file information if it exists
ansible.builtin.debug:
msg:
- "File exists: {{ config_file.stat.exists }}"
- "File size: {{ config_file.stat.size }} bytes"
- "Last modified: {{ config_file.stat.mtime }}"
when: config_file.stat.exists
- name: Create default config if file does not exist
ansible.builtin.template:
src: myapp.conf.j2
dest: /etc/myapp/myapp.conf
owner: root
group: root
mode: '0644'
when: not config_file.stat.exists
- name: Check if backup directory exists
ansible.builtin.stat:
path: /var/backups/myapp
register: backup_dir
- name: Create backup directory if missing
ansible.builtin.file:
path: /var/backups/myapp
state: directory
owner: root
group: root
mode: '0755'
when: not backup_dir.stat.exists
Combining Facts and Registered Variables
---
- name: Dynamic system configuration
hosts: all
tasks:
- name: Check current kernel version
ansible.builtin.command: uname -r
register: kernel_version
- name: Display system summary
ansible.builtin.debug:
msg:
- "Host: {{ ansible_fqdn }}"
- "OS: {{ ansible_distribution }} {{ ansible_distribution_version }}"
- "Kernel: {{ kernel_version.stdout }}"
- "vCPUs: {{ ansible_processor_vcpus }}"
- "RAM: {{ ansible_memtotal_mb }} MB"
- "Primary IP: {{ ansible_default_ipv4.address }}"
- name: Check available disk space on root partition
ansible.builtin.command: df --output=avail -BG /
register: root_disk_avail
- name: Warn if disk space is below 10GB
ansible.builtin.debug:
msg: "WARNING: Low disk space on {{ ansible_hostname }}: {{ root_disk_avail.stdout_lines[1] }} available"
when: root_disk_avail.stdout_lines[1] | regex_replace('[^0-9]', '') | int < 10
Controlling Fact Gathering
---
# Disable fact gathering entirely for a faster play
- name: Quick task with no fact gathering needed
hosts: all
gather_facts: false
tasks:
- name: Restart a service (no facts needed)
ansible.builtin.systemd:
name: nginx
state: restarted
---
# Gather only specific fact subsets for faster collection
- name: Collect only network and hardware facts
hosts: all
gather_facts: true
gather_subset:
- network
- hardware
tasks:
- name: Use network facts
ansible.builtin.debug:
var: ansible_default_ipv4.address
---
# Gather facts manually mid-play after making changes
- name: Re-gather facts after installing new hardware
hosts: all
tasks:
- name: Refresh facts to see current state
ansible.builtin.setup:
- name: Now use updated storage facts
ansible.builtin.debug:
var: ansible_devices
Custom Facts
# On the managed host: /etc/ansible/facts.d/myapp.fact
# Must return valid JSON and be executable
#!/bin/bash
echo '{
"version": "2.4.1",
"install_date": "2026-06-01",
"data_dir": "/var/lib/myapp",
"license": "enterprise"
}'
---
- name: Use custom application facts
hosts: all
tasks:
- name: Deploy the custom fact script
ansible.builtin.copy:
src: files/myapp.fact
dest: /etc/ansible/facts.d/myapp.fact
owner: root
group: root
mode: '0755'
- name: Refresh facts to load the new custom fact
ansible.builtin.setup:
- name: Display custom fact values
ansible.builtin.debug:
msg:
- "App version: {{ ansible_local.myapp.version }}"
- "Install date: {{ ansible_local.myapp.install_date }}"
- "License: {{ ansible_local.myapp.license }}"
- name: Upgrade only if version is outdated
ansible.builtin.dnf:
name: myapp
state: latest
when: ansible_local.myapp.version is version('3.0.0', '<')
Common Mistakes
- Referencing facts before they are gathered. If
gather_facts: falseis set, fact variables are undefined. Always ensure fact gathering has run before usingansible_*variables.- If you disabled fact gathering for performance and then need a specific fact, use
ansible.builtin.setupwith a filter to gather just what you need
- If you disabled fact gathering for performance and then need a specific fact, use
- Treating registered variable output as a string when it is a dict. Registered variables are dictionaries. Accessing
result.stdoutgives the command output as a string;result.stdout_linesgives it as a list.- Use
ansible.builtin.debug: var: resultto see the full structure before using it
- Use
- Not using ignore_errors when registering output from a command that might fail. If a command fails and
ignore_errors: trueis not set, the play stops before the registered variable is ever used.- Common example:
rpm -q packagenamereturns non-zero if the package is not installed
- Common example:
- Using registered variables from a previous play. Registered variables only exist within the play where they were defined. Use
set_factto promote a value to a host variable that persists across plays. - Forgetting that facts use nested dictionary syntax.
ansible_default_ipv4.addressworks in most contexts. Use bracket notationansible_default_ipv4['address']when key names contain special characters.
Quick Reference
| Task | Syntax |
|---|---|
| Display a fact | ansible.builtin.debug: var: ansible_hostname |
| Display all facts | ansible.builtin.debug: var: ansible_facts |
| Register task output | register: my_result |
| Use command stdout | {{ my_result.stdout }} |
| Use command return code | {{ my_result.rc }} |
| Conditional on fact | when: ansible_os_family == "RedHat" |
| Conditional on rc | when: my_result.rc != 0 |
| Conditional on changed | when: my_result.changed |
| Disable fact gathering | gather_facts: false |
| Gather specific subset | gather_subset: [network, hardware] |
| Re-gather facts mid-play | ansible.builtin.setup: |
| Access custom facts | {{ ansible_local.factname.key }} |
Facts, Registered Variables, and the RHCE Exam
Ansible facts and registered variables are core RHCE exam objectives. Common exam task patterns include:
- Write a playbook that uses a fact like
ansible_hostnameoransible_distributionin a debug message or configuration file - Register the output of a command and use the return code or stdout in a conditional that controls whether a subsequent task runs
- Use
ansible_os_familyto make a playbook work correctly on both Red Hat and Debian family systems - Use the
statmodule, register its output, and conditionally create a file or directory based on whether the path exists - Use
ansible_memtotal_mboransible_processor_vcpusto configure an application tuned to the host's hardware
Conclusion
Facts and registered variables are what separate a static script from intelligent automation. Facts tell your playbook what each host is. Registered variables tell your playbook what each task did. Together they make it possible to write one playbook that behaves correctly on every host in your inventory regardless of its OS, hardware, or current state.
- Ansible gathers facts automatically before each play: OS, hostname, IP, CPU, RAM, storage, and more
- Use
ansible hostname -m setupto explore available facts before writing playbook conditionals - The
registerkeyword captures complete task output including stdout, stderr, rc, changed, and failed - Always use
ignore_errors: truewhen registering output from a command that might legitimately fail - The
whenstatement with facts and registered variables is how playbooks make intelligent decisions - Custom facts in
/etc/ansible/facts.d/expose application-specific information underansible_local - Use
gather_facts: falsefor speed when facts are not needed, andansible.builtin.setupto re-gather when the host state changes mid-play
Start the Ansible Facts and Registered Variables lab at LinuxCert.Guru → https://linuxcert.guru/rhce/?name=rhce-ansible-facts