Привет!

Я Давид Бадалян, работаю в Исследовательском центре доверенного искусственного интеллекта ИСП РАН. В статье я хочу поговорить об Ansible – одной из самых популярных систем по автоматизации развёртывания. 

Стоит запустить Ansible программно, и он становится черным ящиком – нет никакого контроля над его выполнением, нет информации о тасках. Эту проблему мы обнаружили, разрабатывая оркестратор Michman для сервисов уровня PaaS. В результате мы создали cotea и gocotea: инструменты для программного исполнения Ansible-плейбуков из языков Python и Go.

Про cotea, её архитектуру и кейсы применения я расскажу подробно под катом. Если вы DevOps-инженер и хотите узнать, как можно гибко использовать Ansible – статья точно для вас.

Итак, в нашем оркестраторе Michman развёртывание сервисов происходит с помощью Ansible, что предполагает его программный запуск. В этом случае Ansible предоставляет либо интерфейс командной строки, либо – для систем на языке Python – запуск через ansible-runner. Но Michman реализован на языке Go. И даже если использовать ansible-runner, управлять выполнением Ansible не выйдет. Ansible-runner только показывает произошедшие события без возможности какого-либо программного контроля.

Между тем для DevOps-инженеров часто важен детальный контроль развёртывания, который предполагает реагирование на различные события по ходу выполнения в автоматическом режиме. Программное управление Ansible позволило бы определять текущее состояние выполнения и принимать гибкие решения в зависимости от ситуации. К примеру, хочется иметь доступ к такой информации:

  • результаты выполнения тасков (tasks – составных частей сценариев Ansible);

  • сообщения об ошибках;

  • значения переменных и фактов Ansible.

Для решения этой задачи мы и разработали cotea и gocotea – инструменты для программного контроля выполнения Ansible-плейбуков (речь исключительно об CLI ansible-playbook) на Python и Go соответственно. Gocotea, по сути, является портом cotea для языка Go. Само портирование мы сделали с помощью нашего собственного инструмента gopython: это библиотека, которая позволяет встраивать произвольный Python-код в программы на Go. Для этого gopython использует пакет cgo, который позволяет вызывать код на С из программы на Go. Это даёт возможность дёргать CPython API. Gopython может помочь в портировании на Go и других Python-библиотек. 

Инструменты cotea, gocotea, gopython и Michman разработаны командой облачных технологий ИСП РАН в рамках имеющихся проектов. О gopython и gocotea будет рассказано в наших следующих статьях. А сейчас я хочу поговорить об архитектуре инструмента cotea и примерах его использования. 

Архитектура cotea

Скажу точнее, что мы имеем в виду под задачей программного контроля. В случае с плейбуками Ansible речь идёт об итерировании по плеям (plays – по сути, это список тасков) и таскам.

Главная идея в Cotea (с латыни COntrol Thread Execution Ansible) – встроить в выполнение Ansible особые обработчики, которые вызываются до или после вызовов определённых функций (методов) Ansible. Обработчики могут приостанавливать работу Ansible и использовать ссылки на внутренние объекты среды выполнения. 

Обработчики встраиваются в исходный код Ansible с помощью так называемых классов-декораторов, которые в свою очередь основаны на питоновских декораторах. Механизм классов-декораторов мы придумали при разработке cotea. Его аналогов среди  общепринятых паттернов Python не нашли. Поэтому мы надеемся, что вам такой подход может пригодиться  сам по себе – он успешно решает ряд задач по мониторингу выполнения питоновских функций. Что же это за задачи?

Классы-декораторы

Классы-декораторы позволяют не только встраивать код до и/или после вызовов определённых функций, но и сохранять ссылки на объекты, полученные из аргументов функции или её возвращаемого значения. Минимальный код класса-декоратора представлен ниже:

class decore_class:
    def __init__(self, target_func):
        self.args_to_store = None
        self.result_to_store = None
        self.target_func = target_func

    def __call__(self, args):
        # сюда можно вставить код, который будет выполняться до вызова
        self.args_to_store = args

        result = self.target_func(args) # выполняется вызов 

        # а сюда – код, который выполнится после вызова
        self.result_to_store = result

        return result

Покажем на примере механику работы классов-декораторов. Предположим, существует некая функция some_func:

def some_func(arg):
    # some actions
    return result

Установим класс-декоратор на вызов этой функции:

decore_obj = decore_class(some_func)
some_func = decore_obj

