Несмотря на то, что я себя позиционирую больше как системный архитектор и 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:

  1. Скорость: Быстрое обнаружение и исправление багов.

  2. Качество: Автоматические тесты гарантируют стабильность.

Автоматизация: Устранение ручных, повторяющихся задач при релизах.

Почему Yandex Cloud?

Грамотно настроить и развернуть всю инфраструктуру с нуля на выделенных/собственных серверах — это целая наука. Туда же относится дальнейшее сопровождение, обновления, безопасность и эксплуатация. Управляемые сервисы, которые предоставляет Яндекс, кратно упрощают жизнь. Обратная сторона — стоимость: за удобство и простоту приходится платить.

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

Да, уверен, что нарвусь на хейтинг, но расписывать плюсы/минусы на две страницы не стану 🙂 Из минусов для меня — в основном ценник.

Для малых компаний это часто мастхэв: дешевле, чем содержать своих админов/девопсов (или искренне удивляться, почему система ведёт себя “как-то странно”). При значительных потреблениях мощностей и когда прайс за инфраструктуру превышает зарплату толкового специали��та — можно смело задумываться об оптимизации расходов.

Ещё частое возражение — конфиденциальность данных. Субъективно для меня это тянет лёгкой паранойей: у Яндекса как у платформы неплохая безопасность, подтверждённая сертификатами. А если есть ощущение, что “платформе нужны именно ваши уникальные данные”, то тут уже противопоставить что-то становится сложно.

GitLab CI/CD + Kubernetes

Типовая схема выглядит следующим образом:

Разработчик вносит изменения в код и отправляет их в git-репозиторий (GitLab). В репозитории хранится файл с описанием пайплайна — последовательностью шагов, которая исполняется при изменениях. Пайплайн состоит из шагов (Stage) и задач (Job).

В GitLab есть понятие Runner — система, которая запускает job’ы. Runner может поднимать изолированное окружение через executor; в нашем случае — Kubernetes executor, который создаёт pod внутри кластера и уже в нём выполняет job. Job исполняет ровно то, что описано в .gitlab-ci.yml — никакой магии: что написали, то и выполняется.

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

В моём примере пайплайн немного не “канонический” из-за перестановки порядка: вперёд деплой, а затем тесты — сделано, чтобы увеличить скорость выкладки в некоторый ущерб потенциальному качеству (да, есть риск “красных тестов уже после выката”). Это осознанный компромисс.

CI: push → Runner в k8s → сборка и публикация docker-образа в Yandex Container Registry (S3 — кэш сборок).
CI: push → Runner в k8s → сборка и публикация docker-образа в Yandex Container Registry (S3 — кэш сборок).
GitLab CI/CD-пайплайн с разделением стадий build / dockerize / migrate / deploy / test.
GitLab CI/CD-пайплайн с разделением стадий build / dockerize / migrate / deploy / test.

Отдельный шаг миграций (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 (переиспользуемые куски пайплайнов).

CD: manual deploy → Runner → deploy job → helm upgrade в namespace через SA token + RBAC.
CD: manual deploy → Runner → deploy job → helm upgrade в namespace через SA token + RBAC.

Небольшое юмористическое отступление: когда “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 Operator в кластере → remote_write → Managed Service for Prometheus (дашборды/алерты), Fluent Bit → Cloud Logging, уведомления в Telegram/Email.
Prometheus Operator в кластере → remote_write → Managed Service for Prometheus (дашборды/алерты), Fluent Bit → Cloud Logging, уведомления в Telegram/Email.

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

Логи pod’ов и сервисов собираются Fluent Bit, развёрнутым как DaemonSet на нодах, и отправляются в Cloud Logging.

Dev и тестирование

Когда кажется, что Kubernetes решит всё сам 🙂
Когда кажется, что Kubernetes решит всё сам 🙂

Наверное, у вас уже закралось подозрение, что 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 дороже. Но я лучше заплачу деньгами, чем временем команды и ночными инцидентами из-за инфраструктурной самодеятельности.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Как у вас сейчас устроен деплой в продакшен?
75%🟢 Полностью автоматизирован (CI/CD, без ручной магии)9
16.67%🟡 Частично автоматизирован, но есть ручные шаги2
8.33%🟠 Делает “тот самый человек”1
0%🔴 Лучше не спрашивайте…0
Проголосовали 12 пользователей. Воздержался 1 пользователь.