Конфигурационный полиглот или обзор языков конфигураций
Конфигурационный Вавилон наших дней: 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 configsKCL
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: backendschema 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/