Facts #

When managing dozens to thousands of servers with varying specifications, automation can’t run blindly. You can’t use the exact same configuration file for a server with 2GB of RAM and a server with 64GB of RAM. You also can’t run the same package management commands on a CentOS server and an Ubuntu server. For your playbooks to run intelligently and adaptively, Ansible must know the physical and logical characteristics of managed nodes in real time.

Ansible solves this system discovery need through the facts mechanism. Facts are automatic system variables Ansible collects directly from target servers at the start of every play execution. This article deeply dissects how facts are collected, presents a reference table of the most popular facts, introduces reading system environment variables (ansible_env), guides accessing facts across target servers, creating your own application custom facts, and configuring performance optimizations through disabling and caching facts in large-scale environments.


How Facts Are Collected #

By default, at the start of every play execution, Ansible inserts an implicit task in your terminal output named TASK [Gathering Facts]. Behind the scenes, this task executes a special module called setup on the target server.

The setup module does a thorough investigation into the target operating system: reads files in the /proc directory, calls system utility commands like ip, df, lshw, and detects virtualization status. All that found data is then compressed into a structured JSON data structure and sent back to the Control Node, where it’s stored as dynamic variables prefixed with ansible_*.

You can try calling the setup module manually from the terminal CLI to inspect all available facts on a target host using an ad-hoc command:

# Collect and display all facts from web-01 in JSON format
ansible -i inventory/ hosts.ini web-01 -m setup

Because the returned JSON data is very large (can reach thousands of lines), you can filter the output using the filter argument:

# Only display data related to the operating system
ansible -i inventory/ hosts.ini web-01 -m setup -a "filter=ansible_distribution*"

# Only display data related to memory allocation
ansible -i inventory/ hosts.ini web-01 -m setup -a "filter=ansible_memory*"

The facts collected by the setup module are grouped into several main categories. Here’s a reference table of fact data most often used as automation logic parameters in playbooks:

1. Operating System Information (OS Facts) #

Variable NameExample ValuesUsage Description
ansible_distribution"Ubuntu", "Debian", "Rocky", "CentOS"Determines the specific target OS distribution name.
ansible_distribution_version"22.04", "9.2", "11"Checks the OS release version for feature alignment.
ansible_distribution_major_version"22", "9", "11"Used for major version operations (e.g. RHEL 8 vs 9).
ansible_os_family"Debian", "RedHat", "Suse"Groups package manager commands (apt vs dnf).

2. Hardware Information (Hardware Facts) #

Variable NameExample ValuesUsage Description
ansible_processor_count2The number of physical CPU sockets installed.
ansible_processor_vcpus4The number of logical CPU threads (very useful for thread tuning).
ansible_memtotal_mb8192Total physical RAM capacity in Megabytes.
ansible_memfree_mb2048Remaining free RAM capacity when gathering facts ran.
ansible_architecture"x86_64", "aarch64"Selects the binary installation package (Intel vs ARM).

3. Network Information (Network Facts) #

Variable NameExample ValuesUsage Description
ansible_hostname"web-prod-01"The short server hostname without the domain (non-FQDN).
ansible_fqdn"web-prod-01.local"The full domain name of the target server.
ansible_default_ipv4.address"192.168.1.50"The main IP address used for the default outbound route.
ansible_default_ipv4.interface"eth0"The active main NIC (eth0, ens3, etc.).
ansible_all_ipv4_addresses["192.168.1.50", "10.0.0.5"]A list containing all IPv4 addresses registered on the system.

4. Date & Time Information (Time Facts) #

Variable NameExample ValuesUsage Description
ansible_date_time.date"2026-06-17"The current date in YYYY-MM-DD format.
ansible_date_time.time"12:00:00"The current local time in HH:MM:SS format.
ansible_date_time.iso8601"2026-06-17T05:00:00Z"The international standard timestamp for audit logging.
ansible_date_time.epoch"1781672400"Unix timestamp for time difference calculations.

Here’s a practical example of writing a task leveraging the facts table above to adjust an application configuration template file:

# Automatically adjust the Apache web server worker thread count
- name: Apply the Apache HTTPD configuration
  template:
    src: httpd.conf.j2
    dest: /etc/httpd/conf/httpd.conf
  vars:
    # Allocate 2 threads per logical CPU, with a minimum limit of 2 threads
    max_workers: "{{ [2, (ansible_processor_vcpus | int * 2)] | max }}"

