One of the first steps in an Ansible playbook run (unless you explicitly disable it) is the gathering of facts via the setup module. These facts are collected on each machine and were kept in memory for the duration of the playbook run before being destroyed. This meant, that a task wanting to reference a host variable from a different machine would have to talk to that machine at least once in the playbook in order for Ansible to have access to its facts, which in turn sometimes means talking to hosts although we just need a teeny weeny bit of information from that host.

One interesting feature of Ansible version 1.8 is called “fact caching”. It allows us to build a cache of all facts for all hosts Ansible talks to. This cache will be populated with all facts for hosts for which the setup module (i.e. gather_facts) runs. Optional expiry of cached entries as well as enabling the cache itself is controlled by settings in ansible.cfg:

fact_caching = redis
fact_caching_timeout = 3600
fact_caching_connection = localhost:6379:0

By default, fact_caching is set to memory. Configuring it as above, makes Ansible use a Redis instance (on the local machine) as its cache. The timeout specifies when individual Redis keys (i.e. facts on a per/machine basis) will expire. Setting this value to 0 effectively disables expiry, and a positive value is a TTL in seconds.

The following small experiment will run over 246 machines.

---
- hosts:
  - mygroup
  gather_facts: True
  tasks:
  - action: debug msg="memfree = {{ ansible_memfree_mb }}"
PLAY [mygroup] *****************************************************************

GATHERING FACTS ***************************************************************
ok: [www01]
...
TASK: [debug msg="memfree = {{ ansible_memfree_mb }}"] ************************
ok: [www01] => {
    "msg": "memfree = 7811"
}
...

Running my sample playbook gathers all facts on each run. This playbook took just over a minute to run (1m11). So, after the run, what’s in Redis?

127.0.0.1:6379> keys *
1) "ansible_cache_keys"
2) "JPM"
3) "ansible_factswww01"
...

Each of the keys in Redis contains a JSON string value – the list of all facts collected by Ansible. Let’s have a look:

#!/usr/bin/env python

import redis
import json

r = redis.StrictRedis(host='localhost', port=6379, db=0)
key = "ansible_facts" + "www01"
val = r.get(key)

data = json.loads(val)
print data['ansible_memfree_mb']  # => 7811

If I configure gather_facts = False, the setup module is not invoked in the playbook, and Ansible accesses the cache to obtain facts. Note, of course, that the value of each fact variable will be that which was previously cached. Also, because the fact gathering doesn’t take place, the playbook runs a bit faster (which may be negligible depending on what tasks it’s set to accomplish). In this particular case, the play ran in just under a minute (0m50) – a slight speedup.

A second caching mechanism exists at the time of this writing: it’s called jsonfile, and it allows me to use a directory of JSON files as the cache; expiry is supported as for Redis even though the JSON file remains on disk after it’s expired (the file’s mtime is used to calculate expiry). If I alter the caching configuration in ansible.cfg, I can activate it:

fact_caching = jsonfile
fact_caching_connection = /tmp/mycachedir

The “connection” setting must point to a writeable directory in which a file containing facts in JSON format for each host are stored. A memcached plugin for the cache also exists. Any playbook which gathers facts effectively populates the cache for the machines it speaks to.

The following playbook doesn’t talk to the www01 machine, but it can access that machine’s facts from the cache. (The city fact isn’t default in Ansible: I set this up using facts.d.)

---
- hosts:
  - ldap21
  gather_facts: False
  tasks:
  - action: debug msg="City of www01 = {{ hostvars['www01'].ansible_local.system.location.city }}"
PLAY [ldap21] **************************************************************

TASK: [debug msg="City of www01 = {{ hostvars['www01'].ansible_local.system.location.city }}"] ***
ok: [ldap21] => {
    "msg": "City of www01 = Boston"
}

As soon as a cache entry expires these fact variables will be undefined, and the play will fail.

Populating or rejuvenating the facts cache is trivial: I’ll be running the following playbook periodically in accordance with the cache timeout I’ve configured:

---
- hosts:
  - all
  gather_facts: True

In case of doubt, clear the cache by invoking ansible-playbook with the --flush-cache option.

Ansible, Redis, and JSON :: 29 Jan 2015 :: e-mail