Теперь при вызове в программе функции some_func вызовется обработчик из  __call__ с нужными нам действиями до и после вызова.  И главное – будут сохранены аргументы функции и её результат в полях args_to_store и result_to_store объекта decore_obj. Этот объект останется в памяти в отличие от объектов контекста завершившейся функции. 

Теперь вернёмся к Ansible. Пусть в исходном коде у нас есть функция RunTask(Task), которая возвращает объект, описывающий результат выполнения таска. Если мы установим класс-декоратор на подобную функцию, мы будем знать, какой таск только что выполнился и с каким результатом завершился. Ровно эта конструкция и лежит в основе cotea. (Эх, был бы в исходном коде Ansible и правда метод RunTask… но тогда разработка была бы не такой интересной)

Перехват управления

Получается, чтобы решить нашу задачу итерирования по плеям и таскам, мы можем расставить классы-декораторы на определённые методы исходного кода Ansible. Чтобы приостановить выполнение после выполнения очередного таска, мы:

  • запускаем Ansible в отдельном потоке;

  • добавляем в классы-декораторы примитивы синхронизации, приостанавливающие поток Ansible и возвращающие управление.

Интерфейсы cotea

Интерфейсы для выполнения Ansible и итерирования по плеям и таскам мы объединили в классе runner. Также в этом классе есть интерфейсы для совершения и других действий в runtime-e, помогающих разделять и властвовать лучше контролировать выполнение и получать информацию о нём. Для начала посмотрим на самые основные интерфейсы:

  • has_next_play() – возвращает истину, если ещё остался невыполненный плей, и продвигает выполнение Ansible до момента начала запуска тасков этого плея;

  • has_next_task() – возвращает истину, если в текущем плее остался невыполненный таск и продвигает выполнение до момента перед его запуском;

  • run_next_task() – продвигает выполнение Ansible до точки, в которой тот самый next task (о наличии которого сигнализировал has_next_task) уже выполнен, и возвращает массив с результатами его (таска) работы на каждом хосте из текущего inventory (списка хостов, на которых выполняются таски);

  • finish_ansible() – завершает выполнение Ansible.

Базовая схема запуска Ansible через cotea выглядит так:

r = runner(...)

while r.has_next_play():
    while r.has_next_task():
        task_results = r.run_next_task()

r.finish_ansible()

Что ещё есть в классе runner:

  • get_next_task()/get_next_task_name() – возвращает Ansible-объект/имя следующего таска;

  • get_prev_task()/get_prev_task_name() – возвращает Ansible-объект/имя предыдущего таска;

  • get_cur_play_name() – возвращает имя текущего плея;

  • get_error_msg() – возвращает сообщение об ошибке таска, неудача которого стала фатальной стала причиной преждевременного завершения плейбука (т.е. на таске не было метки ignore_errors);

  • get_variable(name) – возвращает значение Ansible-переменной или факта с указанным именем;

  • add_var_as_extra_var(new_var_name, value) – создаёт Ansible-переменную с заданным именем и значением в качестве дополнительной (extra-var) переменной (динамически добавленная переменная будет иметь приоритеты обычных extra-vars);

  • add_next_task(new_task: str) – создаёт таск с переданным текстовым описанием и добавляет его следующим в очередь на выполнение. 

grpc-cotea

В дополнение к классу runner мы реализовали и grpc-сервер по выполнению Ansible, который использует описанные выше интерфейсы cotea. Такой сервер позволяет вынести исполнение Ansible в отдельную сущность. Нужда в этом возникла, когда мы переводили оркестратор Michman на стандарт TOSCA с микросервисной архитектурой в основе. Эта версия Michman на данный момент не является open source. 

Пока мы не выложили grpc-cotea в открытый доступ, но планируем это сделать.

Программный контроль выполнения

Теперь я расскажу, как мы применяли cotea для одной внутренней задачи. Вообще при разработке плейбуков часто нужно как-то обрабатывать результаты выполнения тасков Ansible. Например, аргументами очередного таска могут быть результаты одного из предыдущих. Довольно часто приходится извлекать необходимые аргументы из результата предыдущего таска вручную (например, с помощью register). 

При этом возникает куча проблем, связанная с обработкой строк и приведением типов – это всё нужно делать непосредственно в среде выполнения Ansible (рантайме). Можно применить модули Ansible, но часто бывает, что необходимого модуля нет – или никто не сделал, или нет модуля для нужной версии Ansible. А через сotea для такой промежуточной обработки можно применить любые Python-библиотеки, потому что мы даем программный доступ к результатам выполнения.

