Перейти к содержанию

Доставка изменений

Проверки и выкладка разделены сознательно. Проверять код можно где угодно, и это должно происходить на каждый запрос на слияние. Выкладка требует доступа к кластеру, поэтому живёт рядом с ним.

flowchart LR
    dev["Коммит<br/>в ветку"] --> gh["GitHub Actions"]

    subgraph gh_jobs["Проверки, параллельно"]
        j1["Тесты Java"]
        j2["Сборка и анализ<br/>интерфейса"]
        j3["Проверка манифестов<br/>по схемам"]
        j4["terraform validate<br/>ansible syntax-check"]
    end

    gh --> j1 & j2 & j3 & j4
    j1 & j2 & j3 --> img["Публикация образов<br/>в ghcr.io + Trivy"]

    dev -.->|ручной запуск| jenk["Jenkins<br/>агент рядом с кластером"]
    jenk --> build["Сборка образа<br/>на узле"]
    build --> apply["kubectl apply -k"]
    apply --> canary["Argo Rollouts:<br/>канарейка"]
    canary -->|проверки пройдены| ok["Версия принята"]
    canary -->|порог нарушен| back["Откат<br/>без человека"]

Проверки в GitHub Actions

Четыре независимые задачи выполняются параллельно, потому что у них нет общих зависимостей: тесты сервиса, сборка интерфейса вместе со статическим анализом, проверка собранных манифестов по схемам Kubernetes и сторонних определений, проверка описания инфраструктуры.

Проверка манифестов заслуживает отдельного слова. kustomize build собирает всё дерево deploy/, а kubeconform сверяет результат со схемами — включая Rollout, Middleware и ServiceMonitor, схемы которых берутся из каталога сторонних определений. Опечатка в имени поля обнаруживается до применения в кластере, а не по молчаливому игнорированию неизвестного ключа.

Образы собираются один раз, публикуются в ghcr.io с тегом по хешу коммита и проверяются Trivy на уязвимости высокой и критической важности. Отчёт уходит в раздел безопасности репозитория. Найденная уязвимость не роняет сборку: на стенде это привело бы к бессмысленно красным запускам из-за уязвимостей в базовом образе, которые ещё не исправлены авторами. Разбирать их нужно, но осознанно.

Выкладка

Выкладку запускает scripts/deploy.sh — тот же скрипт, который вызывает Jenkins. Порядок шагов:

  1. Сборка образа на узле. У стенда нет реестра, поэтому образ собирается buildah и импортируется прямо в containerd узла. Конвейер в GitHub Actions собирает те же образы в ghcr.io, но для одноузлового стенда лишний обмен через реестр — это только трата места на диске.
  2. Подстановка тега. kustomize edit set image меняет версию в kustomization.yaml. Манифест остаётся описанием желаемого состояния, а версия — параметром выкладки.
  3. Сухая сборка. kustomize build без применения: если манифесты сломаны, это выяснится до того, как кластер получит частичное изменение.
  4. Применение и ожидание. Скрипт следит за состоянием релиза и завершается ошибкой, если контроллер объявил версию негодной.

Канареечный релиз

Argo Rollouts ведёт новую версию тремя шагами: 34 % — пауза 90 секунд — 67 % — пауза 60 секунд — 100 %. На каждом шаге контроллер запускает анализ по трём запросам к Prometheus, отобранным по метке хеша шаблона пода, то есть только по новым подам:

Проверка Условие приёмки
доля ответов с ошибкой сервера не больше 2 %
95-й перцентиль времени ответа не больше 500 мс
перезапуски контейнеров ни одного за три минуты

Каждая проверка выполняется четыре раза с интервалом 30 секунд. Для первых двух одна неудача допускается: при небольшом трафике единичный сбой ещё ничего не значит. Две неудачи означают, что новая версия хуже прежней, — контроллер возвращает вес в ноль и оставляет трафик на старых подах. Перезапуск контейнера не прощается ни разу: под, который упал на старте, не станет здоровее от повторной попытки. Человек в этот момент не нужен.

Проверено на стенде

Механизм проверен не рассуждением, а опытом. В шаблон пода новой версии был подставлен неверный адрес базы данных — версия, которая гарантированно не поднимется:

$ kubectl -n metroskop patch rollout metroskop --type=json \
    -p '[{"op":"replace","path":"/spec/template/spec/containers/0/env/0/value",
          "value":"jdbc:postgresql://postgres.metroskop.svc:5432/does_not_exist"}]'

Через 61 секунду контроллер сам отказался от неё:

Degraded: RolloutAborted: Rollout aborted update to revision 4:
Background analysis phase error/failed:
Metric "pods-restarting" assessed Failed due to failed (1) > failureLimit (0)

Итог анализа по трём показателям:

error-rate       Successful   успешных 3
latency-p95      Successful   успешных 2
pods-restarting  Failed       успешных 2   неудачных 1

Поды прежней версии в это время не были затронуты, и внешний адрес всё время отвечал кодом 200. Человек в происходившем не участвовал: патч был применён, и через минуту стенд вернулся к работавшей версии сам.

Честно про деление трафика

Вес задаётся долей реплик, а не точным делением запросов: за одним Service стоят поды обеих версий, и распределение получается пропорциональным их числу. Точное деление требует внешнего маршрутизатора — Istio или Traefik в режиме разделения весов. Для двух-трёх реплик разница несущественна, и лишний слой не оправдан. Подробно — решение 0004.

Почему и Jenkins, и GitHub Actions

Это не дублирование, а разделение ролей. GitHub Actions даёт бесплатных исполнителей, отчёты в запросах на слияние и публикацию образов — всё, что нужно для проверки кода. Jenkins стоит там, где нужен агент с доступом к кластеру и файлу доступа, который нельзя отдавать облачному исполнителю.

Файл Jenkinsfile в репозитории описывает конвейер полностью: параметры запуска, этапы, разбор отчётов тестов, проверку внешних адресов после выкладки и сбор состояния релиза при неудаче. Сам Jenkins на стенде не запущен — 700 МБ памяти под сервер сборки на узле с 4 ГБ означали бы отнять их у Prometheus. Конвейер описан кодом, и это то, что подлежит проверке; запуск занял бы полчаса на машине, где память есть.

Обновление платформы

Платформа обновляется отдельно от приложения — terraform apply в каталоге terraform/. Версии всех чартов закреплены в переменных, поэтому повторный запуск через месяц даёт то же состояние, а не «последние версии на сегодня». Состояние Terraform хранится в секрете кластера с блокировкой через объект аренды: см. решение 0005.