Как задеплоить сервис
Пошаговая процедура выкатки новой версии Go-сервиса в staging и prod.
Охватывает pre-flight проверки, mechanics rolling update в
Kubernetes, синхронизацию с миграциями, что смотреть после cutover.
Откат — в отдельном how-to:
rollback-deploy.
Handbook не описывает конкретный CI-пайплайн сервиса (имя job’ов, секреты, chart-ы в Helm) — это контекст каждого сервис-репо. Здесь — правила и чеклист, единые для всех сервисов.
Содержание
- Модель деплоя
- Пайплайн целиком
- Pre-flight checklist
- Staging
- Prod cutover
- Миграции и expand-contract
- Наблюдение после cutover
- Canary-паттерны
- Feature flags
- Когда НЕ деплоить
- Что не делать
- Связанные разделы
Модель деплоя
- Unit деплоя — один Docker-образ сервиса. См.
../architecture-overview. - Стратегия — Kubernetes rolling update с
maxSurge=1, maxUnavailable=0. Новая версия поднимается рядом со старой, трафик переключается после/readyz=200на новом pod’е, старый pod уходит. Ноль downtime, если pod’ов >1. - Blue-green не используем как дефолт: rolling update справляется на наших объёмах и не требует удвоенного footprint’а инфраструктуры.
- Canary — опциональный шаг для высокорисковых изменений (поведение под нагрузкой, новая зависимость, большая миграция). См. §Canary-паттерны.
- Релизный триггер — git-tag
vX.Y.Zв сервис-репо. Прямые push’и вmainсобирают staging-образ, но prod не деплоится без тега.
Пайплайн целиком
Rolling update в prod занимает 1–5 минут в зависимости от размера
pool’а, warmup pod’а и terminationGracePeriodSeconds (см.
../conventions/shutdown).
Pre-flight checklist
Перед каждым prod-деплоем автор релиза (обычно — author главного PR из релиза) проходит:
- PR (или серия PR) смержены в
main; CI зелёный. - Образ собран и успешно запущен в staging не менее часа
назад — прошёл
/readyz, принял реальный трафик (или smoke test), Grafana не подсвечивает алерты. - Миграции (если есть) применены в staging из того же образа, что деплоим в prod. Up-миграции прошли без ошибок, down- миграции (где применимо) протестированы.
- Миграции соответствуют expand-contract (см.
../conventions/db-pgx#expand-contract). Ни одна миграция в этом релизе не ломает предыдущую версию сервиса — только текущую или будущую. См. §Миграции. - В
CHANGELOG.mdсервис-репо добавлена запись: bullet’ы изменений, ссылки на PR, upgrade-notes (если есть). - Secret’ы / config-изменения (новые env-переменные) выкачены в
секрет-менеджер заранее — до деплоя образа. См.
../conventions/configuration. - Новые Prometheus-alert’ы и dashboard’ы закоммичены в infra- репо и применены. Alert не должен появиться «после инцидента».
- SLO-budget сервиса на последние 7 дней не пустой (
>20%осталось). Пустой бюджет — повод отложить feature-релиз. См.../conventions/slo-and-budget#error-budget. - Есть on-call дежурный на ближайший час (или автор релиза сам on-call).
- Не пятница после 18:00 и не канун выходных / праздников. В эти окна деплой — только security-hotfix с одобрением lead- инженера.
Чеклист целиком — в ../checklists/production-ready
для первого релиза сервиса; для последующих деплоев — этот
сокращённый список.
Staging
# tag / merge в main → CI автоматически деплоит staging-образ
# имя образа: ghcr.io/<org>/<svc>:<sha>Ручная проверка после staging-деплоя:
curl -sf https://staging.<svc>.kazmaps.internal/healthz
curl -sf https://staging.<svc>.kazmaps.internal/readyz
curl -sf https://staging.<svc>.kazmaps.internal/metrics | head -30Smoke test — минимум:
- Happy-path endpoint (
GET /v1/<ресурс>/<id>или эквивалент) → 200. - Auth-required endpoint с валидным токеном → 200.
- Auth-required endpoint без токена → 401.
- Один write-endpoint из релиза (если изменилась бизнес-логика).
- Прочитать новые метрики в staging-Grafana — появились ли, какие значения.
Минимум 1 час staging-burn-in’а перед prod-cutover’ом для обычных изменений, 24 часа — для изменений, затрагивающих outbox, consumer’ов, background worker’ов (нужно дать forwarder’у / consumer’у пройти заметный объём событий, чтобы поймать регрессии типа memory leak или медленного growth-lag’а).
Prod cutover
После tag vX.Y.Z:
-
Наблюдение заранее открыто. Grafana-dashboard сервиса и dashboard SLO burn-rate (см.
../conventions/slo-and-budget#multi-window-multi-burn-rate-alerts) открыты на отдельном экране. -
Деплой тега. CI/CD раскатывает образ в prod через Helm / kustomize. Типичная команда (пример на GitOps-инструменте используется тот, что в infra-репо):
# пример через Helm, реальные параметры — в infra-репо helm upgrade --install <svc> charts/<svc> \ --namespace backend \ --set image.tag=v1.4.2 \ --wait --timeout 5m -
Rolling mechanics. Kubernetes один за другим создаёт pod’ы новой версии, ждёт их
/readyz=200, снимает трафик со старого pod’а черезpreStop+terminationGracePeriodSeconds(см.../conventions/shutdown), убивает старый. Повторяется, пока все pod’ы не обновлены. -
Точка cut-over прошла, когда в
kubectl get pods -l app=<svc> -n backendвсе реплики имеют новыйimageи статусRunning/READY 1/1:kubectl get pods -l app=<svc> -n backend \ -o jsonpath='{range .items[*]}{.spec.containers[0].image}{"\t"}{.status.phase}{"\n"}{end}' -
Отметить в runbook-чате. Одно сообщение: «deploy
<svc>vX.Y.Zпрошёл, watching SLO 30 мин». Не «раскатил, спасибо, всем хорошего дня».
Миграции и expand-contract
Порядок миграций и кода — всегда expand-contract (см.
../conventions/db-pgx#expand-contract).
Для деплоя это значит:
- Добавляем колонку / таблицу NOT NULL — в два релиза: сначала
миграция
ADD COLUMN NULLABLE+ код, который пишет новое поле, выкатывается. Потом (следующий релиз / через сутки-неделю) миграцияALTER SET NOT NULL. - Удаляем колонку / таблицу — сначала релиз, где код
перестал читать/писать колонку, затем — миграция
DROP. Никогда не наоборот. - Переименование — expand + backfill + contract.
Практическая развилка на деплое:
- Миграция применяется перед новым кодом, если она expand (добавляет nullable) — старый код её переживёт, новый сможет использовать.
- Миграция применяется после нового кода, если она contract (удаляет / добавляет constraint) — старый код уже выключен.
В коде сервиса migrate-runner запускается на старте (см.
../how-to/add-migration), защищён
advisory lock’ом. Первый pod новой версии применит expand-
миграцию; остальные подхватят уже применённую схему.
Если миграция требует большого backfill’а (минуты-часы), она отделяется от деплоя — гонится отдельным CronJob / job’ом перед деплоем. Деплой ждёт её завершения.
Наблюдение после cutover
30 минут минимум автор релиза на связи, смотрит:
-
SLO burn-rate (fast-burn / slow-burn, см.
../conventions/slo-and-budget). Любое срабатывание fast-burn alert’а через 5–15 мин после cutover’а — кандидат на откат, не на «подождём». -
Error-rate 5xx по endpoint’ам релиза. Базовое:
sum by (route, code) (rate(http_requests_total{service="<svc>",code=~"5.."}[5m])) -
Латенси — p95/p99 по ключевым endpoint’ам. Регрессия на 30%+ — повод разобраться здесь и сейчас.
-
Логи в Loki (см.
read-logs) — нет ли новыхlevel=errorпаттернов. -
Outbox / consumer freshness —
outbox_unpublished_rows,kafka_consumer_lag_messages. Если деплой затронул publisher или handler — следи за этими метриками прицельно. -
Ресурсы pod’а — CPU, RSS. Резкий рост RSS у новой версии = memory leak (см.
../troubleshooting/memory-leak).
Если за 30 минут ничего не деградировало — деплой успешен,
уведомление в канал: «deploy <svc> vX.Y.Z: all green».
Если деградация — rollback-deploy.
Canary-паттерны
Rolling update по умолчанию — все pod’ы одновременно получают трафик по мере ready. Для высокорисковых изменений этого мало; нужен canary — часть трафика идёт на новую версию, большая часть — на старую, сравниваешь метрики двух групп.
Случаи, когда canary оправдан:
- Поведение под нагрузкой непредсказуемо (новая зависимость, переработан hot-path).
- Изменение в кросс-сервисном протоколе (новый envelope-header, изменение consumer-handler’а).
- Большая миграция state-машины (версия саги, версия outbox- forwarder’а).
У нас canary реализуется через отдельный Deployment с меткой
track=canary и replicas=1 на весь Service. Cilium Gateway API делит
трафик по весам backend’ов в HTTPRoute / по HTTP-header’у (настройки в
infra-репо).
Шаги:
-
Выкатать
canaryDeployment с новой версией,track=canary. -
Перевести 5–10% трафика на canary (weighted routing в Cilium Gateway API или service-mesh, если был).
-
Ждать 30–60 минут, сравнивать метрики canary vs stable по label’у
track:sum by (track) (rate(http_requests_total{service="<svc>",code=~"5.."}[5m])) -
Регрессии нет →
track=canaryпромотируется в stable (увеличить до 100%), старый Deployment удаляется. Есть регрессия → canary масштабируется до 0, trаffic возвращается на stable.
Canary — не замена rolling update, а дополнение. После успешного canary-окна всё равно идёт rolling update всех stable- pod’ов на новую версию.
Feature flags
Для изменений, затрагивающих бизнес-логику, работает правило code-path-gate: новая логика — под feature flag, включается через env / config. Это отделяет деплой от релиза:
- День 1: деплой с новой логикой под флагом
FEATURE_X=false— прод идёт по старому пути. - День N: переключение
FEATURE_X=true— без нового деплоя образа, простоkubectl rollout restartпосле правки ConfigMap (или hot-reload, если сервис поддерживает). - Rollback фичи — снова
FEATURE_X=false, даже без редеплоя.
Флаги живут в config-е сервиса, не в коде как const. Имена —
FEATURE_<AREA>_<NAME> (FEATURE_SEARCH_RERANKER). После полного
включения и успешного прогона 2 недель — флаг и старый код-путь
удаляются отдельным PR.
Когда НЕ деплоить
- Прод-инцидент активен (SEV-2 / SEV-3 открыты). Новый деплой маскирует симптомы первоначального, путает on-call.
- SLO-budget сервиса сгорел (<10% осталось) и релиз не fix-related. Помощи от фичи = 0, риска = много.
- CI зелёный «случайно» (часть тестов пропущена по skip / flakey). Разбираться до деплоя, не после.
- Нет on-call’а на ближайший час.
- Только что выкатился другой сервис, от которого есть runtime-зависимость. Подожди его стабилизации (30 мин) — проще разобраться, чей регресс.
Что не делать
kubectl edit deploymentпрямо в prod. Нефиксируется в git, следующий GitOps-sync откатит твою правку без предупреждения. Все изменения — через PR в infra-репо.kubectl exec -it <pod> -- /bin/shдля «быстро поправить что-то в runtime». Никогда. Состояние pod’а расходится с образом, при рестарте теряется.- Пропускать staging «на маленьких PR». Маленьких PR в этом смысле не бывает — баг в одной строчке кладёт сервис не хуже большой рефакторной ветки.
- Деплоить без тега в prod «вручную, SHA’ей». Теряется версионирование, следующий rollback не знает, куда откатывать.
- Выкатывать миграцию contract вместе с кодом, который ещё использует старое поле. Rolling update → половина pod’ов на старом, половина на новом, старый обращается к несуществующему полю → 500. Всегда expand-первым-релизом.
- Смешивать миграцию и bug-fix в одном релизе. При откате миграции придётся оставить; при форвард-fix’е сама миграция недоживёт до следующей итерации.
Связанные разделы
rollback-deploy— откат релиза, когда деплой пошёл не так.../checklists/production-ready— полный чеклист для первого релиза нового сервиса.../conventions/shutdown— graceful shutdown,terminationGracePeriodSeconds, как это работает в связке с rolling update.../conventions/slo-and-budget— SLO burn-rate alert’ы, на которые смотрим после cutover.../conventions/db-pgx#expand-contract— правило миграций.add-migration— как сделать миграцию с нуля.rollback-migration— как откатить миграцию отдельно от релиза (через forward-fix).../troubleshooting/memory-leak— если после деплоя RSS/goroutine’ы начали расти.