Капстоун: mini-PaaS — реестр, сборка, run, здоровье и zero-downtime раскат
Mini-PaaS связывает весь трек: реестр пинует неизменяемые digest, сборка даёт тонкие distroless-артефакты, run принуждает лимиты, честное здоровье гейтит трафик, а rolling-деплой с maxUnavailable и preStop меняет версии без потерянных запросов.
Откат не откатил. Инцидент только что «починили» передеплоем прошлой версии — myapp:latest до плохой правки — но новые поды поднялись с сломанным кодом всё равно, и авария тянулась ещё двадцать минут, пока все таращились на деплой со статусом успех. Причина — мутабельный тег: latest был перезаписан плохой сборкой, так что «передеплой latest» притянул баг. Хуже, половина выживших старых подов была ещё на прошлом latest из кэша, так что кластер гонял две разные версии под одним тегом и никто не мог сказать, у какого пода какой код. Фиксом стало правило, превратившее хрупкий воркфлоу в реальную платформу: каждый образ ссылается по неизменяемому digest (sha256-хэш содержимого образа, myapp@sha256:...), никогда по движущемуся тегу. Этот капстоун собирает весь трек — реестр, сборка, run, здоровье, раскат — в наименьшую систему с этим свойством и четырьмя другими, что нужны продовой платформе: неизменяемость, ограниченные ресурсы, честное здоровье и раскат, что никогда не теряет запрос.
Пять стадий как один конвейер
Прежде чем читать пять контрактов ниже, замети, чем был инцидент из Hook на самом деле: не одной упущенной фичей, а упущенной композицией. Каждый контракт по отдельности выглядел приемлемым; вместе им не хватало того единственного, что делает остальные осмысленными.
Всё в этом юните — стадия одной машины. Минимальная продовая платформа — твой mini-PaaS — это композиция пяти контрактов, каждый из которых проваливает другие, если неверен.
1 · Реестр — неизменяемость. Реестр — источник истины для артефактов, и его единственное непреложное правило: деплои ссылаются по неизменяемому digest, а не по мутабельному тегу. myapp:latest можно перезаписать; myapp@sha256:9f2c... — нет: тот же digest всегда тянет ровно те же байты. Пинуй digest в деплой-манифесте — и откат по-настоящему возвращает к известно-хорошему коду, два пода никогда не разойдутся в том, что значит latest, а image-pull воспроизводим по узлам и времени. Теги — удобные ярлыки для людей; digest’ы — контракт для машин.
2 · Сборка — тонкая и воспроизводимая. Multi-stage сборка (из урока про утоньшение) компилирует в тулчейн-стадии и отгружает distroless-артефакт ~25 МБ, так что новая версия тянется на холодные узлы за ~2 с во время раската вместо его застопоривания на 90 с. Сборка пинована (база по digest, зависимости по lock-файлу), так что digest детерминирован — тот же исходник даёт тот же образ.
3 · Run — ограниченные ресурсы. Каждый контейнер бежит с лимитом памяти (из урока про OOM), так что один плохой релиз не может OOM-уронить узел и убрать другую версию посреди раската, плюс cgroup-aware цель рантайма (GOMEMLIMIT / MaxRAMPercentage), так что рантайм держится под стеной. Ограниченные ресурсы — то, что делает безопасным гонять старую и новую версии бок о бок во время раската.
4 · Здоровье — честное гейтирование. Readiness-проба, гоняющая реальный запрос (из урока про здоровье), гейтит трафик: новый под не получает запросов, пока по-настоящему не готов, а больной под сливается. Это сигнал, которого ждёт контроллер раската — без честной readiness «раскат завершён» это ложь, и трафик бьёт в под, что не может обслужить.
Откат передеплоит myapp:latest, но поды всё равно гонят сломанный код, а выжившие старые поды гонят другую сборку под тем же тегом. Какое правило платформы предотвращает этот класс багов?
Раскат — композиция в zero downtime
Пятый контракт — rolling-деплой, и он работает лишь потому, что держатся четыре других. Rolling-обновление заменяет поды инкрементально под двумя ручками: maxUnavailable (сколько существующих подов могут быть недоступны разом — ставь низко, напр. 0 или 25%, чтобы сохранить ёмкость) и maxSurge (сколько лишних подов можно создать сверх желаемого числа, напр. 25%, чтобы добавить ёмкость новой версии до удаления старой).
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0 # никогда не падать ниже желаемой ёмкости
maxSurge: 1 # поднять один новый под до вывода старогоПоследовательность связывает весь трек. Контроллер создаёт новый под из пинованного по digest тонкого образа (стадии 1–2); он тянется за ~2 с и стартует под своим лимитом памяти (стадия 3); контроллер ждёт прохождения честной readiness-пробы нового пода до отправки ему трафика (стадия 4); и лишь тогда завершает старый под, который гонит свой цикл preStop → SIGTERM → слив (из урока про здоровье), так что его запросы в полёте завершаются; и повторяет, пока каждый под не станет новой версией — с maxUnavailable: 0 ёмкость никогда не проседает, а с честной readiness трафик никогда не приземляется на не-готовый под. Убери любой контракт — и раскат ломается: мутабельный тег делает откат ненадёжным, жирный образ застопоривает surge, отсутствующий лимит памяти даёт новой версии OOM-уронить узел с старой, нечестная проба маршрутизирует трафик на мёртвый под, а отсутствующий preStop теряет запросы в полёте выводимых подов. В этом урок всего юнита: продовые контейнеры — не пять независимых лучших практик, а один конвейер, где каждая стадия несущая для следующей.
В rolling-обновлении maxUnavailable:0, maxSurge:1 чего ждёт контроллер перед завершением старого пода и почему этот гейт важен?
- 01Почему деплой обязан ссылаться на образ по digest, а не тегу, и какие конкретные отказы пин digest предотвращает?
- 02Пройди zero-downtime rolling-обновление (maxUnavailable:0, maxSurge:1) и объясни, как каждый из пяти контрактов юнита несущий для него.
Этот капстоун собирает юнит в наименьшую систему, ведущую себя как продовая платформа: mini-PaaS, что есть пять контрактов, скомпонованных в один конвейер, каждый несущий для следующего. Правило реестра — неизменяемость: ссылайся на образы по digest, а не мутабельному тегу, так что откат возвращает к точным известно-хорошим байтам, два пода не разойдутся в том, что значит тег, а pull’ы воспроизводимы по узлам и времени; открывающий инцидент, где передеплой перезаписанного latest заново отгрузил баг, — ровно то, что убивает пин digest. Сборка multi-stage и тонкая, отгружая distroless-артефакт ~25 МБ, что тянется на холодные узлы за ~2 с, так что surge раската быстр, а не застопорен, и пинована, так что тот же исходник даёт тот же digest. Run ограничен по ресурсам — лимит памяти плюс cgroup-aware цель вроде GOMEMLIMIT — так что плохой релиз не может OOM-уронить узел, что хостит другую версию посреди раската, что и делает безопасным гонять две версии бок о бок. Здоровье — честная readiness, гоняющая реальный запрос, гейтящая трафик так, что новый под не берёт запросов, пока по-настоящему не обслужит, а больной под сливается; это сигнал, которого ждёт контроллер раската. И rolling-деплой связывает всё: с maxUnavailable:0 ёмкость не проседает, а maxSurge поднимает готовый новый под до вывода старого, который затем сливается через preStop → SIGTERM, так что его запросы в полёте завершаются. Потяни любую нить — и весь раскат расплетается: мутабельные теги ломают откат, жирные образы застопоривают surge, отсутствующие лимиты роняют узел, нечестные пробы маршрутизируют не туда, а без preStop теряются запросы. Тезис юнита ровно эта композиция: продовые контейнеры — ограниченные ресурсы, честное здоровье, тонкие образы и плавный жизненный цикл, работающие как одна система под оркестратором. Теперь, когда ты ревьюишь Dockerfile, манифест Kubernetes или runbook деплоя, читай его как конвейер: держит ли каждая стадия контракт, от которого зависит следующая?
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.