Несмотря на то, что я себя позиционирую больше как системный архитектор и CTO, порой важно спуститься и на самый низ абстракции — на инфраструктурный уровень. Полагаю, что хорошей архитектуре весьма сложно появиться в компании с плохой инфраструктурой и низкой культурой самой разработки и автоматизации процессов. Ну и лично мне DevOps крайне интересен — можно сказать, как дополнительное хобби.
Предыстория
Малый/средний бизнес МФО, маленькая команда разработки, долгие циклы поставки ценности для клиентов и бизнеса. Отсутствие отдельного DevOps-инженера. Были слабые попытки разработчиков поднять Kubernetes “на коленке”, и хорошо, что не получилось — иначе потом пришлось бы ещё и лечить и это.
Приложение — монолит без тестов. Страшно дорабатывать, страшно выкатывать. Выкатка — отдельная история: есть “специально обученный инженер” (тимлид), который знает “тонкости”: какой костыль за что отвечает и каким ещё подпирается, чтобы всё не упало.
Сам деплой — отдельная песня:
Сборка всегда выполняется вручную.
Приложение запущено на Windows-сервере вместе с 1C, базой данных и Credit Registry. В общем, один сервер на всё. (Угадайте, сколько раз он падал, зависал и испытывал дефицит памяти?)
Специальный .bat для запуска в автозагрузке с параметрами.
Простой приложения при деплое — минут 5 (долгий прогрев кэша). Деплой только ночью.
Логи пишутся в файл, поиск затруднён.
Нет метрик приложения, нет системы мониторинга. О проблемах узнаёшь по факту: клиент пишет менеджеру, менеджер передаёт дальше.
Вишенка на торте — скрипты на бейсике, которые парсят файлы, а монолит просматривает каталог с результатами. Активная завязка на файловую систему там, где её быть не должно.
Пожалуй, кейс, где собраны практически все проблемы в одном.
Запрос руководства
Из переговоров с топ-менеджментом и разработкой потихоньку начинают проясняться ожидания.
А именно:
Высокая доступность. Руководство устало от сбоев и падений и хочет гарантированную доступность сервиса. Основные метрики: доступность API и время ответа (p95/p99 latency), количество 5xx, плюс метрики по целевому сервису (наприм��р, количество синхронизаций с CRM, завершившихся ошибкой).
Масштабируемость и производительность в перспективе. У маркетинга стоит задача по активному привлечению клиентов, система должна переваривать нагрузку.
Выявление проблем до релиза: тестируемость, тестовые окружения. Частая проблема — обнаружение проблем на проде “по факту”.
Простота эксплуатации. В том числе простые релизы: релиз по одной кнопке, автоматизированный, не требующий “сакральных знаний”.
Возможность простого отката. Быстрый rollback в одну кнопку — как крайняя мера.
Восстанавливаемость после сбоев. Сервисы могут падать из-за ошибок, но должны автоматически рестартовать, не нарушая работу системы в целом.
Безопасность. Бизнес не хотел, чтобы доступ к проду был у всех разработчиков, а доступы “жёстко захардкожены” в репозитории.
Задача довольно типовая: примерно 80% решается современными практиками DevOps, а оставшиеся ~20% требуют модификации кода, чтобы он действительно соответствовал требованиям.
Definition of Done: что значит “релиз скучный”
Если кратко, ‘скучный релиз’ — это когда изменения выкатываются быстро, предсказуемо и без героизма.
деплой из GitLab одной кнопкой (prod — manual/approve)
минимум 2 окружения: stage/dev + prod
контейнеризация + registry + версионирование образов
миграции БД встроены в пайплайн, а не побочный эффект при деплое
rollback-стратегия описана и проверена — откат должен быть таким же предсказуемым, как и сам деплой
метрики, логи и алерты есть “из коробки”, описан базовый сценарий реагирования (runbook)
секреты и доступы: кто/куда/чем деплоит — без “пароля в чатике”
воспроизводимость: инфраструктура поднимается из кода
Кому это может быть полезно. Если у вас релизы “по ночам”, нет метрик/алертов, деплой держится на одном человеке, а доступы и секреты живут “как получится” — production baseline обычно даёт самый быстрый эффект при минимуме изменений в продукте. Чаще всего это даёт самый быстрый ROI: меньше простоев, меньше ночных выкаток, меньше зависимости от ‘того самого человека’.
Немного терминологии
Скрытый текст
DevOps (Development + Operations) — это подход, культура и набор практик в IT, которые объединяют команды разработки (Dev) и эксплуатации (Ops) для автоматизации и ускорения процессов создания, тестирования, развертывания и поддержки программного обеспечения, обеспечивая более быструю и надежную доставку ценности пользователям, и сокращая разрыв между написанием кода и его работой в реальной среде.
CI/CD (Continuous Integration/Continuous Delivery) — это методология и набор практик в DevOps, автоматизирующие процессы сборки, тестирования и развертывания программного обеспечения. Она позволяет разработчикам часто (непрерывно) интегрировать код в репозиторий, автоматически тестировать его и доставлять на продакшн, ускоряя выпуск ПО и снижая риск ошибок.
Основные составляющие CI/CD:
CI (Continuous Integration — непрерывная интеграция): Разработчики ежедневно отправляют изменения кода в общий репозиторий. Автоматизированные системы сразу собирают и тестируют код, помогая быстро находить ошибки.
CD (Continuous Delivery/Deployment — непрерывная доставка/развертывание): Прошедший тесты код автоматически доставляется в тестовую среду, а затем — на рабочие серверы (продакшн). Это делает релизы частыми и безопасными.
Зачем нужна CI/CD:
Скорость: Быстрое обнаружение и исправление багов.
Качество: Автоматические тесты гарантируют стабильность.
Автоматизация: Устранение ручных, повторяющихся задач при релизах.
Почему Yandex Cloud?
Грамотно настроить и развернуть всю инфраструктуру с нуля на выделенных/собственных серверах — это целая наука. Туда же относится дальнейшее сопровождение, обновления, безопасность и эксплуатация. Управляемые сервисы, которые предоставляет Яндекс, кратно упрощают жизнь. Обратная сторона — стоимость: за удобство и простоту приходится платить.
Пару раз у меня округлялись глаза, и руки тянулись “развернуть на виртуалке свою версию”, но потом я быстро бил себя по рукам, прикидывая тонкости дальнейшей эксплуатации и обслуживания.
Да, уверен, что нарвусь на хейтинг, но расписывать плюсы/минусы на две страницы не стану 🙂 Из минусов для меня — в основном ценник.
Для малых компаний это часто мастхэв: дешевле, чем содержать своих админов/девопсов (или искренне удивляться, почему система ведёт себя “как-то странно”). При значительных потреблениях мощностей и когда прайс за инфраструктуру превышает зарплату толкового специали��та — можно смело задумываться об оптимизации расходов.
Ещё частое возражение — конфиденциальность данных. Субъективно для меня это тянет лёгкой паранойей: у Яндекса как у платформы неплохая безопасность, подтверждённая сертификатами. А если есть ощущение, что “платформе нужны именно ваши уникальные данные”, то тут уже противопоставить что-то становится сложно.
GitLab CI/CD + Kubernetes
Типовая схема выглядит следующим образом:
Разработчик вносит изменения в код и отправляет их в git-репозиторий (GitLab). В репозитории хранится файл с описанием пайплайна — последовательностью шагов, которая исполняется при изменениях. Пайплайн состоит из шагов (Stage) и задач (Job).
В GitLab есть понятие Runner — система, которая запускает job’ы. Runner может поднимать изолированное окружение через executor; в нашем случае — Kubernetes executor, который создаёт pod внутри кластера и уже в нём выполняет job. Job исполняет ровно то, что описано в .gitlab-ci.yml — никакой магии: что написали, то и выполняется.
Простой пайплайн обычно включает: сборку, докеризацию, деплой на окружение и запуск тестов. Набор шагов и порядок сильно зависят от целей и стека проекта.
В моём примере пайплайн немного не “канонический” из-за перестановки порядка: вперёд деплой, а затем тесты — сделано, чтобы увеличить скорость выкладки в некоторый ущерб потенциальному качеству (да, есть риск “красных тестов уже после выката”). Это осознанный компромисс.


