Императивный скрипт и декларативный манифест: в чём разница
Короткий ответ: всё, что описывает целевое состояние объекта Kubernetes (Deployment, Service, ConfigMap, HPA), держите в манифестах и применяйте через kubectl apply или Helm. Всё, что представляет собой разовое действие или подготовку окружения (установка CNI, миграции базы, проверка внешних зависимостей, первичный namespace и RBAC), выносите в скрипт. Смешивать подходы можно, но у каждого инструмента должна быть чёткая зона ответственности.
Скрипт задаёт последовательность действий: собрать образ, создать namespace, применить файл, дождаться готовности. Манифест задаёт желаемое состояние объекта: три реплики Deployment с конкретным образом, Service на порту 8080, лимит памяти 512Mi. Разница становится очевидной на втором запуске.
Императивный вариант: kubectl create deployment nginx --image=nginx. Повторный запуск вернёт ошибку AlreadyExists, а фактическое состояние кластера скрипт не проверяет. В Docker та же логика: docker run -d --name web -p 80:80 nginx упадёт при повторе из-за занятого имени контейнера.
Декларативный вариант: файл deployment.yaml с полями apiVersion: apps/v1, kind: Deployment, spec.replicas: 3, применённый командой kubectl apply -f deployment.yaml. Повторный apply не ломает ресурс, а приводит его к состоянию из файла: лишние поды удаляются, недостающие создаются. Это идемпотентность, и на ней держится вся работа с Kubernetes.
Почему Kubernetes построен вокруг декларативной модели
Внутри кластера работает kube-controller-manager с набором контроллеров. Каждый контроллер крутит цикл согласования (reconciliation loop): читает желаемое состояние объекта из API, сравнивает с фактическим и убирает расхождение. Вы не командуете «создай под», вы описываете «нужно три пода с этим образом». ReplicaSet сам создаст или удалит поды, а упавший под заменит без вашего участия. Так работает self-healing, о котором говорят в контексте Kubernetes, и по той же схеме действуют HPA, Job, Service и остальные объекты.
Императивные kubectl create и kubectl run остаются в арсенале: они удобны для отладки и быстрых проверок, когда нужно за секунду поднять тестовый под в отдельном namespace. Для продакшн-деплоя они не годятся: после завершения команды состояние никто не контролирует, а истории изменений в Git нет. Структуру YAML с полями apiVersion, kind, metadata и spec разбираем в гайде по манифестам Kubernetes.
Когда императивный скрипт всё ещё уместен
Есть класс задач, которые не сводятся к состоянию объекта Kubernetes. Здесь скрипт объективно удобнее:
- подготовка кластера: установка CNI, ingress-контроллера, storage class, cert-manager;
- миграции базы перед деплоем;
- создание namespace и RBAC-привязок при онбординге команды;
- проверка внешних зависимостей: доступность registry, DNS, порта базы.
Пример: скрипт ждёт готовности базы циклом pg_isready -h db.internal -p 5432 -t 5 и запускает миграции только после успешного ответа, а при таймауте выходит с кодом 1 и останавливает пайплайн. Такой скрипт должен быть идемпотентным (повторный запуск не ломает окружение) и завершаться с ненулевым кодом при любой ошибке. Типовую структуру из пяти этапов, работу с переменными окружения и проверками предусловий разбираем в статье про скрипт развёртывания и его структуру.
Как выбрать подход под конкретную задачу: матрица решений
Критерий выбора один: описывает ли задача целевое состояние объекта Kubernetes. Если да, это манифест, при необходимости параметризованный через Helm. Если задача сводится к разовому действию или подготовке окружения, это скрипт.
| Задача | Рекомендуемый подход | Почему |
|---|---|---|
| Подготовка кластера: CNI, ingress, storage class, cert-manager | Скрипт (или Ansible) | Разовые действия вне модели состояния приложения, порядок шагов важен |
| Деплой приложения | Манифест или Helm-чарт | Нужен контроль состояния, версионирование и откат |
| Миграции базы данных | Скрипт или Kubernetes Job | Разовое действие, важен порядок и ожидание готовности СУБД |
| Управление секретами | Объект Secret плюс внешний менеджер (Vault, SOPS, Sealed Secrets) | Доставка в под декларативна, а хранение значений остаётся вне Git |
| Масштабирование под нагрузкой | Манифест (HPA) | Целевое состояние ресурса, контроллер держит его сам |
| Проверка здоровья | probes в манифесте плюс скрипт проверки после релиза | Kubernetes следит за состоянием, скрипт подтверждает результат деплоя |
| Namespace и RBAC при онбординге команды | Скрипт либо манифест в GitOps-репозитории | Разовая операция, но при GitOps нужен единый источник правды |
Helm-чарты это надстройка над манифестами: шаблоны с параметрами и версия релиза, которую видно в Git и в истории деплоев. Откат выполняется командой helm rollback my-app 3 -n prod, а не восстановлением YAML из бэкапа.
Подготовка кластера и инфраструктуры
Последовательность в скрипте выглядит так: проверить наличие инструментов command -v kubectl и command -v helm, установить CNI (Calico или Cilium) командой helm upgrade --install, дождаться готовности нод через kubectl wait --for=condition=Ready nodes --all --timeout=300s, затем поставить ingress-контроллер и cert-manager. Каждый шаг логируйте с таймстампом, иначе на разборе инцидента не восстановить, где всё встало.
Идемпотентность здесь критична. helm upgrade --install при повторе вернёт тот же релиз и обновит его при изменении values, а kubectl create namespace prod на втором запуске упадёт с AlreadyExists. Для namespace используйте kubectl apply -f namespace.yaml либо проверку kubectl get ns prod или kubectl create ns prod. Скрипт, который валится на втором прогоне, бесполезен в CI.
Миграции баз данных
Миграция это разовое действие, поэтому Deployment для неё плохой инструмент: при трёх репликах приложения миграция выполнится трижды и параллельно. Рабочие варианты: отдельный шаг пайплайна, Kubernetes Job с initContainer, который ждёт базу, либо pre-upgrade hook в Helm. Согласно документации по Helm-хукам, хуки позволяют запускать Kubernetes Jobs на определённых этапах жизненного цикла релиза, а флаг --no-hooks отключает pre/post upgrade hooks.
Пример Job: initContainer на образе postgres:16-alpine выполняет pg_isready -h db.internal -p 5432 -t 5 в цикле, основной контейнер запускает alembic upgrade head, flyway migrate или liquibase update. Проверка завершения: kubectl wait --for=condition=complete job/db-migrate -n prod --timeout=300s. Версия схемы фиксируется в том же коммите, что и код приложения.
Держите миграции обратно совместимыми: сначала добавьте колонку и заполните её, потом переключите код на новое поле, и только через релиз удаляйте старое. Иначе поды предыдущей версии во время rolling update начнут падать на отсутствующем поле. Важное ограничение: откат релиза не откатывает уже применённые миграции базы данных. В документации helm upgrade описан флаг --rollback-on-failure: если он установлен, Helm откатит релиз к предыдущему успешному релизу при сбое, а --wait по умолчанию станет "watcher". Но это откат Kubernetes-объектов, а не схемы СУБД: необратимые изменения (удаление колонки, переименование) выносите в отдельный релиз после стабилизации и предусматривайте отдельный план отката данных.
Управление секретами
Secret в Kubernetes хранит значения в base64, и это кодирование, а не шифрование. Согласно рекомендациям по работе с Secret, значения Secret кодируются как base64-строки и по умолчанию хранятся незашифрованными, хотя их можно настроить для шифрования at rest. Любой, у кого есть доступ к ресурсу Secret, может декодировать его значение, поэтому пароли не держат в Git ни в .env, ни в открытом Secret.
Рабочая схема: значение живёт в Vault или в зашифрованном файле SOPS, а скрипт подготовки достаёт его и создаёт объект Secret перед деплоем. Приложение ссылается на Secret через envFrom или volume, то есть доставка остаётся декларативной. Vault с injector подставляет значения прямо в поды, Sealed Secrets и SOPS с age позволяют хранить зашифрованный файл в репозитории.
RBAC ограничивает чтение: роль для CI-учётки разрешает get и create только для secret в конкретном namespace. Проверка: kubectl auth can-i get secrets -n prod --as system:serviceaccount:ci:deployer. Если команда отвечает yes там, где должна отвечать no, права нужно сузить. Дополнительно настройте шифрование данных Secret at rest в etcd и аудит событий: например, алерт на одновременное чтение нескольких Secret одним пользователем помогает заметить подозрительную активность.
Типичные ошибки при смешивании скриптов и манифестов
Пять сценариев, которые чаще всего ломают прод: ручные правки через kubectl edit, исчезающие после следующего apply; секреты в Git; два инструмента управляют одним объектом (скрипт создал, манифест перезаписал); скрипт без идемпотентности; миграции, запущенные до готовности базы. Управление десятками разрозненных кластеров вручную ведёт к конфигурационным ошибкам, о чём прямо говорят в описании платформы «Боцман», поэтому единый источник правды для манифестов обязателен.
Конфликт ручных изменений и декларативного apply
Согласно документации по декларативному управлению объектами, kubectl apply -f устанавливает на каждый объект аннотацию kubectl.kubernetes.io/last-applied-configuration, содержащую содержимое конфигурационного файла, использованного для создания объекта. При следующем apply используется трёхстороннее слияние на клиенте: сравниваются last-applied (из аннотации), new (новый манифест) и current (текущее состояние в кластере). Diff между last-applied и new применяется к current через Strategic Merge Patch, поэтому поля, которые вы не трогали, сохраняются, а добавленные или изменённые — обновляются.
Отсюда и типичный симптом: инженер вручную поднял replicas с 3 до 5, а после релиза снова видит 3 и считает, что «Kubernetes сам сбросил настройку». Ручные правки в spec, не отражённые в манифесте, при следующем apply затираются. Отдельная ловушка: если кто-то сделал Update или Patch мимо kubectl apply, аннотация last-applied-configuration устаревает, и следующий kubectl apply работает с устаревшим представлением о «вашем» состоянии.
Решение: запретить ручные изменения в проде и включить GitOps. Argo CD или Flux берут манифесты из Git и возвращают ресурс к описанному состоянию, а расхождение видно в веб-интерфейсе и алертах. Перед применением проверяйте diff: kubectl diff -f deployment.yaml показывает, что именно изменится. Правьте манифест и коммит, а не живой объект. Для строгой модели владения полями есть Server-Side Apply: он переносит three-way merge в apiserver и добавляет модель «кто чем владеет» (per-field ownership); фича в GA с Kubernetes 1.22.
Секреты в Git и способы их защиты
Файл .env или Secret с base64-значениями, закоммиченный в репозиторий, это прямая утечка: base64 декодируется одной командой, а история коммитов живёт годами. Схема защиты: SOPS с age или GPG, Sealed Secrets либо Vault.
Пример с SOPS: в репозитории лежит secrets.enc.yaml, расшифровка идёт в CI командой sops -d secrets.enc.yaml | kubectl apply -f -, а ключ шифрования хранится вне репозитория (в Vault, KMS или в защищённых переменных CI). Дополнительно прогоняйте историю коммитов сканером (gitleaks, trufflehog): он ловит случайно попавшие токены. Если утечка уже случилась, сначала ротация секрета и только потом чистка истории, иначе старый ключ останется действительным.
Неидемпотентные скрипты и race condition
Скрипт должен проверять состояние перед действием. Вместо kubectl create namespace prod используйте kubectl apply -f namespace.yaml либо проверку kubectl get ns prod или kubectl create ns prod. Тот же принцип для RBAC, ServiceAccount и registry-секретов: сначала чтение, потом создание или обновление.
Классический race condition при деплое: скрипт запускает миграции, а база ещё поднимается после переключения или восстанавливается из бэкапа. Лечится wait-циклом с pg_isready и жёстким таймаутом, после которого скрипт падает. Добавьте set -euo pipefail в начало bash-скрипта и явный exit 1 в обработчиках ошибок: CI обязан остановиться на первом сбое, а не «доиграть» пайплайн с поломанным окружением. Готовый каркас с этапами и проверками есть в материале про скрипт развёртывания приложения в Kubernetes.
Как выстроить CI/CD-пайплайн: место скрипта и манифеста
Скрипт и манифест не конкурируют, если у каждого есть отдельный шаг с понятным входом и выходом. Скрипт готовит окружение и запускает разовые действия, манифест описывает приложение, а проверка подтверждает результат.
Схема пайплайна с разделением ответственности
- Commit и CI. Сборка образа docker build -t registry.example.com/my-app:$GIT_SHA ., тесты и сканирование на уязвимости, публикация docker push. Тег это SHA коммита, не latest.
- Подготовка окружения (скрипт). Проверка namespace, доставка секретов из Vault или SOPS, проверка доступности базы и registry. Шаг идемпотентный, логирует каждое действие.
- Миграции (Job или скрипт). kubectl apply -f job-migrate.yaml, затем kubectl wait --for=condition=complete job/db-migrate -n prod --timeout=300s.
- Деплой (Helm или kubectl apply). helm upgrade --install my-app ./charts/my-app -n prod -f values-prod.yaml --atomic --timeout 5m. Флаг --atomic откатывает релиз, если поды не поднялись.
- Проверка (скрипт). kubectl rollout status deployment/my-app -n prod --timeout=120s и curl -f https://app.example.com/health.
В GitOps-модели шаги 4 и 5 меняются: Argo CD синхронизирует манифесты из Git, а скрипты подготовки и миграции выполняются как pre-sync hooks и sync waves. Согласно документации Argo CD по фазам и волнам синхронизации, Argo CD применяет ресурсы по фазам: сначала применяются все ресурсы, помеченные как PreSync hooks, и если какой-либо из них падает, весь процесс синхронизации останавливается и помечается как failed. Sync waves задаются аннотацией argocd.argoproj.io/sync-wave: значение — целое число, задающее порядок (Argo CD начинает с наименьшего и заканчивает наибольшим), по умолчанию hooks и ресурсы находятся в wave 0. При синхронизации Argo CD упорядочивает манифесты по фазе, затем по wave, затем по kind (Namespaces первыми, затем services, затем deployments и т.д.) и по имени в возрастающем порядке. PreSync hook может использоваться, например, для резервной копии базы данных перед изменением схемы. Порядок остаётся тем же, меняется только исполнитель: вместо раннера пайплайна команды kubectl отдаёт контроллер Argo CD. Если выбираете между подходами целиком, пригодится сравнение систем развёртывания приложений по скорости запуска, гибкости и стоимости сопровождения.
Проверка результата развёртывания
Минимальный набор команд для проверки после релиза:
- kubectl get pods -n prod: статус Running, READY 1/1, отсутствие рестартов;
- kubectl describe pod my-app-7d9f8c4b5d-abcde -n prod: события и причины CrashLoopBackOff;
- kubectl logs deploy/my-app -n prod --tail=100: ошибки старта приложения;
- kubectl rollout status deployment/my-app -n prod --timeout=120s: завершение обновления;
- curl -f https://app.example.com/health: код ответа 200.
Health-check должен проверять зависимости, а не только живость процесса: подключение к базе, доступность кэша, наличие обязательных переменных окружения. Скрипт проверки ждёт успешного rollout в цикле до таймаута и завершается с кодом 1, если поды не поднялись. Тогда пайплайн помечает релиз красным, а не отдаёт трафик на нерабочую версию.
Безопасность и воспроизводимость: чек-лист перед деплоем
Чек-лист безопасности перед деплоем
- Образы с фиксированными тегами или digest, не latest. Проверка: kubectl get pods -o jsonpath='{.items[*].spec.containers[*].image}'.
- Секретов нет в Git и в открытых значениях. Проверка: kubectl get secrets -n prod и сканер gitleaks по истории коммитов.
- RBAC с минимальными правами. Проверка: kubectl auth can-i --list --as system:serviceaccount:prod:my-app.
- Заданы requests и limits по CPU и памяти, иначе под вытеснит соседей на ноде.
- Настроены readiness и liveness probes с разумными порогами: readiness снимает под с балансировки, liveness перезапускает зависший процесс.
- Включены NetworkPolicy: входящий трафик только от ingress-контроллера и нужных сервисов.
- Образы просканированы на уязвимости до публикации в registry.
Важно понимать границы: базовый Kubernetes из коробки не закрывает все жёсткие требования регуляторов по защите закрытых контуров, аудиту и оперативному сканированию образов на уязвимости, о чём сказано в обзоре платформы «Боцман». Для развёртывания и поддержания распределённых кластеров нужны дорогостоящие инженеры уровня Senior DevOps, а платформа «Боцман» из экосистемы Группы «Астра» позиционируется как средство сделать работу с Kubernetes прозрачной, управляемой и безопасной. Это не отменяет чек-лист, но объясняет, почему часть команд выбирает готовые платформы вместо vanilla-кластера.
Практический ориентир для усиления — отраслевые бенчмарки и гайды по харденингу. Например, CIS Hardening Guide для K3s содержит предписывающие рекомендации по усилению безопасности production-установки и описывает конфигурации и контроли, требуемые для соответствия контролам Kubernetes benchmark от Center for Internet Security (CIS). Из него видно, что часть настроек не включается по умолчанию: K3s не включает pod security и network policies, не включает аудит (конфигурация audit log и audit policy создаются вручную) и не модифицирует хост-ОС — host-level изменения нужно делать вручную. Для соответствия CIS Benchmark требуется ручное вмешательство: определённые CIS policy controls (admission plugins) ограничивают функциональность кластера, и нужно явно включить их через флаги командной строки или конфигурационный файл, а также вручную применить соответствующие политики. Точный набор требований зависит от вашего регулятора и редакции бенчмарка, поэтому сверяйтесь с актуальной версией документа для вашей версии дистрибутива.
Воспроизводимость: почему версии важны
Деплой воспроизводим, когда все версии зафиксированы: тег образа (лучше digest), версия Helm-чарта, версии зависимостей из Chart.yaml, версия самого Helm. Структура чарта одна, а файлы values-dev.yaml и values-prod.yaml отличаются только параметрами: количество реплик, ресурсы, адреса внешних сервисов. Тогда перенос конфигурации между окружениями не превращается в переписывание манифестов.
В GitOps единый источник правды даёт ещё и аудит: в истории Git видно, кто, когда и зачем менял нужный параметр, а откат сводится к revert коммита. С инструментами на экспериментальной стадии будьте осторожны: Docker, к примеру, помечает платформу Agentic Platform как экспериментальную и предупреждает, что функции и поведение могут меняться. Для продакшн-деплоя берите инструменты со стабильным контрактом и понятной политикой поддержки версий.
Итог: алгоритм выбора подхода
- Задача описывает целевое состояние объекта Kubernetes (Deployment, Service, Secret, HPA)? Делайте манифест, при необходимости с параметризацией через Helm.
- Задача сводится к разовому действию или подготовке окружения (CNI, namespace, RBAC, миграции, проверка зависимостей)? Делайте скрипт или Job.
- Сомневаетесь? Выносите в скрипт только то, что не является ресурсом Kubernetes, а всё остальное оставляйте в манифестах.
- Скрипт пишите идемпотентным, с логированием шагов и ненулевым кодом выхода при ошибке. Результат деплоя всегда проверяйте kubectl rollout status и health-check.
Начните с манифестов для приложения и Helm-чарта для параметризации, а скрипты оставьте для подготовки кластера, доставки секретов и миграций. Если вы только переносите сервис в оркестратор, посмотрите гайд по Kubernetes для начинающих: он поможет поднять первый кластер и разобраться с Pod, Deployment и Service на живых примерах kubectl.