Блеск и нищета Ansible
Введение
Иногда на своих внутрибанковских тренингах по Ansible я озвучиваю личную точку зрения на экосистему языка Python. На мой взгляд, она токсична, и располагает к боли и унижениям - эдакое садо-мазо, если угодно.
При этом, как ни странно (ну мне вот не странно, так что пусть и вам не будет странно) - я горячо люблю Python как язык. Смотрите:
он выразителен;
на нём написано много годного кода - библиотеки, фреймворки, приложения;
его часто выбирают в качестве встроенного языка сценариев для продуктов на других различных языках;
в PyPI есть пакеты (=библиотеки) почти на каждый случай жизни;
ставить эти пакеты можно примерно десятком разных способов - выбирай, какой душа просит.
Казалось бы, пусть цветут сто цветов?... Увы, нет, даже у такой зрелой и в хорошем смысле развесистой экосистемы есть тёмные уголки, и многие из них таятся не там, куда хороший разработчик и за версту не подойдёт, а буквально рядом, “за углом” - стоит, к примеру, в CentOS/RHEL/Debian набрать что-то в духе “pip install psycopg2”.
Вполне возможно, кто-то “в теме” уже рассмеялся, потому что в курсе всей несуразицы, которая при этом происходит - типа сборки своих копий libcom_err, libcrypto, libpq, libselinux(!), libssl(!), ну и прочего такого всякого. Кстати, имейте в виду - если собирать из исходников на вашем хосте либо нечем, либо нечего, то вы в лучшем случае автомагически получите “с доставочкой” предсобранные бинарные копии этих библиотек, в худшем - установка просто “упадёт”. Ну действительно, и зачем только мейнтейнеры дистрибутивов придумали эти системные библиотеки, пусть каждый Python-пакет, которому они понадобятся, носит с собой свои копии. Нормальная ситуация, отличная идея, звучит, как план!!!??? Нет. Такой план - откровенная дрянь, и я в нём не участвую. Примерно так я подумал, когда решил попробовать выкорчевать из модулей Ansible ненормальную, на мой взгляд, зависимость от psycopg2.
Ну вы сами посудите: Ansible позиционируется, как легковесная безагентная система, для которой на целевом хосте нужны только(!) Python и ssh, а здесь что же? Что ни БД - то проблема. Захочешь, например, роль в Postgres’е создать, ну или там таблицу какую - так сразу изволь на хост, который выполняет модуль, поставить psycopg2. WASTED “Потрачено”, иначе и не скажешь.
Пути решения
Размышлять над решением проблемы провизионирования объектов БД PostgreSQL из Ansible я начал ещё в 2017 году. Тогда даже нашёл в Интернете подходящий драйвер, целиком написанный на Python (pg8000), но вот беда - тогда квалификации меня как pythonista не хватило, чтобы закончить проект в сжатые разумные сроки. А в ноябре я подписал контракт на перевод и издание двух книг, и всё заверте…
В общем, к идее “вернуть Ansible величие сделать провизионирование PostgreSQL настолько в духе Ansible, насколько это возможно” я вернулся относительно недавно, уже в 2021. Во время очередного тренинга по Ansible для коллег, показывая свой проект и рассказывая, что в нём и как работает, я ощутил лёгкий укол стыда. Ну и то правда: смотрите, на курсах я рассказываю коллегам, почему модули shell и command не должны использоваться, объясняю про идемпотентность, в чате по Ansible рекомендую людям для сложных случаев взять и написать модуль, а у самого в роли нормаааальная такая портянка вызовов psql через shell, причём с “failed_when: false” (здесь как раз место для шуток про “И - идемпотентность”). Короче говоря, я ощутил, что настало время исправить это досадное недоразумение и избавить Ansible от зависимости.
Анатомия модулей Ansible для PostgreSQL
Давайте разбираться вместе. Для полной определённости, разбираться мы будем в версии Ansible 2.9.11. Почему именно в ней - ну как вам сказать… Что было в виртуалке, то и взял. Хотите более свежую версию? Милости просим - Хабр большой, места для серьёзной статьи по Ansible всем хватит.
Итак, лезем в документацию - оттуда понятно, что за поддержку PostgreSQL отвечают не только профильные модули в каталоге “modules/database/postgresql”, которые, собственно, и делают всю полезную работу, но и модуль, непосредственно взаимодействующий с драйвером psycopg2, с нехитрым названием postgres в каталоге “module_utils”.
Если вы смотрели мой доклад на Стачке-2019 в Иннополисе, то уже могли насторожиться, услышав имя этого каталога, и полностью были бы правы: “module_utils” в Ansible - имя каталога с дополнительными внешними зависимостями в проекте. Да вы только представьте себе: ваш модуль/фильтр/плагин при работе может импортировать [почти] всё, что пожелаете! Любой каприз за ваши деньги байты! Важно одно-единственное условие: импорт должен быть частью псевдопакета под названием “module_utils”.
Почему же “псевдопакет”? Потому, что Ansible любит вас при работе собирает в этот псевдопакет не только свои модули из одноимённого каталога, но и модули из вашего проекта, лежащие в каталоге с этим названием, причём с учётом “перекрытий” - то есть если у вас в проекте есть модуль с тем же названием, что и в Ansible, он будет использоваться вместо штатного. Только представьте: достаточно создать в проекте каталог с нужным названием и скопировать в него всё, что требуется вашему коду - и всё, Ansible при старте скопирует этот каталог на удалённый хост и добавит каталог в окружение выполняемого модуля. Тогда ваш код сможет корректно выполнить операцию импорта. Эта схема является полностью штатной, и модули из поставки “коробочных” (до 2.10.x) версий Ansible можно таким образом снабжать зависимостями.
Вот это - декларативность в её лучшем виде, как она есть, то есть размещение файла в проекте уже самим своим фактом определяет его назначение и процесс обработки.
Операция на открытом коде
У наших южных соседей есть пословица “путь в тысячу ли начинается с первого шага”. А мы начнём с файла postgres.py.
Открываем код. Беглый просмотр интерфейсов показал, что оба пакета (psycopg2 и pg8000) должны по интерфейсам совпадать, потому что реализуют спецификацию Python DB API v2 (PEP-249). Что же, можно кричать “ура”, всё заведётся “искаропки”? Увы и ах, импорт того самого psycopg2 в самом начале.
psycopg2 = None # This line needs for unit tests
try:
import psycopg2
HAS_PSYCOPG2 = True
except ImportError:
HAS_PSYCOPG2 = False
Обычная для Python конструкция из арсенала защитного программирования: пробуем импортировать нужный модуль, если его нет - устанавливаем информационный флаг для того, чтобы где-то дальше по коду “упасть” с подходящим сообщением. Меняем psycopg2 на pg8000, ну и следом HAS_PSYCOPG2 на HAS_PG8000. Как вы уже, возможно, поняли, наша первоочерёдная задача в этом файле - заменить все упоминания psycopg2 на pg8000, а затем попробовать отладиться-запуститься.
Кроме того, знатоки Python уже могли сообразить, что pg8000 нужно импортировать по относительному пути, то есть вот так:
from . import pg8000
Почему и зачем - смотрите, наш модуль (напоминаю, мы модифицируем файл postgres.py) уже импортируется с помощью вот такой конструкции:
from ansible.module_utils import postgres
Соответственно, результат будет эквивалентен вот такому вызову:
from ansible.module_utils import pg8000
И это было только начало. Далее на очереди сам драйвер pg8000. С ним всё начиналось предельно прозаично: копируем его в каталог module_utils проекта “как есть”. Кхм, не работает - ну так и я не впервые код на Python вижу. Открываем код - эх, не хватает зависимости. Нужен пакет scramp. Ищем его, содержимое точно так же складываем рядом, в module_utils - не-а, не работает. Хм, и что же этой всей куче кода на Python нужно? Да всё просто - здесь нужны любовь, понимание и отладка.
Пакет scramp оказался самым лёгким по количеству работы компонентом из всей истории: оказалось достаточно подправить импорты следующим образом: “scramp.<module>” → “.<module>“ Похожим образом, кстати, пришлось изменить импорты и в pg8000.
В общем, вносим изменения - и упс, не работает. Что ж, придётся погрузиться в отладку.
Отладка без отладки
Когда я писал статью, то где-то в этом месте понял, что она может получиться скучной. Ну вы, наверное, такие читали не раз. Попробую изменить тренд: про отладку писать не буду вовсе, скорее попробую поделиться “заметками на полях”.
Первое, что бросилось в глаза при работе с кодом: большинство модулей Ansible, предназначенных для работы с PostgreSQL, использует нестандартное расширение Python DB API 2.0 под названием DictCursor. Это расширение, как вы могли догадаться, предоставляется тем самым пакетом psycopg2. Так что же это такое? Фактически это - вариант курсора, который позволяет обращаться к столбцам строки БД по именам, то есть этакий “псевдословарь”. С одной стороны, ничего страшного: редко где понадобится именно работать с данными, которые возвращает БД. С другой стороны, если понадобится - ну что ж, будем наготове: выясняем, где именно хранятся названия столбцов, и пишем функцию-обёртку, чтобы иметь возможность в нужный момент её использовать.
Дальше - больше: ещё одно “расширение” стандарта от psycopg2, теперь по имени statusmessage. Что же это такое? Это атрибут курсора, хранящий последние сообщения от сервера, читай - ответ на последнюю выполненную команду. Как это выглядит? Вспоминайте, при задании пароля для роли в PostgreSQL сервер в ответ сообщает “ALTER ROLE”. Вот это и есть status message. Интересный факт - минимальная реализация этого расширения для pg8000 оказалась относительно простой, потому что вся необходимая от сервера информация на клиенте уже имелась. Ну то есть важного, со смыслом кода набралось аж на одну строку.
Конечно, для отладки понадобилась минимальная программа на Python, которая использовала именно эту, изменённую версию pg8000, но овчинка стоила выделки: функция-замена и расширение реализованы, каких-то принципиальных препятствий для работы логики кода модулей нет.
Следующий затык был уже в самих запросах. Думаю, ни для кого не секрет, что при работе с БД считается хорошим тоном использование “prepared queries” - иначе говоря, заранее подготовленных запросов, в которых подстановку аргументов осуществляет СУБД, а не программа пользователя (это снижает риск SQL-инъекций). Так вот, pg8000 по сравнению с psycopg2 значительно более ограничен в исходных форматах запросов (драйвер должен вставить на место параметров запроса соответствующие указатели для СУБД). Это привело к необходимости переписывать все запросы. Ну как к необходимости… На самом деле можно было бы просто переиспользовать соответствующий код из psycopg2, но я сознательно этот вариант отбросил: и так уже достаточно работы, проект рисковал стать реальным никому не нужным долгостроем.
Окей, запросы переписаны, что дальше? Дальше - тестовый плейбук, в котором вызывается каждый из модулей. Кстати, в ходе его разработки выяснились некоторые нюансы настроек авторизации PostgreSQL “искаропки”, но статья не совсем про них, поэтому спрячу их под спойлер.
Несколько слов о pg_hba.conf
PostgreSQL, помимо обычных ролей/учёток внутри БД, отдельно контролирует доступ через Unix-сокет и сетевые порты, и эти настройки указаны в файле pg_hba.conf. При этом для полного, “ни-в-чём-себе-не-отказывай”-доступа достаточно [локально на сервере БД] переключиться в контекст локального пользователя postgres и подключиться через Unix-сокет - пароль не потребуется. Именно такой способ подключения и реализован в указанном плейбуке, и он работает “искаропки” сразу после старта сервера, до любой настройки доступа в pg_hba.conf.
И вот плейбук работает. Что было дальше? А дальше был пост в https://t.me/pro_ansible, и логгер в PostgreSQL для Ansible, но это уже совсем другая история.
Выводы
Во-первых, проработана и доведена до практической реализации поддержка PostgreSQL для Ansible на чистом Python - теперь на своих тренингах буду с чистой совестью показывать свои плейбуки для настройки объектов в этой замечательной БД.
Во-вторых, надеюсь, что эта разработка станет в некотором смысле примером для всех пишущих свои Ansible-модули: если уж пишете модули - не используйте зависимости; если же используете зависимости - используйте их “портативные” версии. Почему так - потому что внешняя по отношению к Python и Ansible библиотека экономит время разработки, но поддержка её доставки и развёртывания на целевые хосты превращается в персональный ад для тех, кто потом использует такой “полезный” модуль в своих проектах.
P.S. Вместо послесловия: тренинг, подсказанный жизнью
Из-за того, что меня в упоминавшемся чате Telegram по Ansible и в ЛС неоднократно спрашивали "Где можно записаться к тебе на обучение?", я всерьёз задумался, быть или не быть. Самым большим аргументом "за", конечно, был опыт: на работе, в банке, я как раз провожу тренинги по Ansible для коллег. Ну а организационные вопросы - по оплате картами, чекам и прочему всякому важному - были решены продажей тренинга через Интернет-магазин жены (писал о квесте "открой свой интернет-магазин" здесь) и созданием "с нуля" отдельного комплекта материалов.
В общем, я готов поделиться своим опытом не только в виде авторского тренинга по Ansible, но и в виде почасовых консультаций. Буду рад видеть всех желающих!