Как написать Kubernetes-оператор с нуля на Go: полный гайд
Разбираем, как работает паттерн Operator в Kubernetes, и собираем собственный контроллер на Go. Оператор будет создавать Deployment и Service, отслеживать дрейф конфигурации и откатывать несанкционированные изменения.
Deployment умеет держать поды живыми, но не умеет мигрировать базу данных, управлять жизненным циклом приложений с данными (stateful-сервисов) и не защитит от случайного kubectl scale, который разрушит состояние. Для таких задач в экосистеме K8s существуют операторы — специальные контроллеры, которые кодируют человеческий опыт эксплуатации прямо в программный код.
Написать оператор можно на Go с помощью operator-sdk: сгенерировать скелет проекта, определить CRD, реализовать Reconcile и задеплоить в кластер. В этой статье разберём паттерн Operator и соберём рабочий оператор, который создаёт Deployment и Service по декларативной спецификации, отслеживает дрейф конфигурации и мгновенно откатывает несанкционированные изменения.
Ключевые выводы
Оператор Kubernetes — это пользовательский контроллер, который расширяет API кластера собственными ресурсами (CRD) и непрерывно приводит фактическое состояние к желаемому.
Шаблон Reconcile — Observe → Create → Correct → Report — лежит в основе любого оператора: контроллер наблюдает, создаёт недостающее, исправляет отклонения и сообщает статус.
С помощью operator-sdk и kubebuilder-меток можно сгенерировать скелет проекта, CRD и RBAC-манифесты, не писать boilerplate вручную.
Owner Reference связывает дочерние ресурсы с родительским кастомным ресурсом (CR): при удалении WebApp Kubernetes автоматически уберёт связанные Deployment и Service.
Обновление статуса через Status().Update() изолировано от основного ресурса и предотвращает бесконечные циклы реконсиляции.
Почему стандартных примитивов Kubernetes не хватает
Kubernetes предоставляет мощный набор базовых абстракций: Pod, Deployment, StatefulSet, Service, Ingress. Они отлично справляются с запуском контейнеров, балансировкой трафика и базовым масштабированием. Однако эти примитивы агностичны к бизнес-логике приложения.
Представьте, что вам нужно развернуть production-grade кластер PostgreSQL. Помимо самих подов с базой, потребуются: инициализация репликации, управление резервными копиями, обновление версий без простоя, автоматическое переключение при отказе мастера. Всё это — операционная экспертиза, которую DevOps-инженеры накапливают годами. Оператор превращает эту экспертизу в автоматизированный контроллер, который круглосуточно следит за ресурсом и принимает решения.
Сегодня операторы де-факто стали стандартом для управления сложными stateful-приложениями в Kubernetes: от баз данных и брокеров сообщений до сервисных mesh и CI/CD-систем. Концепция была сформулирована инженерами CoreOS ещё в 2016 году, а сейчас поддерживается Cloud Native Computing Foundation (CNCF) как ключевой паттерн платформенной инженерии.
Архитектура оператора: CRD, Reconciler и control loop
Любой оператор состоит из двух ключевых компонентов:
- Custom Resource Definition (CRD) — расширение API Kubernetes, которое определяет новый тип ресурса со своей схемой
Spec(желаемое состояние) иStatus(фактическое состояние). - Контроллер (Reconciler) — программный цикл, который постоянно сравнивает
SpecиStatus, а затем выполняет действия для их сближения.
Контроллер не работает по принципу «выполнил шаги и вышел». Вместо этого он реализует control loop: каждую итерацию можно запускать снова и снова — результат не сломается, потому что код сравнивает «что есть» с «что нужно» и корректирует только расхождения. Это критически важно, потому что события в распределённой системе приходят асинхронно, а состояние объекта могло измениться за время обработки предыдущего события.
Создаём проект с operator-sdk
Вручную писать весь boilerplate контроллера — неэффективно. Инструмент operator-sdk (основанный на Kubebuilder) генерирует стандартную структуру проекта Go, включая точку входа main.go, Makefile с целями для сборки и тестирования, а также инфраструктуру для управления CRD.
Инициализируем проект и создаём API с контроллером:
Флаг --resource сгенерирует Go-структуры, описывающие схему пользовательского ресурса. Флаг --controller создаст шаблон reconciler'а — файла, в котором мы будем писать логику управления.
Определяем схему CRD
Откроем api/v1/webapp_types.go. Здесь мы описываем два структурных блока: WebAppSpec — то, что задаёт пользователь в YAML-манифесте, и WebAppStatus — то, что оператор сообщает о текущем состоянии.
Важнейшая часть — kubebuilder-маркеры над основной структурой:
Маркер +kubebuilder:subresource:status сообщает Kubernetes, что для этого ресурса нужен отдельный endpoint /status. Без него любое обновление статуса будет восприниматься API-сервером как изменение всего объекта, что вызовет каскадную реконсиляцию и может привести к бесконечному циклу.
Маркеры +kubebuilder:printcolumn настраивают вывод команды kubectl get webapps: вместо голого имени ресурса пользователь увидит фазу, желаемое и доступное количество реплик.
Reconciler: сердце оператора
Файл internal/controller/webapp_controller.go содержит функцию Reconcile — точку входа в control loop. Перед ней размещаются RBAC-маркеры, которые генерируют манифесты прав доступа при выполнении make manifests:
Без этих маркеров оператор не получит прав на чтение и запись стандартных ресурсов Deployment и Service, и при запуске в кластере упадёт с ошибкой доступа. Разберём функцию Reconcile по шагам.
Шаг 1. Получение актуального состояния
Reconciler получает не сам объект, а лишь его имя и пространство имён. Это архитектурное решение Kubernetes: между постановкой события в очередь и его обработкой объект мог измениться. Поэтому первое действие — всегда запросить свежую версию ресурса из API.
Здесь apierrors импортируется из пакета k8s.io/apimachinery/pkg/api/errors, а ctrl — из sigs.k8s.io/controller-runtime.
Шаг 2. Реконсиляция Deployment
Сначала проверяем, существует ли связанный Deployment. Если нет — создаём его через вспомогательную функцию deploymentForWebApp.
Вызов return ctrl.Result{Requeue: true}, nil ставит событие обратно в очередь: контроллер немедленно перезапустит Reconcile для того же объекта, чтобы продолжить с следующего шага. Это удобнее, чем ждать следующего внешнего события.
Вот как выглядит функция deploymentForWebApp:
Owner Reference — это механизм garbage collection в Kubernetes. Когда пользователь удаляет ресурс WebApp, кластер автоматически удалит все дочерние объекты, на которые ссылается поле ownerReferences. Без этой связи после удаления кастомного ресурса в кластере останутся «зомби»-поды и сервисы.
Шаг 3. Обнаружение дрейфа конфигурации
Если Deployment уже существует, мы не просто идём дальше — сравниваем желаемое и фактическое состояние. Это и есть то, что отличает оператор от одноразового скрипта.
Представьте, что кто-то из команды в обход оператора выполнил следующую команду или подменил образ на уязвимую версию. Оператор мгновенно фиксирует расхождение и принудительно возвращает ресурс к значениям, заданным в WebApp.Spec.
Шаг 4. Реконсиляция Service
С вычислительным слоем разобрались — теперь нужен сетевой доступ. Логика полностью идентична: проверяем наличие Service, создаём при отсутствии, устанавливаем Owner Reference. Для локального тестирования в Minikube используем тип NodePort.
Вспомогательная функция serviceForWebApp строит объект Service с нужными селекторами и портами:
Шаг 5. Обновление статуса
Последний шаг — сообщить пользователю текущее состояние приложения через status subresource. Здесь критически важно использовать именно r.Status().Update(), а не r.Update().
Разделение основного endpoint ресурса и subresource /status — фундаментальное свойство Kubernetes. Оно гарантирует, что обновление статуса не триггерит новое событие изменения ресурса и, соответственно, не запускает бесконечную реконсиляцию.
Тестирование: как проверить, что оператор работает
Для локального тестирования подойдёт Minikube. Запускаем оператор в одном терминале, а в другом — создаём кастомный ресурс.
Проверяем, что оператор создал инфраструктуру и отчитался о статусе:
Демонстрация: откат несанкционированных изменений
Главная ценность оператора — самовосстановление. Сымитируем вмешательство: масштабируем Deployment в обход кастомного ресурса.
Мгновенно в логах контроллера появляется сообщение:
Лог оператора:
INFO Drift detected! Updating Deployment {"DesiredReplicas": 3, "ActualReplicas": 10}
А через несколько секунд избыточные поды начинают завершаться:
В это время статус WebApp отражает промежуточное состояние Scaling, а после полного схождения — автоматически переключается обратно на Running.
Когда операторы необходимы, а когда избыточны
Не каждому приложению нужен собственный оператор. Для stateless-сервисов, которые достаточно описать парой манифестов Deployment + Service, оператор будет накладным расходом. Однако есть категории систем, где без оператора не обойтись:
- Базы данных — PostgreSQL, MySQL, MongoDB, etcd: репликация, бэкапы, обновления, failover.
- Брокеры сообщений — Kafka, RabbitMQ, NATS: управление партициями, топиками, кластерной топологией.
- Сетевые компоненты — Ingress-контроллеры, service mesh: динамическая маршрутизация и политики безопасности.
- CI/CD и GitOps — Tekton, Argo CD: оркестрация пайплайнов и синхронизация состояния кластера с репозиторием.
В российской инфраструктурной практике операторы активно используются в Managed Kubernetes от крупных облачных провайдеров. Например, в Яндекс Облаке операторы лежат в основе managed-сервисов баз данных, обеспечивая автоматизацию резервного копирования, мониторинга и масштабирования.
Часто задаваемые вопросы
Чем оператор отличается от обычного Helm-чарта?
Helm-чарт — это шаблонизатор манифестов: он создаёт ресурсы один раз при установке и не следит за их состоянием дальше. Оператор — это живой процесс внутри кластера, который постоянно наблюдает за ресурсами, обнаруживает отклонения и корректирует их. Helm разворачивает, оператор — управляет жизненным циклом.
Можно ли писать операторы на языках, отличных от Go?
Да. Официальные client libraries существуют для Python (kubernetes-client/python), Java и JavaScript. Однако экосистема вокруг Go наиболее зрелая: controller-runtime, operator-sdk и kubebuilder написаны именно на Go и предоставляют наиболее полный набор инструментов.
Что произойдёт, если удалить кастомный ресурс WebApp?
Kubernetes запустит garbage collection: благодаря Owner Reference, установленному через ctrl.SetControllerReference, все дочерние ресурсы — Deployment и Service — будут удалены автоматически. Это избавляет от ручной чистки и предотвращает накопление «зомби»-ресурсов.
Как оператор влияет на производительность API-сервера Kubernetes?
Каждый оператор подключается к API-серверу через watch-коннекции и получает события об изменениях целевых ресурсов. В небольших кластерах накладные расходы пренебрежимо малы. В больших кластерах (1000+ узлов) важно настраивать rate limiting и использовать label selectors для фильтрации событий, чтобы снизить нагрузку на API-сервер и etcd.
Как защитить оператор от случайного удаления или изменения?
Используйте RBAC с принципом минимальных привилегий: оператору должны быть выданы только те verbs и resources, которые ему действительно нужны. Дополнительно можно настроить PodDisruptionBudget для pod'ов оператора и использовать leader election (--leader-elect), чтобы при запуске нескольких реплик только один инстанс выполнял реконсиляцию.
Выводы
Паттерн Operator — это не просто модное слово в экосистеме Kubernetes, а проверенный подход к автоматизации эксплуатации сложных приложений. Вместо того чтобы полагаться на runbook'и и ручные действия инженеров, оператор кодирует операционную экспертизу в программу, которая работает круглосуточно, не устаёт и не забывает проверить важный шаг.
В этой статье мы прошли полный путь: от генерации скелета проекта через operator-sdk до работающего контроллера, который создаёт инфраструктуру, отслеживает дрейф конфигурации и автоматически восстанавливает желаемое состояние. Ключевые навыки — понимание асинхронной природы control loop, правильное использование Owner Reference и изолированное обновление статуса — применимы далеко за рамками Kubernetes.
Оператор — это не магия, а дисциплина. Каждая итерация Reconcile — это честный вопрос: «Что должно быть?» против «Что есть сейчас?». Ответить на него правильно — значит построить надёжную систему.
Полный код проекта доступен в репозитории автора оригинального туториала: SandeshOjha06/k8-operator. Если вы планируете развиваться в направлении platform engineering или SRE, умение писать и отлаживать собственные контроллеры станет серьёзным конкурентным преимуществом.
Источники:
- Building a Kubernetes Operator from Scratch with Operator SDK — оригинальный туториал Сандеша Оджха.
- Kubernetes Operators — официальная документация Kubernetes.
- Operator SDK — фреймворк для разработки операторов.
- The Kubebuilder Book — руководство по построению Kubernetes API и контроллеров.