Хуки: pre/post install, upgrade, Job-миграции
Иногда нужно выполнить действие в определённый момент релиза — например, миграцию БД до выката новой версии. Это работа для хуков.
Хук (hook) — обычный ресурс Kubernetes (чаще всего Job), помеченный аннотацией
helm.sh/hook, который Helm выполняет в заданной фазе релиза, а не вместе с основными ресурсами.
Зачем хуки, если есть обычные ресурсы
На первый взгляд кажется, что Job-миграцию можно просто положить в templates/ как любой другой ресурс — Helm её создаст, Kubernetes выполнит. Проблема в порядке и координации. Обычные ресурсы релиза Helm применяет в один присест, отсортировав по типу (сначала Namespace и CRD, потом ConfigMap и Secret, затем Deployment и так далее), но он не ждёт, пока Job-миграция отработает, прежде чем поднять новый Deployment с кодом. В итоге миграция и новый код стартуют практически одновременно, и код может обратиться к колонке, которой ещё нет. Хук решает ровно это: он вырван из общего потока и встроен в конкретную фазу с гарантией ожидания. «Сделай X, дождись успеха, и только потом продолжай» — вот суть хука, недостижимая обычным ресурсом.
Стоит сразу зафиксировать границу применимости. Хуки — это императивный «побочный эффект» внутри декларативного деплоя, и злоупотреблять ими не стоит. Если действие можно выразить декларативно (через сам ресурс, через init-контейнер, через готовность зависимости), почти всегда лучше так и сделать. Хуки оправданы там, где нужна именно межресурсная последовательность с ожиданием: миграции БД, прогрев кеша, регистрация во внешней системе, дымовые тесты после выката. Чем меньше у вас хуков, тем предсказуемее релиз.
Точки хуков
| Хук | Когда срабатывает |
pre-install | до создания ресурсов при install |
post-install | после создания ресурсов при install |
pre-upgrade | до применения изменений при upgrade |
post-upgrade | после применения при upgrade |
pre-delete / post-delete | до/после удаления при uninstall |
test | при helm test |
Классика: миграция БД через pre-upgrade Job
Миграции схемы должны пройти до того, как поднимется новая версия кода. Идеальный кейс для pre-upgrade (и pre-install для первой установки):
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-migrate
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "0"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["python", "manage.py", "migrate"]
Helm запустит этот Job, дождётся его завершения, и только при успехе продолжит upgrade. Если миграция упала — upgrade останавливается, новый код не выкатывается. Это спасает от ситуации «код ждёт колонку, которой ещё нет».
Разберём аннотации построчно, потому что каждая здесь не случайна. pre-install,pre-upgrade — два события сразу: первый деплой пройдёт мимо pre-upgrade (апгрейдить нечего), поэтому без pre-install миграция при самой первой установке не запустится. backoffLimit: 0 и restartPolicy: Never — критичны: миграция должна выполниться ровно один раз, без автоматических перезапусков; иначе Job, упавший на середине, попытается повторить миграцию поверх частично применённой схемы. hook-weight: "0" задаёт порядок, если хуков несколько. А before-hook-creation в политике удаления решает проблему имени: Job в Kubernetes иммутабелен, и второй upgrade не сможет создать Job с тем же именем, пока старый не удалён.
Что считается «успехом» хука
Helm определяет завершение хука по типу ресурса. Для Job успех — это статус Complete (нужное число успешных завершений по completions), провал — Failed. Для Pod — фаза Succeeded. Для большинства остальных ресурсов (например, ConfigMap-хук, создающий разовую конфигурацию) Helm просто создаёт объект и считает хук успешным сразу — ждать там нечего. Время ожидания управляется тем же --timeout, что и основной деплой: если миграция длится дольше таймаута, Helm сочтёт хук провалившимся, даже если в БД она в итоге докрутится. Для долгих миграций таймаут поднимают осознанно — лучше явно дать «миграция может идти до 30 минут», чем ловить ложный провал на десятой минуте.
Веса: порядок нескольких хуков
Если хуков в одной фазе несколько, порядок задаёт helm.sh/hook-weight (строка-число, по возрастанию). Меньший вес выполняется раньше:
"helm.sh/hook-weight": "-5" # выполнится раньше, чем вес 0
Политика удаления хуков
helm.sh/hook-delete-policy управляет уборкой Job-ов хуков:
before-hook-creation | удалить прошлый хук перед созданием нового (иначе Job с тем же именем конфликтует) |
hook-succeeded | удалить после успеха (чисто) |
hook-failed | удалить после провала (но тогда не посмотреть логи!) |
Частая связка — before-hook-creation,hook-succeeded: убирает прошлый Job и чистит за успешным, но оставляет упавший для разбора логов.
Как хуки работают под капотом
Ресурсы с аннотацией helm.sh/hook Helm исключает из обычного набора манифестов релиза — они не применяются вместе со всеми. Вместо этого Helm в нужной фазе: сортирует хуки по весу, создаёт их по очереди, ждёт готовности/завершения каждого (для Job — успешного завершения), затем применяет политику удаления. Важное следствие: хуки не участвуют в трёхстороннем слиянии и не откатываются при rollback — они вне обычного жизненного цикла ресурсов. Поэтому хук-миграция, которая уже изменила БД, при откате релиза сама по себе не отменится; продумывайте обратную совместимость миграций.
Главный архитектурный вывод: миграции должны быть обратимыми
Из того, что хуки вне rollback, следует практический принцип, который ломает многим картину мира: откат релиза не откатывает базу. Вы нажали helm rollback, поды с новым кодом сменились на старые, но колонка, которую добавила миграция, никуда не делась — и хорошо, если старый код её просто игнорирует. А если миграция удалила колонку, на которую опирается старый код, то откат вернёт сломанный код в сломанную против него схему. Отсюда дисциплина «expand and contract»: разносите изменение схемы и изменение кода на разные релизы. Сначала релиз, который добавляет новую колонку, но код умеет жить и без неё (expand). Потом релиз, где код начинает её использовать. И только спустя время, убедившись, что откатываться не придётся, — релиз, который удаляет старое (contract). При такой схеме любой одиночный откат безопасен, потому что соседние версии кода совместимы с одной и той же схемой.
Тот же принцип объясняет, почему упавший хук опасно оставлять без разбора и почему важно мониторить его логи. Если pre-upgrade миграция упала на полпути — часть DDL применилась, часть нет — БД оказывается в промежуточном состоянии, которого не было ни в одной ревизии Helm. Helm тут не помощник: он знает только про свои снимки манифестов, а не про содержимое таблиц. Поэтому миграционный инструмент (Alembic, Flyway, Django migrations) должен сам уметь накатывать изменения транзакционно или идемпотентно, чтобы повторный запуск хука после починки довёл схему до конца, а не сломал её ещё больше.
Частые ошибки
- Гонка хуков и кода. Без
--waitна основном деплое и при неверной фазе миграция может пойти параллельно новому коду. Используйтеpre-upgrade, чтобы гарантировать «миграция до кода». - Удаление упавшего хука. С
hook-failedлоги провала исчезнут; для отладки оставляйте упавший Job. - Ожидание отката миграции. Rollback релиза не откатывает то, что хук уже сделал в БД.
Итог
- Хук — ресурс с аннотацией
helm.sh/hook, выполняемый в заданной фазе; типичный кейс —pre-upgradeJob-миграция. hook-weightзадаёт порядок,hook-delete-policy— уборку; оставляйте упавший хук для логов.- Хуки вне трёхстороннего слияния и rollback — миграции делайте обратимыми/совместимыми.