Helm в CI/CD: автоматический деплой

Связываем всё воедино: как Helm катит приложение из пайплайна автоматически и безопасно.

В CI/CD деплой сводится к одной идемпотентной команде helm upgrade --install с фиксированными версиями и флагами безопасности (--wait --atomic --timeout), запускаемой после сборки и пуша образа.

Типовой пайплайн

build → test → push образа в реестр → helm upgrade --install → smoke (helm test)

Ключевая связь между сборкой и деплоем — тег образа. Обычно это git-SHA коммита: образ собирается с тегом SHA, и тем же SHA Helm разворачивает его. Так деплой всегда соответствует ровно той сборке, что прошла пайплайн.

Почему именно SHA, а не номер сборки CI или семантическая версия? SHA однозначно указывает на конкретное состояние репозитория и доступен в любом окружении пайплайна как переменная. Номер сборки CI привязан к конкретной системе и сбрасывается при миграции раннеров; семантическая версия удобна для релизов наружу, но во внутреннем пайплайне между коммитом и тегом образа должна быть строгая взаимно-однозначная связь, а её проще всего обеспечить именно через SHA. На практике теги часто комбинируют — образ помечают и SHA, и человекочитаемой версией, но в Helm подставляют именно иммутабельный SHA, чтобы кластер всегда знал точную сборку.

Отдельно держите в голове порядок стадий. Деплой должен идти строго после того, как образ реально оказался в реестре, иначе Kubernetes начнёт тянуть несуществующий тег и поды зависнут в ImagePullBackOff. Поэтому стадия push идёт до deploy, а сам деплой не считается успешным, пока поды не поднялись — за это и отвечает --wait.

GitLab CI

deploy:prod:
  stage: deploy
  image: alpine/helm:3.14.0      # фиксированная версия Helm!
  script:
    - helm upgrade --install web ./charts/webapp
        --namespace prod --create-namespace
        -f deploy/values/base.yaml
        -f deploy/values/prod.yaml
        --set image.tag="$CI_COMMIT_SHA"
        --wait --atomic --timeout 5m
  environment:
    name: production
  rules:
    - if: '$CI_COMMIT_TAG'        # деплой только по тегу-релизу

GitHub Actions

- name: Deploy
  run: |
    helm upgrade --install web ./charts/webapp       --namespace prod --create-namespace       -f deploy/values/base.yaml -f deploy/values/prod.yaml       --set image.tag="${{ github.sha }}"       --wait --atomic --timeout 5m

Почему именно --atomic в CI

Пайплайн не сидит у экрана и не откатит вручную. --atomic делает деплой «всё или ничего»: если новые поды не стали готовы за --timeout, Helm сам откатит релиз на прошлую рабочую ревизию, и пайплайн упадёт с понятной ошибкой, а прод останется на старой версии. Это превращает рискованный авто-деплой в безопасный.

Важно правильно выбрать --timeout. Слишком короткий — и медленный, но здоровый старт (прогрев JIT, миграции, подтягивание тяжёлого образа) ошибочно посчитают провалом, вызвав ненужный откат. Слишком длинный — и пайплайн будет минутами висеть на действительно сломанном релизе, прежде чем сдаться. Ориентир — реальное время выхода пода в Ready плюс запас на загрузку образа; для большинства веб-приложений 3–5 минут достаточно. Помните и про подводный камень: при использовании --atomic вместе с хук-Job (например, миграцией БД) откат коснётся релиза, но уже выполненные необратимые миграции назад не откатятся — поэтому миграции проектируют обратносовместимыми.

Где брать kubeconfig

Раннеру нужен доступ к кластеру. Стандарт — секрет пайплайна с base64-кодированным kubeconfig (сервисного аккаунта с ограниченными правами на нужный namespace), который раскладывается в $KUBECONFIG перед helm-командой. Права аккаунта ограничивают: деплой-бот не должен иметь cluster-admin.

