что такое handlers в ansible
Handlers: running operations on change
Sometimes you want a task to run only when a change is made on a machine. For example, you may want to restart a service if a task updates the configuration of that service, but not if the configuration is unchanged. Ansible uses handlers to address this use case. Handlers are tasks that only run when notified. Each handler should have a globally unique name.
Handler example
In this example playbook, the second task notifies the handler. A single task can notify more than one handler.
Controlling when handlers run
By default, handlers run after all the tasks in a particular play have been completed. This approach is efficient, because the handler only runs once, regardless of how many tasks notify it. For example, if multiple tasks update a configuration file and notify a handler to restart Apache, Ansible only bounces Apache once to avoid unnecessary restarts.
The meta: flush_handlers task triggers any handlers that have been notified at that point in the play.
Using variables with handlers
You may want your Ansible handlers to use variables. For example, if the name of a service varies slightly by distribution, you want your output to show the exact name of the restarted service for each target machine. Avoid placing variables in the name of the handler. Since handler names are templated early on, Ansible may not have a value available for a handler name like this:
If the variable used in the handler name is not available, the entire play fails. Changing that variable mid-play will not result in newly created handler.
Instead, place variables in the task parameters of your handler. You can load the values using include_vars like this:
Handlers can also “listen” to generic topics, and tasks can notify those topics as follows:
This use makes it much easier to trigger multiple handlers. It also decouples handlers from their names, making it easier to share handlers among playbooks and roles (especially when using 3rd party roles from a shared source like Galaxy).
Handler names and listen topics live in a global namespace.
Handler names are templatable and listen topics are not.
Use unique handler names. If you trigger more than one handler with the same name, the first one(s) get overwritten. Only the last one defined will run.
You can notify a handler defined inside a static include.
You cannot notify a handler defined inside a dynamic include.
A handler can not run import_role or include_role.
When using handlers within roles, note that:
handlers notified within roles section are automatically flushed at the end of tasks section, but before any tasks handlers.
handlers are play scoped and as such can be used outside of the role they are defined in.
© Copyright Ansible project contributors. Last updated on Dec 01, 2021.
Система управления Ansible
Представьте себе, что вам нужно управлять парком серверов, расположенных к тому же в разных географических точках. Каждый из этих серверов требует настройки, регулярного обновления и мониторинга. Конечно, для решения этих задач можно воспользоваться самым простым способом: подключиться к каждому серверу по ssh и внести необходимые изменения. При всей своей простоте этот способ сопряжен с некоторыми трудностями: он чрезвычайно трудоемок, а на выполнение однообразных операций уходит очень много времени.
Чтобы упростить процессы настройки и конфигурирования серверов, можно также писать shell-скрипты. Но и этот способ вряд ли можно назвать совершенным. Скрипты нужно постоянно изменять, подстраивая их под каждую новую задачу. При их написании необходимо учитывать различие операционных систем и версий. Не будем забывать и о том, что отладка скриптов отнимает много усилий и забирает немало времени.
Оптимальным вариантом решения описанных проблем является внедрение системы удаленного управления конфигурацией. В таких системах достаточно лишь описать нужное состояние управляемого узла. Система должна сама определить, что нужно сделать для достижения этого состояния, и осуществит все необходимые действия.
Со всеми сложностями, о которых идет речь выше, мы хорошо знакомы на собственном опыте: у нас имеется 10 точек присутствия с NS-серверами, расположенные в разных точках планеты. На них необходимо регулярно вносить различные изменения: обновлять операционную систему, устанавливать и обновлять различное ПО, изменять конфигурцию и т.п. Мы решили все эти операции автоматизировать и внедрить систему удаленного управления конфигурациями. Изучив имеющиеся решения, мы остановили свой выбор на Ansible.
В этой статье мы бы хотели подробно рассказать о его возможностях этого инструмента управления конфигурациями и поделиться собственным опытом его использования.
Что такое Ansible?
Ansible — опенсорсное программное решение для удаленного управления конфигурациями, разработанное Майклом Де Хаанном в 2012 году. Название продукта взято из научно-фантастической литературы: в романах американской писательницы Урсулы Ле Гуин ансиблом называется устройство для оперативной космической связи.
Ansible берет на себя всю работу по приведению удаленных серверов в необходимое состояние. Администратору необходимо лишь описать, как достичь этого состояния с помощью так называемых сценариев (playbooks; это аналог рецептов в Chef). Такая технология позволяет очень быстро осуществлять переконфигурирование системы: достаточно всего лишь добавить несколько новых строк в сценарий.
Почему Ansible?
Преимущества Ansible по сравнению с другими аналогичными решениями (здесь в первую очередь следует назвать такие продукты, как Puppet, Chef и Salt) заключаются в следующем:
Установка
Требования для установки Ansible минимальны. На машине с которой производится управление должен быть установлен Python 2.6 или выше. На управляемых узлах должен быть установлен только Python версии не ниже 2.4, но он, как правило, по умолчанию включен в состав большинства дистрибутивов linux-систем. MS Windows не поддерживается.
Вам также могут потребоваться следующие модули Python, устанавливаемые через pip или пакетный менеджер вашей операционной системы:
В Ubuntu установка самого Ansible и зависимостей осуществляется добавлением репозитория и установкой пакета:
О процедуре установки в других ОС можно прочитать в официальной документации.
Группы серверов
Файл hosts
Помимо списка управляемых узлов, в файле hosts могут быть указаны и другие сведения, необходимые для работы: номера портов для подключения по SSH, способ подключения, пароль для подключения по SSH, имя пользователя, объединения групп и т.п. В некоторых случаях — в частности, при работе с большими и сложными конфигурациями, — различные параметры можно выносить в отдельные файлы и каталоги (о структуре каталогов см. ниже).
Более подробно о файле hosts и правилах его написания можно почитать в официальной документации.
Информация об узлах (Facts)
Перед внесением изменений Ansible подключается к управляемым узлам и собирает информацию о них: о сетевых интерфейсах и их состоянии, об установленной операционной системе и т.п. Он может делать это как с помощью собственного модуля, так и с помощью инструментов ohai и facter, если они установлены (такая возможность специально предусмотрена для пользователей, уже имеющих опыт работы с системами удаленного управления конфигурациями: ohai и facter являются библиотеками фактов для Chef и Puppet).
Переменные
Во время деплоя, как правило, требуется не только установить какое-либо приложение, но и настроить его в соответствии с определенными параметрами на основании принадлежности к группе серверов или индивидуально (например, ip-адрес BGP-соседа и номер его AS или параметры для базы данных). Как уже было сказано, загромождать файл hosts будет не очень красиво, поэтому разработчики Ansible пошли следующим путём:
Модули Ansible
В состав Ansible входит огромное количество модулей для развёртывания, контроля и управления различными компонентами, которые можно условно разделить на следующие группы (в скобках приведены названия некоторых продуктов и сервисов):
Примеры простых задач
С помощью Ansible можно одновременно выполнить одну задачу на целой группе серверов. Попробуем, например, отправить запрос ping на серверы выбранной группы:
Следующий пример соберёт информацию о хостах и выведёт её на консоль в формате JSON:
А вот так можно создать логический том (или, в зависимости от текущего состояния, изменить его размер) с именем examplevolume в группе examplegroup:
Ansible позволяет не только выполнять единичные задачи, но и писать сценарии, которые необходимо выполнить на управляемых узлах. Рассмотрим структуру и правила написания таких сценариев более подробно.
Cценарии (playbooks)
Все сценарии в Ansible пишутся на YAML. Это — человекочитаемый формат сериализованных данных, гораздо более простой, чем XML или JSON.
Чтобы выполнить сценарий используется команда ansible-playbook со следующим сиснтаксисом:
В начале сценария обязательно должна присутствовать последовательность символов «» (так в YAML обозначается начало документа). Перед каждым новым разделом списка ставится дефис ( — ):
Основными параметрами/группами простого сценария являются:
В разделе hosts указывается группа управляемых узлов, к которой будут применены описываемые в сценарии изменения.
Так, строка формата:
означает, что изменения будут применены к узлам из группы webservers.
Сценарии могут выполняться не только от имени пользователя, под именем которого установлено соедиение, но и любого другого. В следующем примере авторизация на хосте будет произведена с именем yourname, но задачи будут выполняться от имени пользователя root (если, конечно, этому пользователю это разрешено использовать sudo):
Если добавить параметр “user: postgres”, то все действия будут выполняться с привилегиями пользователя postgres.
В разделе vars указываются переменные, которые будут использованы в сценарии, и их значения:
Список изменений/состояний, которые необходимо произвести на управляемом узле, приводится в разделе tasks. Каждой задаче (task) присваивается имя (name), его можно опустить. Далее указывается модуль Ansible, который будет задействован при её выполнении:
Для каждой задачи можно указывать пользователя, от имени которого она будет выполнена:
Шаблонизация
В Ansbile используется шаблонизатор Jinja2. Приведём пример простого шаблона (часть конфига powerdns):
Обратим внимание на то, что файл шаблона и файл с паролем пользователя базы данных находятся на машине управления, а результатом будет файл на удалённом узле.
Обработчики событий (Handlers)
Ansible не просто выполняет задачи в указанном порядке, но и проверяет их состояние на наличие изменений. Если при выполнении сценария требовалось, например, добавить строку в конфигурационный файл, и в результате выполнения он изменился (необходимой строки действительно не было), то Ansible может выполнить специальную задачу, описанную как обработчик события (handler). Если при выполнении строка уже была в конфигурационном файле, то обработчик выполнен не будет. Обработчики событий описываются в конце сценария; в описании задачи они указываются через параметр notify. Приведём пример:
Контроль выполнения
Допустим, что при выполнении сценария нам нужно проверять определённые переменные или состояния и, в зависимости от них, выполнять или не выполнять какие-либо задачи. Для этого можно использовать оператор “when”:
Делегирование задачи другому хосту
Иногда требуется выполнить задачу на определённом узле, но в контексте другого узла. Например, во время обновления узла может возникнуть необходимость отключить для него мониторинг, находящийся на отдельном сервере. Для этого используется управляющая директива delegate_to. Приведём пример:
Результатом выполнения этой задачи будет отключение сообщений для сервиса dnsserver в Nagios.
Ролью называется типовой набор переменных и задач, назначаемых для одного или нескольких серверов. Если вам нужно применить к серверу или группе серверов типовой набор операций, вам достаточно просто назначить ему роль. Предварительно в проекте каталоге проекта должна быть создана соответствующая структура. В сценариях роли назначаются следующим образом:
Структура проекта
Пример сценария
Чтобы понять, как это все работает, рассмотрим практический пример: простой сценарий развёртывания новой версии PostgreSQL 9.3 на debian-based ОС. Роли в этом примере не используются.
Ansible AWX
Во всех приведенных выше примерах управление Ansible осуществляется с помощью интерфейса командной строки. Но с официального сайта можно загрузить графическую панель управления Ansibleworks AWX, очень симпатичную внешне и удобную в использовании. Собственно, за счет ее распространения и осуществляется монетизация продукта: управление небольшим (до 10) количеством серверов обходится бесплатно, если же серверов больше — нужно приобретать лицензию. Похожие варианты монетизации используются и разработчиками конкурирующих решений — Puppet и Chef.
Заключение
Ansible — достаточно простой, но при этом эффективный инструмент для удаленного управления конфигурациями. В рамках этой статьи мы сделали лишь беглый обзор его возможностей и рассказали, как пишутся сценарии для решения простых задач. Все возможности и варианты использования Ansible в рамках одной статьи охватить невозможно. О применении этого инструмента для решения более специфических задач мы расскажем в последующих публикациях.
Для желающих узнать больше — несколько ссылок:
https://github.com/ansible/ — официальный аккаунт на github c исходным кодом проекта и хорошим набором примеров проектов
http://www.ansibleworks.com/docs/ — официальная документация (активно пополняется);
http://jpmens.net/2012/06/06/configuration-management-with-ansible/ — статья Яна Пита Менса об управлении конфигурациями с помощью Ansible (в его блоге есть и много других материалов по теме).
https://gist.github.com/marktheunissen/2979474 — пример сценария с подробными комментариями, правда для старой версии.
www.ansibleworks.com/docs/contrib.html — ещё больше ссылок на примеры использования, включая в том числе и очень сложные конфигурации.
Для тех кто не может комментировать посты на Хабре, приглашаем к нам в блог.
Основы Ansible, без которых ваши плейбуки — комок слипшихся макарон
Я делаю много ревью для чужого кода на Ансибл и много пишу сам. В ходе анализа ошибок (как чужих, так и своих), а так же некоторого количества собеседований, я понял основную ошибку, которую допускают пользователи Ансибла — они лезут в сложное, не освоив базового.
Для исправления этой вселенской несправедливости я решил написать введение в Ансибл для тех, кто его уже знает. Предупреждаю, это не пересказ манов, это лонгрид в котором много букв и нет картинок.
Ожидаемый уровень читателя — уже написано несколько тысяч строк ямла, уже что-то в продакшене, но «как-то всё криво».
Названия
Главная ошибка пользователя Ansible — это не знать как что называется. Если вы не знаете названий, вы не можете понимать то, что написано в документации. Живой пример: на собеседовании, человек, вроде бы заявлявший, что он много писал на Ансибле, не смог ответить на вопрос «из каких элементов состоит playbook’а?». А когда я подсказал, что «ожидался ответ, что playbook состоит из play», то последовал убийственный комментарий «мы этого не используем». Люди пишут на Ансибле за деньги и не используют play. На самом деле используют, но не знают, что это такое.
Так что начнём с простого: как что называется. Может быть вы это знаете, а может и нет, потому что не обратили внимания, когда читали документацию.
ansible-playbook исполняет playbook. Playbook — это файл с расширением yml/yaml, внутри которого что-то такое:
Мы уже поняли, что весь этот файл — плейбука. Мы можем показать где тут роли (roles), где таски (tasks). Но где тут play? И чем отличается play от role или playbook?
В документации это всё есть. И это пропускают. Начинающие — потому что там слишком много и всё сразу не запомнишь. Опытные — потому что «тривиальные вещи». Если вы опытный — перечитывайте эти страницы хотя бы раз в полгода, и ваш код станет классом лучше.
и вот это тоже ещё одна play:
Что же такое play? Зачем она?
Если вы хотите «что-то» исполнить «где-то» — вы пишете play. Не роль. Не роль с модулями и делегейтами. Вы берёте и пишете play. В которой, в поле hosts вы перечисляете где исполнять, а в roles/tasks — что исполнять.
Просто же, да? А как может быть иначе?
Одним из характерных моментов, когда у людей возникает желание сделать это не через play, это «роль, которая всё настраивает». Хочется иметь роль, которая настраивает и сервера первого типа, и сервера второго типа.
Архетипичным примером является мониторинг. Хочется иметь роль monitoring, которая настроит мониторинг. Роль monitoring назначается на хосты мониторинга (в соотв. play). Но, выясняется, что для мониторинга нам надо поставить пакеты на хосты, которые мы мониторим. Почему бы не использовать delegate? А ещё надо настроить iptables. delegate? А ещё надо написать/поправить конфиг для СУБД, чтобы мониторинг пускала. delegate! А если креатив попёр, то можно сделать делегацию include_role во вложенном цикле по хитрому фильтру на список групп, а внутри include_role можно ещё делать delegate_to снова. И понеслось.
Благое пожелание — иметь одну-единственную роль monitoring, которая «всё делает» — ведёт нас кромешный ад из которого чаще всего один выход: всё переписать с нуля.
Где тут случилась ошибка? В тот момент, когда вы обнаружили, что для выполнения задачи «x» на хосте X вам надо пойти на хост Y и сделать там «y», вы должны были выполнить простое упражнение: пойти и написать play, которая на хосте Y делает y. Не дописывать что-то в «x», а написать с нуля. Пусть даже с захардкоженными переменными.
Вроде бы, в абзацах выше всё сказано правильно. Но это же не ваш случай! Потому что вы хотите написать переиспользуемый код, который DRY и похож на библиотеку, и нужно искать метод как это сделать.
Вот тут вот притаилась ещё одна грубая ошибка. Ошибка, которая превратила множество проектов из терпимо написанных (можно лучше, но всё работает и легко дописать) в совершенный ужас, в котором даже автор не может разобраться. Оно работает, но упаси боже что-то поменять.
Эта ошибка звучит так: роль — это библиотечная функция. Эта аналогия сгубила столько хороших начинаний, что просто грустно смотреть. Роль — не библиотечная функция. Она не может делать вычисления и она не может принимать решения уровня play. Напомните мне, какие решения принимает play?
Спасибо, вы правы. Play принимает решение (точнее, содержит в себе информацию) о том, какие таски и роли на каких хостах выполнять.
Если вы делегируете это решение на роль, да ещё и с вычислениями, вы обрекаете себя (и того, кто ваш код будет пытаться разобрать) на жалкое существование. Роль не решает где ей выполняться. Это решение принимает play. Роль делает то, что ей сказали, там, где ей сказали.
Почему заниматься программированием на Ансибле опасно и чем COBOL лучше Ансибла мы поговорим в главе про переменные и jinja. Пока что скажем одно — каждое ваше вычисление оставляет за собой нестираемый след из изменения глобальных переменных, и вы ничего с этим не можете сделать. Как только два «следа» пересеклись — всё пропало.
play и только play решает на каких хостах что исполняется.
В этом разделе мы разобрались с противостоянием play и role. Теперь поговорим про отношения tasks vs role.
Таски и Роли
… И куда делись tasks?
Мы снова начинаем с азов — устройство play. Если вы плаваете в этом вопросе, вы не можете использовать play как основу для всего остального, и ваш результат получается «шатким».
Устройство play: директива hosts, настройки самой play и секции pre_tasks, tasks, roles, post_tasks. Остальные параметры для play нам сейчас не важны.
Но мы всё ещё не ответили на вопрос: а куда вызов модуля foo писать? Надо ли нам под каждый модуль писать целую роль? Или лучше иметь толстую роль подо всё? А если не роль, то куда писать — в pre или в post?
Разумеется, формулировка вопроса намекает, что сломается. Но что именно?
И, возвращаемся, к нашему ‘foo’. Куда его поместить? В pre, post или в roles? Очевидно, это зависит от того, нужны ли нам результаты работы хэндлера для foo. Если их нет, то foo не нужно класть ни в pre, ни в post — эти секции имеют специальный смысл — выполнение тасок до и после основного массива кода.
Теперь ответ на вопрос «роль или таска» сводится к тому, что уже есть в play — если там есть tasks, то надо дописать в tasks. Если есть roles — надо делать роль (пусть и из одной task). Напоминаю, tasks и roles одновременно не используются.
Понимание основ Ансибла даёт обоснованные ответы на, казалось бы, вопросы вкусовщины.
Таски и роли (часть вторая)
Теперь обсудим ситуацию, когда вы только начинаете писать плейбуку. Вам надо сделать foo, bar и baz. Это три таски, одна роль или три роли? Обобщая вопрос: в какой момент надо начинать писать роли? В чём смысл писать роли, когда можно писать таски?… А что такое роль?
Одна из грубейших ошибок (я про это уже говорил) — считать, что роль — это как функция в библиотеке у программы. Как выглядит обобщённое описание функции? Она принимает аргументы на вход, взаимодействует с side causes, делает side effects, возвращает значение.
Теперь, внимание. Что из этого можно сделать в роли? Вызвать side effects — всегда пожалуйста, это и есть суть всего Ансибла — делать сайд-эффекты. Иметь side causes? Элементарно. А вот с «передать значение и вернуть его» — вот тут-то и нет. Во-первых, вы не можете передать значение в роль. Вы можете выставить глобальную переменную со сроком жизни размером в play в секции vars для роли. Вы можете выставить глобальную переменную со сроком жизни в play внутри роли. Или даже со сроком жизни плейбуки ( set_fact / register ). Но вы не можете иметь «локальные переменные». Вы не можете «принимать значение» и «возвращать его».
Итого: роль — это не функция.
Что же хорошего есть в роли? Во-первых, у роли есть default values ( /default/main.yaml ), во-вторых у роли есть дополнительные каталоги для складывания файлов.
Ещё одна деталь: можно пытаться делать роли, которые будут доступны для переиспользования (через galaxy). После появления коллекций распространение ролей можно считать почти забытым.
Таким образом, роли обладают двумя важными особенностями: у них есть дефолты (уникальная особенность) и они позволяют структурировать код.
Возвращаясь к исходному вопросу: когда делать таски а когда роли? Таски в плейбуке чаще всего используются либо как «клей» до/после ролей, либо как самостоятельный строительный элемент (тогда в коде не должно быть ролей). Груда нормальных тасок в перемешку с ролями — это однозначная неряшливость. Следует придерживаться конкретного стиля — либо таски, либо роли. Роли дают разделение сущностей и дефолты, таски позволяют прочитать код быстрее. Обычно в роли выносят более «стационарный» (важный и сложный) код, а в стиле тасок пишут вспомогательные скрипты.
Существует возможность делать import_role как таску, но если вы такое пишете, то будьте готовы к объяснительной для собственного чувства прекрасного, зачем вы это хотите делать.
Въедливый читатель может сказать, что роли могут импортировать роли, у ролей может быть зависимость через galaxy.yml, а ещё есть страшный и ужасный include_role — напоминаю, мы повышаем навыки в базовом Ансибле, а не в фигурной гимнастике.
Хэндлеры и таски
Давайте обсудим ещё одну очевидную вещь: хэндлеры. Умение их правильно использовать — это почти искусство. В чём разница между хэндлером и таской?
Так как мы вспоминаем основы, то вот пример:
У роли handler’ы лежат в rolename/handlers/main.yaml. Handler’ы шарятся между всеми участниками play: pre/post_tasks могут дёргать handler’ы роли, а роль может дёргать handler’ы из плей. Однако, «кросс-ролевые» вызовы handler’ов вызывают куда больший wtf, чем повтор тривиального handler’а. (Ещё один элемент best practices — стараться не делать повторов имён handler’ов).
Основное различие в том, что таска выполняется (идемпотентно) всегда (плюс/минус теги и when ), а хэндлер — по изменению состояния (notify срабатывает только если был changed). Чем это чревато? Например, тем, что при повторном запуске, если не было changed, то не будет и handler. А почему может быть так, что нам нужно выполнить handler когда не было changed у порождающей таски? Например, потому что что-то сломалось и changed был, а до хэндлера выполнение не дошло. Например, потому что сеть временно лежала. Конфиг поменялся, сервис не перезапущен. При следующем запуске конфиг уже не меняется, и сервис остаётся со старой версией конфига.
Ещё одно положительное свойство handler’а состоит в том, что он не засоряет вывод. Не было изменений — нет лишних skipped или ok в выводе — легче читать. Оно же является и отрицательным свойством — если опечатку в линейно исполняемой task’е вы найдёте на первый же прогон, то handler’ы будут выполнены только при changed, т.е. при некоторых условиях — очень редко. Например, первый раз в жизни спустя пять лет. И, разумеется, там будет опечатка в имени и всё сломается. А второй раз их не запустить — changed-то нет.
Отдельно надо говорить про доступность переменных. Например, если вы notify для таски с циклом, то что будет в переменных? Можно аналитическим путём догадаться, но не всегда это тривиально, особенно, если переменные приходят из разных мест.
… Так что handler’ы куда менее полезны и куда более проблемны, чем кажется. Если можно что-то красиво (без выкрутас) написать без хэндлеров лучше делать без них. Если красиво не получается — лучше с ними.
Чем меньше if’ов (явных или декларативных — в форме when или форме include_vars по набору переменных), тем лучше роль. Иногда приходится делать ветвления, но, повторю, чем их меньше, тем лучше. Так что вроде бы хорошая роль с galaxy (работает же!) с кучей when может быть менее предпочтительна, чем «своя» роль из пяти тасок. Момент, когда роль с galaxy лучше — когда вы что-то начинаете писать. Момент, когда она становится хуже — когда что-то ломается, и у вас есть подозрение, что это из-за «роли с galaxy». Вы её открываете, а там пять инклюдов, восемь таск-листов и стопка when ‘ов… И в этом надо разобраться. Вместо 5 тасок линейным списком, в котором и ломаться-то нечему.