Наша задача заключалась в развёртывании хранилища секретов Vault. Ansible у нас был версии 2.9.4 – это одна из самых популярных. Но модулей для Podman и Vault тогда в ней не было. Какие проблемы нам нужно было решить:

  • Обработать вывод команды “podman ps …” для проверки, не запущен ли уже контейнер Vault;

  • Извлечь ключи Vault из вывода команды “vault operator init ...’”;

  • Обработать вывод команды “vault secrets list …” – узнать, не существует ли уже заданный секрет Vault.

Ниже я привожу код для решения этих задач с помощью cotea. Метод run_next_task возвращает список объектов TaskResult, соответствующих результатам выполнения запущенных тасков на каждом из хостов текущего inventory. TaskResult содержит такие поля, как stdout, stderr, msg. После выполнения таска, который запускал какую-нибудь команду – “podman ps” или одну из двух других, – в поле stdout будет информация, которую нужно обработать. Чтобы понимать в runtime-е, какой именно таск выполнялся, можно вызвать метод get_prev_task_name

# инициализация объекта cotea.runner 
#podman_ps_task_name = “...”
#vault_init_task_name = “...”
#vault_secrets_task_name = “...”
# r = runner(...)

while r.has_next_play():
    while r.has_next_task():
        task_results = r.run_next_task()
        prev_task_name = r.get_prev_task_name()

        if prev_task_name == podman_ps_task_name:
            task_result = task_results[0]
            cmd_stdout = task_result.result["stdout"]

            # загружаем состояние контейнера
            cmd_stdout_json = json.loads(cmd_stdout)
            state_field = cmd_stdout_json[0]["State"]

            r.add_var_as_extra_var("VAULT_CONTAINER_STATE", state_field)  

        elif prev_task_name == vault_init_task_name:
            task_result = task_results[0]
            cmd_stdout = task_result.result["stdout"]

            # извлекаем ключи
            unseal_key = get_unseal_key(cmd_stdout)
            root_token = get_root_token(cmd_stdout)

            r.add_var_as_extra_var("VAULT_UNSEAL_KEY", unseal_key)
            r.add_var_as_extra_var("VAULT_ROOT_TOKEN", root_token)      

        elif prev_task_name == vault_secrets_task_name:
            task_result = task_results[0]
            cmd_stdout = task_result.result["stdout"]

            # извлекаем секреты
            created_secrets = get_secrets(cmd_stdout)
            michman_created = False

            if "michman/" in created_secrets:
                michman_created = True          

            r.add_var_as_extra_var("MICHMAN_SECRETS_CREATED", michman_created)

r.finish_ansible()

if r.was_error():
    print("ansible-playbook launch - ERROR:")
    print(r.get_error_msg())

else:
    print("ansible-playbook launch - OK")

В коде выше, обработка результата podman ps в листинге видна целиком – это всего две строчки. Для остальных команд обработку мы вынесли в отдельные функции. Результаты обработки добавляем Ansible runtime через дополнительные переменные окружения через метод add_var_as_extra_var из cotea.runner. Как вы помните, такие переменные будут доступны в следующих тасках.

Интерактивный режим cotea

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

А тем временем на основе cotea мы уже реализовали возможность перехода в интерактивный режим. Для этого нужно вызвать метод interactive_discotech (вначале название было шуточным, но всем понравилось и я оставил) cotea.runner-а. Вызвать его можно, по идее, где душе угодно - действия пользователя будут менять некоторые внутренние объекты Ansible, влиять на порядок выполнения и т. д. Но наиболее целесообразно и рекомендуется вызывать его при неудаче таска. Такой способ вызова и показан в нашей документации по интерактивному режиму. В том числе, интерактивный режим cotea позволяет добавить новую Ansible-переменную или новый таск – и всё это в runtime-е. Такой инструментарий крайне полезен при работе с большими сценариями. Мы сами в шоке используем интерактивный режим cotea при развертывании OpenStack и весьма довольны жизнью

Вот какой функционал мы реализовали (иду прямо по командам на рисунке):

  • вывод полной информации об упавшем таске, включая все обычные параметры: аргументы, окружение, переменные, теги (команда ft);

  • вывод всех сообщений об ошибках выполнения тасков (включая проигнорированные ошибки) в порядке возникновения (команда msg);

  • повторный запуск упавшего таска (команда re);

  • продолжение выполнения сценария: упавший таск игнорируем и выполнение продолжаем со следующего таска (команда c);

  • добавление новой Ansible-переменной как дополнительной (extra-var, команда v);

  • динамическое добавление нового таска – ввод с консоли в виде строки (команда nt);

  • просмотр значения переменной (команда w).

