Доставка изменений¶
Проверки и выкладка разделены сознательно. Проверять код можно где угодно, и это должно происходить на каждый запрос на слияние. Выкладка требует доступа к кластеру, поэтому живёт рядом с ним.
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. Порядок шагов:
- Сборка образа на узле. У стенда нет реестра, поэтому образ собирается
buildahи импортируется прямо в containerd узла. Конвейер в GitHub Actions собирает те же образы вghcr.io, но для одноузлового стенда лишний обмен через реестр — это только трата места на диске. - Подстановка тега.
kustomize edit set imageменяет версию вkustomization.yaml. Манифест остаётся описанием желаемого состояния, а версия — параметром выкладки. - Сухая сборка.
kustomize buildбез применения: если манифесты сломаны, это выяснится до того, как кластер получит частичное изменение. - Применение и ожидание. Скрипт следит за состоянием релиза и завершается ошибкой, если контроллер объявил версию негодной.
Канареечный релиз¶
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.