Перейти к содержимому
Hogin Hogin
Назад

KRO: составные Kubernetes-API без написания собственного оператора

7 мин чтения

Разработчику, которому нужен «просто веб-сервис с базой», сегодня обычно выдают семь YAML-файлов и просьбу разобраться. Kube Resource Orchestrator предлагает другой путь: platform-команда описывает связку ресурсов один раз как граф, а разработчик применяет один CRD с тремя полями. Разница не в том, что кто-то наконец написал документацию — разница в том, что состав больше не нужно копировать руками.

Содержание

Открыть содержание

Проблема: семь манифестов ради одного сервиса

Типичный «просто веб-сервис» в кластере — это не один ресурс. Это Deployment с нужными лимитами и пробами, Service, ConfigMap с конфигом приложения, отдельный Cluster для базы от CloudNativePG, Secret со строкой подключения, иногда ещё Ingress и HorizontalPodAutoscaler. Разработчик, которому нужна база и HTTP-эндпоинт, получает в лучшем случае Helm-чарт с десятком value-полей, в худшем — стопку YAML, которую копируют из соседнего сервиса и подгоняют руками.

Есть два стандартных ответа на эту боль. Первый — написать полноценный Kubernetes-оператор на Go: kubebuilder или operator-sdk, свой контроллер, свой reconcile-loop, свои тесты. Это тяжеловесно для внутренней платформы, где такой CRD нужен один-два раза, а не как продукт с публичным API. Второй — Helm-чарт с горой {{ if }} и {{ range }}, который быстро превращается в нечитаемый шаблонизатор поверх шаблонизатора: логика зависимостей между ресурсами размазана по условиям, а не описана явно.

Разработчик применяет либо стопку из семи манифестов, либо один составной CRD

Что меняет ResourceGraphDefinition

KRO вводит один новый CRD верхнего уровня — ResourceGraphDefinition (RGD). В нём platform-инженер один раз описывает: какой новый API получит разработчик (kind: WebApp, с полями image, replicas, dbSize), из каких примитивных ресурсов он состоит (Deployment, Service, ConfigMap, внешний Cluster от CloudNativePG) и как поля этих ресурсов ссылаются друг на друга.

Ключевая механика — ссылки через CEL-выражения прямо в манифесте ресурса, а не отдельный DAG. Если Service.spec.selector ссылается на ${deployment.spec.template.metadata.labels}, а ConfigMap.data.DB_HOST — на ${cluster.status.host}, KRO сам строит граф зависимостей из этих выражений и применяет ресурсы в правильном порядке: сначала то, от чего зависят остальные, затем зависимые — и ждёт, пока cluster.status.host реально появится в статусе, прежде чем создавать ConfigMap. Контроллер, который раньше пришлось бы писать руками — «подожди, пока Postgres поднимется, потом создай конфиг» — здесь просто следует из графа ссылок.

После того как RGD применён, KRO генерирует настоящий CRD (WebApp) и разворачивает для него собственный контроллер — динамически, без перекомпиляции и передеплоя KRO. Разработчик после этого видит в кластере только WebApp, а не то, из чего он собран.

RGD компилируется в CRD и контроллер; разработчик применяет только WebApp

KRO, Crossplane Compositions и Helm с condition-ами — не одно и то же

Все три подхода решают задачу «дать разработчику простой API вместо связки ресурсов», но на разных слоях. Crossplane Compositions делают то же самое, что RGD, но для облачной инфраструктуры за пределами кластера — RDS, VPC, IAM-роли — через провайдеров, которые транслируют Kubernetes-ресурсы в вызовы облачного API. KRO работает только внутри кластера: он не умеет создать RDS-инстанс сам по себе, зато умеет собрать Crossplane-ресурс в композицию вместе с обычными Deployment и Service, если Crossplane уже установлен как провайдер.

Helm с условиями — самый доступный вариант, но не решает задачу композиции, а маскирует её текстовым шаблонизатором: зависимости между ресурсами не описаны декларативно, а подразумеваются порядком хуков и wait-флагами. RGD хранит граф зависимостей как данные (CEL-ссылки), а не как порядок строк в файле — это и позволяет KRO автоматически определять порядок применения, чего Helm сделать не может.

KROCrossplane CompositionsHelm + condition
Область действияРесурсы внутри кластераОблачная инфраструктура + кластерЧто угодно, но текстом
Как описаны зависимостиCEL-ссылки в RGD, граф строится автоматическиPatches между полями композицииПорядок хуков, руками
Нужен ли Go-контроллерНет — генерируется динамическиНет, но нужен провайдер под облакоНет
Результат для разработчикаПростой CRD (WebApp)Простой CRD (XDatabase и т.п.)values.yaml с десятками полей
Где сильнееSelf-service поверх кластерных ресурсовМультиоблачная провизия инфраструктурыБыстрый старт без новых зависимостей

Практика: WebApp поверх Deployment, Service и CloudNativePG

