Facts¶
What You'll Learn¶
- What the implicit "Gathering Facts" step does, and what it costs
- How to read facts through the
ansible_factsdictionary - How to gather less, gather nothing, or add your own custom facts
Why This Exists¶
Most plays begin with an implicit setup module run that discovers dozens of details about the host — OS family, IP addresses, memory, mounted disks — before your first real task even starts. Facts are how a playbook adapts to the machine it's actually running against instead of assuming.
Mental Model¶
A fact is a variable the managed node reports about itself. Ansible runs the
setupmodule on each host, the module returns a big JSON document, and Ansible stores it per host asansible_facts. It is ordinary data after that — it only refreshes when facts are gathered again.
sequenceDiagram
participant C as Control node
participant H as Managed node
C->>H: run ansible.builtin.setup (implicit "Gathering Facts")
H-->>C: JSON: OS, network, CPU, memory, mounts...
C->>C: store as hostvars[host]['ansible_facts']
C->>H: your tasks, which can now use those facts
Seeing Facts¶
# Every fact for one host
ansible web01 -m ansible.builtin.setup
# Only matching keys
ansible web01 -m ansible.builtin.setup -a "filter=ansible_distribution*"
Abbreviated output:
"ansible_facts": {
"distribution": "Ubuntu",
"distribution_version": "24.04",
"os_family": "Debian",
"default_ipv4": { "address": "10.0.1.11", "interface": "eth0" },
"memtotal_mb": 3931,
"processor_vcpus": 2
}
To print all facts from inside a playbook:
Commonly Used Facts¶
Fact inside ansible_facts |
Legacy top-level variable (deprecated) | Example value |
|---|---|---|
hostname |
ansible_hostname |
web01 |
fqdn |
ansible_fqdn |
web01.example.internal |
distribution, distribution_version |
ansible_distribution, ansible_distribution_version |
Ubuntu, 24.04 |
os_family |
ansible_os_family |
Debian |
default_ipv4.address |
ansible_default_ipv4.address |
10.0.1.11 |
all_ipv4_addresses |
ansible_all_ipv4_addresses |
["10.0.1.11", "172.17.0.1"] |
memtotal_mb |
ansible_memtotal_mb |
3931 |
memfree_mb |
ansible_memfree_mb |
1210 |
swaptotal_mb |
ansible_swaptotal_mb |
2047 |
processor_vcpus |
ansible_processor_vcpus |
2 |
mounts |
ansible_mounts |
A list of mount points with size and free space |
- name: Report memory, swap, and primary IP
ansible.builtin.debug:
msg: >-
{{ inventory_hostname }} has {{ ansible_facts['memtotal_mb'] }} MB RAM,
{{ ansible_facts['swaptotal_mb'] }} MB swap,
and IP {{ ansible_facts['default_ipv4']['address'] | default('none') }}
Using Facts in Tasks¶
- name: Install the web server package for this OS family
ansible.builtin.package:
name: "{{ 'apache2' if ansible_facts['os_family'] == 'Debian' else 'httpd' }}"
state: present
- name: Size worker processes to the CPU count
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
vars:
nginx_worker_processes: "{{ ansible_facts['processor_vcpus'] }}"
- name: Only on hosts with at least 4 GB of RAM
ansible.builtin.debug:
msg: "Enough memory for the in-memory cache"
when: ansible_facts['memtotal_mb'] >= 4096
Inside ansible_facts, keys have no ansible_ prefix. Ansible also injects the same facts as top-level variables (ansible_distribution, ansible_default_ipv4) for backward compatibility, controlled by the INJECT_FACTS_AS_VARS setting.
Injected fact variables are deprecated
Starting with ansible-core 2.20, reading an injected top-level fact such as ansible_distribution prints a deprecation warning, and the injection is scheduled to be turned off by default in a future release. Write new playbooks against ansible_facts['distribution']. To find old references in an existing repository:
grep -rnE '\bansible_(distribution|os_family|hostname|fqdn|default_ipv4|memtotal_mb|processor_vcpus|architecture|kernel|mounts|pkg_mgr|service_mgr)\b' \
--include='*.yml' --include='*.yaml' --include='*.j2' .
Then replace ansible_os_family with ansible_facts['os_family'], and so on. ansible_facts keys drop the ansible_ prefix.
Controlling What Gets Gathered¶
Turn gathering off¶
- name: Restart an app that never looks at facts
hosts: app
gather_facts: false
tasks:
- name: Restart the app service
ansible.builtin.service:
name: checkout
state: restarted
On a 1,000-host fleet, skipping an unnecessary fact-gathering pass can save minutes per run.
Gather only a subset¶
- name: Configure networking
hosts: all
gather_facts: true
gather_subset:
- "!all"
- "!min"
- network
Subsets include hardware, network, virtual, facter, and ohai. min is a small default set that is always included unless you negate it.
Gather later, explicitly¶
- name: Gather network facts only when we need them
ansible.builtin.setup:
gather_subset:
- network
when: configure_network | default(false)
Custom Facts¶
Drop a file into /etc/ansible/facts.d/ on the managed node. It can be INI, JSON, or an executable that prints JSON:
After the next fact gathering, it appears under ansible_local:
- ansible.builtin.debug:
msg: "Deployed release: {{ ansible_facts['ansible_local']['app']['deploy']['release'] }}"
A common pattern: the deploy playbook writes this file at the end of a successful rollout, and later playbooks read it to know what is currently installed.
- name: Record the deployed release as a local fact
ansible.builtin.copy:
dest: /etc/ansible/facts.d/app.fact
content: |
[deploy]
release={{ app_release }}
mode: "0644"
Facts vs. Registered Variables¶
| Fact | Registered variable | |
|---|---|---|
| Produced by | setup (or a module returning ansible_facts) |
register: on any task |
| Describes | The host | The result of one task |
| Lifetime | Whole run, and across runs with fact caching | Current run only |
| Access from other hosts | hostvars['web01']['ansible_facts'] |
hostvars['web01']['my_result'] |
Fact Caching¶
Fact caching (fact_caching = jsonfile/redis) avoids re-gathering on every run — previewed here, covered in full in Fact Caching.
Common Mistakes¶
- Leaving
gather_facts: true(the default) on every play, including ones that never reference a fact — real cost at scale, covered in Performance. - Using the injected top-level
ansible_distributionstyle instead of the namespacedansible_facts['distribution']form in new playbooks. - Disabling fact gathering, then referencing
ansible_facts['os_family']later and getting an undefined variable error. - Reading another host's facts through
hostvarswhen that host wasn't part of the play, so its facts were never gathered. - Treating facts as live values — a fact gathered at the start of a play does not update after a task changes the host, until you run
setupagain.
Interview Questions¶
- What does
gather_factsactually do, and when would you disable it? - What's the difference between a fact and a registered variable?
- How does fact caching change the cost of repeated runs?
- How would you make "the currently deployed application version" available as a fact on every host?
Next¶
Continue to Registered Variables.