Ansible Facts and Registered Variables: Building Dynamic Playbooks

By LinuxCert.Guru Team·

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 register keyword
  • Use facts and registered variables in debug output, conditionals, and templates
  • Implement conditional task execution with the when statement
  • 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: false is set, fact variables are undefined. Always ensure fact gathering has run before using ansible_* variables.
    • If you disabled fact gathering for performance and then need a specific fact, use ansible.builtin.setup with a filter to gather just what you need
  • Treating registered variable output as a string when it is a dict. Registered variables are dictionaries. Accessing result.stdout gives the command output as a string; result.stdout_lines gives it as a list.
    • Use ansible.builtin.debug: var: result to see the full structure before using it
  • Not using ignore_errors when registering output from a command that might fail. If a command fails and ignore_errors: true is not set, the play stops before the registered variable is ever used.
    • Common example: rpm -q packagename returns non-zero if the package is not installed
  • Using registered variables from a previous play. Registered variables only exist within the play where they were defined. Use set_fact to promote a value to a host variable that persists across plays.
  • Forgetting that facts use nested dictionary syntax. ansible_default_ipv4.address works in most contexts. Use bracket notation ansible_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_hostname or ansible_distribution in 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_family to make a playbook work correctly on both Red Hat and Debian family systems
  • Use the stat module, register its output, and conditionally create a file or directory based on whether the path exists
  • Use ansible_memtotal_mb or ansible_processor_vcpus to 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 setup to explore available facts before writing playbook conditionals
  • The register keyword captures complete task output including stdout, stderr, rc, changed, and failed
  • Always use ignore_errors: true when registering output from a command that might legitimately fail
  • The when statement with facts and registered variables is how playbooks make intelligent decisions
  • Custom facts in /etc/ansible/facts.d/ expose application-specific information under ansible_local
  • Use gather_facts: false for speed when facts are not needed, and ansible.builtin.setup to 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

$_
Ready to go beyond reading?Practice with free, auto-graded RHCSA labs.
Start free →