Каждый, кто хоть раз управлял парком серверов больше десяти машин, знает этот момент: нужно срочно ответить на вопрос "а на каких серверах стоит OpenSSL версии ниже 3.0?" - и вдруг выясняется, что чёткого ответа нет. Где-то есть старые записи в Confluence, где-то - таблица в Excel, последний раз обновлявшаяся полгода назад. Такое положение дел в инфраструктуре сродни навигации по городу с картой трёхлетней давности: формально она существует, но доверять ей нельзя.
Ansible fact caching решает эту задачу элегантно и без сторонних инструментов. Вместо того чтобы каждый раз заново опрашивать все хосты - а на парке из ста машин это занимает минуты - собранные факты сохраняются в кэше и остаются доступными между запусками плейбуков. Правильно выстроенная система превращает этот кэш в живую базу инвентаризации: актуальный срез версий ПО, конфигураций и параметров по всему парку в любой момент времени.
Как Ansible собирает факты и где здесь место кэша
Прежде чем выстраивать систему инвентаризации, стоит разобраться в том, как именно работает сбор фактов внутри Ansible. При запуске плейбука с gather_facts: true (а это поведение по умолчанию) Ansible выполняет на каждом управляемом хосте модуль setup. Этот модуль собирает обширный массив данных: версию ОС и ядра, объём RAM, сетевые интерфейсы с адресами, точки монтирования, список установленных пакетов и многое другое. Всё это упаковывается в словарь ansible_facts и передаётся обратно на управляющий узел.
Проблема в том, что при каждом следующем запуске цикл повторяется заново. На одном хосте сбор фактов занимает секунду-две. На ста хостах - это уже несколько минут только на шаге "Gathering Facts", и при этом ничего полезного ещё не сделано. Реальные измерения показывают: на парке из 37 хостов плейбук без кэширования выполнялся в среднем 46 секунд, тогда как с кэшем - менее 10 секунд на повторных запусках.
Ansible поддерживает три режима сбора фактов, которые задаются параметром gathering в ansible.cfg. Режим implicit - это поведение по умолчанию, когда факты собираются заново при каждом запуске. Режим explicit - факты собираются только там, где явно указано gather_facts: true. Режим smart - ключевой для системы инвентаризации: Ansible сначала проверяет кэш, и только если нужных фактов там нет или они устарели - идёт на хост за свежими данными. Именно сочетание gathering = smart с правильно настроенным бэкендом превращает кэш из оптимизации скорости в полноценную базу данных инвентаризации.
Два бэкенда кэширования - jsonfile и Redis
Ansible поддерживает несколько механизмов кэширования, но два из них наиболее распространены и надёжны: jsonfile для локального хранения и redis для разделяемого кэша в командных средах. Выбор между ними зависит не от личных предпочтений, а от архитектуры инфраструктуры.
jsonfile хранит факты каждого хоста в отдельном JSON-файле на управляющем узле. Никаких зависимостей, никаких дополнительных сервисов - только файловая система. Это рабочий вариант для одного управляющего узла и инфраструктуры до нескольких сотен хостов. Ограничение jsonfile проявляется в командных окружениях: каждый управляющий узел ведёт собственный кэш, и запуск плейбука с одного узла не обогащает кэш другого. Если управляющих узлов несколько или инфраструктуру обслуживает команда - нужен Redis.
Redis хранит все факты в памяти, раздавая их любому управляющему узлу, который знает адрес сервера. Чтение почти мгновенное, конкурентный доступ обрабатывается корректно. Минус - появляется ещё один сервис, который нужно устанавливать, настраивать и мониторить.
Настройка jsonfile в ansible.cfg минималистична:
[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /var/cache/ansible/facts
fact_caching_timeout = 86400
Здесь fact_caching_timeout задаётся в секундах: 86400 - это ровно сутки. По истечении этого времени Ansible при следующем обращении к хосту соберёт факты заново и обновит кэш. Для системы инвентаризации сутки - разумный баланс: данные достаточно свежие, чтобы отражать текущее состояние, но кэш не устаревает слишком быстро.
Директорию кэша нужно создать заранее с правильными правами доступа:
mkdir -p /var/cache/ansible/facts
chmod 750 /var/cache/ansible/facts
chown ansible:ansible /var/cache/ansible/facts
Для Redis-бэкенда сначала устанавливается сам Redis и Python-клиент:
# Debian/Ubuntu
apt install redis-server -y
pip install redis
# RHEL/Rocky
dnf install redis python3-redis -y
systemctl enable --now redis
После этого конфигурация Ansible меняется только в одной строке:
[defaults]
gathering = smart
fact_caching = redis
fact_caching_connection = localhost:6379:0
fact_caching_timeout = 86400
fact_caching_prefix = ansible_facts_
Формат строки подключения - host:port:database_number. Параметр fact_caching_prefix помогает отличать ключи Ansible от других данных в Redis, если один экземпляр используется для нескольких задач. Проверить, что Redis принял данные после первого запуска плейбука, можно через redis-cli:
redis-cli keys "ansible_facts_*"
# Вывод покажет ключ для каждого опрошенного хоста
redis-cli TTL "ansible_facts_web-01.example.com"
# Оставшееся время жизни записи в секундах
Плейбук сбора инвентаризации - от стандартных фактов к кастомным
Стандартный модуль setup собирает впечатляющий объём данных, но для инвентаризации версий ПО его часто недостаточно. Ansible знает о пакетах, установленных через системный пакетный менеджер - apt, yum, dnf. Но Java, установленная вручную в /opt, или кастомный бинарник в /usr/local/bin в эти факты не попадут. Для полноценной инвентаризации нужны пользовательские факты.
Ansible поддерживает механизм facts.d: если на управляемом хосте в директории /etc/ansible/facts.d/ лежат файлы с расширением .fact, их содержимое попадает в словарь ansible_local. Файл может быть статическим JSON или исполняемым скриптом, который возвращает JSON в stdout. Именно исполняемые .fact-файлы открывают возможность собирать версии любого ПО - достаточно написать скрипт, который опрашивает нужные бинарники.
Плейбук, который разворачивает такой скрипт на всех хостах и сразу же собирает факты:
# deploy_inventory_facts.yml
---
- name: Развернуть скрипт инвентаризации ПО
hosts: all
become: true
tasks:
- name: Создать директорию facts.d
ansible.builtin.file:
path: /etc/ansible/facts.d
state: directory
mode: '0755'
- name: Развернуть скрипт сбора версий
ansible.builtin.copy:
dest: /etc/ansible/facts.d/software_versions.fact
mode: '0755'
content: |
#!/bin/bash
# Скрипт возвращает JSON с версиями ПО
java_ver=""
python_ver=""
nginx_ver=""
docker_ver=""
command -v java &>/dev/null && \
java_ver=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}')
command -v python3 &>/dev/null && \
python_ver=$(python3 --version 2>&1 | awk '{print $2}')
command -v nginx &>/dev/null && \
nginx_ver=$(nginx -v 2>&1 | grep -oP '[\d.]+')
command -v docker &>/dev/null && \
docker_ver=$(docker --version | grep -oP '[\d.]+' | head -1)
printf '{"java":"%s","python3":"%s","nginx":"%s","docker":"%s"}\n' \
"$java_ver" "$python_ver" "$nginx_ver" "$docker_ver"
- name: Собрать свежие факты с хостов
ansible.builtin.setup:
gather_subset:
- all
После первого запуска этого плейбука кэш будет содержать не только стандартные системные факты, но и ansible_local.software_versions с версиями Java, Python, nginx и Docker по каждому хосту. Проверить, что скрипт возвращает корректный JSON на конкретном хосте, можно вручную:
ansible web-01.example.com -m setup -a 'filter=ansible_local'
Важная деталь: модуль setup запускается в конце плейбука намеренно, уже после того, как скрипт развёрнут. Это обеспечивает попадание ansible_local в кэш в том же цикле. При последующих запусках с gathering = smart скрипт на хосте будет обновлять только само ПО, а факты будут подхватываться из кэша - если только он не устарел.
Плейбук инвентаризации - извлечение данных из кэша без похода на хосты
Когда кэш заполнен, самое ценное - это возможность строить отчёты без единого SSH-подключения. Следующий плейбук извлекает данные о версиях ПО из кэша и формирует CSV-отчёт на управляющем узле:
# generate_inventory_report.yml
---
- name: Собрать данные о ПО из кэша
hosts: all
gather_facts: true # Берёт из кэша при gathering = smart
tasks:
- name: Собрать инвентарь в структуру
ansible.builtin.set_fact:
host_inventory:
hostname: "{{ inventory_hostname }}"
os: "{{ ansible_distribution }} {{ ansible_distribution_version }}"
kernel: "{{ ansible_kernel }}"
ram_mb: "{{ ansible_memtotal_mb }}"
java: "{{ ansible_local.software_versions.java | default('not installed') }}"
python3: "{{ ansible_local.software_versions.python3 | default('not installed') }}"
nginx: "{{ ansible_local.software_versions.nginx | default('not installed') }}"
docker: "{{ ansible_local.software_versions.docker | default('not installed') }}"
packages_count: "{{ ansible_packages | length | default(0) }}"
- name: Сформировать CSV-отчёт на управляющем узле
hosts: localhost
gather_facts: false
tasks:
- name: Записать заголовок CSV
ansible.builtin.copy:
dest: /var/reports/software_inventory.csv
content: "hostname,os,kernel,ram_mb,java,python3,nginx,docker,packages_count\n"
- name: Добавить строку для каждого хоста
ansible.builtin.lineinfile:
path: /var/reports/software_inventory.csv
line: >-
{{ hostvars[item].host_inventory.hostname }},
{{ hostvars[item].host_inventory.os }},
{{ hostvars[item].host_inventory.kernel }},
{{ hostvars[item].host_inventory.ram_mb }},
{{ hostvars[item].host_inventory.java }},
{{ hostvars[item].host_inventory.python3 }},
{{ hostvars[item].host_inventory.nginx }},
{{ hostvars[item].host_inventory.docker }},
{{ hostvars[item].host_inventory.packages_count }}
loop: "{{ groups['all'] }}"
Ключевой момент - переменная hostvars. Она позволяет одному хосту (в данном случае localhost) обращаться к фактам любого другого хоста из инвентаря. При включённом кэшировании данные берутся не по сети, а из локального файла или Redis. Именно поэтому второй плей с формированием отчёта работает мгновенно - реальных подключений к серверам нет.
Инвалидация кэша и принудительное обновление
Кэш фактов живёт по TTL, но бывают ситуации, когда ждать истечения таймаута нельзя. Провели обновление пакетов, сменили версию Java, изменили конфигурацию ядра - и теперь кэш врёт. Принудительно сбросить кэш для всех хостов и собрать свежие данные:
ansible-playbook site.yml --flush-cache
Флаг --flush-cache очищает кэш для всех хостов в инвентаре перед запуском плейбука. Это тяжёлая операция: Ansible заново обойдёт каждый хост. Для больших парков разумнее инвалидировать кэш точечно - только для тех хостов, которые реально изменились. Для jsonfile-бэкенда это делается прямым удалением файлов:
# Сбросить кэш конкретного хоста
rm /var/cache/ansible/facts/web-01.example.com
# Сбросить кэш группы серверов по шаблону
rm /var/cache/ansible/facts/web-*.example.com
# Посмотреть, когда последний раз обновлялись факты хоста
stat /var/cache/ansible/facts/web-01.example.com | grep Modify
Для Redis аналогичные операции выполняются через redis-cli: удаление конкретного ключа инвалидирует кэш одного хоста, FLUSHDB очищает всё - но это деструктивная операция, которую стоит применять осторожно.
Элегантное решение для автоматической инвалидации - встроить вызов в плейбуки, которые вносят изменения на серверах. Если плейбук обновил пакеты, он же должен в конце сбросить кэш фактов для затронутых хостов:
- name: Обновить список пакетов
ansible.builtin.apt:
upgrade: safe
update_cache: true
notify: Refresh fact cache
handlers:
- name: Refresh fact cache
ansible.builtin.setup:
# setup запускается при наличии изменений,
# обновляя кэш без лишних запусков
Автоматизация сбора и отчётности через cron и systemd
Система инвентаризации приносит пользу только если работает регулярно, без участия человека. Самый простой способ - запускать плейбук сбора фактов по расписанию. Для systemd-таймера создаются два файла:
# /etc/systemd/system/ansible-inventory.service
[Unit]
Description=Ansible inventory fact collection
[Service]
Type=oneshot
User=ansible
WorkingDirectory=/etc/ansible
ExecStart=/usr/bin/ansible-playbook deploy_inventory_facts.yml
ExecStartPost=/usr/bin/ansible-playbook generate_inventory_report.yml
StandardOutput=append:/var/log/ansible/inventory.log
StandardError=append:/var/log/ansible/inventory.log
# /etc/systemd/system/ansible-inventory.timer
[Unit]
Description=Run Ansible inventory collection daily
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
systemctl enable --now ansible-inventory.timer
Таймер запускает сбор фактов каждую ночь в два часа - достаточно часто, чтобы данные оставались актуальными, и достаточно редко, чтобы не создавать нагрузку на инфраструктуру. Параметр Persistent=true гарантирует: если сервер был выключен в момент планового запуска, таймер выполнится сразу при следующем старте.
Итоговая система инвентаризации - это не монолит, а связка из трёх компонентов: регулярный сбор фактов через setup, постоянное хранение через jsonfile или Redis, и плейбуки отчётности, которые работают только с кэшем и не тревожат сами серверы. Такая архитектура масштабируется линейно: добавить хост в инвентарь - значит автоматически включить его в следующий цикл сбора. Никаких ручных обновлений таблиц, никаких устаревших записей в вики. Только факты - свежие, структурированные и всегда под рукой.