Привет! Меня зовут Павел, я DevOps-инженер в YADRO. Где бы я ни работал, я был девопсом и использовал Ansible — где-то активнее, где-то меньше. В YADRO с этим инструментом работают почти все департаменты — от enterprise, где разрабатывают серверы, СХД и другое оборудование, до телекома.
В какой-то момент мы с коллегами поняли, что DevOps-инженеры в разных отделах иногда делают похожие автоматизации с нуля, хотя могли бы переиспользовать уже существующие Ansible-компоненты. Решили исправить это и организовать понятное и удобное общее пространство с компонентами, которые уже были написаны под конкретные задачи.
В статье я расскажу, с какими проблемами мы столкнулись (признаюсь, их было немало), какие варианты решения рассматривали и к чему в итоге пришли.
С какой проблемой мы столкнулись
Расскажу чуть подробнее о проблеме. Сейчас в YADRO работает порядка 5000 человек. Поскольку мы мультипродуктовая компания, сотрудники делятся на разные департаменты: одни занимаются системами хранения данных, другие — телеком-оборудованием, третьи — технологиями машинного обучения в применении к пользовательскому оборудованию. Вне зависимости от направления командам разработки нужна инфраструктура: виртуальные машины для тестовых окружений, серверы для развертывания дополнительных сервисов и так далее.
Внутри каждого большого департамента есть команда DevOps-инженеров, которая поддерживает инфраструктуру, обслуживающую задачи разработчиков и инженеров. И почти все департаменты работают с Ansible.
Каждая команда DevOps-инженеров подключает Ansible в привычные пайплайны, настраивает inventory, перенося туда инфраструктуру, начинает писать плейбуки и роли. Мы столкнулись с проблемой повторения, «неэкономного» распоряжения ресурсами. Одна команда DevOps-специалистов уже могла написать автоматизацию, потребность в которой появилась у команды в другом департаменте. Но последние просто об этом не знают и тратят время на задачу, которую в компании, по сути, уже решили.
Как решить проблему
Решение, в целом, очевидное: нужно найти способ делиться наработками разных DevOps-команд и создать общее хранилище полезного Ansible-контента. Определив задачу, я довольно быстро собрал рабочую группу, и мы начали думать, как ее выполнить.
Набросаем общий план действий. Помимо выбора места хранения (это самое простое), нужно ответить на целый ряд вопросов:
Какими компонентами удобно делиться в компании.
Как мы будем доставлять контент конечному пользователю.
Как поддерживать изменения Ansible-компонентов, которые появляются в ходе их переиспользования.
Как тестировать контент, который поставляется в общее хранилище.
Как обеспечить удобный поиск нужных компонентов.
Давайте отвечать на них по порядку.
Чем мы можем делиться в Ansible
В контексте любого инструмента, языка программирования, технологии мы можем делиться двумя вещами: кодовыми наработками и экспертизой. С экспертизой проще: можно организовать чат в корпоративном мессенджере, сформировать сообщество в компании, обмениваться опытом в рамках «домашних» митапов и так далее. С наработками сложнее. С одной стороны, можно организовать для них общее хранилище. Но важно сделать так, чтобы пользователи могли легко находить в нем искомые блоки кода, а также сразу понимали, какие автоматизации уже есть, подойдут ли они под их задачи, как их переиспользовать и улучшать.
Перед тем как создать полезное хранилище, нужно было подумать, а что вообще мы пишем для Ansible и чем можем делиться.
Выделим четыре больших сущности:
файлы инвентаризации (inventory),
плейбуки,
роли
коллекции.
Inventory рассматривать как компонент для пересылки коллегам мы даже не стали. Во-первых, это нефункциональная единица, а во-вторых, их может быть много — окружения у всех свои, высока вероятность, что что-то пойдет не так. А вот к трем остальным присмотримся внимательнее.
Плейбуки. По сути, это YAML-файлы, которые отвечают на два главных вопроса: какие системы мы конфигурируем и к какому состоянию их приводим. Второй вопрос объясняет, почему делиться плейбуками неэффективно.
Допустим, в телеком-департаменте инженеры написали очень хороший плейбук и поделились с нами. Открываем плейбук и понимают, что там используется инвентарь другого отдела, менять его нужно вручную. Смотрим дальше: плейбук вызывает локальную роль, которая хранится у телеком-департамента. Запрашиваем ее, загружаем, добавляем в директорию с используемыми ролями — вроде как все начинает заводиться, но тут понимаем, что не хватает коллекции. Устанавливаем коллекцию, запускаем все заново — успех. В итоге для переиспользования плейбука нам понадобилось минимум три ручных действия — неудобно.
Роли. В Ansible это набор задач или обработчик переменных, файлов и других артефактов, которые распространяются и подключаются как единое целое к плейбуку. Такие блоки уже можно рассматривать для шеринга коллегам. Но подходящая под нашу задачу роль должна не зависеть от состояния хостов, inventory и окружения. Словом, роль должна быть некой атомарной единицей, которую мы можем изъять из одного плейбука и использовать в другом. И для этого нужно соблюдать немало условий — лучших практик написания ролей.
Коллекции. Относительно новая сущность в мире Ansible. По сути, это формат распространения связанного между собой набора ролей, модулей, плагинов. Например, может быть Ansible Collection для настройки мониторинга, для работы с PostgreSQL и другие. Модули могут использоваться как во внутренних ролях коллекции, так и во внешних плейбуках.
Основная проблема коллекций в том, что они довольно-таки громоздкие. В общей сложности число файлов в коллекции может достигать тысячи, а нам из нее, например, нужен какой-то определенный модуль или одна-две роли. Не хочется загружать их все каждый раз, устанавливая коллекцию.
Чтобы понять, чем же нам делиться, мы решили посмотреть на уже существующие в компании репозитории и написанные автоматизации. Сделали несколько выводов:
В компании не очень любят писать Ansible-модули. Либо инженеры их пишут, но для конкретной роли. Модули хранятся вместе с ней и там же используются. Их можно вынести в отдельную коллекцию, но коллекция ради одного модуля не имеет смысла.
Большое количество автоматизаций написано с использованием ролей. Либо это плейбуки, которые можно легко преобразовать в плейбук и роль, и последнюю отправить коллегам.
Решили, что наше общее хранилище будет чем-то вроде общего сервера с Git-репозиториями. Мы используем Bitbucket, поэтому в нашем случае это Bitbucket-проект с репозиториями внутри.
Как доставлять контент
Теперь нужно понять, как доставлять контент из наших репозиториев конечному пользователю. Нам поможет утилита Ansible Galaxy. Она позволяет установить внешние зависимости, такие как коллекции и роли, из удаленных точек.
Но давайте рассмотрим наши возможности по доставке контента по порядку.
Вариант 1 и 2. Установка из Git-репозитория
Есть два варианта:
можем указать в файле с зависимостями ссылку на локальный Git-репозиторий и полный путь до него на локальной машине.
можем указать ссылку на удаленный репозиторий.
В любом случае роль скачается, поместится в Roles Path и станет доступной для использования в плейбуках или в других ролях.
# Установка из локального репозитория
- src: git+file:///<path>
# Установка через git + https
- src: https://<repo_url>
# Установка через git + ssh
- src: git@<repo_url>
name: <role_name> # Зачем тут это?
version: <git_ref>
Минус: При установке из удаленного репозитория есть возможность заполнить поле version. Роли не имеют версионирования как такового, но в примере версия указывает на git_ref, состояние которого мы хотим получить локально.
Очень хочется поставить в этом поле master и ни о чем не думать, но можно наступить на грабли, характерные для работы с Docker-образами. Когда роли хранятся отдельно от плейбуков, которые их используют, конечный потребитель роли — не всегда ее разработчик. Поэтому изменения в плейбуках и ролях могут происходить асинхронно. Возрастает риск нарваться на мажорные изменения, которые поломают автоматизацию.
Вариант 3. Установка из tar-архива
Tar-архив — валидный способ распространения Ansible-ролей. Он подойдет, если у вас настроена, например, автоматическая сборка артефактов.
Вы собираете артефакт, загружаете его в хранилище артефактов — Nexus Dashboard или иное удобное место. А пользователи с помощью Ansible Galaxy, указав полную ссылку до файла, могут его скачать. Поле version
уже ни на что не влияет, потому что мы устанавливаем конкретный файл. Ниже — requirements.yml, который понимает ansible-galaxy.
- name: <role_name>
src: https://<url_to_artfact>.tar.gz
version: <role_version>
Минус: При использовании такой системы хранения возникает ненулевая вероятность, что кто-то случайно (а может, специально) перезапишет файл, которым уже пользуются другие. В итоге значительная часть плейбуков может просто сломаться. Здесь, как и в предыдущих случаях, нужно указывать прямую ссылку до файла, чтобы скачать его.
Вариант 4. Установка роли из Ansible Galaxy
Вариант для тех, кому вообще нечего скрывать и кто готов делиться наработками со всем миром. Это отдельный сервис, у которого довольно понятный интерфейс и который позволяет скачать Ansible-контент откуда угодно.
В отличие от предыдущих вариантов установка файла происходит не по прямой ссылке, а по имени.
Вернемся к предыдущим вариантам и посмотрим на код. Почему здесь имя важно, а в предыдущих вариантах нет? Для установки файла откуда бы то ни было, кроме Ansible Galaxy, указывать поле name нелогично. Во-первых, мы используем полную ссылку до файла, можно обойтись без имени. Во-вторых, у роли и так есть установленное имя. Кроме того, первые три варианта касались внутренних взаимодействий: с каким именем вы установили роль, по такому сможете использовать ее локально в своих плейбуках.
В варианте с установкой роли из Ansible Galaxy, если мы укажем имя по умолчанию, утилита пойдет на глобальный сервис, посмотрит, есть ли такая роль, и выберет версию:
- name: <role_name>
version: <version>
В этом случае версии уже имеют значение, поскольку роли хранятся в общем пространстве, в открытых репозиториях на GitHub, а не локально. Из-за тесной связки сервиса с GitHub он подтягивает все версии из Git-дерева и тегов.
К какому варианту мы в итоге пришли
После аудита мы поняли, что чаще всего пишем роли и храним их в отдельных репозиториях внутри проекта. Для скачивания Ansible-контента мы решили использовать Ansible Galaxy с установкой из Git-репозитория напрямую. Мы можем указать ветку либо тег, который хотим использовать, и привести нашу инфраструктуру в необходимое состояние.
Как поддерживать изменения в репозиториях внутри общего проекта
Ситуация: у нас в компании разработали новую систему мониторинга. Нужно было подключить к нему серверы и виртуальные машины и установить на них агента, работающего с метриками. Мне как инженеру по инфраструктуре в департаменте пришла задача: нужно подключить наши машины к новому мониторингу. Не проблема, машин много — буду использовать Ansible.
Написал первый плейбук. Решил оформить наработки в роль и запустил ее. Все работает: серверы появились в мониторинге, метрики показываются и сохраняются — все счастливы, задача закрыта.
В результате у меня есть роль, которая запущена в приватный репозиторий команды, есть документация, есть закрытая задача — в общем, есть все, чтобы забыть про этот компонент. Но я понимаю, что буду не единственным инженером, которому потребуется привязать систему мониторинга к инфраструктуре, и выгружаю код в публичный проект. Сообщаю своим коллегам: если перед вами встанет подобная задача, пользуйтесь моими наработками.
Спустя несколько дней ко мне действительно пришел человек и сказал, что он нашел в репозитории мою роль, но она ему не до конца подходит. Ему нужно добавить поддержку OpenSUSE — хочет подправить. В итоге коллега создал отдельную ветку в публичном репозитории, добавил в роль поддержку различных Linux-дистрибутивов и опубликовал в мастер.
Через некоторое время пришел еще коллега, сказал, что конфигурационный файл, который мы используем, ни к чему не подходит. Создал свою ветку, доделал шаблон, опубликовал. В итоге моя роль из наработок плейбука для приватного репозитория команды стала инструментом, который доступен всей компании и подходит под большее число сценариев, чем это было изначально.
Как проверять общий контент: линтеры и тестирование
С точки зрения проверки написания все просто: для Ansible есть линтеры. Два самых популярных — YAMLLint и ansible- lint. Есть еще ansible-later, но он сложнее в подключении и использовании. Мы сразу подключили два первых линтера, чтобы править файлы в валидные YAML, которые распознаются в Ansible. Также мы выработали определенный стайлгайд для нюансов, которые линтерами не покрываются, их мы проверяем на этапе pull-request.
А что с тестированием ролей? Мы используем один общепризнанный инструмент — Molecule.
Напомню, что Molecule оперирует понятием «платформа». По сути, это что-то с Python-интерпретатором и доступом до консоли, чтобы Ansible смог зайти и запустить там свои действия. В ответственности Molecule — создание платформ, предоставление доступов и их удаление после тестирования. За всю операцию отвечают драйверы, которые создают платформы определенного типа. «Из коробки» доступны драйверы, например, для Docker и Podman — для тестирования Ansible-ролей внутри контейнеров. Также есть драйверы для AWS, Azure и других популярных решений. Если этих драйверов не хватает, можно пойти на GitHub, найти, скачать и использовать в сценариях самописный драйвер сторонних разработчиков.
Также есть ультимативное решение — драйвер Delegated. Он подходит в тех случаях, когда мы понимаем, что нас не устраивает ни один существующий драйвер и проще написать самим. Есть проблема, что для драйвера Delegated нужно написать два плейбука: на создание площадки и на ее удаление. И если мы выбираем делегированный драйвер как основной, мы должны будем копировать эти плейбуки из сценария в сценарий и из роли в роль. А теперь представим, что нам нужно скорректировать что-то в описании — например, способ создания площадки. Предстоит много ручной работы.
Если возвращаться к нашему кейсу, мы быстро поняли, что тестирование внутри контейнеров нас не устраивает. Стали изучать, что используется в компании. У инженеров одного из департаментов нашли огромный реестр qcow2-образов и подумали, что тестирование на виртуальных машинах может нам подойти.
Так мы определились с тем, где мы хотим тестировать, теперь определимся — как. Нашли драйвер для работы с Libvirt и начали его использовать: роли начали запускаться, но были претензии к скорости работы и порядку создания платформы (нам нужны были дополнительные действия в процессе создания). Поняли, что Delegated не избежать, но мы можем подумать, как не столкнуться с ситуацией, когда нам придется вносить изменения вручную — во всех плейбуках и ролях.
Что мы нашли?
Чтобы не заниматься копипастой файлов, можно создать репозиторий, от которого можно копировать (форкать) все роли. Плюс — все файлы уже будут на месте, минус — изменения туда не подтягиваются.
Можно использовать стороннюю библиотеку — например, Cookiecutter. На основе Jinja2-шаблонов она вам сгенерирует не только файлы с определенным контентом, но и целые структуры файлов, включая имена директорий и файлов. Все это будет браться из Jinja-переменных. Эту библиотеку используют все официальные драйверы молекулы.
Можно написать собственный драйвер — эта опция стала доступна с версии Molecule 5.0.1. До этого, чтобы драйвер был воспринят инструментом как рабочий, его нужно было занести в JSON-схему внутри репозитория Molecule. Но теперь, если имя вашего драйвера соответствует одному из четырех шаблонов (molecule-, molecule_, custom-* или custom_*), то он автоматически становится доступным для использования.
В итоге мы написали собственный драйвер Molecule, где учли все наши потребности, и начали его использовать. У нас появилась поддержка изменений, мы начали его использовать почти во всех ролях, а количество файлов в сценарии Molecule уменьшилось с шести до трех.
Процесс разработки драйвера я подробно описывал в своем тексте на Хабре.
Подключаем реестр образов
Теперь немного о пайплайне. Нам нужно было решить две большие проблемы.
Первая проблема: откуда брать список платформ
Нужно было понять, откуда брать список платформ, в которых роль, по словам разработчика, должна работать. У нас было два пути.
Первый завязан на доверии: разработчик просто указывает все платформы для тестирования внутри Molecule, а мы ему верим в надежде, что роль будет действительно там запускаться. Нам он не понравился.
Второй вариант, который мы рассматривали и который в итоге выбрали, это работа с метафайлом. В meta можно указать набор из имени и версии дистрибутива, где эта роль должна работать. Используя реестр qcow2-образов, про который я упоминал ранее, мы организовали такую механику: при коммите в репозиторий с ролью информация из meta парсится, и для каждого набора вида «дистрибутив-версия» определяется набор платформ, где мы должны протестировать роль.
Вторая проблема: нужно экономить ресурс виртуальных машин
Molecule — очень прожорливый инструмент. Когда мы создаем сценарий и запускаем тестирование через Molecule, у нас создаются все платформы, которые указаны в сценарии. Указали две платформы — создадутся две платформы, указали 20 — время молиться, чтобы ничего не упало. Напомню, что мы выбрали тестирование внутри виртуальных машин и не можем точно сказать, сколько ресурсов нам потребуется для тестирования.
Поэтому мы начали собирать пайплайны на уровне Jenkins. Поняли, что у Molecule нет инструментов, которые решат проблему, и пошли выше. В первом подходе к задаче мы делили список платформ, где хотим протестировать роли, на партии (батчи) по n штук. Тестирование запускается последовательно на всех партиях, и в принципе мы точно знаем, сколько ресурсов нам нужно — нам нужно мощностей под n виртуалок.
Сработало, но не без проблем. Если хотя бы одна виртуальная машина в партии помечалась как ошибочная (то есть роль на ней не отработала), то ошибочным помечался весь батч. И вот нам нужно лезть в многострочные логи всей партии и искать, что в итоге упало. Тратить на это время не хотелось.
В итоге мы пришли к использованию worker-пулов, очередей — все называют это по-разному, но суть одна: у нас есть слоты под создание виртуальных машин, и мы наполняем их по мере освобождения ресурсов. Таким образом, для каждой платформы у нас создается отдельный сценарий в Molecule, и тесты прогоняются ровно на одной виртуалке. Если тест завершился падением, мы точно знаем где. И вместо анализа простыни логов исследуем логи конкретной платформы, быстрее находим баг и исправляем его.
Как искать контент в общем репозитории
Со временем к проекту начали присоединяться все больше коллег. За полгода количество ролей в нашем проекте выросло с 10 до 60. Это проверенные, протестированные роли, которые работают не то что на нескольких дистрибутивах, но даже на нескольких архитектурах.
С ростом числа участников мы столкнулись с еще одной проблемой — в общем проекте стало очень сложно искать нужный контент. Ниже — главная страница нашего проекта в Bitbucket: лаконичный список из двух колонок — имени и краткого описания.
По этим данным пользователям довольно сложно найти что-то подходящее именно им. Начали думать, как предоставить больше информации об Ansible-контенте, которым делимся.
Снова пошли исследовать и определили для себя два возможных варианта: создание отдельного сервиса для хранения Ansible-ролей либо создание автодокументации. И то, и другое дает пользователям единую страницу, где они могут посмотреть описания интересующих ролей или профильтровать их по ключевым словам.
Первое направление — сервисы для хранения Ansible-контента. Мы рассмотрели два решения: Galaxy NG — новый вариант привычного Ansible Galaxy — и Galactory (наследник решения Amanda), который представляет собой простой прокси для хранения Ansible-контента на Artifactory. Оба нам, однако, не очень подошли. Краткое сравнение — в таблице.
Основным недостатком стало то, что они работают только с коллекциями. Мы нашли тикеты, где разработчики добавляли поддержку ролей в Galaxy NG, но не очень верили, что изменения дойдут до open source.
Второй вариант — создание единого портала, где будет храниться документация о всех ролях, доступная пользователям. Мы выбрали путь Ansible-разработчиков, а именно — автодокументацию. Для ее генерации они используют утилиту antsibull-docs в связке со Sphinx.
По умолчанию она работает только с коллекциями и работает следующим образом. Все необходимые данные вытаскиваются из модулей и ролей, это в том числе файлы main.yml и argument_specs.yml. Все это подается на вход Sphinx, и мы получаем набор HTML- и CSS-файлов, которые выгружаем, куда нам удобно, и получаем документацию, которая не отличается от официальной.
Минус в том, что решение работает только с коллекциями, но это поправимо. Например, можно написать кастомный обработчик, который будет работать по тому же принципу, но для ролей. Но мы пошли немного другим путем.
У нас есть проект в Bitbucket: большой, востребованный. И мы видим, что некоторые роли можно объединить общей логической задачей или областью применения. Например, роли для настройки гипервизора. На картинке ниже это роли A, B, С.
Все начинается с pull request (PR). В какую-то роль внесли изменения, и к работе подключается пайплайн на базе Jenkins. Он скачивает все роли локально и заворачивает их в валидную коллекцию Ansible. После этого он генерирует сообщение (можно сравнить с Release Notes) на основе Git-сообщений в ролях и выгружает это все обратно в публичный проект. Далее запускаются процессы по генерации документации и выгрузка нашего артефакта на Artifactory через сервис Galactory.
В итоге мы получаем полностью автосборные коллекции с полной обратной совместимостью как для пользователей, так и для разработчиков. Пользователи могут продолжить пользоваться ролями, а при желании — переключиться на коллекции. Со стороны разработчиков все еще проще: все изменения, которые делались до этого в ролях по всем стандартным пайплайнам, остаются, потому что коллекция никак не влияет на процесс разработки и получения контента. Пересборка коллекции происходит при каждом изменении в master любой роли из коллекции, так что конфликтов не возникает.
Результаты изменений
Изначально у нас было много разрозненных закрытых репозиториев, которые хранились в пространствах команд. Сейчас — единый проект, в котором хранятся наработки инженеров разных департаментов. Конечно, некоторые приватные репозитории остались, но их стало намного меньше.
Когда мы проводили аудит репозиториев, увидели, что роли оформляются очень по-разному: где-то нет README, где-то нет тестирования, где-то переменные называются кое-как. Сейчас же, обменявшись опытом, мы поняли, что роли можно оформлять единообразно, и это удобно.
Задачи по автоматизации, которые ранее занимали по два-три дня, решаются теперь использованием общего Ansible-контента как конструктора. Инженеры приходят в общий проект, видят подходящие им роли и переиспользуют в своих задачах. Не нужно тратить время на разработку с нуля. Если пользователь сталкивается с багом в какой-нибудь из ролей, он может исправить его и опубликовать обратно в мастер.
Ну и наконец, у проекта был важный социальный эффект: сотрудники разных департаментов, которые работают с Ansible, добровольно объединились, чтобы найти решение задачи, и много общались. Все это, несомненно, внесло вклад в инженерную культуру нашей компании.