Отдельный шаг миграций (Flyway) — must have
Отдельный шаг миграций — тоже best practice: запускать Flyway в отдельном job до деплоя приложения. Это снимает сразу несколько классов проблем.
Во-первых, по пайплайну мгновенно видно, где именно сломалось: миграция или деплой. Во-вторых, долгие миграции перестают блокировать rollout и превращать релиз в лотерею. И наконец, исчезают гонки при нескольких репликах: один job — один исполнитель миграций, без race condition и с предсказуемым временем выката.
Миграции вынесены в отдельный шаг пайплайна: если миграция не прошла — приложение даже не начинает выкатываться. Это убирает классическую ситуацию “код уже в проде, а схема нет”.
CD: деплой в Kubernetes (kubectl/Helm)
Ключевой момент: в современном контейнерном мире обязательно происходит сборка docker-образа и последующий запуск в среде исполнения — например, Kubernetes, который по сути является стандартом де-факто.
В зависимости от ветки может быть разная логика деплоя. Например:
для фича-окружений деплой может происходить автоматически,
для продового окружения — строго вручную,
для препрода/релиз-кандидата/dev-окружения автоматизация — опциональна (кому как удобнее).
В самом простом варианте деплой выполняется командой kubectl apply -f .... В более зрелом — через Helm.
Helm даёт ряд преимуществ. Например:
helm upgrade --install --atomic --wait --timeout 10m
--wait ждёт readiness всех компонентов
--atomic откатывает релиз, если rollout не поднялся
Это добавляет надёжности процессу деплоя: атомарность + лимитированное время выкатки. Плюс Helm — это шаблонизатор для типового кода: если у вас десять похожих сервисов на Java, Helm будет очень кстати.
А раз уж заговорили про уход от boilerplate — нельзя не упомянуть и GitLab templates (переиспользуемые куски пайплайнов).