Возьмём тот самый типовой случай — веб-сервис с базой на CloudNativePG (её уже разбирали в отдельном материале про schema-as-code). Platform-команда описывает ResourceGraphDefinition один раз:

apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
  name: webapp
spec:
  schema:
    apiVersion: v1alpha1
    kind: WebApp
    spec:
      image: string
      replicas: integer | default=2
      dbSize: string | default="1Gi"
    status:
      dbHost: ${cluster.status.host}
      ready: ${deployment.status.readyReplicas}
  resources:
    - id: cluster
      template:
        apiVersion: postgresql.cnpg.io/v1
        kind: Cluster
        metadata:
          name: ${schema.metadata.name}-db
        spec:
          instances: 1
          storage:
            size: ${schema.spec.dbSize}
    - id: deployment
      template:
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: ${schema.metadata.name}
        spec:
          replicas: ${schema.spec.replicas}
          template:
            spec:
              containers:
                - name: app
                  image: ${schema.spec.image}
                  env:
                    - name: DB_HOST
                      value: ${cluster.status.host}
    - id: service
      template:
        apiVersion: v1
        kind: Service
        metadata:
          name: ${schema.metadata.name}
        spec:
          selector: ${deployment.spec.template.metadata.labels}
          ports:
            - port: 80

Разработчик после этого не видит ни Cluster, ни Deployment, ни Service по отдельности — он применяет одно:

apiVersion: v1alpha1
kind: WebApp
metadata:
  name: checkout
spec:
  image: registry.internal/checkout:1.4.2
  replicas: 3
  dbSize: "5Gi"

KRO сам создаёт Cluster, дожидается, пока CloudNativePG выставит status.host, и только потом создаёт Deployment с уже подставленным адресом базы — потому что ${cluster.status.host} в шаблоне Deployment и есть та самая ссылка, из которой контроллер выводит порядок.

Self-service через Backstage: один software template вместо трёх YAML

Связка с каталогом сервисов делает шаг ещё короче: если в Backstage уже настроен scaffolder (как в материале про каталог с нуля), software template может генерировать не стопку файлов, а один манифест WebApp и пушить его в GitOps-репозиторий. Разработчик заполняет форму с тремя полями — имя, образ, размер базы — и получает в кластере рабочий сервис с базой, ни разу не увидев ResourceGraphDefinition и не зная, что за WebApp стоит три примитивных ресурса. Именно это и есть точка, ради которой существует связка каталог + KRO: self-service API, а не просто более короткий YAML.

Где KRO не подходит

KRO хорош, когда всё, что нужно составить, живёт внутри одного кластера или доступно через уже установленный Crossplane-провайдер. Он плохо подходит, когда основная задача — сложная мультиоблачная провизия: поднять VPC в трёх регионах AWS, связать их peering-ом, выдать IAM-роли и только потом развернуть кластер поверх этого. Здесь у Crossplane есть то, чего у KRO нет из коробки — зрелая экосистема провайдеров под конкретные облака, composite-функции для сложной трансформации полей и managed resource lifecycle с учётом состояния облачного API, а не только Kubernetes-объектов. Пытаться замоделировать такую провизию через RGD технически возможно, но означает переизобретать то, что в Crossplane уже отлажено и покрыто провайдерами. Правило простое: если составляете Kubernetes-ресурсы — берите KRO, если составляете облачную инфраструктуру — Crossplane, а KRO при желании ставится поверх него для последнего слоя composition внутри кластера.

Как проверить, что работает

После kubectl apply -f resourcegraphdefinition.yaml первым делом смотрим, что KRO принял граф и сгенерировал CRD: kubectl get resourcegraphdefinition webapp -o jsonpath='{.status.state}' должен вернуть Active, а не Inactive с ошибкой в .status.conditions. Дальше kubectl get crd webapps.kro.run должен существовать — это и есть динамически сгенерированный API. После применения инстанса kubectl get webapp checkout -o jsonpath='{.status.ready}' должен со временем показать число реплик, равное spec.replicas, а kubectl get cluster checkout-db — что CloudNativePG действительно поднял базу, а не завис в ожидании несуществующего storage-класса.

Итог

KRO не заменяет ни Crossplane, ни написание операторов там, где они реально нужны — он закрывает промежуток между «руками копировать YAML» и «писать Go-контроллер ради одного внутреннего API». Цена входа — выучить, как CEL-ссылки в ResourceGraphDefinition превращаются в граф зависимостей и порядок применения. Выигрыш — разработчики получают WebApp вместо трёх файлов, а platform-команда получает self-service API, который можно завернуть в Backstage software template, не открывая Go-модуль ради одной сущности. Выбирать между KRO и Crossplane стоит не по моде, а по вопросу «что именно я составляю» — Kubernetes-ресурсы или облачную инфраструктуру за его пределами.


Поделиться:

Следующая статья
Scorecards для платформенной команды: как измерить, что каталог сервисов работает