Найти в Дзене
Linux | Network | DevOps

Основы Ansible Часть 2

Я продолжаю выразительно пересказывать документацию Ансибла и разбирать последствия её незнания (ссылка на предыдущую часть). В этой части мы обсуждаем инвентори. Я обещал ещё и переменные, но
инвентори оказалась большой темой, так что посвящаем ей отдельную
статью. Мы будем разбирать каждый элемент инвентори (кроме host_group_vars plugin) и обсуждать зачем он, как его использовать правильно, и как неправильно. Оглавление: Инвентори — это список хостов, групп, а так же вспомогательные
переменные. Изучая основы, мы будем разбирать каждый момент подробно, с поиском того, как "надо" и осуждением того, "как не надо". Хост в инвентори — это элементы словаря hosts для группы в yaml-инвентори (в ini-инвентори — это первый элемент строки): somegroup:
hosts:
somehost1:
somehost2: somehost1, somehost2 — это хосты. Что записывать как "хост" в инвентори, а что нет? Для ситуации, когда
у вас два сервера, всё понятно — два сервера, два хоста. Но бывают
ситуации и
Оглавление

Я продолжаю выразительно пересказывать документацию Ансибла и разбирать последствия её незнания (ссылка на предыдущую часть).

В этой части мы обсуждаем инвентори. Я обещал ещё и переменные, но
инвентори оказалась большой темой, так что посвящаем ей отдельную
статью.

Мы будем разбирать каждый элемент инвентори (кроме host_group_vars plugin) и обсуждать зачем он, как его использовать правильно, и как неправильно.

Оглавление:

  • Что такое хост? (и немного про транспорты)
  • Доступ IP vs FQDN; inventory_hostname vs ansible_host
  • ansible_user — писать или не писать?
  • Группы
  • Переменные: в инвентори или в плейбуку?
  • Классификация инвентори по происхождению.

Инвентори — это список хостов, групп, а так же вспомогательные
переменные. Изучая основы, мы будем разбирать каждый момент подробно, с поиском того, как "надо" и осуждением того, "как не надо".

Инвентори: хосты

Хост в инвентори — это элементы словаря hosts для группы в yaml-инвентори (в ini-инвентори — это первый элемент строки):

somegroup:
hosts:
somehost1:
somehost2:

somehost1, somehost2 — это хосты.

Что записывать как "хост" в инвентори, а что нет? Для ситуации, когда
у вас два сервера, всё понятно — два сервера, два хоста. Но бывают
ситуации и посложнее. Например, у нас могут быть гипервизоры и VM,
коммутаторы, маршрутизаторы, ipmi'и и т.д.

Правильный подход: мы считаем отдельным хостом каждый объект, к
которому может подключиться Ансибл через какой-либо транспорт. Это
означает, что хостом являются: аппаратный сервер, виртуалка с ssh (даже
если эта виртуалка запущена на сервере, который тоже есть в инвентори);
апплайнс вендора (если к нему есть рабочий транспорт); коммутатор с
доступом вовнутрь, lxc-контейнер. И даже контейнер докера может быть
хостом, если вам что-то приспичило делать внутри него.

Антипаттерн: пытаться что-то сделать на сервере, которого нет в
инвентори, через хаки и спецпеременные. Иногда такое возникает у
новичков при работе с libvirt. В инвентори есть только гипервизоры, а
виртуалки — в словаре "vms" или как-то так. Антипаттерн начинается так:
Создали виртуалку на гипервизоре, потом приспичило что-то по ssh
посмотреть на виртуалке после её запуска...

… история достигает кульминации где-то в глубоком инклюде, в стиле include_role: configure_vm, внутри которой миллион странных переопределений ansible_host, парсинг вывода ssh vm_ip somecommand,… на что люди не пойдут, лишь бы заставить негодный код работать.

Повторим: инвентори описывает то, на чём Ансиблу надо что-то делать (менять) через доступный транспорт.

Вопрос: если у нас виртуальная машина создаётся Openstack'ом
провайдера, надо ли эндпоинт API провайдера вписывать в инвентори? И
почему?

Ответ: не надо. Потому что мы не можем иметь к нему полноценный
транспорт. При том, что мы подключаемся к нему из соответствующих
модулей, это подключение не квалифицируется как "транспорт".

Другой вопрос: а надо ли делать отдельным хостом в инвентори коммутатор у которого есть management_ip и к котому подключены ваши сервера?

Ответ: Если можете что-то поменять на коммутаторе через его модули (Условный dlink_configure) и вам надо что-то там менять, то вписывайте. Если не можете, или можете, но не нужно, то и вписывать не нужно.

Существует ровно две причины, почему вы можете хотеть вписать что-либо в инвентори:

а) Вы его настраиваете штатными методами (у вас есть туда транспорт и вы что-то делаете).

б) Вы на него делегируете (delegate_to).

Ещё один антипаттерн, обратного типа, добавлять в инвентори лишнее. В
инвентори добавляется что-то, что не существует (и не будет
существовать) и используется в качестве помойки для перменных. Не
делайте так. Во-первых у вас уже есть localhost для project-global
переменных (хотя помойка переменных — это не очень хорошо само по себе).
Во-вторых, если вы вписываете в инвентори что-то, что заведомо не
работает, вы ломаете группу all (а группа all у
нас существует всегда). Это вызывает мелкие шероховатости и WTF каждый
раз, когда вы натыкаетесь на несуществующий хост. Я считаю это
анти-паттерном, который делает простой и хорошо работающий механизм
(связь хост-плейбука) шатким и полным условностей.

Инвентори: ansible_host vs FQDN

В этой главе мы хорошо разбираемся с тем, что такое inventory_hostname, что такое ansible_host, с понятием транспорта.

При том, что транспорт уже не совсем "инвентори", к содержимому
инвентори он относится наипрямейшим образом, потому что смена транспорта
внутри play — это уже экстремальный спорт, на который не
распространяется ваша медстраховка.

Что такое "транспорт"? Это результат использования "connection
plugin" Ансибла, через который модуль копируется в целевую систему (или,
в ряде случаев, не копируется, но получает доступ к целевой системе).
Какой-то транспорт используется всегда. Самый популярный транспорт ssh
(используется по-умолчанию), но их на самом деле
много. Каждый плагин может использовать набор переменных, выделенных для подключения: ansible_host, ansible_user, ansible_port и т. д. А может и не использовать. Например, если транспорт lxc (который выполняет код через lxc-execute), то зачем ему порт?

Если же ansible_host не задан, то используется inventory_hostname. Это — имя хоста в инвентори.

Вот пример:

---
somegroup:
hosts:
somehost:
ansible_host: 254.12.11.10

Вот somehost тут — это inventory_hostname. Если нет ansible_host, то используется inventory_hostname.
И всё было бы понятно, если бы не следующий уровень преобразований,
который не имеет никакого отношения к Ансибл, но может попортить много
нервов.

Внутри как inventory_hostname, так и ansible_host
может быть либо адрес, либо имя. С адресом всё понятно, а вот с именем
уже интереснее. Оно передаётся "как есть" в нижележащий исполнитель.
Интерпретация имени оставляется на усмотрение транспорта. Например, lxc
использует его для выбора контейнера. А вот ssh (самый распространённый
транспорт, напоминаю) использует кое-что более сложное.

Во-первых, он смотрит в конфиг ~/.ssh/ssh_config (или
другой, заданный через переменные окружения). Если кто пропустил,
напоминаю, что конфиг ssh тьюринг-полный и может делать странное через
комбинацию регэкспов и сниппетов для исполнения баша. Т.е. переданное
имя становится (в общем случае) аргументом к частично-рекурсивной
функции, которая (может быть) выдаёт реальные параметры соеднения на
выходе. Может быть, соединение пойдёт через цепочку jump-хостов,
редиректов портов и прочего ssh-цирка. А может быть, такого хоста не
найдётся. Если же из ssh_config выползает другое имя (или искомого нет в ssh_config), то ssh делает gethostbyname(). Это вызов libc, который получает адрес по имени. Который, в свою очередь, руководствуется пачкой конфигурационных файлов (/etc/nsswitch.conf, /etc/hosts)
и ответами DNS-ресолвера (если конфигурационные файлы это разрешают).
Который, в свою очередь, может дописывать к имени домен, смотреть на
разные рекурсивные DNS-сервера, которые могут отвечать разное, а могут
посмотреть на ресурсную запись CNAME пойти куда сказано… Просто
волшебная простыня
возможностей того, что может пойти не так.

Из этого вытекает моё, выстраданное, мнение: при работе с SSH, всегда (кроме спецслучаев) использовать ansible_host внутри которого IP-адрес.

Я пробовал другой путь, и он мне местами аукается до сих пор. Давайте разберём этот вопрос подробно.

Если вы используете любое вне-ансибловое, но host-local определение имени (ssh_config, /etc/hosts), то ваши плейбуки перестают быть портабельными между машинами. Вы ссылаетесь на что-то, что существует только у вас в голове и с вами разговаривает только в конфигурации вашего компьютера. Вы не можете перетащить эти плейбуки на CI, на машину коллеги или даже на вторую вашу машину. Точнее, можете, но для этого нужно что-то (что?) прописать в конфигурацию, которой не видно в репозитории. Опечатки трудно отлаживать (у меня всё работает), изменения почти невозможно распространять. НЕ ДЕЛАЙТЕ ТАК.

Хотя, разумеется, есть исключения. Например, моя маленькая уютная
оверлейная сеточка для домашних нужд живёт с именами из /etc/hosts и все
плейбуки полагаются на эти имена. Но это моё осознанное решение,
которое к индустриальному продакшену никакого отношения иметь не должно.

Если вы используете DNS, то вы получаете себе регэксп ещё одну
проблему. Когда изменения в DNS дойдут до вашей машины?
Негативное/позитивное кеширование, всё такое. А даже если оно дошло до
вас, то когда оно дойдёт до резолвера, которым пользуется ваш
динамический слейв CI? Слейв-то помер, а DNS-ресолвер — нет. Удачи в
отладке. НЕ ДЕЛАЙТЕ ТАК.

Второй момент, куда более тонкий. Надо ли всегда указывать ansible_host или inventory_hostname достаточно?

В плейбуках рано или поздно возникает потребность указать "адрес соседа". В самых трудных случаях этот процесс требует модуля setup и выполнения головоломного кода:

- name: Ping neighbor
command: ping -c 1 {{ neighbor_ip }} -w 1
changed_when: false
vars:
neighbor_ip: '{{ (hostvars[item].ansible_all_ipv4_addresses|ipaddr(public_network))[0] }}'
with_items: '{{ groups[target_group] }}'

(имея на руках public_network мы проверяем, что хосты могут общаться со всеми серверами в группе target_group).

Но, это трудный случай, поскольку у серверов несколько интерфейсов. В
99% случаев вам нужен просто "адрес соседа". Если вы договорились, что у
каждого хоста есть ansible_host и внутри там обязательно IP-адрес, то вот он. Никакого setup. Бери и используй. Прелесть ansible_host
с IP-адресом трудно переоценить, потому что, помимо "какого-то IP
соседа", этот адрес ещё неявно (явно!) отвечает вам на вопрос, какой из
IP-адресов сервера является его "access address" при наложении всяких
файрвольных правил, конфигурации доступов и т.д. Делайте так. Это хорошо
и удобно.

… Но тут может возникнуть вопрос: а если у нас сервера появляются на
свет динамически, или у нас внешная система оркестрации (а-ля докер) у
которой точно есть хороший DNS? Ну, тогда используйте их. А, заодно,
страдайте, если вам понадобились IP. Разумеется, к любой общей
рекомендации всегда можно найти частные исключения.

Инвентори: ansible_user

Следующая интереснейшая проблема: надо ли в инвентори хранить имя
пользователя? Это важный вопрос, но у него нет однозначного ответа. Вот
набор моментов, о которых надо подумать перед выбором.

  1. Есть ли доступ к этому хосту из-под "спецаккаунта" у других пользователей? Если есть, то ansible_user в инвентори разумно.
  2. Есть ли доступ к серверу под "своими" аккаунтами у других пользователей? Если есть, то ansible_user в инвентори создаёт проблемы.
  3. Если вы не указываете пользователя в инвентори, то опция -u у ansible-playbook
    позволяет пользователя задать, причём так, что его можно переопределить
    из любого места в инвентори или плейбуке для необычных видов коннектов.
    Это удобно. Каждый под своим пользователем, CI использует -u (или тоже под своим пользователем), все счастливы.
  4. Но тогда абстракция протекает. Например, ваш сосед может быть
    залогинен на своём ноутбуке под именем 'me'. Это ж его ноутбук. А на
    сервере он — m.gavriilicheynko. Неудобненько.
  5. В то же самое время, использование опции ansible-playbook -e ansible_user=ci
    (для CI, например) с одной стороны позволяет использовать правильное
    имя вне зависимости от содержимого инвентори, с другой стороны ломает
    все нестандартные подключения (к коммутаторам, например).
  6. Если у вас стоит проблема "первого логина" (плейбука создаёт всех
    пользователей, но только после первого запуска), то первый запуск можно
    сделать и с опцией -u, и никто не помрёт.

В моей практике (и обстоятельствах, в которых я работаю), мне удобно указывать ansible_user для "себя" (т.е. инвентори, к которыми работаю только я). Если инвентори используется более одним человеком — ansible_user используется только для специальных случаев (например, доступ к коммутаторам при первом провизе и т.д.), а обычные хосты ansible_user не используют.

Группы

Как только мы начинаем обсуждать группы, мы уже обсуждаем не только и
не столько "что должно быть в инвентори", сколько онтологическое
понятие "группы". Это тонкий хрупкий мир архитектурного Ансибла, где
одно неловкое движение оставляет от красивого замка колючие обломки.
Группы — очень сильный механизм в Ансибл, но его неправильное применение может очень сильно всё поломать.

Для чего использует группы Ansible?

Во-первых, группы используются как встроенные "списки хостов" (в переменной hosts в play и внутри магического словаря groups).
Во-вторых, группы предоставляют групповые переменные, наследуемые
хостами из группы. В целом, технически, можно писать плейбуки используя
только переменные (вы можете использовать в hosts переменные, если
переменные хотя бы одного хоста были инициализированы). Но, разумеется,
так делать не надо. А надо использовать группы.

Для чего вы используете группы (почувствуйте разницу — использует Ансибл, используете вы):

  1. Для назначения на них play. (директива hosts). Например, группа 'prometheus' может включать в себя все сервера, на которых надо настраивать Prometheus.
  2. Для хранения общих переменных у каких-то серверов. Заметим, я не
    говорю, что перменные надо хранить в инвентори ("где хранить переменные"
    мы будем разбирать отдельно), я говорю, что вы всё-таки решили, что
    нужно, то переменные группы — отличное место хранения общих (одинаковых)
    переменных для всех серверов группы.
  3. Для семантической аннтоации кода.

Первая задача самоочевидная, ей пронизаны все примеры, так что пропускаем.

Вторая задача — общие переменные. Про переменные мы говорим потом, а
пока скажем, что отдельная группа с настройками (группа, для которой нет
play) — это не самая плохая идея. Даже, наоборот, отличная идея.

Так что основной фокус будет на семантику. Группа — это возможность
дать общее название нескольким серверам. До этого у вас были сервера
jc-r4, xcore-lu1 и ams1-se-r2, а теперь появилось имя
"netflow_collectors". Насколько у вас увеличилось понимание зачем эти
сервера? Я бы сказал, что до появления имени группы, это были просто
буковки, а после появления имени, вам даже в содержимое ролей не надо
заглядывать, вы плюс/минус и так знаете, что эти сервера делают.

Имена групп позволяют наделить смыслом инвентори. Человек, который
читает инвентори уже видит не просто список хостов с машиночитаемой
информацией, а некий рассказ — у нас есть сервера такого типа, сервера
такого типа, а ещё у нас есть группа серверов, у которых есть доступ в
базу данных. А есть группа серверов с включенным эникастом.

Другими словами, инвентори с именами групп — это рассказ про ваш
проект. Если ваши имена невнятные или ничего не рассказывают, то и
рассказ у вас получается в стиле "этот к тому и так его что тот аж
туда".

Имена групп — это первый проблеск смысла в вашем проекте, который встречает читающего.

При этом группы — это компромисс между инвентори и play. Дело в том,
что play накладывает требования на инвентори (хочешь получить запущенным
докер — положи хост в группу docker). Но инвентори может добавлять свои
группы, которые не используются в play (те самые группы для
переменных), использовать наследование, то есть мягко корректировать
ожидания play.

Отдельно надо рассказать про наследование. Наследование устроено просто — одна группа может быть потомком другой группы.

Вот пример простого наследования:

---
foo:
hosts:
foo1:
foo2:

bar:
hosts:
bar1

foobar:
children:
foo:
bar:

Наследование — это инструмент инвентори и только инвентори. Никогда
play не должна полагаться на какое-либо наследование. (Вы не поверите,
но между моментом, пока я написал эти строчки и моментом, когда я
опубликовал эту статью, я исправил свою же ошибку, в которой плейбука
неявно полагалась на то, что группа grafana-servers является потомком
группы mons — а я как раз сделал её потомком группы mgrs в новой версии
инвентори).