Для примера давайте рассмотрим такую задачу. Предположим, что имеется плейбук с большим количеством тасков, и ближе к концу в нём есть следующих два таска:

 - name: Create conf file
   file:
     path: "{{ target_conf_file_path }}"
     state: touch
     mode: '0600'

 - name: Run python script
   shell: "python {{ working_dir }}/main.py"
   register: command_output

Теперь давайте предположим, что переменной target_conf_file_path почему-то нет, а на целевых хостах не сделали ссылку /usr/bin/python на интерпретатор Python. Это тривиальные проблемы с тривиальными решениями. Но всё равно придётся перезапускать плейбук, а когда для его выполнения нужно минут 30, а то и больше, это очень неудобно. Именно с подобного рода ситуациями мы нередко сталкиваемся, работая с большими Ansible-сценариями.

На скриншоте я показал, как этот инцидент разрешается с помощью интерактивного режима cotea – весь сценарий перезапускать не придётся. 

  • Сначала падает таск “Create conf file”, так как переменная target_conf_file_path действительно оказалась не определена. 

Решение: с помощью команды “v” добавляем переменную target_conf_file_path с соответствующим значением, а потом командой “re” перезапускаем таск “create conf file”. Как видите, она завершается успешно.

  • Потом падает таск “Run python script”, так как не найден интерпретатор python. 

Решение: с помощью команды “nt” добавляем новый таск. Этот таск создаст правильную символическую ссылку /usr/bin/python на всех целевых хостах. Task вводится пользователем в виде строки. Пока при вводе нужно соблюсти все отступы и табуляции (как видно на рисунке выше) так же, как это делается в плейбуке. Затем используем команду “с”, которая в данном случае (как и написано в подсказке пользователю) выполнит сначала новый таск, а затем упавший ранее. Как видим, таск “Create a symbolic link” добавлен и успешно выполнен, а потом успехом завершился и таск “Run python script”.

Сравнение со встроенным отладчиком Ansible

Когда мы начали разработку cotea, встроенный отладчик Ansible уже существовал, но обладал меньшим функционалом, чем сейчас. Но до сих пор интерактивный режим cotea в нескольких моментах опережает отладчик Ansible. Вот что мы можем:

  • Добавлять переменную, которая будет доступна всем последующим таскам;

  • Если какой-то таск упал, то можно добавить полностью новый таск (через задание строкового описания, как в плейбуке). Инструмент сначала выполнит добавленный таск который все починит, расцветут цветы и запоют птицы, а затем еще раз попробует запустить таск упавший;

  • Реализовать через интерфейсы полноценный отладчик Ansible в виде плагинов для IDE. Компоненты встроенного отладчика вшиты в исходный код Ansible, и использовать их извне не получится.

Заключение

Инструмент cotea позволяет программно контролировать выполнение Ansible через итерирование по плеям и таскам, получать информацию о ходе выполнения, а также добавлять новые Ansible-сущности в runtime-е (скажем, таски и переменные).

