Безопасная миграция CRD в production: стратегии версионирования и бесшовного обновления | AdminWiki
Timeweb Cloud — сервера, Kubernetes, S3, Terraform. Лучшие цены IaaS.
Попробовать

Безопасная миграция CRD в production: стратегии версионирования и бесшовного обновления

01 апреля 2026 8 мин. чтения

В 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:

  1. Можно добавлять новые необязательные поля в схему. Старые клиенты их просто проигнорируют.
  2. Нельзя удалять уже используемые поля. Вместо удаления поле помечается как устаревшее (deprecated: true) в схеме, и его поддержка прекращается через несколько версий.
  3. Нельзя переименовывать поля без деприкации. Старое имя должно оставаться в схеме как deprecated, новое имя добавляется параллельно. Миграция данных между именами требует конверсионного веб-хука.
  4. Изменять тип поля (например, string -> integer) можно только с обеспечением конвертации. Встроенный механизм Kubernetes не умеет в такие преобразования — необходим веб-хук.
  5. Добавлять обязательные поля можно только со значением по умолчанию, чтобы существующие ресурсы автоматически получили это значение при чтении/конвертации.

Стратегия 1: Поэтапное обновление с поддержкой нескольких версий

Это основная и наиболее безопасная стратегия для большинства изменений, таких как добавление новых необязательных полей или введение новой стабильной версии API (v1beta1 -> v1). Её суть — постепенное, контролируемое перемещение клиентов на новую версию без принудительного обновления.

Шаг за шагом: добавляем v1beta1 к существующему v1alpha1

Допустим, у вас есть работающий CRD с версией v1alpha1 в качестве storage version. Вы хотите ввести новую, более стабильную версию v1beta1 с теми же полями. План действий:

  1. Обновляем манифест 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
    
  2. Обновляем оператор (контроллер): Модифицируем код оператора так, чтобы он мог обрабатывать Custom Resources как в версии v1alpha1, так и в v1beta1. Это гарантирует, что после применения CRD ничего не сломается.
  3. Тестируем: В тестовом кластере применяем обновленный 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

  1. Аудит существующих ресурсов: Выполните kubectl get <crd-name> --all-namespaces -o yaml | head -50, чтобы понять структуру и объем данных.
  2. Проверка CRD конфигурации: Посмотрите текущее состояние через kubectl get crd <crd-name> -o yaml, обратите внимание на storageVersion в статусе и аннотацию kubectl.kubernetes.io/last-applied-configuration для понимания последних примененных изменений.
  3. Анализ логов оператора: Убедитесь, что текущий контроллер работает без ошибок: kubectl logs -l app=my-operator -c manager.
  4. Готовность веб-хука (если используется): Проверьте, что 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 версии и очистка кода поддержки устаревших версий в операторе.

Поделиться:
Сохранить гайд? В закладки браузера