В production-кластерах Kubernetes некорректное изменение Custom Resource Definitions (CRD) — одна из самых опасных операций. Она может привести к полной остановке операторов, потере данных в пользовательских ресурсах (Custom Resources) и длительным простоям. В отличие от обновления Deployment или ConfigMap, изменение схемы CRD затрагивает фундаментальный слой API сервера, с которым работают все контроллеры и клиенты. Эта статья предлагает проверенные на практике стратегии безопасного версионирования и миграции CRD, которые позволят вам вносить изменения без риска для стабильности кластера. Вы научитесь управлять storage version, настраивать конверсионные веб-хуки для сложных преобразований и планировать обратно совместимые изменения по четкому чек-листу.
Почему обновление CRD — это риск для production?
CRD определяет схему данных для нового типа ресурсов в Kubernetes API. Когда вы изменяете эту схему, вы меняете «контракт», на который полагаются все существующие Custom Resources и операторы, их обрабатывающие. В development-среде последствия ошибки можно нивелировать пересозданием кластера, но в production цена сбоя измеряется в минутах простоя бизнес-приложений и часах на аварийное восстановление. Основная опасность кроется в механизме хранения: все ресурсы, независимо от версии API, в которой они были созданы, физически хранятся в etcd в едином формате — storage version. Неподготовленное изменение этой версии или её схемы может сделать накопленные данные нечитаемыми для контроллеров.
Типичные сценарии, которые приводят к проблемам
Рассмотрим наиболее частые и рискованные сценарии изменений CRD:
- Изменение типа существующего поля: Переход, например, с
spec.replicas: stringнаspec.replicas: integer. Контроллер, ожидающий строку, получит число и может завершиться с ошибкой парсинга, перестав обрабатывать все ресурсы этого типа. - Удаление используемого поля: Если поле
spec.oldConfigактивно используется старыми версиями оператора, его удаление из схемы приведет к тому, что при чтении ресурсов это поле будет игнорироваться. Контроллер может перейти в состояние ошибки из-за отсутствия ожидаемых данных. - Добавление обязательного поля без значения по умолчанию: При попытке обновить существующий ресурс (например, через
kubectl apply) валидация отклонит операцию, так как обязательное поле отсутствует. Это заблокирует любые манипуляции с ресурсами до ручного исправления каждого из них. - Переход между версиями API (v1alpha1 -> v1beta1 -> v1): Если новая версия не объявлена как
served: trueодновременно со старой, клиенты, не обновленные для работы с новой версией, потеряют доступ к API.
Основы версионирования CRD: storage version, served versions и схема
Безопасное управление изменениями в CRD строится на понимании трех ключевых концепций, которые определяются в секции spec.versions манифеста CRD. Каждая версия API в этом списке имеет независимую схему валидации OpenAPI v3, но связана с другими версиями через общее логическое имя ресурса.
Storage Version: единственная версия истины в etcd
Представьте, что CRD — это определение таблицы в базе данных, а storage version — это физический формат хранения строк в этой таблице. В любой момент времени ровно одна версия API в CRD помечена флагом storage: true. Все Custom Resources, созданные через любую из версий API (например, v1alpha1 или v1beta1), преобразуются и сохраняются в etcd именно в этом формате. Вот как это выглядит в YAML:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: myapps.example.com
spec:
...
versions:
- name: v1alpha1
served: true
storage: false # Не хранится в etcd
schema: {...}
- name: v1beta1
served: true
storage: true # Единственная версия для хранения
schema: {...}
Критически важно: нельзя просто переключить флаг storage: true с одной версии на другую. Данные, уже хранящиеся в etcd в формате старой storage version, не будут автоматически преобразованы. При попытке прочитать их через новую схему API сервер вернет ошибку. Для такого перехода требуется механизм конвертации — либо встроенный (для совместимых изменений), либо конверсионный веб-хук.
Правила обратной совместимости для схемы
Чтобы изменения CRD были безопасными для уже работающих ресурсов и клиентов, необходимо следовать правилам обратной совместимости Kubernetes API:
- Можно добавлять новые необязательные поля в схему. Старые клиенты их просто проигнорируют.
- Нельзя удалять уже используемые поля. Вместо удаления поле помечается как устаревшее (
deprecated: true) в схеме, и его поддержка прекращается через несколько версий. - Нельзя переименовывать поля без деприкации. Старое имя должно оставаться в схеме как deprecated, новое имя добавляется параллельно. Миграция данных между именами требует конверсионного веб-хука.
- Изменять тип поля (например, string -> integer) можно только с обеспечением конвертации. Встроенный механизм Kubernetes не умеет в такие преобразования — необходим веб-хук.
- Добавлять обязательные поля можно только со значением по умолчанию, чтобы существующие ресурсы автоматически получили это значение при чтении/конвертации.
Стратегия 1: Поэтапное обновление с поддержкой нескольких версий
Это основная и наиболее безопасная стратегия для большинства изменений, таких как добавление новых необязательных полей или введение новой стабильной версии API (v1beta1 -> v1). Её суть — постепенное, контролируемое перемещение клиентов на новую версию без принудительного обновления.
Шаг за шагом: добавляем v1beta1 к существующему v1alpha1
Допустим, у вас есть работающий CRD с версией v1alpha1 в качестве storage version. Вы хотите ввести новую, более стабильную версию v1beta1 с теми же полями. План действий:
- Обновляем манифест CRD: Добавляем новую версию в список
versions, выставляем ейserved: true, но оставляемstorage: false. Старая версияv1alpha1остаетсяserved: trueиstorage: true.versions: - name: v1alpha1 served: true storage: true schema: {...} - name: v1beta1 # Новая версия served: true storage: false # Пока не храним в ней schema: {...} # Схема идентична v1alpha1 - Обновляем оператор (контроллер): Модифицируем код оператора так, чтобы он мог обрабатывать Custom Resources как в версии
v1alpha1, так и вv1beta1. Это гарантирует, что после применения CRD ничего не сломается. - Тестируем: В тестовом кластере применяем обновленный CRD, затем оператор. Проверяем, что создание, чтение и обновление ресурсов работает через обе версии API (например,
kubectl get myapp.v1alpha1.example.comиkubectl get myapp.v1beta1.example.com).
На этом этапе кластер работает в гибридном режиме: данные хранятся в формате v1alpha1, но могут быть прочитаны и через v1beta1 (API сервер выполняет преобразование на лету).
Как управлять откатом при такой стратегии
Безопасность стратегии обеспечивается простым откатом:
- Если новая версия оператора с багом, откатываем его на предыдущую версию, которая поддерживает только
v1alpha1. - Поскольку storage version осталась
v1alpha1, а эта версия всё ещёserved: true, после отката оператора кластер вернется в исходное стабильное состояние. - Важно: Откат CRD (удаление версии
v1beta1из манифеста) возможен только до тех пор, пока вы не перенесли на неё storage version. После переноса откат станет сложной операцией, требующей преобразования данных.
Стратегия 2: Конверсионные веб-хуки для сложных изменений
Когда изменения не укладываются в правила обратной совместимости (например, переименование поля spec.host -> spec.server), необходим механизм преобразования данных между версиями API. Конверсионный веб-хук — это ваш кастомный сервис, который Kubernetes вызывает для преобразования ресурса из формата одной версии в формат другой, особенно при смене storage version.
Настройка CRD для работы с веб-хуком
В манифесте CRD указывается стратегия конверсии и параметры вызова веб-хука:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: myapps.example.com
spec:
...
conversion:
strategy: Webhook
webhook:
clientConfig:
service:
namespace: default
name: myapp-conversion-webhook
path: /convert
port: 443
caBundle: "<ваш-CA-бандл>" # Обязательно при TLS
conversionReviewVersions: ["v1"]
versions:
- name: v1alpha1
served: true
storage: false
- name: v1beta1
served: true
storage: true # Storage version меняется!
Ключевой момент: когда storage version меняется на v1beta1, API сервер, встречая в etcd ресурс в старом формате v1alpha1, вызовет ваш веб-хук, чтобы получить его представление в v1beta1 перед обработкой запроса.
Пишем и разворачиваем простой conversion webhook
Логика веб-хука заключается в обработке запроса типа ConversionReview. Рассмотрим на упрощенном псевдокоде обработку переименования поля:
func handleConvert(w http.ResponseWriter, r *http.Request) {
var review ConversionReview
// Декодируем запрос
json.NewDecoder(r.Body).Decode(&review)
response := ConversionResponse{
UID: review.Request.UID,
Result: metav1.Status{Status: "Success"},
}
// Проходим по всем объектам, которые нужно сконвертировать
for _, obj := range review.Request.Objects {
// Если конвертируем из v1alpha1 в v1beta1
if review.Request.DesiredAPIVersion == "example.com/v1beta1" {
// Копируем значение старого поля в новое
newObj := obj.DeepCopy()
newObj.Spec["server"] = obj.Spec["host"]
delete(newObj.Spec, "host")
response.ConvertedObjects = append(response.ConvertedObjects, newObj)
}
// Обратная конвертация (v1beta1 -> v1alpha1) для операций чтения
if review.Request.DesiredAPIVersion == "example.com/v1alpha1" {
// ... обратная логика
}
}
// Отправляем ответ
json.NewEncoder(w).Encode(ConversionReview{Response: &response})
}
После развертывания такого веб-хука в виде Deployment и Service, протестируйте его, используя raw-запрос к API серверу: kubectl get --raw /apis/example.com/v1alpha1/namespaces/default/myapps/myapp-name — запрос пройдет через веб-хук для конвертации в storage version, и вы увидите результат.
Практический чек-лист для миграции в production
Соберите все шаги в единый план действий, который минимизирует человеческий фактор.
Что проверить перед применением изменений в prod
- Аудит существующих ресурсов: Выполните
kubectl get <crd-name> --all-namespaces -o yaml | head -50, чтобы понять структуру и объем данных. - Проверка CRD конфигурации: Посмотрите текущее состояние через
kubectl get crd <crd-name> -o yaml, обратите внимание наstorageVersionв статусе и аннотациюkubectl.kubernetes.io/last-applied-configurationдля понимания последних примененных изменений. - Анализ логов оператора: Убедитесь, что текущий контроллер работает без ошибок:
kubectl logs -l app=my-operator -c manager. - Готовность веб-хука (если используется): Проверьте, что Pod веб-хука в состоянии Ready, и протестируйте его endpoint на внутренние ошибки.
Инструменты и утилиты для автоматизации проверок
- kubectl diff: Примените CRD с флагом
--dry-run=serverи используйтеkubectl diffдля просмотра отличий от текущей версии в кластере. - Dry-run режим: Для проверки валидации схемы попробуйте создать тестовый ресурс с новой версией API:
kubectl create -f test-resource.yaml --dry-run=client. - Скриптованные проверки: Напишите простой скрипт на Python с использованием библиотеки kubernetes, который считает все Custom Resources, проверит их аннотации и убедится в отсутствии неожиданных полей.
- Плагины: Инструменты вроде
kubeconformпозволяют валидировать CRD манифесты на соответствие схеме Kubernetes.
Помните, что контейнеризация, которую мы подробно разбирали в практическом руководстве по Docker, является фундаментом для развертывания как ваших операторов, так и конверсионных веб-хуков. Понимание принципов работы контейнеров критически важно для надежной упаковки и изоляции этих компонентов инфраструктуры.
Итоговый план миграции выглядит так: 1) Анализ и выбор стратегии. 2) Разработка и тестирование в dev-среде (обязательно используйте реалистичные данные). 3) Подготовка манифестов. 4) Поэтапный rollout: CRD -> Контроллер -> Миграция storage version (при необходимости). 5) Активный мониторинг метрик (латентность API, ошибки контроллера). 6) Финализация: отключение старой served версии и очистка кода поддержки устаревших версий в операторе.