Блог
Инженерия Автор
29.01.2022

Суки

Системы управления конфигурацией все еще используются для управления серверами. В тексте для краткости я буду называть их суками. Большинство из них реализует парадигму “инфраструктура как код” (IaC). Мне довелось поработать аж с тремя суками: ansible, puppet и chef и все они по-своему ужасны.

Что за код

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

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

Большинство примеров в интернете ограничивается настройкой веб-сервера на коленке, со словами “посмотрите как хорошо и быстро все получилось!”. Но в этих примерах никогда не учитывается та сложность, которая стоит за всей парадигмой IaC. В них никогда нет хороших примеров того, как надо строить большую и сложную архитектуру. А это реально гораздо больше, чем просто “установить nginx и положить ему конфиг из темплейта”.

Да че там сложного-то

Первая сложность заключается в выборе инструмента и архитектурного решения. Берём ансибл, он вроде популярен? Или паппет, им кто-то тоже пользуется. Но чтобы сделать выбор, нужно понимать разницу и понимать свои потребности, желательно в перспективе.

Инструмент выбрали, а как делить модули? Вот есть сервис, он всегда идёт вместе с другим, описать их в одном модуле или лучше разделить. А вдруг через месяц надо будет запустить один из сервисов отдельно, тогда лучше будет описать его повторно в другом модуле или вынести существующий в отдельный модуль?

Часто у команд нет опыта, когда они начинают строить инфраструктуру. И никто не задумывается об архитектуре. Стартап растет, время летит, рефакторить некогда и некому. Выбранный в самом начале подход не адаптируется. Никто не задумывается о том, как будет работать текущее решение при переходе от 10 серверов к 100 и к 1000.

И бац!

В какой-то момент мы оборачиваемся и понимаем — это пиздец.

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

Сука прогоняется по серверам вручную, на каждый прогон надо тратить огромное количество времени. Мало того, код обычно прогоняется частями, по тэгам. В итоге глобальные изменения не доходят до всех серверов. При попытке прогнать суку по всем хостам вылезает куча не примененных изменений, которые никто не знает как исправить. Это страшно и неприятно трогать, инфраструктурой невозможно управлять.

Ситуация усложняется наличием псевдо-декларативной обертки над нормальными языками программирования. Ладно, иногда она помогает. Но по-началу инженеров вводит в заблуждение наличие такого интерфейса для кодинга. Кажется, как будто и программировать не нужно уметь. На самом деле часто нужно именно программировать, а не стараться нарисовать цикл на ямлах. А декларативный интерфейс обычно реализован посредственно и пользоваться им не так удобно, как хотелось бы, хотя зависит от конкретной суки. Когда декларативность возводят в абсолют — это становится причиной кучи трудностей и контринтуитивных нюансов, как, например, это сделано в терраформе.

Ansible

Ансибл учит людей плохому с самого начала. Давайте инженеры будут запускать суку прямо со своих ноутбуков? Это ведь так удобно, несмотря на то, что код уже лежит в гите. Пусть гит будет как бэкап и точка синхронизации!

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

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

Можно сколько угодно говорить, что “они неправильно работают”, что “это ответственность инженеров, нужно быть собранными и делать аккуратно”. Но нет, инженеры тоже люди и они не хотят страдать. Не у всех хватает понимания, сил и желания изменить текущее положение дел, поэтому у многих опускаются руки. Инструмент должен быть удобным, его нужно делать таким, итеративно, постепенно.

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

У ансибла нет центральной базы данных, а это значит, что для описания сложной инфры нужно использовать сторонние ресурсы, например консул. Без них задачи уровня “апгрейдим инстансы этого сервиса по одному, проверяя хелсчек” в автоматическом режиме не решить.

Ансибл хорош одним — быстрый старт.

Puppet

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

Узкое место паппета, с ростом инфры, это его сервер. Сервер компилирует все модули всего окружения перед выполнением любого кода на любой ноде. Это значит, что ошибка в любом модуле, не имеющем отношения к данному хосту, приводит к падению и код на сервере не выполняется. В то же время, нагрузка на сервер растет с каждым новым узлом в инфраструктуре. В итоге эти сервера надо скейлить, а нагрузку балансить. Если сервисы распределены географически, то и сервера паппета стоит распределить. И для его базы данных нужно настроить репликацию. А так как используется уже классическая postgresql, то и настройка не тривиальна.

