- PVSM.RU - https://www.pvsm.ru -

Язык конфигураций Dhall как еще один способ написания манифестов для Kubernetes

Dhall [1] — программируемый язык для создания конфигурационных файлов различного назначения. Это Open Source-проект, первый публичный релиз которого состоялся в 2018 году.

Как и всякий новый язык для генерации конфигурационных файлов, Dhall призван решить проблему ограниченной функциональности YAML, JSON, TOML, XML и других форматов конфигурации, но при этом оставаться достаточно простым. Язык распространяется всё шире. В 2020-м году представили его bindings [2], сделанные специально для Kubernetes.

Язык конфигураций Dhall как еще один способ написания манифестов для Kubernetes - 1

Рассказывая о Dhall применительно к созданию K8s-манифестов, начнем все же с краткого общего описания.

Чем Dhall отличается от других языков

Авторы проекта предлагают рассматривать Dhall как продвинутый JSON: с функциями, типами, импортами. Зачем нужен новый формат, если уже есть проверенные? 

Just because
xkcd про Standards
xkcd про Standards

Главный аргумент создателей: упомянутые JSON и YAML — не программируемые языки. Это сужает их возможности и порой приводит к неоптимальным решениям. Например, к повторениям.

​​Фокус на DRY

Хороший тон для разработчика — следовать правилу DRY [3] («Don’t repeat yourself»). Когда работаешь с ​​JSON и YAML, не повторять себя трудно. Из-за функциональной ограниченности в конфигурационных файлах часто приходится использовать повторяющиеся блоки конфигурации. Их нельзя упростить или отбросить.

Dhall позиционируется как язык, который помогает придерживаться принципа DRY. Там, где в ​​JSON- или YAML-файл нужно вставить дополнительный блок кода, в Dhall можно подставить результат выполнения функции или значение переменной. В качестве иллюстрации в документации Dhall приводится пример [4], в котором сравниваются два конфигурационных файла, в JSON- и Dhall-формате соответственно. Каждый выполняет одну и ту же задачу: описывает место хранения публичного и приватного SSH-ключей пользователей.

Исходный JSON-файл:

[
    {
        "privateKey": "/home/john/.ssh/id_rsa",
        "publicKey": "/home/john/.ssh/id_rsa.pub",
        "user": "john"
    },
    {
        "privateKey": "/home/jane/.ssh/id_rsa",
        "publicKey": "/home/jane/.ssh/id_rsa.pub",
        "user": "jane"
    },
    {
        "privateKey": "/etc/jenkins/jenkins_rsa",
        "publicKey": "/etc/jenkins/jenkins_rsa.pub",
        "user": "jenkins"
    },
    {
        "privateKey": "/home/chad/.ssh/id_rsa",
        "publicKey": "/home/chad/.ssh/id_rsa.pub",
        "user": "chad"
    }
]

Та же конфигурация в Dhall-формате:

-- config0.dhall

let ordinaryUser =
      (user : Text) ->
        let privateKey = "/home/${user}/.ssh/id_rsa"

        let publicKey = "${privateKey}.pub"

        in  { privateKey, publicKey, user }

in  [ ordinaryUser "john"
    , ordinaryUser "jane"
    , { privateKey = "/etc/jenkins/jenkins_rsa"
      , publicKey = "/etc/jenkins/jenkins_rsa.pub"
      , user = "jenkins"
      }
    , ordinaryUser "chad"
    ]

Пока по количеству строк файлы почти не отличаются. 

Добавим нового пользователя — alice. Для этого в JSON-файл нужно вставить дополнительный блок из 5 строк:

[
    …
    {
        "privateKey": "/home/alice/.ssh/id_rsa",
        "publicKey": "/home/alice/.ssh/id_rsa.pub",
        "user": "alice"
    }
]

При этом даже при простом копипасте можно ошибиться: например, скопировать конфиг из блока для chad, но в одном из полей не поменять имя на alice.

Для той же цели в Dhall-файл достаточно вызвать ещё раз ранее определенную функцию ordinaryUser — это займет одну строку:

…
in  [ ordinaryUser "john"
    , ordinaryUser "jane"
    , { privateKey = "/etc/jenkins/jenkins_rsa"
      , publicKey = "/etc/jenkins/jenkins_rsa.pub"
      , user = "jenkins"
      }
    , ordinaryUser "chad"
    , ordinaryUser "alice" -- та самая новая строка
    ]

Чем сложнее конфигурационный файл, тем более очевидна негибкость JSON по сравнению с Dhall.

Быстрый экспорт в другие форматы

