Деплой в Kubernetes и Auto DevOps

Учимся деплоить в Kubernetes из пайплайна и узнаём, что предлагает Auto DevOps.

Auto DevOps — набор готовых шаблонов GitLab, автоматически собирающий, тестирующий, сканирующий и деплоящий приложение в Kubernetes с минимальной настройкой.

Kubernetes стал фактическим стандартом для запуска контейнеров в продакшене, и рано или поздно ваш пайплайн упрётся в вопрос: «как доставить свежесобранный образ в кластер?». На предыдущих уроках мы собирали образ и клали его в реестр — но реестр это лишь склад. Деплой в Kubernetes превращает артефакт со склада в работающий сервис, который принимает трафик пользователей. Этот шаг — мост между «код собрался» и «фича в проде», и именно здесь чаще всего ломается доставка: то нет доступа к кластеру, то джоба зеленеет раньше, чем поды поднялись, то старый образ продолжает крутиться, потому что тег не поменялся.

Важно понимать философию Kubernetes, без которой деплой кажется магией. Kubernetes работает декларативно: вы не командуете «запусти три копии», вы описываете желаемое состояние («я хочу три реплики вот этого образа»), а контроллеры кластера сами приводят реальность к этому описанию. Задача пайплайна — не «развернуть приложение» в императивном смысле, а донести до кластера новое желаемое состояние. Всё остальное (остановка старых подов, запуск новых, проверка готовности) кластер берёт на себя. Поэтому деплой из CI почти всегда сводится к одной из двух операций: либо «поменяй тег образа в существующем объекте», либо «примени вот этот набор манифестов».

Ручной деплой: kubectl и helm

Самый прозрачный способ — вызывать в джобе kubectl или helm, как вы делали бы вручную. Джобе нужен образ с этими утилитами и доступ к кластеру (kubeconfig в защищённой переменной).

deploy-k8s:
  stage: deploy
  image: bitnami/kubectl:latest
  script:
    - kubectl config use-context my-cluster
    - kubectl set image deployment/web web=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
    - kubectl rollout status deployment/web
  environment:
    name: production
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

Здесь kubectl set image обновляет образ деплоймента на свежесобранный (с тегом по SHA), а rollout status ждёт завершения раскатки — это и есть rolling update из прошлого урока. Для более сложных релизов берут helm upgrade с чартом.

Почему тег по SHA, а не latest

Обратите внимание на деталь, которая кажется мелочью, но критична: образ тегируется по $CI_COMMIT_SHORT_SHA, а не по latest. Если деплоить latest, вы получаете два больших зла. Во-первых, Kubernetes не понимает, что образ изменился — манифест-то остался прежним, тег тот же самый, и контроллер не видит причины перезапускать поды (приходится костылить imagePullPolicy: Always и принудительный рестарт). Во-вторых, вы теряете воспроизводимость: непонятно, какая именно версия кода крутится в проде прямо сейчас, и откат превращается в гадание. Уникальный иммутабельный тег решает обе проблемы: смена тега в манифесте — это видимое изменение желаемого состояния, и по тегу всегда восстановим точный коммит. Это золотое правило промышленного деплоя: образы иммутабельны, тег = коммит.

Подключение кластера

GitLab умеет подключать кластер через агент (GitLab Agent for Kubernetes) — безопасный канал, при котором кластер сам соединяется с GitLab, а не наоборот. Это предпочтительнее, чем хранить мощный kubeconfig в переменных. Агент даёт джобам контекст для kubectl без раздачи прямых учёток.

Разница между «агент» и «kubeconfig в переменной» — это разница между pull- и push-моделью доступа, и она важнее, чем кажется. При push-модели (kubeconfig в переменной) ваш раннер хранит ключи от кластера и сам стучится в его API-сервер. Значит, API кластера должен быть доступен из сети раннеров, а полноценный admin-kubeconfig лежит в переменной — украв его, атакующий получает весь кластер. При pull-модели агент — это под, живущий внутри кластера; он сам инициирует исходящее соединение к GitLab и слушает команды. Кластеру не нужен публичный API-эндпоинт, а в GitLab не хранится мощный токен. Схематично:

push-модель (kubeconfig):
  GitLab Runner  --kubeconfig-->  [API-сервер кластера]
    (раннер инициирует, нужен доступ к API, ключи у раннера)

pull-модель (agent):
  [GitLab]  <--исходящее соединение--  Agent (под в кластере)
    (кластер инициирует, API закрыт наружу, токена в GitLab нет)

Для учебного проекта kubeconfig в protected-переменной приемлем. Для боевого кластера с реальными пользователями агент — почти обязательный выбор, особенно если кластер не должен торчать в публичную сеть.

helm для управляемых релизов

deploy-helm:
  stage: deploy
  image: alpine/helm:latest
  script:
    - helm upgrade --install web ./chart
      --set image.tag=$CI_COMMIT_SHORT_SHA
      --namespace prod --wait
  environment:
    name: production

helm upgrade --install применяет чарт идемпотентно: создаст релиз, если его нет, или обновит существующий. Флаг --wait заставляет дождаться готовности подов.

Когда kubectl, а когда helm

