Привет, Хабр!

Эта статья о том, как мы избавились от шаблонизации в пользу понятных слоёв в управлении инфраструктурой. Расскажу про предпосылки, поиск решения и инструмент, который мы выбрали — CUElang. Поехали!

В управлении Kubernetes-инфраструктурой рано или поздно наступает момент, когда шаблоны из удобного инструмента превращаются в источник боли.

Эпоха шаблонов

Манифесты генерировались через Jinja2-шаблоны прямо перед применением (kubectl apply). Это давало гибкость, но со временем проявились серьёзные недостатки:

  • Шаблоны становились нечитаемыми — вложенные условия, макросы и переменные превращали конфиги в «лапшу»;

  • Отладка превращалась в квест — ошибки проявлялись только при рендеринге (т.е. в runtime), а сообщения об ошибках не указывали на корень проблемы;

  • Непредсказуемость — сгенерированный YAML мог внезапно сломаться из-за пропущенной переменной или неверного отступа.

ВАЖНО: Каждую из этих проблем можно решить отдельно, но суть в том, что проблема с императивным кодом внутри шаблонов остаётся. Шаблоны — это по сути форматированные строки, которые ничего не знают о структуре данных и не понимают, с каким API они взаимодействуют.

Мы решили полностью отказаться от шаблонов — но как?

Поиск решения

✏️Необходимо хранить манифесты as-is, чтобы видеть результаты в истории Git.

Это решает проблему runtime-генерации: теперь можно точно увидеть, что изменилось и к чему это привело.

✏️Начать использовать GitOps-практики

Это позволит вносить изменения только в Git, а оператор автоматически синхронизирует кластер в соответствии с этими изменениями.

✏️Избавиться от шаблонов!

CUE — язык, который:

✅ Позволяет описывать конфиги декларативно (без шаблонов!);

✅ Обеспечивает строгую типизацию и валидацию;

✅ Позволяет переиспользовать компоненты без сложных шаблонов.

Как CUE изменил наш workflow

  • Конфигурации теперь хранятся "as-is" — в YAML и получаются путём сериализации структуры данных с проверкой всех типов из .cue-файлов, аналогичных соответствующим YAML;

  • GitOps упростился — ArgoCD/Flux применяют конфиги напрямую, без промежуточного рендеринга;

  • Разработка ускорилась — ошибки видно сразу в IDE, а не после деплоя.

P.S. CUE полезен даже если вы не используете GitOps и не хотите хранить объекты в виде "as-is". Даже при runtime-генерации он принесёт пользу.

CUE Lang

CUE — это язык данных с открытым исходным кодом, основанный на логическом программировании, реализует *механизм логического вывода. Хотя этот язык не является языком программирования общего назначения, он решает множество задач, таких как проверка данных, создание шаблонов данных, генерация кода и даже написание скриптов.

Механизм логического вывода — это часть системы, которая автоматически делает выводы на основе предоставленных данных и правил. Например, если вы говорите: 

Факт_1: "Все люди смертны." 
Факт_2: "Сократ -- человек."
Вывод: "Сократ смертен."

Основные принципы и концепции

  1. Единая модель типов и значений

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

// user constraints
user: {
  name: string
  age: uint8 & >18 & <128
  isActive: bool
}

// user data
user: {
  name: "John Doe"
  age: 33
  isActive: true
}
# output yaml
user:
  name: "John Doe"
  age: 33
  isActive: true

Можно заметить сходство между CUE и JSON. Дело в том, что JSON является подмножеством CUE, т.е. любой JSON - это CUE-файл.

Обязательно используйте типы

Типы CUE действуют как средства проверки данных, а также служат механизмом шаблонизации. Это мощный инструмент, но требует несколько иного мышления. При классическом подходе к наследованию типы указывают явно в месте описания структуры. Вместо этого в CUE типы указывают в месте использования.

// user type with isActive true by default
#User: {
 name: string
 age: uint8 & >=18 & <128
 isActive: bool | *true
}

Достаточно указать в одном месте, что все элементы списка наследуют #User