Для экспорта конфигурации в нужный формат достаточно одной команды. Вот, например, как превратить вышеприведенный Dhall-файл в JSON:

dhall-to-json --pretty <<< './config0.dhall'

По тому же принципу организован экспорт в YAML, XML, Bash. Да, это не ошибка: dhall-bash [5] превращает инструкции на Dhall в Bash-скрипты, однако для этого поддерживается только ограниченное количество конструкций.

Акцент на безопасности

Dhall — программируемый язык, но при этом не тьюринг-полный [6]. Авторы проекта говорят, что такое ограничение повышает безопасность кода и конфигурационных файлов, написанных на нем.

Некоторые инструменты для своих конфигураций используют существующие языки программирования. Например, webpack поддерживает для этого TypeScript (и не только), Django — Python, sbt — Scala и т. п. Однако обратная сторона такой гибкости и свободы — это возможные проблемы со вставками ненадежного кода, межсайтовым скриптингом (XSS), подделками запросов со стороны сервера (SSRF) и другими атаками. Dhall от этого защищен [7].

Dhall и Kubernetes

Ок, а что насчет применимости Dhall к генерации манифестов K8s? 

Неудобство работы со множеством объектов в Kubernetes во многом обуcловлено особенностью дизайна YAML. Хотя YAML поддерживает минимальную шаблонизацию, прежде всего это формат для хранения конфигурации. Обойти его ограничения можно, например, с помощью Helm-шаблонов, но это не всегда просто и даже не всегда выполнимо (у нас была подробная статья [8] на эту тему). Альтернатива — использовать другой, более гибкий язык для конфигурации, например Dhall (а некоторые другие примеры будут приведены ниже). Потому что это язык со встроенными шаблонами, которые в то же время не строго типизированы. Он, как отмечают [9] создатели, предлагает простое описание конфигурации независимо от того, сколько абстракций создается.

Объекты Kubernetes можно генерировать с помощью выражений Dhall, а затем экспортировать их в YAML-формат утилитой dhall-to-yaml. В публичном GitHub-репозитории dhall-kubernetes [2] содержатся так называемые bindings — типы и функции Dhall, предназначенные для работы с объектами Kubernetes. Вот, например, как выглядит Dhall-конфигурация Deployment’а:

-- examples/deploymentSimple.dhall

let kubernetes =
      https://raw.githubusercontent.com/dhall-lang/dhall-kubernetes/master/package.dhall sha256:532e110f424ea8a9f960a13b2ca54779ddcac5d5aa531f86d82f41f8f18d7ef1

let deployment =
      kubernetes.Deployment::{
      , metadata = kubernetes.ObjectMeta::{ name = Some "nginx" }
      , spec = Some kubernetes.DeploymentSpec::{
        , selector = kubernetes.LabelSelector::{
          , matchLabels = Some (toMap { name = "nginx" })
          }
        , replicas = Some +2
        , template = kubernetes.PodTemplateSpec::{
          , metadata = Some kubernetes.ObjectMeta::{ name = Some "nginx" }
          , spec = Some kubernetes.PodSpec::{
            , containers =
              [ kubernetes.Container::{
                , name = "nginx"
                , image = Some "nginx:1.15.3"
                , ports = Some
                  [ kubernetes.ContainerPort::{ containerPort = +80 } ]
                }
              ]
            }
          }
        }
      }

in  deployment

При его экспорте в привычный YAML-формат с помощью dhall-to-yaml получится следующее:

## examples/out/deploymentSimple.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      name: nginx
  template:
    metadata:
      name: nginx
    spec:
      containers:
        - image: nginx:1.15.3
          name: nginx
          ports:
            - containerPort: 80

Говоря о «модульности» Dhall, создатели языка рассматривают [10] случай, когда нужно определить: а) некоторый тип MyService с настройками для разных deployment’ов, б) функции, которые можно применять к MyService, чтобы создавать объекты для K8s. Это удобно, потому что позволяет определять сервисы не только в контексте Kubernetes и переиспользовать абстракции для работы с другими типами конфигурационных файлов. При этом принцип DRY остается в силе: чтобы внести небольшое изменение в конфигурации нескольких объектов, чаще всего достаточно изменить функцию в одном Dhall-файле — вместо того, чтобы руками править все связанные YAML’ы.

Пример такой «модульности» Dhall — конфигурация контроллера Nginx Ingress, который настраивает TLS-сертификаты и маршруты для нескольких сервисов:

-- examples/ingress.dhall

let Prelude =
      ../Prelude.dhall sha256:10db3c919c25e9046833df897a8ffe2701dc390fa0893d958c3430524be5a43e

