Конфигурационный полиглот или обзор языков конфигураций
Конфигурационный Вавилон наших дней: KCL, KDL, Jsonnet и Cue lang — словно ручейки многоязычной реки Финнегана, текущие сквозь цифровую Лиффи современности. Riverrun, через конфиги и абстракции, от схем к значениям мы приходим... Джеймс Джойс о языках конфигураций
В данной статье мы рассмотрим не такие популярные языки конфигураций, как YAML, JSON, XML, INI, HCL, но более экзотические, но и не такие маргинальные. В данной статье рассмотрим cue-lang, dhall, kdl, jsonnet, KCL. Будем смотреть от простого к сложному.
Основные ограничения таких языков, как YAML, JSON, XML, INI, HCL:
- отсутствие проверки типов,
- отсутствие логических проверок,
- отсутствие поддержки абстракций и ограниченных возможностей повторного использования (в HCL есть базовые возможности через блоки и переменные; в YAML - якоря, но имеют ограничения использования в рамках одного файла, а также затрудняют чтение при сложных вложенных структурах),
- отсутствующие или ограниченные возможности программирования,
- склонность к синтаксическим ошибкам (отступы в YAML, скобочки и запятые в JSON),
- проверка осуществляется внешними инструментами (Json schema, XML schema),
- проверка происходит постфактум, после создания конфигурации.
Эти ограничения становятся особенно проблематичными по мере роста сложности и масштаба конфигураций инфраструктуры и приложений.
KDL
Этот претендент сильно выбивается из нашего short list конфигурационных монстров, но должны же мы порадовать тех, кому хочется выбрать хороший язык конфигурации для своего pet проекта.
- Структура на основе узлов: все в KDL — это узел с именем, необязательными свойствами и необязательными дочерними элементами.
- Типы данных: строки, числа, логические значения (boolean) и null.
- Поддерживает многострочные выражения.
- Комментарии: поддерживает как строчные, так и блочные комментарии.
- Аннотации: предоставляет метаданные об узлах с использованием синтаксиса (аннотаций).
- Поддержка Unicode: полная поддержка Unicode, включая идентификаторы.
- Библиотеки, есть почти для всех языков программирования
KDL отлично подходит для определения настроек приложения, благодаря своей интуитивно понятной структуре:
config { server { port 8080 host "0.0.0.0" timeout 30s } database { url "postgres://localhost:5432/myapp" max-connections 100 (sensitive) password "my-password" } }
KDL можно использовать для обмена данными, аналогично JSON, но с дополнительной выразительностью
users { user { id 1 name "Alice" roles ["admin", "user"] } user { id 2 name "Bob" roles ["user"] } }
Есть руководства по тому, как JSON конвертировать в KDL,
а также XML в KDL
Спецификация языка описана здесь
JSONNET
Является языком шаблонизации, который расширяет JSON такими фичами, как переменные, функции и условия.
// Простой пример { person: { name: "Alice", greeting: "Hello " + self.name + "!", }, // Используем локальные переменные local tax = 0.07, prices: { item: 100, withTax: self.item * (1 + tax), }, }
- Управление ресурсами Kubernetes.
- В утилитах из категории IaC, в т.ч. Terraform, Pulumi.
- Конфигурирование приложений, централизованное управление конфигурацией.
- CI/CD пайплайны: шаблонизация конфигурационных файлов (Gitlab CI, Github Actions), стандартизация пайплайнов между проектами.
Например, такие проекты как Tanka, Qbec используют jsonnet для конфигурации Kubernetes.
Dhall
Основные свойства языка Dhall: он полностью функциональный язык конфигураций, а также в нем сделан упор на безопасность.
Основные характеристики Dhall:
- Строгая система типов: Dhall статически типизирован, перехватывает ошибки во время компиляции, а не во время выполнения.
- Полные функции: все функции завершаются, предотвращая бесконечные циклы.
- Dhall неполный по Тьюрингу язык, что по замыслу позволяет избегать проблем полных по Тьюрингу языков (проверка типов за конечное время, отсутствие рекурсии).
- Нормализация: выражения можно нормализовать до стандартной формы, что упрощает сравнение конфигураций.
- Безопасные импорты пакетов с проверкой целостности.
- Биндинги для различных языков.
- Есть различные пакеты для поддержки конфигурации Dhall в популярных инструментах ansible, kubernetes.
let Config : Type = {- What happens if you add another field here? -} { home : Text , privateKey : Text , publicKey : Text } let makeUser : Text -> Config = \(user : Text) -> let home : Text = "/home/${user}" let privateKey : Text = "${home}/.ssh/id_ed25519" let publicKey : Text = "${privateKey}.pub" let config : Config = { home, privateKey, publicKey } in config let configs : List Config = [ makeUser "bill" , makeUser "jane" ] in configs
KCL
KCL - это язык конфигураций на основе ограничений с возможностями проверки, модульности и применения политик.
- Строгая статическая типизация с выводом типов, перехватом ошибок до времени выполнения.
- Проверка на основе схемы для определения структурированных конфигураций с правилами проверки.
- Политика как код для определения и обеспечения соблюдения организационных стандартов.
- Неизменяемость (иммутабельность) - переменные по умолчанию неизменяемы, что способствует более безопасным конфигурациям.
- Богатая стандартная библиотека со встроенными функциями.
- Система импорта, обеспечивающая модульную конфигурацию с пакетами.
Хорошо интегрируется с большим количеством DevOps инструментов, в т.ч. Kubernetes (есть специальная спецификация https://github.com/kcl-lang/krm-kcl), Terraform, CI/CD пайплайны, GitOps (ArgoCD, FluxCD).
Расширяемость за счет написания собственных плагинов
Поддержка больше 10 SDK для разных языков
Библиотека с несколькими сотнями модулей
Большая библиотека примеров для различных случаев использования
title = "KCL Example" owner = { name = "The KCL Authors" data = "2020-01-02T03:04:05" } database = { enabled = True ports = [8000, 8001, 8002] data = [["delta", "phi"], [3.14]] temp_targets = {cpu = 79.5, case = 72.0} } servers = [ {ip = "10.0.0.1", role = "frontend"} {ip = "10.0.0.2", role = "backend"} ]
kcl server.k
title: KCL Example owner: name: The KCL Authors data: "2020-01-02T03:04:05" database: enabled: true ports: - 8000 - 8001 - 8002 data: - - delta - phi - - 3.14 temp_targets: cpu: 79.5 case: 72.0 servers: - ip: 10.0.0.1 role: frontend - ip: 10.0.0.2 role: backend
schema DatabaseConfig: enabled: bool = True ports: [int] = [8000, 8001, 8002] data: [[str|float]] = [["delta", "phi"], [3.14]] temp_targets: {str: float} = {cpu = 79.5, case = 72.0}
CUE LANG
Cue lang - это язык проверки данных с механизмом вывода, основанным на логическом программировании. Ключевая вещь, которая отличает Cue от других языков то, что Cue объединяет типы и значения в единую концепцию.
Основные характеристики Cue lang:
- Унификация схем и данных, CUE рассматривает схемы и данные как одно и тоже, что позволяет объединить их посредством унификации.
- Декларативная природа языка и идемпотентность: повторение ограничений не меняет результат.
- Отделение вычислений от конфигурации: данные, которые необходимо вычислить, могут быть вычислены отдельно и помещены в файл.
- Ограничения (constraints) CUE действуют как валидаторы данных, а также как механизм для сокращения шаблонного кода.
- Система типов на основе решеток: значения в CUE образуют решетку, где любые два значения имеют уникальное наиболее конкретное значение, которое обобщает оба (наименьшая верхняя граница), и уникальное наиболее общее значение, которое специализируется на обоих (наибольшая нижняя граница).
Cue может быть использован для управления конфигурацией kubernetes, terraform, управление конфигурацией CI/CD gitlab, github.
У CUE есть поддержка интеграций с YAML, JSON, Jsonschema, OpenAPI, Go (для Go есть даже кодогенерация и извлечение данных), Protobuf и Java.
package example import "net" [_]: net.IPv4 v4String: "198.51.100.14" v4Bytes: [198, 51, 100, 14] // невалидные ip адреса tooManyOctets: "198.51.100.14.0" octetTooLarge: [300, 51, 100, 14] v6NotV4: "2001:0db8:85a3::8a2e:0370:7334"
cue vet -c octetTooLarge: invalid value [300,51,100,14] (does not satisfy net.IPv4): ./file.cue:6:6 ./file.cue:14:16 tooManyOctets: invalid value "198.51.100.14.0" (does not satisfy net.IPv4): ./file.cue:6:6 ./file.cue:13:16 v6NotV4: invalid value "2001:0db8:85a3::8a2e:0370:7334" (does not satisfy net.IPv4): ./file.cue:6:6 ./file.cue:15:10
Пример валидации конфигурации с использованием CUE
Workflow: { jobs: deploy: { environment!: string // для окружения production запускать нужно на ubuntu-latest if environment == "production" { "runs-on"!: "ubuntu-latest" } } } .github/workflows/deploy-to-ecs.yml name: Deploy to Amazon ECS on: push: branches: [ $default-branch ] env: AWS_REGION: MY_AWS_REGION ECR_REPOSITORY: MY_ECR_REPOSITORY ECS_SERVICE: MY_ECS_SERVICE ECS_CLUSTER: MY_ECS_CLUSTER ECS_TASK_DEFINITION: MY_ECS_TASK_DEFINITION CONTAINER_NAME: MY_CONTAINER_NAME permissions: contents: read jobs: deploy: name: Deploy runs-on: ubuntu-20.04 environment: production steps: - name: Checkout uses: actions/checkout@v3 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ${{ env.AWS_REGION }} - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v1 - name: Build, tag, and push image to Amazon ECR id: build-image env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} IMAGE_TAG: ${{ github.sha }} run: | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT - name: Fill in the new image ID in the Amazon ECS task definition id: task-def uses: aws-actions/amazon-ecs-render-task-definition@v1 with: task-definition: ${{ env.ECS_TASK_DEFINITION }} container-name: ${{ env.CONTAINER_NAME }} image: ${{ steps.build-image.outputs.image }} - name: Deploy Amazon ECS task definition uses: aws-actions/amazon-ecs-deploy-task-definition@v1 with: task-definition: ${{ steps.task-def.outputs.task-definition }} service: ${{ env.ECS_SERVICE }} cluster: ${{ env.ECS_CLUSTER }} wait-for-service-stability: true
$ cue vet -c check.cue .github/workflows/deploy-to-ecs.yml -d 'Workflow' jobs.deploy."runs-on": conflicting values "ubuntu-latest" and "ubuntu-20.04": .github/workflows/deploy-to-ecs.yml:22:14 ./check.cue:6:3 ./check.cue:7:16
Итоги
- KDL: простой синтаксис со структурой на основе узлов. Отлично подходит для сценариев, где удобство чтения человеком имеет первостепенное значение. Простота KDL - его сильная сторона.
- JSONNET подходит для конфигураций с большим количеством шаблонов.
- Dhall больше подходит для конфигураций, где корректность и безопасность имеют решающее значение, а также там, где для разработчиков будет более близким и знакомым функциональный стиль программирования.
- KCL подойдет для cloud-native приложений со сложными требованиями к проверке.
- CUE подойдет в случаях, где требуется унифицированная схема и конфигурация со строгими ограничениями.
Дополнительные материалы
- Как описать 100 Gitlab джоб в 100 строк JSONNET https://habr.com/ru/articles/483626/
- Развертывание программных систем в Kubernetes c помощью JSONNET https://habr.com/ru/articles/720556/
- Официальный сайт KDL https://kdl.dev/
- Официальный сайт JSONNET https://jsonnet.org/
- Официальный сайт KCL https://www.kcl-lang.io/
- Официальный сайт CUE https://cuelang.org/
- Cтатья по конфигурации Kubernetes c использованием CUE - https://engineering.mercari.com/en/blog/entry/20220127-kubernetes-configuration-management-with-cue/