Как стать автором
Обновить
70.12
Слёрм
Учебный центр для тех, кто работает в IT

Пишем сложные операторы Kubernetes

Время на прочтение 11 мин
Количество просмотров 4.8K
Автор оригинала: Stefanie Lai

Советы по созданию операторов уровня продакшена с помощью Kubebuilder.

В этой статье рассматривается простой пример оператора для сценария автоматического создания ServiceAccount и ClusterRoleBinding с помощьюKubebuilder.

Не всякий оператор подходит для продакшена. Вот такой, например, не подойдёт:

Нехороший оператор
Нехороший оператор

Очевидно, что для продакшена нам нужны более продуманные операторы.

В этой статье мы рассмотрим несколько советов по созданию стабильного и функционального оператора и обсудим, какие препятствия подстерегают нас на этом пути.

Начнём с контроллера оператора — что это и что мы хотим с ним сделать в Kubebuilder.

  • Контроллер реализует цикл согласования и управляет им.

  • Контроллер считывает желаемое состояние из YAML-файла ресурса и следит за тем, чтобы ресурсы пребывали в этом состоянии.

Советую почитать рекомендации по операторам, прежде чем браться за дело. Я кратко перескажу самое важное:

  • Оператор должен решать одну задачу и делать это хорошо, по принципу единственной ответственности (Single Responsibility Principle, SRP). Это нужно для стабильности, производительности и простоты разработки и расширения.

  • На один контроллер — одно CRD, в продолжение первого пункта.

  • Пространства имён должны настраиваться, как того требуют рекомендации по применении пространств имён в Kubernetes. В нашем примере разные ServiceAccounts лучше разместить в разных пространствах имен.

  • Отслеживаем метрики. Очевидно, что нам нужен Prometheus для мониторинга операторов.

  • Один оператор не должен быть связан с другим. Не стоит усложнять.

  • Используем вебхуки для проверки CRD. Если у CRD есть разные версии, нам нужны вебхуки.

Там ещё много всего, почитайте сами. Хороший оператор похож на удобный дом.

Хороший оператор
Хороший оператор

Совершенствуем наш оператор

Мы хотим сделать наш оператор более надёжным и стабильным. Оставим код UserIdentity для сравнения, добавим новый kind и контент.

Первым делом используем Kubebuilder, чтобы создать следующую версию типа CRD и контроллер.

kubebuilder create api --group identity --version v2 --kind UserIdentityv2 

Если не указать новый kind, Kubebuilder не создаст новый контроллер, а потребует написать логику согласования двух версий одного контроллера.

Команда Create создаст классы Go, например useridentityv2_types.go, в каталоге api/v2.

Давайте напрямую скопируем поля v1.UserIdentity.

Команда создаст новый файл useridentityv2_controller.go в каталоге контроллера и одновременно скопирует похожую логику из контроллера v1.

Добавление логов

Для начала добавим побольше логов. У Kubebuilder есть logr для записи логов. Добавим имя переменной к объекту log по умолчанию.

log := r.Log.WithValues("useridentity", req.NamespacedName)

Обычно логи нужны для обработки ошибок.

