Что вы теряете на самом деле
Ситуация обычно такая: сервер есть, артефакт собран, кластера нет — и не потому, что вы против него, а потому что собирать его некому. Поисковый запрос «деплой без kubernetes» почти всегда означает именно это: не выбор идеологии, а вопрос «как выкатывать тем, что есть».
Чтобы отвечать честно, полезно разделить, что кластер даёт на самом деле. Если убрать обёртку, остаётся три вещи: автоматическое вытаскивание сервисов на живых машинах, когда одна нода умирает целиком; раскатка одного и того же на много машин как одно действие; общий каталог «кто где живёт». Всё остальное из списка «фич оркестрации» — рестарт упавшего процесса, атомарное переключение версий, откат, проверка живости — существовало до кластеров и живёт без них. «Что осталось живым» из заголовка — это оно и есть: старые скучные механизмы, которые пережили несколько поколений модных инструментов.
Правда, есть и четвёртое, о котором часто забывают: кластер — это ещё и дежурный, который в три ночи перезапускает умершее. Без кластера дежурный — вы. Смиритесь с этим сразу, и остальное станет проще: вы не ищете волшебный инструмент, вы раскладываете ответственность по местам.
Дальше — практика. Сначала о тупике, потому что в него попадают почти все.
Тупик: кластер из одной ноды «на всякий случай»
Первое, что пробуют, когда кластера нет, но страшно: поставить мини-дистрибутив кубернетеса одной командой, из одной ноды — «чтобы остаться в экосистеме». Со стороны это выглядит компромиссом: и полноценного кластера нет, и API на месте.
Не работает. Выгода кластера в том, что смерть одной ноды не останавливает сервис и что раскатка на много машин — одно действие. На одной ноде нет ни того, ни другого: нода умерла — умерло всё, включая сам кластер. А плату вы внесли полностью: обновления управляющего слоя, сертификаты, сетевые надстройки, права и роли — всё это теперь ваше, и этим надо заниматься. Вы подписались на эксплуатацию распределённой системы ради API, которым всё равно не пользуетесь: деплоить больше некуда.
Отдельная ловушка — отладка. Когда что-то не стартует, путь к причине идёт через слои: манифест, контроллер, рантайм, сеть. На голой машине причина обычно лежит в журнале systemd, до неё один шаг. Слои — это цена, и платить её имеет смысл, только когда есть что оркестрировать.
Мы уже разбирали спор о том, нужен ли кластер команде из двенадцати человек. Однонодовый вариант — худший из возможных ответов в нём: расходы кластера при полном отсутствии его доходов.
Рабочая схема: релиз-каталоги и systemd
Схема ниже рассчитана на машины, которые вы знаете поимённо, и сервисы, которые помещаются в голову. Если у вас автоскейлинг — это другая история. Разберём по шагам.
Артефакт собирается один раз. Тарбол или образ — не принципиально; принципиально, что то, что вы проверили на стенде, байт в байт то, что окажется на проде. Имя артефакта привяжите к идентификатору коммита, чтобы откат был осмысленным: не «вернуть старое», а «вернуть вот это».
Раскладка — релиз-каталоги и переключаемый симлинк. На машине каталог releases/, в нём подпапки по идентификатору релиза, и симлинк current, указывающий на выбранный. Конфиги и данные живут вне релиз-каталогов — релиз должен быть одинаковым на всех машинах. Деплой: доставить артефакт, распаковать в новый подкаталог, прогнать проверки, переключить current, перезапустить сервис.
Переключать надо атомарно, и это единственное место, где есть хитрость:
ln -sfn /opt/app/releases/$NEW /opt/app/current.tmp
mv -T /opt/app/current.tmp /opt/app/current
mv -T переименовывает симлинк поверх старого одной операцией: читатель видит либо старый релиз, либо новый, половинного состояния не бывает. Набросок — пути и имена у вас будут свои.
systemd — супервизор. Юнит с Restart=on-failure и ограничением на число рестартов: упавший процесс поднимется сам, зациклившийся — остановится и останется в журнале. ExecStart указывает через current, поэтому смена релиза — это systemctl restart, а не переписывание юнита. Паузу между рестартами подберите по своему сервису: один надо поднимать сразу, другому нужна передышка.
Проверка живости до переключения. Новый релиз сначала поднимается рядом — на отдельном порту или в отдельном контейнере, — и вы спрашиваете у него readiness-эндпоинт. Жив — переключаете трафик и гасите старый. Не жив — деплой останавливается, current не тронут, прод остаётся на старом. Это главный выигрыш схемы: неудачный релиз не доезжает до трафика.
Откат — та же операция в обратную сторону. Симлинк на предыдущий каталог, рестарт, готово. Дёшево это потому, что старые релизы вы не удаляете сразу: место на диске дешевле, чем время на восстановление.
Скажем прямо о простое: рестарт — это пауза, и для большинства внутренних сервисов она терпима. Если не терпима — рядом ставится обратный прокси и держатся два юнита: новый поднялся, прокси перевёл апстрим, старый погас. Учтите: это ровно тот момент, когда схема начинает обрастать. Как только прокси, юниты и скрипт вместе перестают помещаться в голову — вы уезжаете туда, откуда уходили.
Если вам ближе контейнеры, всё то же ложится на compose почти без изменений: тег образа вместо каталога, pull и up -d вместо распаковки, healthcheck встроен. Оговорка одна: демон, который этим управляет, попадает в критический путь — при его падении контейнеры, возможно, и продолжат работать, но управлять ими вы сможете только после его перезапуска. Пока машин мало, это приемлемая цена — но знать о ней надо заранее.
Когда машин несколько, схема не меняется — она просто запускается по очереди: одна машина, проверка, остальные. Порядок и паузы — единственное, что придётся написать руками; кластер делает это за вас, но здесь это цикл по списку хостов.
И последнее: сам деплой — один скрипт в репозитории рядом с сервисом. Не система, не фреймворк — скрипт, который помещается в один экран и который прочитает любой в команде. Если скрипт разрастается до самодельного оркестратора с проверками, паузами и логикой отката — остановитесь. Вы заново изобретаете то, от чего уходили.
Миграции: где схема ломается
Всё выше работает, пока релиз не тянет за собой миграцию базы. Дальше начинается то, за чем люди и идут в оркестраторы: согласованность. Даже без кластера есть окно, когда старый код ещё отвечает на запросы, а схема уже новая — или наоборот.
Снимает боль правило «миграция в два хода». Сначала расширяете схему: новые колонки и таблицы появляются, старые остаются. Релиз с новым кодом пишет в новое и умеет читать старое. Когда релиз закрепился, отдельным шагом старое убирается. Откат тогда тривиален: старый код совместим со схемой «на вырост», симлинк переключается назад без оговорок. Если же миграция сразу ломает старый код, откат невозможен в принципе — и никакой кластер этого не исправит, он лишь аккуратнее управляет окном, в котором старое и новое работают одновременно.
Миграции при этом запускает деплой-скрипт отдельным явным шагом, до переключения симлинка, а результат — в журнал релиза. «Схема сама обновится при старте» — путь к выкладке, которую нельзя повторить и нельзя откатить.
Честно говоря, это самая скучная и самая важная часть заметки. Релиз-каталоги выручают от упавшего процесса, обратно совместимые миграции — от упавшего релиза.
Остальное — служебное, по одной строке. Конфиги: файлы окружения рядом с сервисом, в артефакт их не класть. Секреты: права на файлы и отдельный пользователь для деплоя — втискивать их в образ нельзя по определению. Логи: journald, точка. Наблюдение: скрипт, который дёргает readiness с другой машины и будит вас, если молчит, — единственное, чего схема не умеет, это поднять сервис на другой машине, когда эта умерла целиком.
Позиция редакции
Мы считаем: деплой без кластера — не «пока не выросли» и не компромисс, а нормальное состояние команды, у которой машины пересчитываются по пальцам, а сервисы — по списку, который помещается в одну голову. Релиз-каталоги, systemd, один скрипт в репозитории и обратно совместимые миграции закрывают настоящую боль — упавший процесс, неудачный релиз, необходимость откатиться — и делают это механизмами, которые можно прочитать целиком.
Наша позиция неверна, как только переворачивается арифметика. Посчитайте сами: умножьте число сервисов на частоту релизов и на число машин. Если произведение растёт от квартала к кварталу, а деплой перестал быть действием одного человека за время, которое вы готовы выделять на релиз, — схема перестаёт масштабироваться, и вопрос превращается из «как выкатить» в «как оркестрировать». Второе условие: падение одной машины перестало быть событием «перезапустили и пошли дальше» — как только сервис обязан жить, когда нода умерла, без шедулера не обойтись. Третье: в команде появился человек, чья работа — платформа; тогда расходы на кластер амортизируются и расчёт меняется.
Граница проходит не по моде, а по арифметике — с этим, кажется, согласятся обе стороны спора о двенадцати людях. Пока она по вашу сторону — не стесняйтесь скучных механизмов. Они живее, чем кажутся.