Введение

Иногда на своих внутрибанковских тренингах по 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, но и в виде почасовых консультаций. Буду рад видеть всех желающих!

Комментарии (27)


  1. maksasila
    16.12.2021 22:14
    +3

    Может объясните где здесь нищета Ансибля? Я вижу один блеск здесь.


    1. tnt4brain Автор
      16.12.2021 22:17
      +2

      Объясняю.
      Без установки питонячьей зависимости (а именно - psycopg2, на КДПВ есть) Ansible - инструмент, который изначально задумывался как легковесный и безагентный - оказывается непригодным для провизионирования PostgreSQL, потому что модули "искаропки" рассчитаны именно на эту зависимость.


      1. maksasila
        16.12.2021 23:08
        +4

        Всё равно не вижу нищеты. Всегда нужно для чего-то устанавливать зависимости. Блеск и нищета Убунту, чтобы работать с постгрес нужно установить постгрес.


        1. Hermit0H
          16.12.2021 23:56
          +8

          В том то и дело. Для того чтобы из ansible работать s postgresql требовалось поставить не postgres а библиотеку которая за собой тащила кучу других библиотек, которые к тому же не всегда были в скомпилированном виде под все операционные системы. В результате часть зависимостей устанавливалась через ОС, часть через pip. А где-то приходилось собирать из исходников. Квест так себе. И чаще задумываешься а не проще ли дёргать psql на прямую из консольки через command или shell, но возможность получать поддержку check_mode при операциях с БД заставляет проходить эти круги ада.


  1. tnt4brain Автор
    17.12.2021 00:00

    UPD Исправлена очепятка - благодарю,@emaxx!!!


  1. heroOfOurTime
    17.12.2021 00:01
    +2

    А что-нибудь вроде python-psycopg2 из системной репы не решает проблему?


    1. tnt4brain Автор
      17.12.2021 00:13

      В принципе установка этого пакета - да, решает проблему зависимости, но мы же говорим о минимальном "отпечатке" и хотя бы минимальной ИБ-гигиене? На мой взгляд, установка пакета на сервер БД, то есть туда, где этот пакет вообще-то не нужен для основных задач, выполняемых хостом - это в чистом виде облегчение атаки потенциальному злоумышленнику. Что-то в духе "слушай, мы тут тебе пакетик удобный питонячий залили - располагайся, чувствуй себя как дома".


      1. heroOfOurTime
        17.12.2021 00:27
        +3

        Если он за собой не тянет gcc или *-devel (не проверял), то, кажется, что злоумышленник с таким же успехом может с собой pg8000 принести.

        Я к тому, что я почему-то чуть больше доверяю системным пакетам, чем дополнительному коду внутри ансибл.


        1. tnt4brain Автор
          17.12.2021 00:45

          Лично я тоже системным пакетам больше доверяю - они хотя бы подписаны и всё такое, только вот "pip install psycopg2" им почему-то не доверяет.


          1. gpchelkin
            17.12.2021 01:08
            +2

            А "pip install psycopg2-binary" случаем не решает проблему, не пробовали? Там предсобранные бинари, не требующие ничего от системы https://www.psycopg.org/docs/install.html#quick-install

            За статью и репозиторий спасибо, познавательно.


            1. tnt4brain Автор
              17.12.2021 01:12

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

              Пожалуйста! Читайте, и кормите свою любознательность досыта: пусть приносит вам только пользу :-)


          1. heroOfOurTime
            17.12.2021 01:10

            То, что pip не доверяет системным пакетам, как раз и нормально.

            Если не хочется ставить пакеты с клиентами БД на сервер БД можно делегировать таск на локалхост или вспомогательный сервер.

            В общем, я бы сказал, что для Ansible надо стараться ставить системные пакеты, а pip использовать в виртуальных окружениях, но не для установки недостающих пакетов для Ansible.


            1. tnt4brain Автор
              17.12.2021 01:22

              Если делегировать на контроллер или ещё хзкуда, то сначала придётся настроить pg_hba.conf на целевом сервере, так что лучше, увы, не стало.

              Мой посыл очень прост - проекты на Ansible не только должны, но и могут быть самодостаточными, без всяких "yum install вон то", "apt-get install вот это", "pip install то - не знаю что".


              1. heroOfOurTime
                17.12.2021 08:18
                +2

                Ага-ага. То есть мы не ставим пакет из репы (со всеми плюшками вроде обновлений), чтобы аналогичный по функциональности пакет затаскивать каждый раз при выполнении ансибл в рантайме!

                Какая-то это мнимая ИБ-гигиена.


  1. amarao
    17.12.2021 00:19
    +4

    Спасибо, интересный разбор.

    Вы его как коллекцию не планируете выложить? В идеале, до community бы дотащить...

    В целом, боль при установке psychopg касается только центосей. На дебианах apt install python3-psycopg2 и всё пушисто.


    1. tnt4brain Автор
      17.12.2021 00:51

      Я не знаю, заинтересованы ли мейнтейнеры репы community в таких штуках. Если судить не по абстрактным рассуждениям, а по действиям, то всех устраивает текущее положение вещей. Соответственно, не вижу смысла кому-либо навязываться.

      Если мы говорим про пакеты - то на CentOS одинаково доступны и python-psycopg2 (см.выше), и python3-psycopg2. И ровно по тем же соображениям не вижу смысла тащить этот пакет на серверы БД: он там не нужен.


      1. amarao
        17.12.2021 00:59
        +1

        Ну, хотя бы в виде коллекции. С момента приведения коллекций в приличный вид (upgrade и т.д.) с ними стало можно жить и принципиальная разница "свой модуль" или "в составе ансибла" перестала быть.


        1. tnt4brain Автор
          17.12.2021 01:19
          +1

          Я в энтерпайзе живу - у нас Ansible имеет версию 2.9.27, а коллекции бесполезны, ибо всё происходит внутри тридевятого окружения за тридесятым файрволом :-))))

          Но мысль насчёт "выложить в коллекцию" всё равно интересная, и я её, конечно, обещаю обдумать.


  1. 13werwolf13
    17.12.2021 07:02
    +1

    снимаю шляпу, я бы руки рубил тем кто тянет в систему что-то в обход штатного пакетного менеджера..


  1. usego
    17.12.2021 07:54
    -1

    упаковать ансибл и все приблуды в докер, не?


    1. tnt4brain Автор
      17.12.2021 08:19
      +1

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


      1. usego
        17.12.2021 08:24
        -3

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


  1. ras22
    17.12.2021 10:42
    +1

    Спасибо! Буквально на прошлой неделе маялся с этим. Плюнул и решил все с помощью "pip3 install psycopg2". Теперь переделаю наверно плейбуки


    1. aim
      17.12.2021 15:40
      +1

      как раз чтобы этого НЕ делать и есть системные пакеты. ибо иметь сборочный цех на сервере — помогать потенциальному взломщику.


  1. Cykooz
    17.12.2021 11:07

    Если есть достпу через ssh, то что мешает пробросить тунель с ноды на котроллер и подключаться к постгрессу со стороны контроллера? Тогда достаточно будет только на контроллере один раз установить psycopg2 любым способом и не тащить его на ноды.

    PS: Я сам с Ansible не работал, поэтому не знаю на сколько моя идея "запускать код на контролере" вписывается в его концепцию.


    1. amarao
      17.12.2021 13:01
      +2

      Такая мысль постоянно витает при работе с почти чем угодно, но у неё есть определённые проблемы - как только мы начинаем городить альтернативный транспорт (а это - транспорт, просто поверх ssh). Одно из важных свойств ансибла - волшебная работа транспорта, о которой многие просто не задумываются. Как только в этом месте возникают нюансы и проблемы, модель плейбук начинает плыть (если оно ломается, оно ломается не в модуле и не в плейбуке, а где-то в другом месте).


    1. tnt4brain Автор
      17.12.2021 13:05

      В тридевятом энтерпрайзе, за тридесятым файрволом жили-были ИБшники. Невзлюбили они ssh-туннели.... Но это совсем другая история.