Паппет дает возможность писать на руби и это плюс. В паппете есть hiera, которая позволяет очень гибко оверрайдить переменные. Это и плюс и минус одновременно, потому что люди эти оверрайды часто переусложняют и превращают иерархию в лапшу, а работу в постоянный поиск ответов на вопрос “как это значение тут оказалось” или “не оказалось”. Но хотя бы паппет предлагает вариант агента, который будет постоянно держать конфиг сервера актуальным из коробки.

Chef

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

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

Что в шефе сделано плохо, так это наличие сервера, который отвечает за хранение кода и переменных. К сожалению, ему нужно постоянно загружать новые значения. А старый код нужно удалять. И следить за этим придется самим, реализуя не тривиальную логику в CI.

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

Как с этим работать

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

Применение конфигурации происходит по таймеру на постоянной основе.

Чем чаще таймер, тем лучше: тем ниже будет тайм ту маркет, тем быстрее будет видно мисконфиг, тем проще инженерам с этим работать. Да, с тестами инфраструктурного кода не все просто, но что поделать. Исполнение придется мониторить.

Используется pull-модель для применения конфигурации.

Казалось бы, зачем так жестко. На самом деле pull-модель здесь сильно проще масштабировать. И большинство сук работают именно по pull-модели. Даже если взяли ансибл, то есть ansible-pull.

Пишется императивно всё, что проще описать так.

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

Управляйте жизнью сервисов этой же системой.

Если сервисы управляются системой управления конфигурацией, то все обновления должны выполняться через нее. Да, нужно писать логику проверки через хэлсчек. Да, вероятно придется брать лок в сторонней базе, чтобы обновлять пакеты/контейнеры поочередно. Да, придется написать кубернетес на минималках. А вы думали как он появился?

Как писать код

От теории к практике. Постараюсь описать некоторые детали реализации того, о чем я тут пишу. Ладно, это всего лишь пара хаков, но они удобны и отражают подход.

Файлы конфигурации

Шаблонизирование конфигов одна из рутинных операций при работе с инфраструктурой. По какой-то причине очень распространена практика, когда шаблон делается в духе шаблонов в Ворде: записана конфигурационная опция и в ее значение подставляется значение одноименной переменной из кода.

Многим такой подход нравится, якобы зато сразу знаешь что такая опция есть! Но современного софта так много, и у него может быть так много опций в конфиге, что шаблонизировать таким образом весь конфиг — это тратить несколько часов работы. А что, если при обновлении софта нужно будет добавить какую-то опцию? Или одну из них переименовали? Теперь нам нужно идти в код и менять название переменной в коде и опцию в шаблоне конфига, приурочив это к обновлению софта.

Многие не заполняют все возможные опции в шаблоне, в итоге для добавления новой опции опять надо править и шаблон и код и новую переменную декларировать.

Зачем заниматься этой херней? Шаблонизируем конфигурацию полностью! Пусть у нас будет конфигурация в словаре, где ключ — это название опции, а значение — значение опции. Все, мы имеем шаблон конфига, где в простом случае три строки, а все остальное определяется кодом и данными в словаре.

{% for k, v in opts.items() %}
{{ k }}={{ v }}
{% endfor %}

Каталоги.d

Иногда приходится брать под управление каталоги с конфигами, содержащими большое количество файлов. Иногда, так получается, что файлы туда закидываем не только мы, а например, пакетный менеджер при установке софта. Яркий пример — правила udev, cron и systemd-таймеры, sysctl.

Иногда мейнтейнеры поставляют с пакетом набор файлов, которые закидываются туда и начинают что-то делать в системе по таймеру, или меняют поведение работы с устройствами. Эти конфиги могут конфликтовать с нашими конфигами. Как с этим работать?

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

Тоже самое можно реализовать и там, где мы полностью управляем конфигом, только ещё проще: дропаем все, что не задано явно.

Мониторинг

Мониторить нужно две вещи: что конфигурация применяется и что сука работает.

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

Второй случай — это какие-то проблемы, при которых сука не завелась и падает, даже не отправив никуда ошибку. Используем подход “выключатель мертвеца” — при настройке конфигурации первым делом записываем куда-то в файл unix timestamp. Демоном мониторинга проверяем, насколько это время отстало от текущего. Если слишком сильно, значить несколько раз подряд конфигурация не применилась, мы теряем управление! Отправляем алерт.

Короче

Относиться к инфраструктурному коду нужно как к проекту. Это подход к управлению инфраструктурой, а не просто инструмент. Об этом надо помнить постоянно. Выбор суки сильно влияет на будущую архитектуру и возможности проекта, в том числе и возможности масштабирования. Выбирайте с умом!

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

Не накручивайте лишних переменных, делайте шаблоны действительно шаблонами. Захватывайте власть над каталогами и мониторьте работу системы.


₽ с или .