Небольшое юмористическое отступление: когда “DevOps-инженер” всё же сильно больше Dev, чем Ops
Скрытый текст
На одном из проектов деплой был настроен через Ansible, и делал его явно “самый настоящий Dev”. Никакого дублирования — ну правильно, мы же знаем принцип DRY.
Общие деплойменты — в одной папочке, переменные — в другой, переменные для окружений — ещё в третьей. Специальные скрипты на Python, которые обходили каталоги большого монорепозитория, “коллектили” манифесты Kubernetes в одну папку. Другой скрипт на Python делал REST-запросы, чтобы получать теги образов из CI (GoCD). Все шаблоны рендерились, после чего выполнялся условный kubectl apply.
Плюс отдельные скрипты для создания новых окружений, работы с базами данных, брокером и т.д. Разве что ракета только в космос не взлетала по одной из кнопок.
Про что эта история? Про неоправданную сложность — и то, что порой топорно и прямолинейно не равно хуже.
K8s-кластер
Именно Kubernetes обеспечивает ряд нефункциональных требований.
Масштабируемость
Масштабируемость достигается запуском одного микросервиса в нескольких экземплярах. Если один сервис способен обработать N запросов, то теоретически при горизонтальном масштабировании в два раза получаем ~2N. Фактически, конечно, упираемся в закон Амдала, но это уже тема со звёздочкой. На практике чаще упираемся в более “земные” узкие места: БД, локи, очереди и синхронные интеграции — и baseline как раз помогает это быстрее увидеть по метрикам.
Важно, чтобы сервисы были без состояния, чтобы они могли легко тиражироваться и дублироваться. В нашем кейсе для монолита потребовался рефакторинг: уход от файловой системы, перевод на событийную архитектуру и переход с “чудо-парсера карточек Excel на бейсике” на выделенный микросервис под эту задачу (на Java).
Высокая доступность
Высокая доступность достигается избыточностью: pod’ы сервиса могут быть развернуты на нодах в разных дата-центрах. Если выходит из строя один сервер или наблюдаются проблемы с сетью между дата-центрами — сервис продолжает работать.
При этом нужно учитывать целевую пропускную способность “после деградации”: когда одна нода упала, оставшиеся должны выдержать нагрузку. Kubernetes пересоздаёт pod’ы на живых нодах, но это требует времени на восстановление и ресурсов.
Потеря ноды не равна падению сервиса: при нескольких репликах сервис продолжает отвечать, просто временно уменьшается ёмкость.
Практически можно добиться очень высокого SLA, но это всегда компромисс денег. Практика показывает, что высокая доступность не всегда “нужна” ровно в том объёме, в котором её хотят на словах — когда речь заходит о бюджете.
Один кластер или два?
Два кластера полезны, если хочется жёстко изолировать ресурсы прода: отдельный контур управления и изолированная безопасность. Но неплохой компромисс — один кластер:
отдельные рабочие узлы под продовую нагрузку,
отдельные узлы под тестовые окружения и там же запуск сборок.
И у Яндекса есть классная фишка — прерываемые (preemptible) ноды: они дешевле, но их могут остановить в любой момент. Под GitLab Runner / сборки это часто норм: job просто перезапустится, а экономия получается ощутимой.
В представленном примере изначально был развёрнут один кластер без HA и хотели в дальнейшем HA-кластер, но по факту uptime и риск устроили — решили экономить :)
Kubernetes baseline: namespaces, limits, ingress, rollout
1) Namespaces: изоляция
Kubernetes содержит абстракцию namespaces. По сути это “изолированные квартиры в большом доме”: никто ни к кому не ходит в гости и не мешает. Полезно для изоляции окружений друг от друга.
Плюс Kubernetes позволяет задавать квоты ресурсов для всех подов в namespace — ещё больше повышая изоляцию и убирая тревогу “а если dev съест ресурсы прода”.
2) Requests/Limits: чтобы один сервис не съел весь кластер
Самая забываемая тема — requests/limits.
На основе requests Kubernetes выполняет планирование размещения pod’ов по нодам. Я несколько раз наступал на грабли, когда забывал указать requests: сервис в какой-то момент начинал “не помещаться”, рестартовал и снова поднимался на ноде с заведомо низкими ресурсами — и через время опять падал. Хотя изначально он мог быть запланирован на “правильную” ноду с подходящими ресурсами.
Для Kubernetes важно понимать, что не все pod’ы одинаково “худые”. А про limits и вовсе молчу: когда всё начинает тормозить, потому что один pod с утечкой памяти съел все ресурсы, — становится поздно спорить, нужны лимиты или нет.
3) Liveness / Readiness / Startup probes
С пробами история деликатная. Особенно осторожно отношусь к liveness: много раз наблюдал каскадные падения всего сервиса.
Типовой сценарий: при росте трафика pod долго не отвечает (не потому что умер, а потому что всё тормозит). Kubernetes считает его мёртвым и рестартует. Нагрузка перераспределяется на оставшиеся pod’ы, которые тоже уже на последнем дыхании. И так падают все pod’ы один за другим как карточный домик, а новые не успевают стартовать, потому что нагрузка высокая — и Kubernetes продолжает их рестартовать. Получается рестарт-шторм.
В таких кейсах безопаснее оказывается readiness: она не перезапускает pod, а просто обрубает трафик, чтобы перегруженный pod мог прийти в себя.
Поэтому:
liveness — аккуратно, там где рестарт безопасен
startupProbe — для долгого старта, чтобы Kubernetes не убивал сервис в процессе прогрева
readiness — обязателен почти всегда
Дополнительно в таком кейсе помогает autoscaling: Kubernetes сам добавляет pod’ы при росте нагрузки.
4) Rollout strategy: деплой = постепенная замена, а не “рубильник”
Об этом тоже часто забывают. Хотя казалось бы база: rolling update — стартовать новые pod’ы прежде, чем убить старые.
5) PDB и replicas: чтобы сервис жил при падении ноды
Baseline минимум:
replicas: 2 на критичных сервисах в проде
PodDisruptionBudget: “хотя бы один pod всегда должен оставаться”
Кейс не редкий. Да, сбои нод — не ежедневная история. Но как минимум порой требуется обслуживание узлов. Особенно “любим” автообновления и прочие радости. Яндекс, кстати, официально рекомендует использовать PDB.
6) Ingress: единая точка входа и стандартизация
Ingress — дефолтный инструмент, который помимо маршрутизации позволяет настраивать TLS.
TLS мы автоматизировали через cert-manager с Yandex Cloud DNS ACME webhook: cert-manager сам проходит DNS-01 challenge и обновляет сертификаты, а Ingress просто использует готовые secrets.
Observability: минимум, который реально работает
Мониторинг — обычно последнее, о чём вспоминают. И вспоминают, как правило, когда уже упало. Другая частая проблема — мониторинга слишком много. Когда я ловлю себя на мысли, что держу мышку над кнопкой “отключить уведомления”, потому что устал от спама, а коллеги сделали это ещё вчера — то от такого мониторинга тоже смысла нет.
Поэтому мой baseline принципиально простой: минимум сигналов, но чтобы они были надёжны и обслуживались без выделенного DevOps.
Для baseline я держу небольшой набор метрик, который отвечает на два вопроса: пользователям уже больно? и где копать? Поэтому на каждый сервис обязательно:
RPS
доля 5xx
latency p95/p99
На уровне Kubernetes:
рестарты
readiness
Для зависимостей:
хотя бы соединение с БД
или lag по очереди
Всё остальное добавляется только после того, как этот минимум начинает стабильно работать.
В Yandex Cloud для baseline у нас легли два managed-компонента:
Yandex Managed Service for Prometheus (в экосистеме Yandex Monitoring) — долговременное хранение метрик, PromQL-запросы, дашборды и алерты.
Cloud Logging — централизованный сбор и хранение логов.

