На экзамене EX294 это ядро. Тебе не дадут писать ad-hoc - тебе дадут задание вроде "настрой веб-сервер на группе webservers", и ты обязан написать многозадачный плейбук, который отработает с первого прогона и не сломается со второго. Поэтому в этом уроке разбираем структуру плейбука по косточкам: из чего состоит play, из чего task, как туда подставляются переменные и что означают цветные строчки ok/changed в выводе.
Из чего собрана структура плейбука: play и task
Плейбук - это список (YAML-массив) из одного или нескольких play. Каждый play - это связка "к каким хостам применяем" плюс "что делаем". Внутри play живут задачи (tasks), а каждая задача вызывает ровно один модуль.
Иерархия простая:
- playbook - файл целиком, список play
- play - элемент списка, начинается с дефиса, привязан к группе хостов
- task - один шаг внутри play, дёргает один модуль
- module - готовый кирпич (copy, dnf, service...), который и меняет систему
Код: Выделить всё
---
- name: Базовая настройка веб-сервера
hosts: webservers
become: true
gather_facts: true
vars:
http_port: 80
web_pkg: httpd
tasks:
- name: Установить пакет {{ web_pkg }}
ansible.builtin.dnf:
name: "{{ web_pkg }}"
state: present
- name: Положить главную страницу
ansible.builtin.copy:
content: "Привет с {{ inventory_hostname }}\n"
dest: /var/www/html/index.html
owner: root
group: root
mode: "0644"
- name: Запустить и включить httpd в автозагрузку
ansible.builtin.service:
name: httpd
state: started
enabled: true
- name: Открыть HTTP в firewalld
ansible.posix.firewalld:
service: http
state: enabled
permanent: true
immediate: true
hosts - обязательная директива, отвечает на вопрос "ansible playbook host - где это всё выполнять". Сюда идёт имя группы из инвентаря (webservers), отдельный хост, all, или выражение типа webservers:&production. Если в инвентаре нет такой группы - play просто отработает вхолостую, ни на ком.
become - повышение привилегий (по умолчанию sudo до root). На RHEL/Fedora почти всё системное требует become: true. На Ubuntu/Debian механизм тот же, меняются разве что пакеты (apt вместо dnf) и имена сервисов (apache2 вместо httpd).
gather_facts - собирать ли факты о хосте перед задачами (это переменные вроде ansible_facts['os_family'], ansible_default_ipv4). По умолчанию true. Если факты не нужны, ставь false - play стартует заметно быстрее.
vars и vars_files - переменные прямо в play или вынесенные в отдельный файл. Это то, что в Яндексе ищут как "ansible playbook vars". Пример с вынесением:
Код: Выделить всё
- name: Настройка с внешними переменными
hosts: dbservers
become: true
vars_files:
- vars/db_secrets.yml
vars:
db_port: 5432

