May 8

Конфигурационный полиглот или обзор языков конфигураций

Конфигурационный Вавилон наших дней: 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:

  • Структура на основе узлов: все в KDL — это узел с именем, необязательными свойствами и необязательными дочерними элементами.
  • Типы данных: строки, числа, логические значения (boolean) и null.
  • Поддерживает многострочные выражения.
  • Комментарии: поддерживает как строчные, так и блочные комментарии.
  • Аннотации: предоставляет метаданные об узлах с использованием синтаксиса (аннотаций).
  • Поддержка Unicode: полная поддержка Unicode, включая идентификаторы.
  • Библиотеки, есть почти для всех языков программирования

Примеры использования KDL:

1. Конфигурация приложения

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"
  }
}

2. Сериализация данных

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),
  },
}

Основные сферы применения:

  1. Управление ресурсами Kubernetes.
  2. В утилитах из категории IaC, в т.ч. Terraform, Pulumi.
  3. Конфигурирование приложений, централизованное управление конфигурацией.
  4. 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 -  это язык конфигураций на основе ограничений с возможностями проверки, модульности и применения политик.

Основные характеристики KCL:

  • Строгая статическая типизация с выводом типов, перехватом ошибок до времени выполнения.
  • Проверка на основе схемы для определения структурированных конфигураций с правилами проверки.
  • Политика как код для определения и обеспечения соблюдения организационных стандартов.
  • Неизменяемость (иммутабельность) - переменные по умолчанию неизменяемы, что способствует более безопасным конфигурациям.
  • Богатая стандартная библиотека со встроенными функциями.
  • Система импорта, обеспечивающая модульную конфигурацию с пакетами.

Хорошо интегрируется с большим количеством DevOps инструментов, в т.ч. Kubernetes (есть специальная спецификация https://github.com/kcl-lang/krm-kcl), Terraform, CI/CD пайплайны, GitOps (ArgoCD, FluxCD).

Расширяемость за счет написания собственных плагинов

Поддержка больше 10 SDK для разных языков

Библиотека с несколькими сотнями модулей

Большая библиотека примеров для различных случаев использования

Картинка взята с https://www.kcl-lang.io/

Пример простого конфига

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"}
]

В результате выполнения

kcl server.k

получим следующий YAML файл

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

- ip: 10.0.0.2

Пример описания схемы

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.

Пример валидации IP адреса

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

check.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

$ 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 подойдет в случаях, где требуется унифицированная схема и конфигурация со строгими ограничениями.

Дополнительные материалы