Привет, Хабр!
Эта статья о том, как мы избавились от шаблонизации в пользу понятных слоёв в управлении инфраструктурой. Расскажу про предпосылки, поиск решения и инструмент, который мы выбрали — 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: "Сократ -- человек."
Вывод: "Сократ смертен."
Основные принципы и концепции
Единая модель типов и значений
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.
ВСЁ!
А как вы управляете инфраструктурой? Было бы интересно узнать — напишите в комментариях!
llia6an
Именование бомба. Читал и думал, каким образом CUE-плейлисты могут решить подобную задачу. Оказалось - никак, потому что это не плейлисты, а что-то совсем другое с идентичным названием. Прямо как GPT (GUID Partition Table) превратился в GPT (чат-бот).