ansible task: атрибуты задачи, которые надо знать наизусть
Любой ansible task - это словарь. Один ключ - имя модуля и его параметры, остальные ключи - атрибуты задачи, которые управляют тем, как и когда модуль выполнится.
name - человекочитаемое описание. Технически необязательно, но пиши его всегда. Без name в выводе ты увидишь сырой вызов модуля, а с тегами и --start-at-task без имён вообще не поработаешь. Хорошее имя - глагол + объект: "Установить nginx", "Открыть порт 443", а не "task1" или "nginx". В name можно вставлять {{ переменные }}, как в примере выше.
loop - повторить задачу по списку. Текущий элемент доступен как item:
Код: Выделить всё
- name: Поставить набор пакетов
ansible.builtin.dnf:
name: "{{ item }}"
state: present
loop:
- vim
- git
- tmux
when - условие. Задача выполнится, только если выражение истинно. Внутри when имена переменных пишутся без {{ }}:
Код: Выделить всё
- name: Поставить firewalld только на RHEL-семействе
ansible.builtin.dnf:
name: firewalld
state: present
when: ansible_facts['os_family'] == "RedHat"
Код: Выделить всё
- name: Проверить статус сервиса
ansible.builtin.command: systemctl is-active httpd
register: httpd_status
changed_when: false
- name: Показать вывод
ansible.builtin.debug:
var: httpd_status.stdout
Код: Выделить всё
tasks:
- name: Залить конфиг nginx
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
handlers:
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted
tags - метки для выборочного запуска. ansible-playbook site.yml --tags web прогонит только помеченные задачи, --skip-tags пропустит их.
Переменные через Jinja2 и идемпотентность задач
Везде, где нужно подставить значение переменной, используется синтаксис Jinja2 - двойные фигурные скобки {{ var }}. Это работает в параметрах модулей, в name, в content. Отдельное правило про кавычки: если значение параметра НАЧИНАЕТСЯ с {{, весь параметр надо взять в кавычки, иначе YAML решит, что это начало словаря, и упадёт с ошибкой. Поэтому пишут name: "{{ web_pkg }}", а не name: {{ web_pkg }}.
Теперь про порядок выполнения - он строго сверху вниз. play за play, внутри play задача за задачей. Ansible выполняет первую задачу на ВСЕХ хостах группы, потом переходит ко второй, и так далее. Это важно: не "весь плейбук на хосте 1, потом весь на хосте 2", а наоборот - построчно по всему парку.
Несколько play в одном файле - это нормально и часто нужно, когда разные группы хостов настраиваются по-разному:
Код: Выделить всё
---
- name: Настройка веб-серверов
hosts: webservers
become: true
tasks:
- name: Поставить httpd
ansible.builtin.dnf:
name: httpd
state: present
- name: Настройка баз данных
hosts: dbservers
become: true
tasks:
- name: Поставить mariadb-server
ansible.builtin.dnf:
name: mariadb-server
state: present
И главное понятие всего Ansible - идемпотентность. Хорошая задача описывает СОСТОЯНИЕ ("пакет установлен", "строка есть в файле"), а не действие ("выполни команду"). Поэтому повторный прогон ничего не делает, если система уже в нужном виде. В выводе ты увидишь статусы:
- ok (зелёный) - проверено, уже как надо, менять нечего
- changed (жёлтый) - состояние было изменено сейчас
- failed (красный) - задача упала, по умолчанию хост выбывает из дальнейших задач
- skipped (голубой) - пропущено по when
Типичные грабли
- Параметр начинается с {{ без кавычек - YAML-ошибка про "could not find expected ':'". Лечится кавычками.
- В when написали {{ var }} вместо var. when - это уже выражение Jinja2, фигурные скобки лишние.
- Отступы пробелами, причём строго. Один таб - и плейбук не парсится. Настрой редактор на пробелы.
- Забыл become: true - получаешь Permission denied на системных задачах.
- command/shell вечно changed. Добавь changed_when: false для чисто проверочных команд или creates для тех, что создают файл.
- notify ссылается на handler, имя которого не совпадает буква в букву с name обработчика - тихо ничего не произойдёт.
Руками, на тестовой RHEL/Fedora или RED OS:
- Напиши плейбук web.yml с одним play на группу webservers: установить httpd, положить index.html через ansible.builtin.copy, запустить и enabled сервис, открыть http в firewalld.
- Запусти дважды. Убедись, что второй прогон - сплошь ok, ни одного changed. Если где-то changed - разберись почему.
- Добавь второй play на группу dbservers (поставить mariadb-server) в тот же файл. Прогони, посмотри порядок в выводе.
- Вынеси http_port и имя пакета в секцию vars, подставь через {{ }} в name и параметры.
- Добавь задачу с template + notify на handler, перезапускающий сервис. Поменяй шаблон, прогони - поймай changed и срабатывание handler в конце.
- Навесь tags на веб-задачи и запусти с --tags.
- Чем play отличается от task, и сколько модулей вызывает одна задача?
- В каком порядке Ansible выполняет задачи, если в инвентаре пять хостов: построчно по всем хостам или по очереди весь плейбук на каждом?
- Почему правильный плейбук на втором запуске не должен давать changed, и какие модули чаще всего это правило ломают?
- Когда и в каком порядке запускаются handlers, и при каком условии задача их вообще уведомляет?
Плейбук = список play, play = "где" (hosts) + директивы (become, vars, gather_facts) + список task, task = один модуль плюс атрибуты (name, loop, when, register, notify, tags). Переменные подставляются через {{ }}, выполнение идёт строго сверху вниз и построчно по хостам, а мерило качества - идемпотентность: changed на первом прогоне, чистый ok на втором. Освоишь эту структуру руками - и многозадачные плейбуки EX294 перестанут быть проблемой.