let map = Prelude.List.map

let kubernetes =
      https://raw.githubusercontent.com/dhall-lang/dhall-kubernetes/master/package.dhall sha256:532e110f424ea8a9f960a13b2ca54779ddcac5d5aa531f86d82f41f8f18d7ef1

let Service = { name : Text, host : Text, version : Text }

let services = [ { name = "foo", host = "foo.example.com", version = "2.3" } ]

let makeTLS
    : Service → kubernetes.IngressTLS.Type
    = λ(service : Service) →
        { hosts = Some [ service.host ]
        , secretName = Some "${service.name}-certificate"
        }

let makeRule
    : Service → kubernetes.IngressRule.Type
    = λ(service : Service) →
        { host = Some service.host
        , http = Some
          { paths =
            [ { backend =
                { serviceName = service.name
                , servicePort = kubernetes.IntOrString.Int +80
                }
              , path = None Text
              }
            ]
          }
        }

let mkIngress
    : List Service → kubernetes.Ingress.Type
    = λ(inputServices : List Service) →
        let annotations =
              toMap
                { `kubernetes.io/ingress.class` = "nginx"
                , `kubernetes.io/ingress.allow-http` = "false"
                }

        let defaultService =
              { name = "default"
              , host = "default.example.com"
              , version = " 1.0"
              }

        let ingressServices = inputServices # [ defaultService ]

        let spec =
              kubernetes.IngressSpec::{
              , tls = Some
                  ( map
                      Service
                      kubernetes.IngressTLS.Type
                      makeTLS
                      ingressServices
                  )
              , rules = Some
                  ( map
                      Service
                      kubernetes.IngressRule.Type
                      makeRule
                      ingressServices
                  )
              }

        in  kubernetes.Ingress::{
            , metadata = kubernetes.ObjectMeta::{
              , name = Some "nginx"
              , annotations = Some annotations
              }
            , spec = Some spec
            }

in  mkIngress services

Результат экспорта dhall-to-yaml:

## examples/out/ingress.yaml

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.allow-http: 'false'
    kubernetes.io/ingress.class: nginx
  name: nginx
spec:
  rules:
    - host: foo.example.com
      http:
        paths:
          - backend:
              serviceName: foo
              servicePort: 80
    - host: default.example.com
      http:
        paths:
          - backend:
              serviceName: default
              servicePort: 80
  tls:
    - hosts:
        - foo.example.com
      secretName: foo-certificate
    - hosts:
        - default.example.com
      secretName: default-certificate

Здесь определенная функция services была вызвана дважды: с параметрами, указанными в defaultService (с хостом default.example.com), и переданными вручную значениями (с хостом foo.example.com). Таким образом, в итоговом манифесте получаем ресурс Ingress с двумя этими хостами.

Примеры использования в сообществе

На конференции OSDNConf 2021 Олег Николин из Portside рассказал [11], как Dhall помог его инженерной команде. После перехода на Kubernetes и усложнения CI/CD-процесса количество YAML-конфигураций, используемых в компании, выросло до 12 тысяч. Если нужно было добавлять новую переменную в один из сервисов, приходилось вносить изменения в 40 файлов, которые лежали в разных репозиториях. Проводить ревью кода было очень сложно. Проблемы накапливались, но при этом проявлялись не всегда сразу после деплоя. Если сервис некоторое время работал с некорректной конфигурацией, отследить исходную причину проблемы было тяжело.

После перехода на Dhall и рефакторинга команда избавилась от 50% ненужной конфигурации. Dhall повысил безопасность CI/CD: стало легче проверять манифесты K8s и переменные окружения до деплоя. Также появилась общая библиотека с описанием всех используемых ресурсов K8s. Всё это упростило работу DevOps-инженеров и проверку корректности конфигураций.

Другой интересный пример того, как Dhall упрощает работу с YAML-файлами, приводит [12] Christine Dodrill из Tailscale. Она хотела упростить проверку конфигурационных файлов K8s на предмет их корректности. С этим ей не помогали ни Helm, ни Kustomize — в отличие от Dhall. И она пришла к такому выводу: «Dhall, вероятно, наиболее жизнеспособная замена Helm или другим инструментам для создания манифестов Kubernetes».

Альтернативы для создания манифестов