users: [...#User] // all struct is #User

users: [
  {
    name: "John Doe"
    age: 33
  },
  {
    name: "Jane Doe"
    age: 18
    isActive: false
  }
]
# output yaml

users:
  - name: "John Doe"
    age: 33
    isActive: true
  - name: "Jane Doe"
    age: 18
    isActive: false

Сложным вычислениям - простая конфигурация

CUE решает конфликт между сложными вычислениями и простотой конфигурации, разделяя их. Вычисления можно выполнять вне CUE (например, в скриптах), а результаты — подмешивать в конфигурацию без учёта порядка. Это не только сохраняет простоту правок, но и позволяет использовать любые внешние инструменты для генерации данных.

Примечание: можно генерировать как в формате cue, так и yaml/json, то есть использовать форматы, которые не содержат типы и ограничения.

Старт и масштабирование

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

  • В малых проектах он предотвращает ошибки через строгую валидацию;

  • В средних — автоматически удаляет дублирование (например, сокращая тысячи строк конфигурации);

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

Автоматизация

Автоматизация — ключевой принцип CUE.

 ✅простой парсинг;

 ✅гибкая работа с данными из разных источников;

 ✅независимость от порядка применения правил.

Это позволяет легко:

✅ генерировать;

✅ анализировать;

✅ изменять конфигурации.

Все это без потери предсказуемости. CUE специально ограничивает импорты и организует пакеты на уровне директорий, что упрощает масштабирование и автоматизацию в больших проектах.

Теперь, когда мы знаем о CUE, можно посмотреть на то, как будет выглядеть GitOps-репозиторий для вашего продукта, как если бы вы захотели перейти на CUE прямо сейчас.

GitOps — это подход к управлению инфраструктурой Kubernetes, при котором желаемое состояние кластера описывается в Git-репозитории и автоматически синхронизируется с помощью инструментов, таких как ArgoCD или Flux. Этот метод обеспечивает прозрачность, контроль версий и возможность быстрого отката изменений, что делает развертывание более надежным и предсказуемым. Итак, у вас уже наверняка есть какое-то количество yaml-описаний ваших объектов Kubernetes.

guestbook

├── prod
│   ├── deployment
│   │   └── guestbook.yaml
│   └── service
│       └── guestbook-svc.yaml
└── stage
   ├── deployment
   │   └── guestbook.yaml
   └── service
       └── guestbook-svc.yaml

Теперь предлагаю выполнить

cd guestbook

cue import -p k8s ./... --path 'kind'  --path 'metadata.name'

find ./prod -name "*.cue" -exec sed -i '' '1s/^/@if(prod)\'$'\n/' {} \; # добавляет в заголовок if(prod) для исходных файлов prod

find ./stage -name "*.cue" -exec sed -i '' '1s/^/@if(stage)\'$'\n/' {} \; # добавляет в заголовок if(stage) для исходных файлов stage

Эта команда импортирует все ваши файлы .yaml в .cue

.
├── prod
│   ├── deployment
│   │   ├── guestbook.cue
│   │   └── guestbook.yaml
│   └── service
│       ├── guestbook-svc.cue
│       └── guestbook-svc.yaml
└── stage
   ├── deployment
   │   ├── guestbook.cue
   │   └── guestbook.yaml
   └── service
       ├── guestbook-svc.cue
       └── guestbook-svc.yaml

Далее предлагаю реорганизовать ваш проект так, чтобы каждый модуль состоял из каталога src и build, где src - исходный код ваших объектов, а build - готовые yaml

mkdir -p src/prod src/stage  # Создаем целевую структуру
mv prod/*/*.cue src/prod/  # Копируем prod
mv stage/*/*.cue src/stage/  # Копируем stage

Ну и перенесем наши yaml в build

mkdir build
mv prod build/
mv stage build/

На этом этапе рекомендую вам закоммитить эти изменения в git, чтобы потом вы могли увидеть в чем разница между вашими yaml AS-IS и продуктом компиляции CUE. Хоть он и уверяет из коробки, что компиляция идемпотентна, я предпочитаю это контролировать + в объекты можно зашить информацию о коммите/коммиторе/дате и т.д. Ну вот теперь можно начинать использовать CUE арсенал по максимуму! 

Результат:

Deployment: guestbook: {...}

Вот как раз Deployment - это глобальный контейнер для всех обьектов 

kind: Deployment помним это! 

Теперь нам необходимо добавить в проект этот глобальный контейнер -> Cоздадим в корне файлик def.cue для всех таких контейнеров

Service:    [Name=string]: {metadata: name: Name}
Deployment: [Name=string]: {metadata: name: Name}

Теперь можно добавить скриптов для вывода yaml в stdout или распихать все по файликам. CUE умеет импортировать схемы из исходников golang/protobuf/.. 

НО, как показывает практика, переносится все пока не так детально, как хотелось бы, из-за процессов сериализации/десериализации, поэтому рекомендую все-таки просматривать получившиеся схемы глазками. Мы в этом примере возьмем и скопируем все из репозитория.

  • Каталог tool содержит команды(скрипты в CUE) для работы с исходниками;

  • Каталог lib содержит дефолтные значения и какие-то узкие констрейнты связанные с вашим соглашением о разработки конфигураций;

  • Ну и вишенка на торте cue.mod это корневой каталог модуля CUE который как раз содержит пользовательские(на самом деле просто мной отредактированые продакшен) пакеты провайдеров апи такие как Istio/Argo-CD/K8s для валидации ваших конфигураций.

Обратите внимание, как изменился def.cue в него будут добавлены lib_v1.#Deployment схемы, которые, в свою очередь, являются расширением для исходного типа k8s.io.apps.Deployment 

Также есть уже Makefile, который любезно скрывает от пользователя/ранера сложность (и местами не очевидность) команд для cue :) Все схемы cгенерированы из go-зависимостей - proto, декомпозированы исходные файлы, так, чтобы наглядно была видна разница окружений для компонента.

P.S:

Мир ускоряется и будет ускоряться дальше. Давайте доверим такие скучные и рутинные задачи, как валидация и проставление дефолтных параметров, умным инструментам. CUE, на мой взгляд, имеет кучу преимуществ (особенно почитайте ещё раз раздел про автоматизацию), чтобы стать лидером в своей нише. Напоминаю, он потомок GCL — языка конфигурации для Borg, предка Kubernetes.

ВСЁ!

А как вы управляете инфраструктурой? Было бы интересно узнать — напишите в комментариях!

Комментарии (1)


  1. llia6an
    17.07.2025 10:00

    Именование бомба. Читал и думал, каким образом CUE-плейлисты могут решить подобную задачу. Оказалось - никак, потому что это не плейлисты, а что-то совсем другое с идентичным названием. Прямо как GPT (GUID Partition Table) превратился в GPT (чат-бот).