Грубое правило: kubectl apply хорош, пока манифестов мало и они почти не зависят от окружения. Как только появляется три окружения (dev/staging/prod) с разными именами хостов, числом реплик и лимитами ресурсов, голые манифесты начинают расползаться в копипасту — те же YAML с мелкими отличиями. Helm решает это шаблонами: один чарт с параметрами (values.yaml) на окружение, и вы передаёте отличия через --set или отдельный values-файл. Бонусом helm ведёт историю релизов: helm rollback web 1 мгновенно откатит на предыдущую ревизию — кластер сам вернёт прошлое желаемое состояние. Для пет-проекта хватит kubectl; для сервиса с несколькими окружениями и потребностью в быстром откате helm экономит много нервов.

Auto DevOps: всё из коробки

Auto DevOps — противоположный полюс: вы почти ничего не пишете, GitLab сам строит пайплайн build → test → SAST → review app → staging → production по соглашениям. Подходит для типовых приложений и быстрого старта, но для нестандартных случаев его обычно отключают в пользу явного .gitlab-ci.yml. Знать о нём полезно: многие шаблоны GitLab (Code Quality, SAST) — кирпичики того же Auto DevOps.

Здесь полезно сравнить подход GitLab с GitHub Actions, если вы пришли оттуда. В GitHub Actions нет встроенного аналога Auto DevOps: вы либо пишете workflow руками, либо собираете его из готовых actions из Marketplace (например, azure/k8s-deploy, helm-actions). Деплой в кластер там — это тоже kubectl/helm в шаге, плюс настройка доступа через секрет с kubeconfig или OIDC-федерацию с облаком. Ключевые отличия: у GitLab есть родной GitLab Agent (pull-модель из коробки) и встроенные Environments с историей деплоев и кнопкой отката; в GitHub окружения (Environments) есть тоже, но связка с кластером собирается из сторонних кирпичей. Auto DevOps — фишка именно GitLab: целостный «всё-в-одном» путь от коммита до прода без написания YAML, чего у Actions концептуально нет.

Как работает под капотом

Деплой в Kubernetes — это, по сути, обновление объектов кластера (Deployment, Service) через API. kubectl/helm в джобе аутентифицируются (через агент или kubeconfig) и шлют кластеру желаемое состояние; контроллеры Kubernetes приводят реальное состояние к желаемому, выполняя rolling update. Auto DevOps просто оборачивает эти шаги в готовые шаблонные джобы.

Заглянем чуть глубже в момент, когда джоба «зеленеет». Когда kubectl set image отрабатывает, API-сервер лишь записывает новое желаемое состояние в etcd и сразу отвечает «ок» — поды ещё даже не начали меняться. Дальше Deployment-контроллер создаёт новый ReplicaSet, постепенно поднимает свежие поды и гасит старые, сверяясь с readiness-пробами. Именно поэтому rollout status и --wait так важны: без них джоба считается успешной в момент «API принял запрос», а не «приложение реально живо». Если новый образ падает на старте, без ожидания пайплайн покажет зелёный, а пользователи увидят пятисотки. С rollout status джоба честно дождётся либо успеха, либо таймаута, и упадёт, если поды не поднялись — что и нужно, чтобы триггерить откат.

Связка с механизмом Environments довязывает картину. Указав environment: name: production, вы регистрируете деплой в GitLab: на странице окружения видно, какой коммит сейчас в проде, когда он туда попал и кто это сделал. Это не косметика — отсюда же доступна кнопка отката на предыдущий деплой и связь с review apps. По сути окружение — это журнал «что и когда мы выкатили», который ведётся автоматически по факту прохождения деплой-джоб.

Частые ошибки

  • Хранить полный admin-kubeconfig в обычной (не protected) переменной — серьёзный риск.
  • Не дожидаться rollout status/--wait — джоба зеленеет до того, как поды реально поднялись.
  • Включить Auto DevOps поверх своего сложного пайплайна и получить конфликтующие джобы.
  • Деплоить тег latest — кластер не видит изменения, поды не перезапускаются, версия в проде неизвестна.
  • Давать деплой-джобе на прод доступ без when: manual или защиты окружения — любой merge сразу едет в продакшен.
  • Забыть про namespace и нечаянно раскатить staging-релиз поверх продового деплоймента.

Итоги

  • Деплой в Kubernetes — вызов kubectl/helm из джобы с доступом к кластеру.
  • GitLab Agent безопаснее, чем kubeconfig в переменных (pull-модель против push).
  • Образ тегируем по коммиту, а не latest — иначе кластер не увидит изменения.
  • Всегда дожидаемся раскатки через rollout status/--wait, иначе джоба врёт о готовности.
  • Auto DevOps даёт готовый пайплайн «из коробки» по соглашениям; для нестандартного — явный файл.
Проверьте себя
1. Что делает Auto DevOps?
AШифрует переменные
BАвтоматически строит готовый пайплайн build/test/scan/deploy в Kubernetes по соглашениям
CЗаменяет GitLab Runner
DУправляет тегами раннеров
2. Почему GitLab Agent предпочтительнее kubeconfig в переменных?
AОн быстрее деплоит
BКластер сам соединяется с GitLab, не нужно хранить мощный kubeconfig, что безопаснее
CОн не требует Docker
DОн работает только с helm