Мы разработали сotea при выполнении различных проектов ИСП РАН. Сейчас мы применяем этот инструмент в составе оркестратора Michman и при развёртывании OpenStack. Хотите узнать больше? Ждем на странице облачных технологий. Читайте наши следующие статьи! Пока.

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


  1. Tamerlan666
    08.11.2023 22:23

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

    Можно применить модули Ansible, но часто бывает, что необходимого модуля нет – или никто не сделал, или нет модуля для нужной версии Ansible. А через сotea для такой промежуточной обработки можно применить любые Python-библиотеки, потому что мы даем программный доступ к результатам выполнения.

    Если необходимого вам модуля нет или функционала имеющихся не хватает - не проще ли написать просто дополнительный модуль/плагин для ансибла, используя его нативные механизмы интеграции, а не обмазываться гораздо большим объемом внешнего кода, который никак не интегрирован в экосистему ансибла?


    1. davidbadd Автор
      08.11.2023 22:23
      +1

      Спасибо за отзыв. Cotea расширяет возможности Ansible, добавляя новые функции, а не просто копирует то, что уже есть. Инструмент позволяет управлять исполнением Ansible прямо из программы на Python, таким образом, можно проверять и регулировать процесс после каждого таска, используя все возможности Python. В сравнении с этим, команда ansible-playbook - 'черный ящик', без возможности вмешательства в процесс.

      Также благодаря cotea и gocotea, существенно проще встраивать Ansible в программы на Python и Golang, потому что не нужно запускать внешние команды через exec, что часто может быть неудобно и неэффективно.

      не проще ли написать просто дополнительный модуль/плагин для ансибла

      Не всегда у разработчиков есть время на это. Иногда им просто нужно быстро обработать результат (например, просто stdout) от предыдущего таска и двигаться дальше. Cotea дает возможность легко обрабатывать эти данные с помощью Python, а не ограничиваться возможностями Ansible.


  1. Tamerlan666
    08.11.2023 22:23

    Инструмент позволяет управлять исполнением Ansible прямо из программы на Python, таким образом, можно проверять и регулировать процесс после каждого таска, используя все возможности Python.

    Ансиблом и так можно управлять из программы на Python с помощью нативного API. Я все еще не понимаю - в чем смысл своего велосипеда в данном контексте.

    Не всегда у разработчиков есть время на это. Иногда им просто нужно быстро обработать результат (например, просто stdout) от предыдущего таска и двигаться дальше. Cotea дает возможность легко обрабатывать эти данные с помощью Python, а не ограничиваться возможностями Ansible.

    Не очень понятно, на чем именно экономится время и в каком месте? Если вам нужно реализовать какую-то специфичную логику на питоне, вам в любом случае придется ее реализовать и написать для этого весь код. Просто тут вы еще зачем-то занимаетесь реверс-инжинирингом и залезаете внутрь процессов ансибла своим кодом, вместо того, чтобы пользоваться готовыми возможностями фреймворка, расширяя функционал именно там, где вам чего-то не хватает. Какой конкретный юзкейс помогает решить ваш велосипед, что не покрывается простым написанием своего плагина к ансиблу?


    1. davidbadd Автор
      08.11.2023 22:23

      Ансиблом и так можно управлять из программы на Python с помощью нативного API.

      Вынужден Вас расстроить, но без cotea этого делать нельзя)
      Нет Python API, позволяющего запускать конкретный таск либо плей, добавлять новый таск или Ansible-переменную по ходу выполнения и тд (всё то, что предоставляет cotea).
      Если Вы про страничку Python API документации Ansible, то там плейбук запускается разом, как и при запуске Ansible из командной строки. То есть, возможности контроля между тасками не предоставляется. Также там сказано, что это API для внутреннего использования и external use is not supported by Ansible. И далее приводится ссылка на ansible-runner, который упоминался в статье (он также не предоставляет возможности программного итерирования по таскам).

      Не очень понятно, на чем именно экономится время и в каком месте?

      Разработка Ansible модуля - это куда более общая задача, требущая бОльшего времени разработки, чем, к примеру, парсинг строки в конкретной ситуации. При написании playbook-ов, разработчики обычно выкручиваются, используя существующий функционал Ansible, т.к. это всё равно быстрее написания и отладки собственного модуля. Cotea же предлагает выкручиваться, используя всё богатсво Python;)


  1. Tamerlan666
    08.11.2023 22:23

    Если Вы про страничку Python API документации Ansible, то там плейбук запускается разом, как и при запуске Ansible из командной строки. То есть, возможности контроля между тасками не предоставляется.

    Там просто пример с иллюстрацией. Все возможности там есть, если смотреть на код.

    Также там сказано, что это API для внутреннего использования и external use is not supported by Ansible.

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

    Разработка Ansible модуля - это куда более общая задача, требущая бОльшего времени разработки, чем, к примеру, парсинг строки в конкретной ситуации.

    При чем тут парсинг строки? Речь о написании обвязки типа той же cotea. Ну, и если говорить про парсинг строки, то я что-то сильно сомневаюсь, что разработка cotea потребовала меньше времени, чем плагин из двух строчек на питоне.

    При написании playbook-ов, разработчики обычно выкручиваются, используя существующий функционал Ansible, т.к. это всё равно быстрее написания и отладки собственного модуля. Cotea же предлагает выкручиваться, используя всё богатсво Python;)

    В чем преимущество велосипеда в виде cotea по сравнению с написанием собственного модуля? В cotea какой-то свой особый питон? Я все еще не понимаю, где и что именно вы выигрываете. Можно более конкретный пример увидеть?