Reading System Environment Variables with ansible_env #

Besides hardware and OS information, Ansible also captures all active environment variables for the SSH user used to execute commands on the managed node. These variables are stored in the ansible_env dictionary.

You can use ansible_env to detect important system variables like the home directory path, HTTP proxy configuration, or system executable paths.

# Reading target system environment variables
- name: Create a downloads directory in the target user's home
  file:
    path: "{{ ansible_env.HOME }}/downloads"
    state: directory
    mode: '0755'

- name: Use a binary from the target's custom PATH if available
  command: my-custom-tool --version
  environment:
    PATH: "{{ ansible_env.PATH }}:/opt/custom/bin"

Accessing Other Hosts’ Facts (hostvars) #

By default, fact variables are only available in the execution thread of the relevant host. However, in production cluster orchestration, you often need information from other servers.

For example, when configuring the front proxy web server (Nginx), you need to know the default IP addresses of several backend application servers (PHP-FPM or NodeJS) to put into the Nginx upstream block.

Ansible solves this need through the global hostvars variable. With hostvars, you can break through host isolation limits and read facts from any server registered in your inventory, as long as the target server is in the same play run or its data has been stored in cache.

# FILE: playbook.yml
- name: Load Balancer Proxy Configuration
  hosts: loadbalancers
  tasks:
    - name: Configure the Nginx Upstream proxy
      template:
        src: upstream.conf.j2
        dest: /etc/nginx/conf.d/upstream.conf
      notify: Restart Nginx

# FILE: templates/upstream.conf.j2
# Upstream configuration using the dynamic IP addresses of backend servers
upstream backend_app {
  {% for host in groups['webservers'] %}
  # ✓ Takes the default IP of each server in the webservers group
  server {{ hostvars[host]['ansible_default_ipv4']['address'] }}:8080;
  {% endfor %}
}

[!WARNING] Using hostvars requires the target server whose data you want to fetch to have already executed the Gathering Facts stage in the same play or a previous play. If that host is skipped or excluded from the play targets, Ansible returns an Undefined Variable error. To fix this, you can enable the Fact Caching feature.


Custom Facts Implementation #

Besides the built-in system facts collected by the setup module, you can also define your own custom facts on managed nodes. Custom facts are very useful for storing application-specific statuses managed by developer teams (for example microservice release versions, database replication status, or server responsible owners).

Custom facts must be stored on the target server in the /etc/ansible/facts.d/ directory as files with the .fact extension. These files can be:

  • Static files in INI format
  • Static files in JSON format
  • Executable script files (like Bash or Python) producing standard JSON output to stdout.

1. Example Static Custom Fact (JSON) #

You can deploy a static metadata configuration file using Ansible:

// /etc/ansible/facts.d/app_info.fact
{
  "metadata": {
    "application": "payment-api",
    "version": "1.4.2",
    "owner": "fintech-team"
  }
}

2. Example Dynamic Custom Fact (Executable Script) #

If you want the fact to be dynamic (for example checking remaining backup space dynamically), you can place a shell script returning JSON output:

#!/bin/bash
# /etc/ansible/facts.d/system_health.fact
# Executable scripts must return valid JSON format to stdout
FREE_SPACE=$(df -h / | awk 'NR==2 {print $4}')
echo "{\"disk_root_free\": \"$FREE_SPACE\"}"

Custom Facts Deployment Playbook Scenario: #

Here’s a playbook to install the directory, copy custom facts, and trigger a system facts reload:

# 1. Prepare the custom facts storage directory on the managed node
- name: Ensure the facts.d directory is available
  file:
    path: /etc/ansible/facts.d
    state: directory
    owner: root
    group: root
    mode: '0755'

# 2. Copy the custom fact data file to the target server
- name: Deploy the static custom fact
  copy:
    src: files/app_info.fact
    dest: /etc/ansible/facts.d/app_info.fact
    owner: root
    group: root
    mode: '0644'
  notify: Update System Facts

# 3. Trigger the update so Ansible reads the new variables right away
handlers:
  - name: Update System Facts
    setup:
      filter: ansible_local
    # ✓ The setup module is re-run specifically to load the ansible_local variables

Once the .fact file is installed, Ansible automatically reads it under the ansible_local namespace. You can access its values inside playbook tasks like this:

- name: Display application metadata from custom facts
  debug:
    msg: "Application {{ ansible_local.app_info.metadata.application }} version {{ ansible_local.app_info.metadata.version }}"

