Ansible facts are system properties collected from managed hosts during playbook execution. They include hardware specs, network configuration, OS details, storage layout, and more. This data is useful for building dynamic inventories, generating host reports, and making playbooks adapt to different environments automatically.
This guide covers how to gather and filter Ansible facts, use them in playbooks, generate CSV and HTML host overview reports with Jinja2 templates, set up custom facts, and enable fact caching for performance at scale.
Prerequisites
Before you start, make sure you have the following in place:
- Ansible control node running any modern Linux distribution (Ubuntu 24.04, Rocky Linux 10, Debian 13, etc.)
- Ansible 2.15 or later installed on the control node – see our guide on installing Ansible AWX on Ubuntu / Debian if you need a web-based management layer
- One or more managed hosts reachable via SSH with key-based authentication
- A working Ansible inventory file with your target hosts defined
- Python 3 on both control node and managed hosts
Step 1: Gather Ansible Facts from Hosts
Ansible collects facts automatically at the start of every playbook run using the setup module. You can also run it manually with the ad-hoc command to inspect all available facts from a specific host.
Gather all facts from a single host:
ansible webserver01 -m setup
This returns a large JSON output containing every fact Ansible can discover – hardware, network interfaces, OS details, mount points, and more. To gather facts from all hosts in your inventory:
ansible all -m setup
You can also save the output to files for later processing. This creates one JSON file per host in the specified directory:
mkdir -p /tmp/ansible_facts
ansible all -m setup --tree /tmp/ansible_facts/
Each file is named after the hostname and contains the full fact tree. These files are useful for offline analysis and report generation.
Step 2: Filter Specific Ansible Facts
The full fact output is large – typically thousands of lines per host. Use the filter parameter to pull only what you need.
Get just the OS distribution name:
ansible all -m setup -a "filter=ansible_distribution"
The output shows the distribution for each host in your inventory:
webserver01 | SUCCESS => {
"ansible_facts": {
"ansible_distribution": "Ubuntu"
},
"changed": false
}
dbserver01 | SUCCESS => {
"ansible_facts": {
"ansible_distribution": "Rocky"
},
"changed": false
}
Use wildcards to match multiple facts at once. Pull all memory-related facts:
ansible all -m setup -a "filter=ansible_mem*"
This returns memory facts like ansible_memtotal_mb, ansible_memfree_mb, and ansible_memory_mb.
You can also limit fact collection to specific subsets for faster execution. The gather_subset option controls which fact categories to collect:
ansible all -m setup -a "gather_subset=network,hardware"
Available subsets include all, min, hardware, network, virtual, ohai, and facter. Using a targeted subset avoids collecting unnecessary data and speeds up the gathering process.
Step 3: Use Ansible Facts in Playbooks
Facts are available as variables inside playbooks through the ansible_facts dictionary or directly as top-level variables with the ansible_ prefix. This lets you write playbooks that adapt to each host’s configuration automatically.
Here is a reference table of the most commonly used fact variables:
| Fact Variable | Description |
|---|---|
ansible_hostname | Short hostname of the managed host |
ansible_fqdn | Fully qualified domain name |
ansible_os_family | OS family – RedHat, Debian, Suse, etc. |
ansible_distribution | Distribution name – Ubuntu, Rocky, Debian |
ansible_distribution_version | Full version string – 24.04, 10.0 |
ansible_default_ipv4.address | Primary IPv4 address |
ansible_memtotal_mb | Total RAM in megabytes |
ansible_processor_vcpus | Number of virtual CPUs |
ansible_kernel | Running kernel version |
ansible_architecture | CPU architecture – x86_64, aarch64 |
Create a playbook that prints a host summary using these facts. Save the following as host_overview.yml:
sudo vi host_overview.yml
Add the following playbook content:
---
- name: Display host overview from facts
hosts: all
gather_facts: true
tasks:
- name: Print host summary
ansible.builtin.debug:
msg: |
Hostname: {{ ansible_hostname }}
FQDN: {{ ansible_fqdn }}
OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
OS Family: {{ ansible_os_family }}
Kernel: {{ ansible_kernel }}
Architecture: {{ ansible_architecture }}
CPU Cores: {{ ansible_processor_vcpus }}
Total RAM: {{ ansible_memtotal_mb }} MB
Primary IP: {{ ansible_default_ipv4.address }}
Python: {{ ansible_python_version }}
Run the playbook against your inventory:
ansible-playbook -i inventory host_overview.yml
Each host prints a clean summary block showing its key system properties. You can use conditional logic with these facts to run tasks only on specific OS families:
---
- name: OS-specific package installation
hosts: all
gather_facts: true
tasks:
- name: Install packages on Debian-based systems
ansible.builtin.apt:
name: htop
state: present
when: ansible_os_family == "Debian"
- name: Install packages on RedHat-based systems
ansible.builtin.dnf:
name: htop
state: present
when: ansible_os_family == "RedHat"
This pattern is essential for writing playbooks that work across mixed-OS environments without manual intervention.
Step 4: Create Host Inventory Report with Jinja2 Template
Jinja2 templates let you transform raw Ansible facts into structured, readable reports. The template module processes a Jinja2 file on the control node and writes the rendered output to the managed host or locally.
Create a directory structure for the report project:
mkdir -p ~/ansible-reports/templates
cd ~/ansible-reports
Create the Jinja2 template that generates a plain text host report. Save it as templates/host_report.txt.j2:
vi templates/host_report.txt.j2
Add the following template content:
==============================
Host Report - Generated by Ansible
Date: {{ ansible_date_time.date }}
==============================
{% for host in groups['all'] %}
{% set facts = hostvars[host] %}
Host: {{ facts.ansible_hostname | default('N/A') }}
FQDN: {{ facts.ansible_fqdn | default('N/A') }}
IP Address: {{ facts.ansible_default_ipv4.address | default('N/A') }}
OS: {{ facts.ansible_distribution | default('N/A') }} {{ facts.ansible_distribution_version | default('') }}
Kernel: {{ facts.ansible_kernel | default('N/A') }}
Architecture: {{ facts.ansible_architecture | default('N/A') }}
CPU Cores: {{ facts.ansible_processor_vcpus | default('N/A') }}
Total RAM: {{ facts.ansible_memtotal_mb | default('N/A') }} MB
Disk Mounts:
{% for mount in facts.ansible_mounts | default([]) %}
- {{ mount.mount }} ({{ mount.fstype }}) - {{ (mount.size_total / 1073741824) | round(1) }} GB total, {{ ((mount.size_total - mount.size_available) / 1073741824) | round(1) }} GB used
{% endfor %}
Uptime: {{ facts.ansible_uptime_seconds | default(0) | int // 86400 }} days
------------------------------
{% endfor %}
Create the playbook that gathers facts from all hosts and renders the report. Save it as generate_report.yml:
vi generate_report.yml
Add the following playbook content:
---
- name: Generate host inventory report
hosts: all
gather_facts: true
tasks:
- name: Render host report on control node
ansible.builtin.template:
src: templates/host_report.txt.j2
dest: /tmp/host_report.txt
delegate_to: localhost
run_once: true
Run the playbook to generate the report:
ansible-playbook -i inventory generate_report.yml
View the generated report to verify the output:
cat /tmp/host_report.txt
The report shows a structured overview of every host with its hardware specs, OS details, and disk usage – all pulled directly from live system facts.
Step 5: Generate CSV Host Report from Ansible Facts
CSV format is ideal for importing host data into spreadsheets, databases, or monitoring dashboards. The Jinja2 template handles the formatting while Ansible provides the data.
Create the CSV template as templates/host_report.csv.j2:
vi templates/host_report.csv.j2
Add the following CSV template:
Hostname,FQDN,IP Address,OS,Version,Kernel,Architecture,CPU Cores,RAM (MB),Primary Disk (GB),Uptime (days)
{% for host in groups['all'] %}
{% set facts = hostvars[host] %}
{{ facts.ansible_hostname | default('N/A') }},{{ facts.ansible_fqdn | default('N/A') }},{{ facts.ansible_default_ipv4.address | default('N/A') }},{{ facts.ansible_distribution | default('N/A') }},{{ facts.ansible_distribution_version | default('N/A') }},{{ facts.ansible_kernel | default('N/A') }},{{ facts.ansible_architecture | default('N/A') }},{{ facts.ansible_processor_vcpus | default('N/A') }},{{ facts.ansible_memtotal_mb | default('N/A') }},{{ (facts.ansible_mounts | default([]) | selectattr('mount', 'equalto', '/') | first | default({})).get('size_total', 0) | int // 1073741824 }},{{ facts.ansible_uptime_seconds | default(0) | int // 86400 }}
{% endfor %}
Create the playbook to generate the CSV report as generate_csv_report.yml:
vi generate_csv_report.yml
Add the following playbook content:
---
- name: Generate CSV host report
hosts: all
gather_facts: true
tasks:
- name: Render CSV report on control node
ansible.builtin.template:
src: templates/host_report.csv.j2
dest: /tmp/host_inventory.csv
delegate_to: localhost
run_once: true
- name: Display report location
ansible.builtin.debug:
msg: "CSV report saved to /tmp/host_inventory.csv"
run_once: true
Run the playbook and check the output:
ansible-playbook -i inventory generate_csv_report.yml
The generated CSV file can be opened directly in Excel, Google Sheets, or LibreOffice Calc for sorting, filtering, and sharing with your team. If you manage automated tasks with Ansible, this report integrates well into scheduled cron jobs for daily inventory snapshots.
Step 6: Generate HTML Host Report
An HTML report provides a professional, browser-viewable inventory page with styling and structure. This is useful for sharing with management or embedding in internal documentation portals.
Create the HTML template as templates/host_report.html.j2:
vi templates/host_report.html.j2
Add the following HTML template:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Host Inventory Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
h1 { color: #333; }
.meta { color: #666; margin-bottom: 20px; }
table { border-collapse: collapse; width: 100%; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
th { background: #2c3e50; color: #fff; padding: 12px 8px; text-align: left; font-size: 13px; }
td { padding: 10px 8px; border-bottom: 1px solid #eee; font-size: 13px; }
tr:hover { background: #f0f7ff; }
.summary { margin-top: 20px; padding: 15px; background: #fff; border-radius: 4px; }
</style>
</head>
<body>
<h1>Host Inventory Report</h1>
<p class="meta">Generated: {{ ansible_date_time.iso8601 }} | Total Hosts: {{ groups['all'] | length }}</p>
<table>
<thead>
<tr>
<th>Hostname</th>
<th>IP Address</th>
<th>OS</th>
<th>Kernel</th>
<th>CPU</th>
<th>RAM (MB)</th>
<th>Architecture</th>
<th>Uptime (days)</th>
</tr>
</thead>
<tbody>
{% for host in groups['all'] | sort %}
{% set facts = hostvars[host] %}
<tr>
<td>{{ facts.ansible_hostname | default('N/A') }}</td>
<td>{{ facts.ansible_default_ipv4.address | default('N/A') }}</td>
<td>{{ facts.ansible_distribution | default('N/A') }} {{ facts.ansible_distribution_version | default('') }}</td>
<td>{{ facts.ansible_kernel | default('N/A') }}</td>
<td>{{ facts.ansible_processor_vcpus | default('N/A') }}</td>
<td>{{ facts.ansible_memtotal_mb | default('N/A') }}</td>
<td>{{ facts.ansible_architecture | default('N/A') }}</td>
<td>{{ facts.ansible_uptime_seconds | default(0) | int // 86400 }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="summary">
<strong>OS Distribution Summary:</strong><br>
{% for distro in groups['all'] | map('extract', hostvars, 'ansible_distribution') | unique | sort %}
{{ distro }}: {{ groups['all'] | map('extract', hostvars, 'ansible_distribution') | select('equalto', distro) | list | length }} host(s)<br>
{% endfor %}
</div>
</body>
</html>
Create the playbook as generate_html_report.yml:
vi generate_html_report.yml
Add the following playbook content:
---
- name: Generate HTML host inventory report
hosts: all
gather_facts: true
tasks:
- name: Render HTML report on control node
ansible.builtin.template:
src: templates/host_report.html.j2
dest: /tmp/host_inventory.html
delegate_to: localhost
run_once: true
- name: Show report path
ansible.builtin.debug:
msg: "HTML report saved to /tmp/host_inventory.html - open in a browser to view"
run_once: true
Run the playbook to generate the HTML report:
ansible-playbook -i inventory generate_html_report.yml
Open /tmp/host_inventory.html in a browser to see a styled table with all your hosts, complete with OS distribution summary at the bottom. For teams managing encrypted credentials alongside these reports, the Ansible Vault cheat sheet covers how to protect sensitive variables in your playbooks.
Step 7: Create Custom Facts
Ansible supports custom facts through the /etc/ansible/facts.d directory on managed hosts. Any file ending in .fact placed in this directory gets picked up by the setup module and becomes available under the ansible_local variable. This lets you extend the default fact set with application-specific or environment-specific data.
Custom facts support two formats – static INI/JSON files and executable scripts that output JSON. Here is a playbook that deploys both types to your managed hosts. Save it as deploy_custom_facts.yml:
vi deploy_custom_facts.yml
Add the following playbook content:
---
- name: Deploy custom facts to managed hosts
hosts: all
become: true
tasks:
- name: Create facts.d directory
ansible.builtin.file:
path: /etc/ansible/facts.d
state: directory
mode: '0755'
- name: Deploy static custom fact (INI format)
ansible.builtin.copy:
dest: /etc/ansible/facts.d/environment.fact
mode: '0644'
content: |
[general]
environment=production
datacenter=us-east-1
team=platform-engineering
cost_center=CC-4521
- name: Deploy dynamic custom fact (executable script)
ansible.builtin.copy:
dest: /etc/ansible/facts.d/app_versions.fact
mode: '0755'
content: |
#!/bin/bash
DOCKER_VER=$(docker --version 2>/dev/null | awk '{print $3}' | tr -d ',')
PYTHON_VER=$(python3 --version 2>/dev/null | awk '{print $2}')
NGINX_VER=$(nginx -v 2>&1 | awk -F/ '{print $2}')
cat <
Run the playbook to deploy and verify the custom facts:
ansible-playbook -i inventory deploy_custom_facts.yml
After execution, the custom facts appear under ansible_local. Access them in templates and playbooks like this:
{{ ansible_local.environment.general.datacenter }}
{{ ansible_local.app_versions.docker }}
Custom facts are particularly useful for tagging hosts with metadata that does not exist in standard system facts - environment names, application versions, team ownership, or compliance tags. These show up in your reports alongside built-in facts.
Step 8: Cache Ansible Facts for Performance
Fact gathering runs on every playbook execution by default, which adds overhead - especially with hundreds or thousands of hosts. Fact caching stores previously gathered facts and reuses them across playbook runs, cutting execution time significantly.
Ansible supports several cache backends. The JSON file cache is the simplest - no external dependencies required. Edit your ansible.cfg to enable it:
vi ansible.cfg
Add or update the following settings in the [defaults] section:
[defaults]
# Enable fact caching
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts_cache
fact_caching_timeout = 86400
The key settings explained:
gathering = smart- only gathers facts when they are not already cached or the cache has expiredfact_caching = jsonfile- stores facts as JSON files on disk (one file per host)fact_caching_connection- directory path where cached fact files are storedfact_caching_timeout = 86400- cache lifetime in seconds (86400 = 24 hours)
Create the cache directory:
mkdir -p /tmp/ansible_facts_cache
For larger environments, Redis provides a faster and more scalable cache backend. Install Redis on your control node and configure Ansible to use it:
[defaults]
gathering = smart
fact_caching = redis
fact_caching_connection = localhost:6379:0
fact_caching_timeout = 86400
The Redis backend requires the redis Python package on the control node:
pip3 install redis
With fact caching enabled, the first playbook run collects all facts normally. Subsequent runs within the cache timeout period skip fact gathering entirely and pull from cache - reducing execution time from minutes to seconds on large inventories.
To force a fresh fact collection when needed, override the cache with:
ansible-playbook -i inventory playbook.yml --flush-cache
This clears cached facts and forces a full re-gather on the next run.
Conclusion
Ansible facts give you a reliable, automated way to collect system information across your entire infrastructure. Combined with Jinja2 templates, you can generate text, CSV, and HTML inventory reports that stay current with every playbook run. Custom facts extend the built-in data with application and environment metadata, and fact caching keeps things fast at scale.
For production use, schedule fact gathering and report generation as a cron job that runs daily, store reports in a shared location or push them to a web server, and use the --flush-cache flag periodically to ensure data freshness.