Using Ansible custom, or local, facts
Welcome to this guide that will dig down on how to write, add, and use this thing called custom, or local, facts with Ansible.

What are custom, or local, facts?
Perhaps "local facts" is a bit of a misnomer, it means "locally supplied user values" as opposed to "centrally supplied user values", or what facts are - "locally dynamically determined values".
- https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#local-facts-facts-d
Or, to put it differently, custom facts is a file with the .fact
file extension containing or returning INI or JSON formatted information.
And, if you’re written a script or program returning INI or JSON formatted information, that file has to be executable by the user running the Ansible playbook or task.
Writing the fact
The only requirement when writing a fact is that is should return INI or JSON, it does not matter if it’s written in Python, C, or GNU Bash as long as it got the .fact
file extension and is executable.
For example, a custom fact script to return the the Ansible host version of systemd
, or false, in JSON:
#!/bin/sh
if command -v systemctl 1>/dev/null;
then echo "{ \"version\" : $(systemctl --version |\
grep '^systemd\s' | awk '{print $2}') }"
else
echo "{ \"version\" : false }"
fi
But why use custom facts?
Ansible gathers a lot of facts from a host, but that doesn’t matter if it doesn’t include the facts that you need.
Let us pretend that your environment consists of a number of servers running various GNU/Linux distributions with varying age, you might be using Debian Stretch alongside Ubuntu 20.04 for example.
This was never a problem until a new company policy stated that DNS-over-TLS should be used on all platforms if possible, and after some searching you realized that DNS-over-TLS was added to systemd
in version 239 and your Debian Stretch servers only got version 232 available.
This is were custom facts comes into play.
How to add custom facts
By default the systemd
version isn't included in the facts gathered on the hosts, so that information needs to be added.
Lets use the above shell script as it will return the version or false in JSON format.
A custom fact file or script needs to be placed in the /etc/ansible/facts.d
directory or the directory specified by the fact_path
keyword, but the directory doesn't usually exists so we need to make sure it does before we move on:
- name: create custom facts directory
become: 'yes'
file:
path: /etc/ansible/facts.d
recurse: true
state: directory
mode: 0755
owner: root
group: root
tags:
- fact
We also need to copy the script to that directory:
- name: systemd version fact
become: 'yes'
template:
src: etc/ansible/facts.d/systemd.fact
dest: /etc/ansible/facts.d/systemd.fact
mode: 0755
owner: root
group: root
tags:
- systemd
- fact
Be sure to update the src:
path if needed.
Since we only created the /etc/ansible/facts.d
directory and copied the systemd.fact
into that directory, Ansible will not execute the script and thus not add the information to the host facts. We will do this by running the setup
module as a task.
- name: update facts
setup: ~
tags:
- fact
If everything worked, our fact will be available in the ansible_local
namespace.
Retrieving custom facts
We’ve added our script, it returns the expected value, but how to put it to good use?
Let’s start by retrieving the fact available to Ansible.
We’ll run ansible bastion01 -i hosts -m setup -a "filter=ansible_local"
, where the host bastion01
is present in the local hosts
file, and we will filter on any facts in the ansible_local
namespace.
Hopefully you will see the fact added:
bastion01 | SUCCESS => {
"ansible_facts": {
"ansible_local": {
"systemd": {
"version": 245
}
}
},
"changed": false
}
Since we want to use a specific fact, the version number, in a template to fix the issue with different systemd
versions, we need to verify the correct JSON structure.
In order to verify the JSON structure, but also show a couple of ways to access a fact, we’ll use this debug playbook:
---
- hosts: all
tasks:
- name: "systemd_version handling, <= 100"
debug:
msg: "systemd version is {{ ansible_local.systemd.version }}, <= 100"
when: ansible_local.systemd.version <= 100
- name: "systemd_version handling, >= 100"
debug:
msg: "systemd version is {{ ansible_local.systemd.version }}, >= 100"
when: ansible_local.systemd.version >= 100
- name: "systemd_version handling, info"
debug:
msg: "systemd version is {{ ansible_local.systemd.version }}"
- name: "systemd_version handling, info"
debug:
msg: "systemd version is {{ ansible_local['systemd']['version'] }}"
...
Running ansible-playbook -i hosts ansible_systemd_debug.yml -l bastion01
:
TASK [Gathering Facts]
************************************
ok: [bastion01]
TASK [systemd_version handling, <= 100] ************************************
skipping: [bastion01]
TASK [systemd_version handling, >= 100] ************************************
ok: [bastion01] => {
"msg": "systemd version is 245, >= 100"
}
TASK [systemd_version handling, info] **************************************
ok: [bastion01] => {
"msg": "systemd version is 245"
}
TASK [systemd_version handling, info] **************************************
ok: [bastion01] => {
"msg": "systemd version is 245"
}
Notice how the systemd
version fact is accessed using both ansible_local.systemd.version
and ansible_local['systemd']['version']
.
Using custom facts
We’ve now verified that the script returns the correct information and we have also verifed the JSON structure using the debug playbook.
Lets get back to the original issue:
We need to enable systemd
DNS-over-TLS on all platforms if possible, but DNS-over-TLS was first implemented in systemd
version 239, and Debian Stretch is using version 232.
Our playbook template templates/etc/systemd/resolved.conf.j2
therefore needs to use the fact we created so that the line DNSOverTLS=opportunistic
only will be added if the systemd
version is newer than 239.
We achieve this by adding the following lines to the template:
{% if ansible_local.systemd.version >= 239 %}
DNSOverTLS=opportunistic
{% endif %}
When not to write custom facts?
If the fact you’re interested in, e.g. load balancer status or database replica set information, requires rather advanced scripting it might slow down the systems or create unnessecary complexity and is better gathered using other tools.
Originally published at http://github.com.