Да, кроме Dhall есть и другие фреймворки и языки, с которыми можно обойти ограничения YAML, сделать работу с манифестами в Kubernetes более удобной. Примеры Open Source-проектов:

  • Cue [13]. Язык с широким набором инструментов для определения, генерации и валидации конфигурационных файлов, API, схем баз данных и других типов данных.

  • Jsonnet [14]. Язык для создания шаблонов конфигураций. Как видно из названия, jsonnet — комбинация JSON и sonnet. Язык во многом похож на Cue.

  • jk [15]. Шаблонизатор для написании структурированных конфигурационных файлов, включая манифесты K8s.

  • HCL [16]. Язык, разработанный в HashiCorp. У HCL есть собственный «человекоориентированный» синтаксис, а также вариант на основе JSON, адаптированный для машинной обработки.

  • cdk8s [17]. Фреймворк для «программирования» Kubernetes-манифестов на JavaScript, Java, TypeScript и Python. Обзор [18] по нему мы недавно публиковали.

Критика Dhall

Хотя синтаксис Dhall несложный [19], а варианты использования языка подробно описаны в документации [20], кому-то он может показаться трудным для изучения. Чтобы освоить Dhall более или менее быстро, нужен хотя бы базовый опыт работы с программируемыми языками.

Некоторые согласны [21] с тем, что у существующих языков для создания конфигураций есть проблемы с гибкостью, но не согласны с решением, которое предлагает Dhall. «Привычным языкам не хватает полезных инженерных свойств, — говорит Andy Chu, программист и создатель Oil Shell, — но зато у них нет и побочных эффектов. И Dhall не избавлен от этих эффектов». Ему вторит сотрудник Earthly [22] Adam Gordon Bell который считает [23], что «Dhall странный» — даже более странный, чем HCL, а ведь последний лучше известен в сообществе.

Резюме

Несмотря на свою относительную новизну, Dhall — это достаточно зрелый фреймворк, который развивается с учетом отзывов и пожеланий сообщества. У проекта 3300+ звезд на GitHub, уверенная база контрибьюторов и регулярные релизы. Dhall, по меньшей мере, достоин рассмотрения как один из вариантов для случаев, когда простых манифестов и их шаблонов перестает хватать.

Язык применяется в production [24] рядом компаний; в частности, среди пользователей, которые используют Dhall для создания и управления манифестами Kubernetes, упоминаются KSF Media, Earnest Research и IOHK.

P.S.

Читайте также в нашем блоге:

Автор: Oleg Zinovyev

Источник [25]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/open-source/370374

Ссылки в тексте:

[1] Dhall: https://github.com/dhall-lang/dhall-lang

[2] его bindings: https://github.com/dhall-lang/dhall-kubernetes

[3] DRY: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself

[4] пример: https://docs.dhall-lang.org/discussions/Programmable-configuration-files.html#programmable-configuration-file

[5] dhall-bash: https://github.com/dhall-lang/dhall-haskell/tree/master/dhall-bash

[6] тьюринг-полный: https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D0%BB%D0%BD%D0%BE%D1%82%D0%B0_%D0%BF%D0%BE_%D0%A2%D1%8C%D1%8E%D1%80%D0%B8%D0%BD%D0%B3%D1%83

[7] защищен: https://docs.dhall-lang.org/discussions/Safety-guarantees.html#

[8] подробная статья: https://habr.com/ru/company/flant/blog/529158/

[9] отмечают: https://github.com/dhall-lang/dhall-kubernetes#why-do-i-need-this

[10] рассматривают: https://github.com/dhall-lang/dhall-kubernetes#more-modular-defining-an-ingress

[11] рассказал: https://youtu.be/UUhkZcqZVGM

[12] приводит: https://christine.website/blog/dhall-kubernetes-2020-01-25

[13] Cue: https://cuelang.org/

[14] Jsonnet: https://jsonnet.org/

[15] jk: https://jkcfg.github.io/#/

[16] HCL: https://github.com/hashicorp/hcl

[17] cdk8s: https://cdk8s.io/

[18] Обзор: https://habr.com/ru/company/flant/blog/577624/

[19] несложный: https://learnxinyminutes.com/docs/dhall/

[20] подробно описаны в документации: https://docs.dhall-lang.org/howtos/How-to-integrate-Dhall.html

[21] согласны: https://lobste.rs/s/1nxt6g/intercal_yaml_other_horrible#c_pc0dt6

[22] Earthly: https://github.com/earthly/earthly

[23] считает: https://earthly.dev/blog/on-yaml-discussions/#dhall-is-strange

[24] применяется в production: https://docs.dhall-lang.org/discussions/Dhall-in-production.html

[25] Источник: https://habr.com/ru/post/593321/?utm_source=habrahabr&utm_medium=rss&utm_campaign=593321