Каждый, кто хоть раз управлял парком серверов больше десяти машин, знает этот момент: нужно срочно ответить на вопрос "а на каких серверах стоит 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, и плейбуки отчётности, которые работают только с кэшем и не тревожат сами серверы. Такая архитектура масштабируется линейно: добавить хост в инвентарь - значит автоматически включить его в следующий цикл сбора. Никаких ручных обновлений таблиц, никаких устаревших записей в вики. Только факты - свежие, структурированные и всегда под рукой.