Performance Optimization: Disabling Gathering Facts #

Although facts are very useful, the fact-gathering process takes a fairly long time. For every target host, Ansible must make an SSH connection, transfer the setup module, execute it, and wait for the JSON payload to come back. This process takes an average of 1 to 3 seconds per server.

If you have a playbook that only does simple tasks (like distributing static files or triggering service restarts) without needing OS or memory condition evaluation, you’re strongly advised to disable the fact-gathering process to save execution time.

You can disable fact collection by adding the gather_facts: false parameter at the play level:

# Playbook with high-speed optimization without fact gathering
- name: Fast Static Image Asset Deployment
  hosts: static_servers
  gather_facts: false  # ✓ Disables Gathering Facts (Saves initial SSH time)
  tasks:
    - name: Copy the promo banner file
      copy:
        src: files/promo.png
        dest: /var/www/html/assets/promo.png

In large-scale production environments with hundreds to thousands of servers, writing gather_facts: false can cut total playbook execution waiting time by several minutes.


Fact Caching Configuration in ansible.cfg #

If you still need fact data for complex orchestration but want your playbook to run lightning fast without repeated SSH connection overhead, the best solution is enabling Fact Caching.

With Fact Caching, Ansible stores fact data collected on the first execution into external storage media. On subsequent executions, Ansible reads the facts directly from the cache without querying the target server.

Caching Backend Options #

  1. jsonfile: Stores static JSON files in a local folder on the Control Node. Easy to configure, no extra dependency server needed, great for small teams.
  2. redis: Stores fact data in the memory of a centralized Redis server. Highly recommended for enterprise-scale teams due to incredibly fast memory read-write performance and high data consistency across runners.
  3. memcached: An alternative distributed memory-based backend for session caching management.

Here’s a flow diagram visualizing the fact-reading decision process using system caching:

flowchart TD
    Start(["Start Playbook Run"]) --> CheckSmart{"Rule: gathering = smart?"}
    
    CheckSmart -- "Yes" --> CheckCache{"Is Host X's Cache Available & Valid?"}
    CheckSmart -- "No (gathering = implicit)" --> RunSetup["Run the setup module via SSH (Slow)"]
    
    CheckCache -- "Yes (Not Expired)" --> LoadCache["Load Facts from Cache (Very Fast)"]
    CheckCache -- "No (Expired / Empty)" --> RunSetup
    
    RunSetup --> SaveCache["Save the New Facts Payload to the Cache Backend"]
    SaveCache --> RunTasks["Continue Playbook Task Execution"]
    LoadCache --> RunTasks
    RunTasks --> End(["Playbook Run Finished"])

To enable caching based on local JSON files, just add the following configuration to your project’s ansible.cfg file:

# FILE: ansible.cfg
[defaults]
# Set smart fact gathering (only if the cache is expired)
gathering = smart

# Use the local JSON file storage backend
fact_caching = jsonfile

# Determine the cache storage folder path on the Control Node
fact_caching_connection = .ansible_facts_cache

# Set the cache validity period (86400 seconds = 24 hours)
fact_caching_timeout = 86400

If you want to migrate to a centralized Redis database for large teams:

# FILE: ansible.cfg (Redis Enterprise Version)
[defaults]
gathering = smart
fact_caching = redis
# Connection format: host:port:db_index:password (optional)
fact_caching_connection = redis-server.internal.net:6379:0
fact_caching_timeout = 86400

Summary #

  • System Discovery: Facts are automatic system discovery variables collected by the setup module at the start of every play under the ansible_* namespace.
  • Manual Setup Module: You can investigate available facts on target servers from the command line using the ad-hoc command ansible <host> -m setup.
  • Cross-Host Variables: Use the hostvars['host_name']['fact_name'] syntax to access fact data from other servers for upstream/cluster integration needs.
  • Environment Variables: The ansible_env dictionary provides direct access to all environment variables (like HOME, PATH) of the SSH user on the managed node.
  • Custom Facts: You can deploy .fact extension files to the /etc/ansible/facts.d/ directory to define custom metadata under the ansible_local namespace.
  • Performance Optimization: Use the gather_facts: false parameter on playbooks that don’t need system evaluation to save SSH handshake time.
  • Fact Caching: Enable the caching feature (jsonfile or redis) in ansible.cfg to avoid repeatedly calling the setup module on the same target servers.

← Previous: Scope & Precedence Next: Debugging →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact