Для разработки и отладки playbook нужен полигон. Вы же помните, что мы имеем дело с production? А в моем случае, еще и BCAs (business-critical applications). Поэтому как у сапера – право только на одну ошибку. Да и перед регулярным применением тоже лучше проверять на полигоне. Почему не на stage? Так кто нам его отдаст – это епархия команд разработки. Лучше иметь все инструменты под рукой. (Кстати, тут произошла забавная опечатка – «инфструменты» - решил закрепить этот термин – инструменты для инфраструктуры).
Ну, раз мы тут говорим про ansible, то и полигон будем разворачивать с его помощью. Тем более, что все необходимое у нас есть прямо в core.
Я живу в условиях Debian, поэтому в нем и будем разворачиваться. Скорее всего, все предлагаемое почти без переделки применимо к Ubuntu (как младшему ребенку в семье), но я не проверял, так что ваши комментарии приветствуются.
Структура полигона
Нам нужно несколько управляемых по ssh сущностей, в максимально изолированной от остального мира среде. В идеальном случае – возможность быстро разворачивать и удалять. Поэтому lxc прямо на хосте оказывается идеальным решением. Ssh работает (если поставить), изолированы немаршрутизируемой сетью на bridge, расположены ближе некуда, что исключает избыточные ожидания при отладке.
Для управления lxc есть соответствующие модули lxc и lxc_container. К сожалению, оба community (и non-production в нашем случае, так как неизвестно кем и как поддерживаются), поэтому проходим мимо. К счастью, они нам вообще не нужны.
Из коробки, lxc устанавливает lxc-net и создает bridge, который снабжен dhcp (10.0.3.0/24) от притаскиваемого зависимостью dnsmasq. А нашим подопытным нужны стабильные ip адреса. Есть два выхода: отключить автоматику и создать собственный bridge или прибить гвоздями контейнеры к заданным адресам. Создание собственного bridge в Debian и Ubuntu различается кардинально (боюсь даже заглядывать в Mint), поэтому будем учить dhcp адресам.
Первое правило IaC– никому не рассказывай про реализацию, пока не описал инфраструктуру.
В нашем inventory необходимо явно указать кто у нас будет lxc хостом, а кто кроликами.
Уровень английского от С1, плавно сползающий без надлежащей практики к B2, а также опыт внедрения распределенных балансировщиков обязывают перейти к соответствующей терминологии. Поэтому красим кроликов в желтый и натягиваем на них джинсовые комбинезоны.
У нас есть хост (который local, и connection у него соответствующий) и три миньона на одном хосте. И магловая (не man ansible) переменная lxc_host, которая подсказывает на каком хосте должны быть миньоны.
Настраиваем lxc хост
В начале было слово, и слово это apt. А произнести его надо на нашем пока еще не lxc хосте.
Фактически, первый play, применяется только к группе lxc_hosts и всего лишь превращает localhost в lxc хост. Чуть не забыли про идемпотентность. Вдруг он уже? Тогда на нем могут быть миньоны, и о них лучше узнать заранее. Поэтому «ура, перепись!» - зарегистрируем их в фактах хоста командой lxc-ls.
Ansible устроен так, что специально после этого делать set_fact, как это любят предлагать тьюториалы, вовсе не обязательно. Наши existing_minions после register и так уже в фактах.
Настроим dhcp записи для миньонов. При ближайшем рассмотрении (с помощью debug), выясняется что магическая переменная groups - это всего лишь словарь массивов. Поэтому мы можем устроить итерацию по группе minions и подсмотреть необходимые значения в не менее магическом словаре hostvars. Осталось убедиться, что lxc-net знает про конфигурацию dhcp и перезапустить его, если были изменения.
Запускаем, и у нас есть lxc хост.
$ ansible-playbook lxc.yml -i playground.yml
Создаем миньонов
Начинаем второй play. Он будет проходить по группе миньонов.
От чисто декларативного программирования переходим к логическому - создаем только если миньон не присутствует в запомненных ранее фактах хоста, используя переменную lxc_host и условие in. Кстати, существующие контейнеры, не имеющие отношения к полигону уцелеют. Создавать надо на хосте, поэтому делегируем. Свежих и не очень миньонов запускаем.
Раз полигон у нас для отладки, имитируем ситуацию, когда один (а для чистоты эксперимента - второй) миньон у нас уже есть и будем отлаживаться.
$ lxc-create -n minion102 -t debian -- -r bookworm
Запускаем, миньоны создались, запустились и… не получили адреса. Минута паники, пять минут исследований, десять минут на воспроизведение. Ну, конечно, nftables на хосте. Выпиливаем (или допиливаем nftables.conf до разрешения tcp/udp 67, удаляем неудачные контейнеры), запускаем, пошло развертывание нашего полигона.
Настраиваем связь с миньонами
Подключаться к миньонам мы будем по ssh, поэтому, во избежание консольного требования подтвердить fingerprint, соберем все отпечатки миньонов и добавим себе в known_hosts. Здесь нас поджидают две проблемы. Во-первых, thread-unsafe запись в файл known_hosts — два запуска из трех один миньон из трех придавливал на записи соседа. Пламенный и лучистый «привет» разработчикам одноименного модуля — теперь вы понимаете, почему в сторону community я даже не смотрю? Поэтому делаем throttle, потери времени невелики (всего-то в три раза). Во-вторых, ssh-keyscan тоже многопоточный, и выводит хоть и одни и те же ключи, но каждый раз в произвольном порядке. Тут посылать лучи некуда — религия unix соблюдена — утилита выполняет одну функцию, и делает это хорошо (хорошо = быстро, а то, что оно не scriptable — так вам и не обещали). Спасает то, что один ключ это одна строка stdout, значит, просто сортируем.
Устанавливаемые в lxc образы максимально минималистичны и требуют дополнительной настройки. К созданному миньону пробуем подключиться с штатными реквизитами.
Если у нас timeout – там явно не хватает ssh. А если нас отвергли – нашего ключа. И не забываем, что если нам пришлось поставить ssh, то нашего ключа тоже нет, и ключи миньона тоже надо пересканировать. Так как все это выполняется через lxc-attach (связи-то все еще нет), то оно делегируется на хост.
Ну, и, ansible никак не может жить без тридцати восьми попугаев. Жаль, что версия уже ушла вперед от 3.8 и теперь это не так эпатажно звучит. А вот к этому моменту связь уже должна быть, поэтому используем raw, заодно эту самую связь и проверим.
Запускаем, и из трубы идет белый дым — полигон развернулся. Согласно религии ansible запускаем еще раз. Для тех кто не понял, можно еще раз. Идемпотентность соблюдается. Для тех, кто все еще не понял, или обладает обостренным любопытством — выключаем первый миньон, удаляем второй, а в третьем удаляем python. И опять запускаем. Мои поздравления!
Убираем с дороги грабли
Еще одно замечание. В процессе отладки playbook и отработки выхода из разных состояний я сделал на работающем полигоне:
$ systemctl stop lxc
Вполне предсказуемо, контейнеры выключились вместе с сервисом, который как раз и предназначен для их автозапуска и «нежного» выключения перед выключением хоста. А вот запущенный повторно идемпотентный playbook без всяких задних мыслей их снова включил. Но не сервис lxc. Теперь если погасить в таком состоянии хост, контейнеры получат выстрел на поражение в свою файловую систему, без предупредительного в воздух. И, если, в них будет крутиться какая-то база данных, то ни о каком flush buffers речи идти не будет – все что было в памяти, останется в вечной памяти. Поэтому в первый play сразу после установки lxc имеет смысл добавить проверку состояния сервиса.
Вместо заключения
Остается единственный вопрос, зачем же на самом деле нужна переменная lxc_host?
Код этого playbook для изучения вы можете скачать по ссылке https://gitflic.ru/project/wingedfox/dzen-ansible/file/?file=lxc_pg&branch=master из моего репозитория.
Ну, и, обязательный дисклеймер: в процессе написания статьи ни один кролик не пострадал!