Принцип наименьших привилегий здесь не формальность. Секрет пайплайна по определению доступен любому, кто может изменить конфиг CI или запустить джобу, поэтому компрометация репозитория не должна автоматически означать компрометацию всего кластера. Дайте сервис-аккаунту права только на тот namespace и только на те типы ресурсов, что реально разворачивает чарт. Полезно завести отдельные аккаунты под staging и prod, чтобы джоба staging физически не могла дотянуться до продакшена. И никогда не печатайте содержимое kubeconfig в лог — пометьте переменную как защищённую/маскируемую, иначе токен утечёт в открытые артефакты сборки.

Push vs Pull (GitOps)

Описанный подход — push: пайплайн сам зовёт Helm и пушит в кластер. Альтернатива — pull/GitOps (Argo CD, Flux): в git лежит желаемое состояние (чарт + values), а оператор в кластере сам подтягивает и применяет изменения. Helm-чарты прекрасно работают и там: Argo CD рендерит чарт через helm template и применяет результат, отслеживая дрейф.

У push-модели есть скрытая цена, которая и породила GitOps. Чтобы пайплайн мог пушить в кластер, ему нужны постоянные сетевые доступы и креды на кластер — а это и есть та самая широкая поверхность атаки. В pull-модели кластер сам тянет изменения изнутри: наружу не открыто ничего, кредов в CI на кластер нет вовсе, а git становится единственным источником истины. Любое расхождение между git и кластером (кто-то поправил ресурс руками через kubectl edit) оператор замечает как дрейф и либо чинит автоматически, либо подсвечивает. Откат в GitOps — это просто git revert: вернули коммит, оператор привёл кластер к прошлому состоянию.

Минус pull-модели — она требует отдельного оператора в кластере и меняет ментальную модель команды: «задеплоить» теперь значит «слить изменение в git», а не «запустить джобу». Для небольших проектов это лишняя инфраструктура, и честный push с helm upgrade --install остаётся прагматичным выбором. Важно, что сам чарт писать по-разному не нужно: один и тот же чартом разворачивается и пушем, и пуллом — меняется лишь то, кто вызывает рендеринг и применение.

МодельКто катитПример
PushCI-раннер вызывает helmGitLab CI, GitHub Actions
Pull (GitOps)оператор в кластереArgo CD, Flux

Как воспроизводимость достигается под капотом

Детерминированный деплой держится на трёх фиксациях: версия Helm (образ alpine/helm:3.14.0), версия чарта (--version или чарт из git по коммиту) и версия образа (image.tag = SHA). Если все три зафиксированы, повтор пайплайна даёт идентичный результат, а откат — это просто повторный деплой прошлого SHA. Идемпотентность upgrade --install позволяет гонять деплой-стадию сколько угодно раз без побочных эффектов: при отсутствии изменений кластер не трогается, при наличии — применяется только дельта (трёхстороннее слияние из урока про жизненный цикл).

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

  • «latest» вместо SHA. Тег latest ломает воспроизводимость и откат — кластер не знает, какой образ реально крутится.
  • Без --atomic/--wait. Авто-деплой «успешен», а поды в CrashLoop — пайплайн зелёный, прод лежит.
  • cluster-admin у деплой-бота. Избыточные права раннера — риск; ограничивайте namespace и действия.

Итог

  • Деплой в CI — идемпотентный helm upgrade --install с тегом образа = git-SHA и --wait --atomic --timeout.
  • Фиксируйте три версии (Helm, чарт, образ) — это даёт воспроизводимость и простой откат.
  • Push-модель (CI зовёт helm) и pull/GitOps (Argo CD/Flux подтягивают) — обе дружат с чартами.
Проверьте себя
1. Почему тег образа в CI делают равным git-SHA, а не latest?
ASHA короче
BЧтобы деплой точно соответствовал сборке и был воспроизводим/откатываем
Clatest запрещён в Kubernetes
DSHA быстрее тянется
2. Что даёт флаг --atomic при авто-деплое из пайплайна?
AУскоряет деплой
BПри неудаче сам откатывает релиз на прошлую рабочую ревизию — деплой «всё или ничего»
CШифрует values
DУдаляет историю
3. Чем pull-модель (GitOps) отличается от push?
AНичем
BВ pull оператор в кластере (Argo CD/Flux) сам подтягивает желаемое состояние из git
Cpush не использует Helm
Dpull не работает с чартами