if err != nil {
log.Error(err, fmt.Sprintf("Error create ServiceAccount for user:
 %s, project: %s", user, project)) 
 return ctrl.Result{}, nil
 }  

Добавляем логи для ключевых событий.

log.V(10).Info(fmt.Sprintf("Create Resources for User:%s, Project:%s", user, project))
log.V(10).Info(fmt.Sprintf("Create ServiceAccount for User:%s, Project:%s finished", user, project))

Пара слов об уровне детализации логов:

Для типа info можно указывать произвольный уровень важности логов, не используя семантически значимые обозначения, вроде warning, trace или debug.

Источник: https://github.com/go-logr/logr

Благодаря своей гибкости уровни детализации всё чаще используются в приложениях на Go и становятся стандартом. Дополнительная изоляция упрощает отладку.

Для просмотра логов мы выполняем в kubectl следующую команду. (В двойные фигурные скобки {{}} заключены значения, которые нужно заменить на свои.)

kubectl get po -n {{ns}} -L {{label}}={{value}} --sort-by='{{field}}' -v10

Не переборщите с логами, иначе при отладке придётся копаться в миллионах записей. Как работает механизм логирования в Kubernetes см. здесь

Задаем условия

Если говорить об управлении ресурсами Kubernetes, условия (condition) очень важны и связаны с жизненным циклом pod’ов. Если неправильно задать условие в цикле синхронизации, возникнут проблемы с пробами, например пробами готовности. Подробнее в статье об условиях в контроллерах Kubernetes.

Чтобы задать условия для CRD, добавим поле conditions в определение статуса UserIdentity.

type UserIdentityV2Status struct {
// Conditions is the list of error conditions for this resource
 Conditions status.Conditions `json:"conditions,omitempty"`
}

Теперь в контроллере можно менять условия CRD.

  • Добавим условие UpdateFailed для ошибки.

Статус условия при обновлении

  • После успешного выполнения задаем для условия UpToDate.

condition := status.Condition{
  Type:    Ready,
  Status:  v1.ConditionTrue,
  Reason:  UpToDate,
  Message: err.Error(),
 }

Когда контроллер задаст условие, можно будет легко найти проблемное CRD через kubectl.

kubectl get po -n {{ns}} -L {{label}}={{value}}

Добавление проверок работоспособности

Kubernetes может как автоматически закрывать неработоспособные pod’ы, так и перезапускать их.

Для этого ему нужны проба готовности и проба работоспособности. Для согласования CRD нужно добавить код для этих проб.

Вот пример пробы работоспособности и проверки работоспособности.

Добавив нужную логику, мы можем включить пробу работоспособности в файл config/manager/manger.yaml.

livenessProbe:
  httpGet:
  path: /healthz
  port: 6789
  initialDelaySeconds: 20
  periodSeconds: 10

Это YAML не для CRD, а для менеджера развёртывания.

Добавляем логику удаления ресурса

В цикле синхронизации у нас есть только логика для увеличения числа пользователей и настройки соответствующих ресурсов, но если пользователь удаляется, ресурсы тоже нужно удалить.

Способ реализации зависит от того, как мы получаем информацию об обновлении пользователей.

  • Интерфейс FindAll. Получаем информацию обо всех пользователях, а потом добавляем и удаляем соответствующие ресурсы по этому списку.

  • Уведомление о событиях. Можно подписаться на события, чтобы узнавать о добавлении и удалении пользователей, и архитектура на основе событий сейчас очень популярна.

Цикл согласования на основе событий
Цикл согласования на основе событий

Сервер API Kubernetes нативно поддерживает отслеживание событий через функцию watch. Получается, в контроллере остаётся выполнить всего два действия:

  • Создаём события, за которыми нужно следить. Здесь это событие обновления пользователя из топика pubsub.

  • Добавляем вотчер в функцию SetupWithManager контроллера.

// define userevent and run
ch := make(chan event.GenericEvent)
subscription := r.PubsubClient.Subscription("userevent")
userEvent := CreateUserEvents(mgr.GetClient(), subscription, ch)
go userEvent.Run()

return ctrl.NewControllerManagedBy(mgr).
   For(&identityv2.UserIdentityV2{}).
   Watches(&source.Channel{Source: ch, DestBufferSize: 1024}, &handler.EnqueueRequestForObject{}).
   Complete(r)

Наконец, настроим PubsubTopic и разрешения RBAC.

Расширяем функционал

Мы можем и дальше улучшать UserIdentity. Например, добавить поддержку kubectl и дополнительных возможностей Kubernetes.

Вот как должен выглядеть относительно надежный оператор:

Надежный оператор
Надежный оператор

Как реализовать эти возможности?

Используем Kubebuilder

У Kubebuilder есть много удобных функций, и функция комментариев, наверное, лучшая из них.

Зачем нужны комментарии?

  • Добавляем детали в CRD. Например, если добавить к полю следующий комментарий, информация в поле будет отображаться в выходных данных kubectl get.

// +kubebuilder:printcolumn
  • Добавляем дефолтное значение или проверку в поле CRD. Например, если добавить следующий комментарий, поле будет принимать только значения 1, 2, 3. При другом значении будет выдаваться ошибка.

// +kubebuilder:validation:Enum=1,2,3
  • Запрещаем серверу API отсекать поля без значений.

// +kubebuilder:pruning:PreserveUnknownFields

Больше о комментариях читайте в документации по Kubebuilder.

С помощью нашего кода мы можем вывести столбец RoleRef, который используется UserIdentityV2.

// +kubebuilder:printcolumn
RoleRef rbacv1.RoleRef `json:"roleRef,omitempty"`

Поддержка неструктурированных данных

Иногда в CRD требуются нестандартные решения. Мы не хотим ограничиваться имеющимися Kubernetes API или типами ресурсов при выполнении особых операций с неструктурированными данными.

Возьмём для примера UserIdentity. Здесь мы жестко закодировали создание ServiceAccount и ClusterRoleBinding, что приводит к следующим проблемам:

  • Мы должны использовать библиотеки core/v1 и rbac/v1, соответствующие ServiceAccount и ClusterRoleBinding. Чем больше ресурсов мы создаем, тем больше у нас API, но не все обязательные типы поддерживаются Go.

  • Мы не можем создавать обязательные ресурсы, динамически меняя CRD. Нужно будет каждый раз менять логику контроллера.

Поэтому так важна поддержка неструктурированных данных. Наш YAML-файл с CRD может выглядеть так:

Этот код нужен для парсинга шаблона, и давайте изменим его в файле UserIdentityV3_types.go. Заметили, что здесь мы создали UserIdentityV3? Он реализует неструктурированную функцию для сравнения кода v1 и v2.

// Template is a list of resources to instantiate per repository in Governator
Template []unstructured.Unstructured `json:"template,omitempty"`

Этот шаблон использует функцию шаблона Go, чтобы мы могли парсить шаблон, внедрять параметры в контроллер и создавать объекты с помощью неструктурированного API. Изучите код.

Поддержка событий

Версия v2 уже поддерживает условия, не хватает только событий.

В Kubernetes ресурсы сообщают об изменении своего статуса и других новостях с помощью событий, поэтому мы можем использовать команду kubectl get events, чтобы не копаться в объёмных логах.

Здесь нам нужен пакет Kubernetes client-go/tools/record.

import  "k8s.io/client-go/tools/record"

Ещё в функции согласования нужно определить Recorder.

Recorder     	record.EventRecorder

Теперь можно создавать события.

r.Recorder.Event(&userIdentity, corev1.EventTypeNormal, string(condition.Reason), condition.Message)
// or
r.Recorder.Event(&userIdentity, corev1.EventTypeWarning, string(UpdateFailed), "Failed to update resource status")

Используем вебхуки

Вебхук — это обратный HTTP-вызов: HTTP POST, который выполняется, когда что-то происходит, простое уведомление о событии через HTTP POST. Источник: kubernetes.io

Kubebuilder нативно поддерживает вебхуки, но только вебхуки доступа.

Чтобы добавить в UserIdentityV3 вебхук верификации, выполним следующую команду.

kubebuilder create webhook --group identity --version v3 --kind
UserIdentityV3 --defaulting --programmatic-validation

Будет создан файл useridentityv3_webhook.go в каталоге api/v3/. Вебхук по умолчанию настроен на менеджер в main.go.

if err = (&identityv3.UserIdentityV3{}).SetupWebhookWithManager(mgr); err != nil {
   setupLog.Error(err, "unable to create webhook", "webhook", "UserIdentityV3")
   os.Exit(1)
}

Как мы уже говорили, если добавить комментарий // +kubebuilder:validation к полю типов, будет выполняться валидация, но вебхук способен на большее.

Правда, для UserIdentity пока ничего в голову не пришло. Если интересно, можете дополнить мой код своей логикой.

Периодическое согласование

Если добавить следующий код в конец функции согласования, оператор будет выполнять согласование каждые 10 минут.

return ctrl.Result{RequeueAfter: 10 * time.Minute}, nil

Задаём OwnerReference

В Kubernetes при удалении владельца удаляются всего его подресурсы, так что если мы выполним kubectl delete useridentityv3, все подресурсы удалятся. Можно задать set cascade = false, чтобы отключить это поведение.

Мы хотим оптимизировать сборку мусора, особенно если у нас есть много тесно связанных ресурсов. Представьте, как тесно связаны друг с другом объекты Deployment, Service, Pod.

Мы можем назначить CRD владельцем всех неструктурированных ресурсов. Потом можно удалить оператор, и они удалятся вместе с ним.

return ctrl.SetControllerReference(userIdentity, &existing, r.Scheme)

Препятствия на нашем пути

Препятствия
Препятствия

В целом, с Kubebuilder приятно работать, но без проблем, конечно, не обходится. Проблемы возникают с самим Kubebuilder, с библиотекой Kubernetes controller-runtime и даже с Go, включая использование Google Pubsub.

Самые заметные проблемы:

Непредсказуемые тесты Ginkgo

Или в Kubebuilder что-то не так с suite_test и ginkgo или это я не умею ими пользоваться.

  • В IDE нельзя запускать разные тест-кейсы отдельно. В отличие от JUnit, где это возможно, suite_test не поддерживается. Во всяком случае в GoLand. А что если у нас 20 тест-кейсов? А если вообще 100?

  • Состояние гонки откровенно раздражает. Мы пишем модульные тесты по принципам F.I.R.S.T. Так вот в ginkgo принцип изолированности постоянно нарушается, и я никак не могу найти причину. Даже если создать разные CRD с разными именами и использовать разные методы согласования в одном suite_test, возникает гонка (включите race detector) и результаты получаются совершенно дикие.

CreateOrUpdate

Вот это настоящая подстава. В комментарии к методу написано, что он создаёт или обновляет объект obj в кластере Kubernetes. Текущее состояние объекта должно согласовываться с желаемым с помощью переданной функции Reconcile. obj должен быть структурным указателем, чтобы его можно было обновлять с учетом содержимого, возвращаемого сервером.

// CreateOrUpdate creates or updates the given object obj in the Kubernetes

// cluster. The object’s desired state should be reconciled with the existing

// state using the passed in ReconcileFn. obj must be a struct pointer so that

// obj can be updated with the content returned by the Server.

Можно подумать, что мы передаем объект, и если значение его поля не изменилось, запустится апдейт. На деле всё не так. Оказывается, ресурсы, которые мы создаём оператором, удаляются, а потом создаются снова при апгрейде CRD. Даже если по факту ничего не поменялось.

Если посмотреть на код CreateOrUpdate, мы увидим, что объект выполняет копию, полагаясь на функцию DeepCopy в zz_generated.deepcopy.go, который автоматически создается инструментом Kubebuilder после определения CRD.

Если объект не структурирован или создается динамически и без реализации функции DeepCopy, его придется каждый раз создавать заново!

Чтобы решить эту проблему, нужна функция mutate, определённая этим методом. Функция mutate вызывается независимо от создания или обновления объекта. Она возвращает ожидаемую операцию и ошибку.

// The MutateFn is called regardless of creating or updating an object.
//
// It returns the executed operation and an error.
func CreateOrUpdate(ctx context.Context, c client.Client, obj runtime.Object, f MutateFn) (OperationResult, error) {}

Инициализация объекта с функцией MutateFn позволит не создавать его заново.

Установка времени ожидания для контекста

При запуске оператор застревал на каком-то этапе, но у нас не было ни логов, ни событий, ничего, что помогло бы решить проблему.

Мы снова и снова проверяли код и прокомментировали буквально каждую его часть для тестирования.

Наконец, мы обнаружили, что вышестоящий gRPC-интерфейс, к которому мы подключились, уже перемещался, а время ожидания подключения не истекало, так что оператор просто зависал. Мы перешли с gRPC на SRV, чтобы решить проблему.

Но мы все равно хотели бы, чтобы оператор нормально прекращал работу, а не зависал. Для этого мы чуть-чуть изменили context, который используется повсюду.

ctx, cancel := context.WithTimeout(context.Background(), 20*time.Minute)
defer cancel()

Всегда добавляйте к контексту время ожидания!

Эти три проблемы, к сожалению, не единственные, но такая уж у нас работа — находить и решать проблемы. А ещё создавать их.

Заключение

После всех наших изменений UserIdentityV3 превратился во вполне приличный оператор, годный для продакшена.

Если не обращать внимания на некоторые недостатки, Kubebuilder можно считать удобным инструментом создания операторов, который освобождает время разработчиков на написание логики контроллера.

Работая над операторами, мы начинаем лучше понимать принципы работы вебхуков, контроллера и т.д. Разбираться в опенсорс-коде в Kubernetes SIG и даже писать код в некоторых SIG при желании.

Ждём выхода Kubebuilder 3.0 в Kubernetes slack или группе Google.

Весь код для этой статьи можно найти на Github.

Пишем собственный оператор Kubernetes

Для тех, кто хочет углубить свои знания в Kubernetes, будет полезен наш курс Kubernetes:Мега. Вас ждет разбор кейсов вместе со спикерами и 6 часов практики.

На курсе вы: установите Kubernetes в ручном режиме, авторизуетесь в кластере, настроите autoscaling, разберете такие темы, как Open Policy Agent, Network Policy, безопасность и высокодоступные приложения, ротация сертификатов, аутентификация пользователей в кластере, хранение секретов, Horisontal Pod Autoscaler, создание собственного оператор K8s.

«Мега» подойдет всем, кому предстоит запускать Kubernetes в продакшн и отвечать за работу проекта в дальнейшем: специалистам по безопасности, системным инженерам, администраторам, архитекторам, DevOps и др.

Зачем нужен интенсив, когда есть документация?

  • Экономия времени: полтора месяца, вместо нескольких, которые вы потратили бы на чтение и самостоятельные эксперименты

  • Документация может месяцами лежать в папке «прочитать», а интенсив это конкретные даты.

  • Здесь есть практика, где помогают находить ошибки. Вас ждут ама-сессии со спикерами, обмен кейсами с единомышленниками и куратор, который поможет сформировать мотивацию на обучение.

Кстати, видеокурс доступен уже сейчас. 
Узнать подробнее: slurm.club/3ykvPur

Теги:
Хабы:
+7
Комментарии 1
Комментарии Комментарии 1

Публикации

Информация

Сайт
slurm.io
Дата регистрации
Дата основания
Численность
51–100 человек
Местоположение
Россия
Представитель
Антон Скобин