Prometheus в кластере (развёрнутый через Prometheus Operator) скрейпит метрики и отправляет их через remote_write в Yandex Managed Service for Prometheus. Это важно: Kubernetes среда эфемерная, и тащить на себе долговременное TSDB-хранилище Prometheus — отдельная операционка. В итоге история метрик, запросы, дашборды и алерты живут в managed-слое, а кластер остаётся “тонким” и предсказуемым.
Логи pod’ов и сервисов собираются Fluent Bit, развёрнутым как DaemonSet на нодах, и отправляются в Cloud Logging.
Dev и тестирование

Наверное, у вас уже закралось подозрение, что Kubernetes и Яндекс Облако могут всё. Но важно отметить: “магия DevOps” работает только тогда, когда сами сервисы к ней готовы.
Как правило, нельзя взять произвольное приложение, “засунуть” его в контейнер, а затем — в Kubernetes, и ожидать, что оно внезапно станет облачным. В нашем случае исходный монолит был тесно завязан на файловую систему, поэтому его пришлось переделать в stateless-приложение: без хранимого состояния на локальном диске, без зависимости от конкретной ноды и без “важных” временных файлов.
Отдельный класс проблем появляется, когда сервис запускается в нескольких экземплярах. Нужно помнить про race condition. Например, задача по расписанию (scheduler) при горизонтальном масштабировании может начать выполняться параллельно на нескольких репликах — и это легко приводит к ошибкам и двойной обработке. Решения типовые: либо использовать distributed lock, либо изначально проектировать задачу как безопасную к параллельному запуску (идемпотентность, shard’инг, “claim” в БД и т.д.).
И ещё момент, который часто недооценивают: автоматизация и вся практика CI/CD становятся существенно менее эффективными, если в проекте нет нормальных unit-тестов и других уровней автоматическо��о тестирования, которые запускаются на каждый change. Иначе пайплайн превращается в “автоматическую доставку багов”, а не в систему, которая снижает риски релиза.
Следующий шаг: GitOps и Argo CD (не обязателен для baseline)
Скрытый текст
Сейчас деплой у нас push-based: GitLab Runner выполняет helm upgrade в кластер через ServiceAccount + RBAC.
Это уже даёт “скучные” релизы и воспроизводимость.
Следующий уровень зрелости — GitOps: когда состояние кластера описано в Git, а Argo CD (или аналог)
сам синхронизирует desired state, отслеживает drift и помогает с аудитом/rollback.
Мы рассматриваем это как следующий шаг, когда появится потребность в более строгом контроле изменений
и масштабировании количества сервисов/команд.
Итоги
После выката у системы появляется “зрение”: метрики показывают деградацию, алерт поднимает тревогу, логи отвечают на вопрос “почему”.
Что получилось по факту:
Релизы в любое время. Релиз перестал быть ночным ритуалом и историей про “специально обученного инженера”.
Простой стремится к нулю за счёт rollout-стратегии: новые инстансы поднимаются и проходят readiness до того, как старые выводятся из ротации.
Скорость релиза сократилась с ручных ~20–30 минут до ~2–5 минут.
Ручных шагов почти нет (по сути — нажатия кнопок в пайплайне).
Достаточно двух окружений: dev и prod — фича-окружения не стали городить, потому что базовые риски закрылись и так.
Аптайм API превзошёл желаемый SLA в “три девятки”.
Отладка и поиск проблем упростились: централизованные логи и поиск по traceId/snapId позволяют быстро собирать картину происходящего. Это не только ускоряет разбор инцидентов, но и помогает массово находить типовые ошибки, которые раньше “жили” в лог-файлах и чаще игнорировались, чем системно исправлялись.
Отдельно изменилась коммуникация с бизнесом. Раньше о проблемах узнавали по цепочке “клиент → менеджер → бизнес → разработка”. Сейчас сигнал прилетает сразу в TG-канал уведомлений, где могут быть и разработчики, и заинтересованные люди со стороны бизнеса. Разработчики реагируют комментарием “взято в работу”, тревожность снижается, бизнес видит, что проблема уже решается, а пользователи внутри компании понимают, что возможны задержки. Плюс появляется прозрачность: когда началась проблема и когда была исправлена — а значит, становится проще оценивать потери и принимать решение, “можем ли мы это терпеть” или уже надо срочно ускоряться.
В итоге baseline превращает проблемы из слухов и тревоги в измеримые сигналы и понятные действия.
Если вам близка идея “скучных релизов”
Я оформил это как повторяемый подход — production baseline: минимальный набор инфраструктурных и процессных решений, после которых продакшен становится управляемым, а релизы — предсказуемыми.
Подробное описание (что входит, формат работы): https://korobovn.tech/production-baseline
Практические заметки, схемы и разборы — в TG: t.me/cto_way
P.S.
Хочется отдать должное Яндекс Облаку (дисклеймер: статья не проплачена, просто личные ощущения 😄). Мне субъективно очень нравится эта платформа. Есть чувство, что ты собираешь инфраструктуру как из кубиков — быстро и аккуратно, но при этом без ощущения “соломенного домика”, который развалится при первом порыве ветра.
Я увлекаюсь DevOps и не раз поднимал кластера с нуля, поэтому хорошо понимаю, сколько усилий обычно уходит на “низ” — сеть, плагины, апгрейды, обслуживание и бесконечные нюансы. И именно здесь managed-подход выигрывает: он помогает строить систему, а не коллекцию костылей. Ты начинаешь думать на уровне архитектуры и процессов доставки, а не спорить сам с собой, какой network plugin для Kubernetes “правильнее” и почему.
Да, managed дороже. Но я лучше заплачу деньгами, чем временем команды и ночными инцидентами из-за инфраструктурной самодеятельности.
