Рано или поздно ты упрёшься в стену: плейбук должен принять решение на основе того, что вернула предыдущая команда. Версия пакета, наличие файла, код возврата скрипта, имя текущего интерфейса. Ansible по умолчанию выполняет задачу и забывает результат - он нигде не лежит, в следующей задаче его не достать. Вот тут и нужен ansible register: ключевое слово, которое сохраняет полный результат модуля в переменную, чтобы дальше с ним работать.
Без register люди начинают городить shell-конвейеры с временными файлами на /tmp, теряют идемпотентность и злятся. С register всё становится на свои места: запустил команду, поймал её вывод, проверил ansible stdout или код возврата, и принял решение через when. Связка register + when - это база, и на экзамене RHCE (EX294) она встречается постоянно. Если её не чувствуешь руками, часть задач просто не закроешь в отведённое время.
В этом уроке разберём, что именно попадает в зарегистрированную переменную, как её отлаживать через debug, как ведут себя результаты внутри циклов (привет уроку про loop), и зачем нужны magic-переменные - hostvars, groups, inventory_hostname и компания.

Что лежит внутри зарегистрированной переменной
Берём простой пример. Запускаем команду и сохраняем результат:
Код: Выделить всё
- name: Проверить версию ядра
hosts: webservers
gather_facts: false
tasks:
- name: Узнать текущее ядро
ansible.builtin.command: uname -r
register: kernel
changed_when: false
- name: Показать всю структуру результата
ansible.builtin.debug:
var: kernel
В переменной kernel лежит не просто строка, а целый словарь. Главные поля, которые тебе реально пригодятся:
- rc - код возврата (return code). 0 - успех, всё остальное - ошибка.
- stdout - стандартный вывод одной строкой, с символами переноса внутри.
- stdout_lines - тот же вывод, но уже разбитый в список по строкам. Удобно для loop.
- stderr и stderr_lines - поток ошибок, аналогично.
- changed - изменила ли задача состояние системы (true/false).
- failed - провалилась ли задача.
- skipped - была ли пропущена из-за when.
Код: Выделить всё
- name: Решение на основе вывода
ansible.builtin.debug:
msg: "Ядро: {{ kernel.stdout }}, код возврата {{ kernel.rc }}"
Код: Выделить всё
- name: Проверить, установлен ли nginx
ansible.builtin.command: rpm -q nginx
register: nginx_check
changed_when: false
failed_when: false
- name: Поставить nginx, если его нет
ansible.builtin.dnf:
name: nginx
state: present
when: nginx_check.rc != 0
Register внутри циклов: ловушка results
Вот место, где спотыкаются почти все, кто только прошёл урок про loop. Если ты регистрируешь результат у задачи с циклом, структура переменной меняется. Привычных kernel.stdout и kernel.rc на верхнем уровне больше нет. Вместо этого появляется список results - по одному элементу на каждую итерацию.
Код: Выделить всё
- name: Пинговать список хостов
ansible.builtin.command: "ping -c1 {{ item }}"
loop:
- 8.8.8.8
- 1.1.1.1
register: ping_result
changed_when: false
failed_when: false
- name: Разобрать результаты по итерациям
ansible.builtin.debug:
msg: "{{ item.item }} -> rc={{ item.rc }}"
loop: "{{ ping_result.results }}"
Главная ошибка новичка - написать ping_result.stdout после цикла и удивляться ошибке "dict object has no attribute stdout". Запомни правило: есть loop - значит результат в .results, и его надо перебирать.
Когда не уверен в структуре - не гадай, а отлаживай. debug с var: показывает дерево целиком:
Код: Выделить всё
- name: Глянуть всю структуру
ansible.builtin.debug:
var: ping_result
verbosity: 1
Magic-переменные: hostvars, groups, inventory_hostname
Кроме того, что ты регистрируешь сам, Ansible всегда держит наготове набор служебных переменных. В русскоязычной документации их называют специальными, в англоязычной - magic variables ansible. Их не нужно объявлять, они появляются автоматически и описывают сам процесс выполнения: кто текущий хост, какие есть группы, что вообще происходит.
Самые ходовые:
- inventory_hostname - имя текущего хоста ровно так, как он записан в инвентаре. Не FQDN из фактов, а именно метка из inventory. Незаменим, когда надо генерировать пути или конфиги по имени узла.
- groups - словарь всех групп инвентаря и их членов. groups['webservers'] вернёт список хостов в группе webservers.
- group_names - список групп, в которые входит текущий хост.
- hostvars - доступ к переменным и фактам ЛЮБОГО хоста, а не только текущего.
- ansible_play_hosts - список хостов, которые ещё участвуют в текущем play (упавшие отсюда выбывают). Это современная замена устаревшего play_hosts.
Код: Выделить всё
- name: Собрать IP веб-серверов на балансировщике
ansible.builtin.debug:
msg: "{{ hostvars[item]['ansible_default_ipv4']['address'] }}"
loop: "{{ groups['webservers'] }}"
А вот пример с inventory_hostname и group_names вместе, типичная развилка по роли узла:
Код: Выделить всё
- name: Открыть порт только на фронтендах
ansible.posix.firewalld:
service: https
permanent: true
immediate: true
state: enabled
when: "'webservers' in group_names"
Типичные грабли
- Забыл про .results в цикле. Самая массовая ошибка. Есть loop у зарегистрированной задачи - значит верхнего .stdout нет, всё внутри списка results.
- Падение до проверки when. Если регистрируешь команду, которая штатно возвращает ненулевой rc (rpm -q, grep, test), без failed_when: false плейбук упадёт раньше, чем дойдёт до твоего условия.
- changed на read-only командах. command и shell всегда красятся жёлтым. Для проверок добавляй changed_when: false, иначе отчёт врёт и идемпотентность ломается.
- hostvars без фактов. Лезешь в hostvars[other]['ansible_eth0'], а там пусто, потому что на том хосте не было gather_facts. Либо включи сбор фактов, либо собери их явной задачей setup.
- Кавычки в when. Когда условие начинается со строки в кавычках ('webservers' in group_names), всё выражение нужно завернуть в кавычки целиком, иначе YAML спотыкается на двоеточии и одиночных кавычках.
Собери инвентарь с двумя группами - webservers (2 хоста) и lb (1 хост). Руками прогони:
- Зарегистрируй вывод команды hostname -I и выведи через debug сначала всю переменную (var), потом только .stdout.
- Сделай register на задаче с loop по трём пакетам (rpm -q ...), failed_when: false, и перебери .results, печатая item.item и item.rc.
- На хосте из группы lb выведи через hostvars и groups IP-адреса всех машин из webservers.
- Добавь задачу с firewalld, которая срабатывает только when 'webservers' in group_names. Прогони и убедись, что на lb она пропущена (skipped).
- Запусти плейбук с -v и без, посмотри, как ведёт себя debug с verbosity: 1.
- Чем отличается структура зарегистрированной переменной у обычной задачи и у задачи с loop? Где искать stdout в каждом случае?
- Зачем нужен failed_when: false при register команды rpm -q, и что будет без него?
- Что вернёт inventory_hostname и чем оно отличается от ansible_hostname из фактов?
- Как с балансировщика получить факт (например IP) каждого веб-сервера, не заходя на него отдельным плеем?
register превращает Ansible из "запустил и забыл" в инструмент, принимающий решения: ловишь rc, stdout, changed и ветвишь логику через when - это рабочая лошадка и на проде, и на экзамене. Помни про .results в циклах и про failed_when при проверках. А magic-переменные - hostvars, groups, group_names, inventory_hostname, ansible_play_hosts - дают тебе вид сверху на весь инвентарь сразу, без чего нормальные конфиги через шаблоны не собрать. Дальше эти кирпичи лягут в шаблоны Jinja2 и роли.