Наследование позволяет передать ещё кусочек семантики "мы размещаем
mgrs на хостах mons" в явном виде. Это одновременно и механизм DRY (do
not repeat yourself, один из принципов хорошей разработки) для
инвентори, и ещё один метод более выразительной передачи смысла
читателю.

Немного о динамических группах и динамических инвентори.

Динамическая инвентори — это результат исполнения какого-то кода,
выдающего на выходе "обычную" инвентори. Динамические группы создаются модулем group_by или модулем add_host внутри плейбук.

Есть ситуации, когда они оправданы. Например, у вас инвентори всегда
генерируется роботом (третий вариант в разделе ниже). Или, вы не хотите
загромождать инвентори второстепенными группами, формирующимися по
специальным правилам. Такие ситуации есть, но они — очень пограничный
случай. Если можете избежать — избегайте, потому что они несут с собой
несколько фундаментальных минусов. Например, динамические группы не
позволяют нормального --limit. Вам надо выполнить таску group_by, а для каких хостов исполнять не понятно, т.е. мимо --limit
оно пролетает. Возникает особый культ тега [always], потому что любая
попытка использовать теги натыкается на отсутствие динамических групп.
Вообще, group_by — это момент, когда плейбука начинает диктовать вместо inventory что у вас в инвентори. Ой.

Динамические же инвентори делают невозможным воспроизведение
проблемы, если источник инвентори "дрожит" (т.е. меняется от запуска к
запуску). Вы же помните, что список хостов в группе — это на самом деле
словарь? Далеко не все языки программирования сохраняют порядок в
словаре (в Питоне это называют "словарь", в других языках это hashmap,
map, object, и т.д.). Более того, даже в обычном Питоне порядок
сериализации элементов словаря не определён. Ансибл специально
прикладывает усилия к тому, чтобы порядок хостов в группе соответствовал
порядку перечисления в инвентори (начиная с 2.4 даже есть специальный
параметр play: order, дефолтное значение которого inventory).

Когда это портит жизнь? В тот момент, когда:

  1. Вы полагаетесь на groups.somegroup[0] как на "основной
    сервер". Не то, чтобы это была уж очень хорошая практика, но
    встречается. После изменения порядка серверов в динамической инвентори на следующем прогоне Ансибла у вас это окажутся разные сервера. Не всегда взаимнозаменяющие.
  2. Вы формируете списки (например, pg_hba.conf, allowed в nginx.conf,
    etc). У вас меняется порядок, файл changed. Мало того, что лишние
    reload'ы, так ещё и постоянные changed в выводе. Что очень-очень плохо, и
    во всей документации вам многократно говорили, что надо писать
    идемпотентно.

Эти проблемы устранимы, но если у вас инвентори "дрожит", вам приходится с этой дрожью бороться.

Второй источник боли для динамической инвентори в некотором пофигизме
отдельных механизмов. Например, если у вас инвентори создаётся из
содержимого региона openstack'а, то если вы случайно оставили в
переменных среды окружения более высокоприоритетную переменную для
подключения к Openstack, чем то, что вы используете обычно, то вы
получите вывод другого региона или тенанта. (если вы получаете ошибку,
всё, проблема обнаружена — я про ситуацию, когда изменение "прокатило").
Вам выдали
другой комплект хостов. Один раз. В следующий раз
(в соседней консоли) всё будет хорошо. Вы пошли куда-то сделали что-то.
Возможно, фатальное. Возможно, записав пароли к продакшен базе в staging
сервер. Или вообще, куда-то в публично-доступное место. Боль-боль-боль,
а главное, никаких шансов на адекватную отладку. Инвентори-то
динамическая. Аналогично вас ждёт боль и неожиданное, если у приложения
расслабленная модель обработки ошибок. Нет каких-то данных из-за
временной ошибки? Ок, пускай будет "пусто". Что такое пусто? Ну, пусть
будет пустой словарь. Ррр… аз, и у вас в списке клиентов базы данных
пусто. Вы берёте и пишите в конфиг СУБД новый список разрешённых IP, в
котором никаких клиентов нет. Чпок, даунтайм. При следующем прогоне
Ансибла всё опять поднялось. Виноваты программисты, а отлаживать вам.

Именно по этой причине я, в проекте, где инвентори формируется
роботами, я эту инвентори не использую как инвентори, а сохраняю в файл,
который объявлен артефактом для джобы. Это не решает всех проблем, но,
по-крайней мере, есть бумажный след случившегося.

Инвентори: переменные

Последняя составляющая инвентори — это переменные. Поскольку внутри
инвентори могут быть и хосты и группы, все переменные в инвентори
являются либо переменными хоста, либо переменными группы. Оба вида
переменных одинаково доступны в play и ролях, разница между ними (кроме
эргономики DRY) проявляется при определении, какие переменные "важнее"
(variable precedence). Вопросы приоритетов переменных и области их жизни
мы будем обсуждать в следующей части, а в этом разделе фокус будет на
том, какие переменные класть в инвентори, а какие не в инвентори.

… И это нас подводит к другому вопросу: что есть инвентори?

Давайте сделаем шаг наверх и попытаемся описать структуру проекта на
Ансибле общими терминами. У нас есть плейбуки — это код и данные. У нас
есть инвентори, которое в нормальном режиме содержит только данные
(игнорируем лукапы и программирование на jinja). Мы объединяем плейбуки и инвентори и получаем рабочее "нечто". Как это "нечто" называется?

Кто-то это может назвать "инсталляцией", кто-то "средой", кто-то
"стейджем". Точное название не важно (хотя я буду использовать
"инсталляция"). Важно, что комбинация инвентори и плейбуки делает
конкретные вещи на конкретных серверах (даже если эти сервера появляются
на свет в процессе исполнения плейбуки и умирают в по окончанию).
Плейбука описывает что делать, а инвентори — где делать.

Плейбука контролирует взаимоотношения между "участниками" инвентори. В ассортименте делегация, списки, изменение ansible_host,
заглядывание в hostvars и т.д. (я не говорю, что это хорошо, но может
быть). Инвентори в свою же очередь контролирует плейбуку посредством
переменных и разной группировки хостов.

Но не смотря на возникающее взаимопроникновение, нужно сохранять
принцип, что плейбука (и её переменные) это "что", а инвентори (и её
переменные) — "где". Чем меньше эта граница размывается, тем легче
сопровождать проект.

… Если бы было всё так просто. Например, пароль в базу данных,
очевидно, является объектом инвентори (исходя из best practices, что
переиспользовать пароли — зло, и мы хотим на каждую инсталляцию иметь
свой пароль). В логику "где" это совсем не укладывается, так что
инвентори, это не только указание на то, где выполнять, но и все
отличительные особенности инсталляции.

Название "отличительные особенности" мне нравится своей ёмкостью. Мы
перечисляем в инвентори чем одна инсталляция отличается от другой. С
применением DRY список отличий должен быть настолько малым, насколько
можно, а все производные — вычисляться где-то в другом месте. Попробуем
применить этот принцип на практике.

Вопрос: Объём памяти, выделяемый под java-приложение должен
задаваться в инвентори или внутри плейбуки, которая это приложение
настраивает?

Ответ: если разные инсталляции должны иметь разный объём памяти, и мы
не можем определить его автоматически (например, по числу хостов в
группе), то это переменная для инвентори. Если объём памяти — это
результат изысканий специалиста и он должен быть одинаковым в staging и
production, то это переменная для роли или плейбуки.

Вопрос: номер порта на localhost, на котором слушает приложение (сверху там nginx в режиме proxy_pass), это переменная плейбуки или инвентори?

Ответ: это переменная плейбуки, если нет специальных причин делать эти порты разными между инсталляциями.

Вопрос: список пользователей — это переменная плейбуки или инвентори?

Ответ: зависит от того, разный у вас список пользователь между инвентори
или нет. Если разный, то это переменная инвентори, если во всех
инсталляциях список пользователей одинаковый — это плейбука.

Надеюсь, это даёт некоторую интуицию по переменным инвентори.
Основной вопрос, который надо себе задавать: "почему эта переменная
должна быть в инвентори"? Другими словами, инвентори — это
специальное место для перменных, и вам нужны специальные причины записывать их туда.

Происхождение инвентори

Есть ещё один аспект инвентори, про который редко говорят. Кто пишет инвентори?

Общего ответа тут нет, так что я расскажу "как бывает".

Первый вариант — инвентори жёстко привязана к репозиторию с плейбуками. У вас есть production.yaml, staging.yaml, или даже каталоги инвентори production/ и staging/,
или же у вас пять регионов, и каждый имеет свою инвентори. В этом
случае развитие (изменение) инвентори происходит одновременно с
развитием плейбук. В этом случае для вас "происхождение инвентори"
звучит странно. Вы придумываете себе схему именования инвентори и правил
работы с инвентори и всё хорошо. Это случай обычного инфраструктурного
проекта, который пишут и сопровождают одни и те же люди. Это же случай,
когда вы пишите "для себя" (конфигурация лабораторий, стендов,
конфигурация плейбук для сайта вашей компании, etc).

Второй вариант — инвентори пишут другие люди. Где-то там есть git с
плейбуками, и может быть, с примерами инвентори, а где-то есть другой
git с инвентори. Такая ситуация часто бывает, если разработка и
эксплуатация различаются. Все крупные проекты по развёртыванию чего-либо (ansible, ceph, openshift, etc) пишутся в этом режиме. Пишет одна
группа, эксплуатируют разные другие группы. В этой ситуации инвентори
становится подобием API, интерфейсом между кодом плейбук и
"конфигурацией" инвентори. У меня есть ощущение, что апстрим Ансибла не
особо думал про этот случай, потому что тут бывает очень много трудных
моментов, но в модели разработки с разными группами людей, это
неизбежно.

Ключевым моментом плейбук в этом случае является обеспечение
минимального уровня связности с инвентори. Чем меньше, тем лучше. (И
именно тут, на уменьшении связности, Ансибл не очень хорош). Ещё этот
вариант приводит к понятию "сценария" — у вас один и тот же код
(плейбуки) может использоваться в самых разных ситуациях, которые
покрываются разными участками плейбук или одни и те же таски имеют
разный смысл в разных ситуациях (сравните, например, развёртывание
ceph-ansible'а в контейнерах ради RGW в динамической среде приложения
или на бареметал в роли хранилища бэкапов на века).

Третий вариант — инвентори пишут роботы (или другие плейбуки). Это
подмножество предыдущего варианта, но с ещё более жёсткими
ограничениями. Развёртывание среды для тестов в CI с генерацией
инвентори — один пример. Другой — использование ансибла для управления
слейвами последователями в системах со встроенной оркестрацией. В такой ситуации структура инвентори перестаёт ориентироваться на человеков и начинает служить нуждам машиночитаемости — удобства генерации, отладки, модульности. Можно забывать про DRY, про выразительность и семантику. Зато надо быть очень строгим по типам и наличию значений. Пишут роботы для роботов.

При работе над проектом надо для себя точно определить какие варианты
вы хотите делать. Одно дело, когда у вас инвентори — это 3000 коммитов
за 10 лет эксплуатации, другое дело, если инвентори — файл, который
создаёт одна плейбука для другой плейбуки на время жизни джобы на CI.

Составные инвентори

Есть ещё один режим работы с инвентори — это составные инвентори. Я
сомневался писать про них или нет, но, раз уж я посвятил целый раздел
только инвентори, видимо, писать.

Ансибл поддерживает больше одной инвентори.

ansible-playbook -i inventory1.yaml -i inventory2.yaml play.yaml

Содержимое инвентори объединяется по принципу "последний побеждает".
Первый-второй уровень объединяется (группа состоит из хостов из первой и
второй инвентори), дальше перезаписываются последней инвентори
(например, если inventory2.yaml даёт users: [...], то она будет перезаписывать аналогичную из inventory1.yaml).

Где это полезно? Например, если у вас часть данных динамическая, вы
можете иметь одну инвентори динамической, а вторую статической.

Второй момент: инвентори поддерживает переменные в файлах (host_vars/, group_vars в каталоге с инвентори). Если у вас инвентори пишут роботы, то вы (как авторы плейбук) можете подкладывать дополнительные переменные инвентори в чужую инвентори (робота). Edge case, мягко говоря.

Это точно не "основы Ансибла" и плюсы/минусы применения такого
подхода надо взвешивать очень внимательно. Основное, что нужно помнить,
что чем сложнее у вас связи в проекте, тем ближе вы к предельному
состоянию проекта на Ансибле, который пишут долго и старательно,
соблюдая второй закон термодинамики. Это предельное состояние называется "комок слипшихся макарон". И вы этого не хотите.

Навигация:

  • Предыдущая часть
  • Следующая часть

Обсудить эту статью можно в Телеграм канале: https://t.me/linautonet