告別配置復雜性:領域特定語言(DSL)能幫你嗎?
譯自:Can Configuration Languages (config DSLs) solve configuration complexity?[1]
作者:Brian Grant
配置語言能否顯著降低配置復雜性?
過去幾年涌現出大量旨在生成配置的領域特定語言 (DSL),即配置語言:HCL[2],Dhall[3],Jsonnet[4],Starlark[5],CUE[6],Nickel[7],KCL[8],Pkl[9],以及其他[10]。我敢肯定至少有15種[11]。我通過包含表達式、條件語句、變量和其他語法結構來區分這些語言和 JSON、XML、TOML、INI 等數據序列化語言,這些結構有助于根據輸入生成多個具體的配置作為輸出。我將 YAML 歸類為數據序列化類別。YAMLScript[12] 比較新,我還沒有看到任何使用案例,因此不會介紹它。有關不同類型語言的更詳細細分,請參閱KCL 項目的這篇文章[13],該文章還比較了 KCL 與許多這些語言[14]。
為什么有人會選擇使用配置語言來編寫配置生成器/模板,而不是通用語言或模板語言(例如,Go 模板、Jinja、Mustache)?
對于工具構建者而言,與通用語言相比,一個好處是這些語言(大多)是解釋型語言,并且可以嵌入到工具中,盡管模板語言也具有此特性。配置語言也可能比通用語言或模板語言更容易進行靜態分析,并且可以輕松地確保它們不會產生副作用[15]。
除了根據自己的喜好塑造語言[16]之外,創建新的配置語言的好處還在于它可以更好地控制包和注冊表系統以及標準庫。事實上,其中一些語言最初是為特定工具創建的[17],例如 Terraform 的 HCL,Bazel 的 Starlark Bazel[18],Nix 包管理器的 Nickel Nix 包管理器[19],以及 KusionStack 的 KCL KusionStack[20]。
對于用戶而言,語法可能比通用語言更簡潔。此外,我讀到一些非程序員發現 HCL 與腳本語言(如 shell、awk 和/或 perl?)足夠相似,與 Python 和 Typescript 等通用語言相比更容易上手。對于程序員而言,使用熟悉的通用語言[21] 是 Pulumi 等工具的一個賣點,但也許配置語言可以在多個通用語言用于應用程序的環境中提供一個中立的中間地帶。與模板語言相比,配置語言具有更強大的表達能力,并且通常具有更高的類型安全性和模式驗證能力。
當然,每位語言設計者在設計語言時都有一些具體目標。例如,CUE[22] 基于從 Google 內部配置語言中吸取的經驗教訓(Jsonnet[23] 也是如此),CUE 的一個目標是通過不允許覆蓋來更容易確定最終值設置的位置[24]。Dhall 的一個目標是使導入安全[25]。Starlark 是一種可嵌入的 Python 方言[26],對于熟悉 Python 的人來說很熟悉。Jsonnet 是JSON 的超集[27]。Nickel 中的類型是可選的[28]。Pkl[29]……等等。至少從編程語言設計的角度來看,它們很有趣。好的,這些語言看起來是什么樣的?由于我不精通大多數這些語言,我使用了Claude來生成每個語言的Kubernetes Deployment示例,其中資源名稱、標簽值和容器鏡像都是參數化的。我不得不說,我對Claude印象非常深刻。Claude包含了如何運行每個工具的說明,提到了每種語言的一些好處,并提供了使用特定語言功能進一步改進的建議。我將結果與我能找到的其他示例進行了比較,但沒有通過這些工具運行它們。這只是為了說明這些語言的特點。
以下是Deployment的YAML,其中包含一些屬性值,這些值是字符串、整數和布爾值,以及映射和數組/列表:
apiVersion: apps/v1
kind:Deployment
metadata:
labels:
app:mydep
name:mydep
namespace:example
spec:
replicas:3
selector:
matchLabels:
app:mydep
template:
metadata:
labels:
app:mydep
spec:
dnsPolicy:ClusterFirst
containers:
-image:nginx:latest
name:nginx
ports:
-containerPort: 8080
使用Kubernetes 提供程序[30] 的HCL:
# Variables
variable "deployment_name" {
description = "Name of the Kubernetes deployment"
type = string
}
variable "container_image" {
description = "Container image to deploy"
type = string
}
# Deployment resource
resource "kubernetes_deployment" "deployment" {
metadata {
name = var.deployment_name
namespace = "example"
labels = {
app = var.deployment_name
}
}
spec {
replicas = 3
selector {
match_labels = {
app = var.deployment_name
}
}
template {
metadata {
labels = {
app = var.deployment_name
}
}
spec {
dns_policy = "ClusterFirst"
container {
name = "nginx"
image = var.container_image
port {
container_port = 8080
}
}
}
}
}
}
Dhall (官方示例[31]):
-- Type definitions for our configuration
let Kubernetes =
https://raw.githubusercontent.com/dhall-lang/dhall-kubernetes/master/package.dhall
let deploymentName : Text = "mydep"
let containerImage : Text = "nginx:latest"
let deploymentLabels =
toMap { app = deploymentName }
let containerPort =
Kubernetes.ContainerPort::{
, containerPort = 8080
}
let container =
Kubernetes.Container::{
, name = "nginx"
, image = Some containerImage
, ports = Some [ containerPort ]
}
let podTemplateSpec =
Kubernetes.PodTemplateSpec::{
, metadata = Some Kubernetes.ObjectMeta::{ labels = Some deploymentLabels }
, spec = Some Kubernetes.PodSpec::{
, containers = [ container ]
, dnsPolicy = Some "ClusterFirst"
}
}
let deploymentSpec =
Kubernetes.DeploymentSpec::{
, replicas = Some 3
, selector = Kubernetes.LabelSelector::{ matchLabels = Some deploymentLabels }
, template = podTemplateSpec
}
in Kubernetes.Deployment::{
, metadata = Kubernetes.ObjectMeta::{
, name = Some deploymentName
, namespace = Some "example"
, labels = Some deploymentLabels
}
, spec = Some deploymentSpec
}
Jsonnet (更多Kubernetes示例[32]):
// Input parameters
local params = {
deploymentName: 'mydep',
containerImage: 'nginx:latest',
};
// Helper to generate consistent labels
local labels = {
app: params.deploymentName,
};
// Main deployment definition
{
apiVersion: 'apps/v1',
kind: 'Deployment',
metadata: {
name: params.deploymentName,
namespace: 'example',
labels: labels,
},
spec: {
replicas: 3,
selector: {
matchLabels: labels,
},
template: {
metadata: {
labels: labels,
},
spec: {
dnsPolicy: 'ClusterFirst',
containers: [
{
name: 'nginx',
image: params.containerImage,
ports: [
{
containerPort: 8080,
},
],
},
],
},
},
},
}
CUE (Kubernetes 教程[33]) — 我刪除了模式,因為它可能已被導入:
// Input parameters
params: {
deploymentName: string
containerImage: string
}
// Default values
params: {
deploymentName: "mydep"
containerImage: "nginx:latest"
}
// Deployment configuration
deployment: #Deployment & {
metadata: {
name: params.deploymentName
namespace: "example"
labels: {
app: params.deploymentName
}
}
spec: {
replicas: 3
selector: {
matchLabels: {
app: params.deploymentName
}
}
template: {
metadata: {
labels: {
app: params.deploymentName
}
}
spec: {
containers: [{
name: "nginx"
image: params.containerImage
ports: [{
containerPort: 8080
}]
}]
}
}
}
}
// Output the deployment
deployment
Pkl (示例[34]):
module deployment
import "package://pkg.pkl-lang.org/k8s/apps/v1/1.27" as apps
import "package://pkg.pkl-lang.org/k8s/core/v1/1.27" as core
// Input parameters
deployCfg {
name: String = "mydep"
image: String = "nginx:latest"
}
// Create deployment using official K8s types
output = new apps.Deployment {
metadata {
name = deployCfg.name
namespace = "example"
labels = new {
app = deployCfg.name
}
}
spec {
replicas = 3
selector {
matchLabels = new {
app = deployCfg.name
}
}
template {
metadata {
labels = new {
app = deployCfg.name
}
}
spec {
dnsPolicy = "ClusterFirst"
containers = List(
new core.Container {
name = "nginx"
image = deployCfg.image
ports = List(
new core.ContainerPort {
containerPort = 8080
}
)
}
)
}
}
}
}
Nickel (示例[35]):
# Type contracts
let DeploymentConfig = {
name | Str,
image | Str,
}
# Function to generate labels
let makeLabels = fun name => {
app = name
}
# Main deployment generator function
let makeDeployment = fun config | DeploymentConfig => {
apiVersion = "apps/v1",
kind = "Deployment",
metadata = {
name = config.name,
namespace = "example",
labels = makeLabels config.name,
},
spec = {
replicas = 3,
selector = {
matchLabels = makeLabels config.name,
},
template = {
metadata = {
labels = makeLabels config.name,
},
spec = {
dnsPolicy = "ClusterFirst",
containers = [
{
name = "nginx",
image = config.image,
ports = [
{
containerPort = 8080,
},
],
},
],
},
},
},
}
# Default configuration
let defaultConfig = {
name = "mydep",
image = "nginx:latest",
}
# Generate the deployment with default config
makeDeployment defaultConfig
KCL (示例[36]):
import k8s.api.apps.v1 as appsv1
import k8s.api.core.v1 as corev1
# Configuration parameters
schema DeploymentConfig:
name: str = "mydep"
image: str = "nginx:latest"
# Configuration values
config = DeploymentConfig {}
# Generate deployment using standard library types
deployment = appsv1.Deployment {
metadata = corev1.ObjectMeta {
name = config.name
namespace = "example"
labels.app = config.name
}
spec = appsv1.DeploymentSpec {
replicas = 3
selector = corev1.LabelSelector {
matchLabels.app = config.name
}
template = corev1.PodTemplateSpec {
metadata = corev1.ObjectMeta {
labels.app = config.name
}
spec = corev1.PodSpec {
dnsPolicy = "ClusterFirst"
containers = [
corev1.Container {
name = "nginx"
image = config.image
ports = [
corev1.ContainerPort {
containerPort = 8080
}
]
}
]
}
}
}
}
Starlark (示例[37]):
# Helper function to create consistent labels
def make_labels(name):
return {"app": name}
# Main deployment generator function
def make_deployment(name = "mydep", image = "nginx:latest"):
"""Creates a Kubernetes deployment configuration.
Args:
name: The name of the deployment
image: The container image to deploy
Returns:
Dictionary containing the deployment configuration
"""
return {
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": name,
"namespace": "example",
"labels": make_labels(name),
},
"spec": {
"replicas": 3,
"selector": {
"matchLabels": make_labels(name),
},
"template": {
"metadata": {
"labels": make_labels(name),
},
"spec": {
"dnsPolicy": "ClusterFirst",
"containers": [
{
"name": "nginx",
"image": image,
"ports": [
{
"containerPort": 8080,
},
],
},
],
},
},
},
}
# Default deployment configuration
deployment = make_deployment()
def main(ctx):
"""Main entry point for Starlark configuration.
Args:
ctx: The rule context
Returns:
The deployment configuration
"""
return deployment
正如不同的通用編程語言一樣,語法[38] 顯然也略有不同:是否使用大括號,是否允許尾隨逗號,雙引號與單引號與無引號,嚴格嵌套與否,冒號與等號,是否使用類型名稱,是否在語言內部定義模式,是否需要顯式生成語句,額外的關鍵字或標點符號等等。有些語言的樣板代碼稍多一些,有些則稍少一些。類型安全在每種語言中的工作方式也略有不同。不同的語言會讓不同的人感覺更熟悉,這取決于他們了解的其他語言。例如,Dhall 可能對熟悉 Haskell 的人來說更熟悉[39]。
在這個例子中,這些語言并沒有什么顯著的優勢。我本可以使用 envsubst
。我沒有使用更復雜的例子,例如圍繞 Deployment 構建可重用的函數或模塊,部分原因是為了保持例子的簡單性,部分原因是我已經多次看到這種抽象被削弱[40],并且已經看到試圖使配置更可重用適得其反[41] 的嘗試。在任何這些語言中,具有大量參數的 Kubernetes Deployment 也不會更簡單。
無論如何,沒有任何配置語言能夠比使用 cdk8s 或 Pulumi 等通用語言的工具更強大。配置語言在 JSON 和 YAML 等數據格式與通用語言之間是一種折衷方案。對某些人來說,這是一個恰到好處的選擇,而對另一些人來說則不然。或者只是配置復雜性時鐘[42]上的一個停頓點。
其目的是,語言施加的約束應該使更容易發現和防止錯誤,并可能使配置更容易閱讀和/或編寫。但是,雖然我已經閱讀了許多關于哪些語言“更好”或“更差”的冗長辯論[43],但它們都是主觀的,并且沒有達成共識。我沒有看到任何關于用不同語言表達配置生成器的定量益處的研究。如果您知道任何此類研究,請告訴我!
此外,雖然這些語言周圍的生態系統有時優于模板語言(Pkl 更強調其集成[44]而不是特定語言特性),但它們實際上無法與通用語言相比。配置語言可用的文檔、示例、教育內容、工具集成、服務集成等都較少。
原因之一是,所有配置語言的使用范圍都遠不如 Python 或 Javascript 等流行的編程語言廣泛。其中最流行的語言是 HCL,這當然是因為 Terraform 的流行。但是,我沒有看到 HCL 在 Terraform 生態系統之外使用,有些人甚至用 YAML 包裝它的用途[45]。就像Helm 的模板語法[46]一樣,并非每個人都喜歡它,但它通常都能完成工作。
好的,我的觀點是什么?
如果您讀到這里,您可能已經猜到我認為配置語言并不是解決配置復雜性的最佳方案。每種語言都有其優缺點,但沒有任何一種語言能帶來顯著的改變。它們是微優化而不是宏優化。正如我之前提到的[47]那樣,沒有任何新的配置語言能夠解決IaC 的根本問題[48]。為了取得顯著改進,我們需要對整體方法進行一些宏觀層面的改變。
您是否有我未涉及的喜歡的配置語言?它的優勢是什么?您是否發現使用配置語言與其他表示和方法相比有任何顯著的、可衡量的益處?您是否發現該語言的任何靜態分析工具特別有用?您組織中的其他人學習該語言是否遇到任何困難?您是否想知道為什么我們到 2025 年仍在手動編寫配置文件?
請隨時在此處回復,或通過LinkedIn[49], X/Twitter[50], 或Bluesky[51]向我發送消息,我計劃將此內容交叉發布。
如果您覺得這篇文章有趣,您可能還會對我的基礎設施即代碼和聲明式配置系列[52]中的其他文章感興趣。
引用鏈接
[1]?Can Configuration Languages (config DSLs) solve configuration complexity?:https://itnext.io/can-configuration-languages-dsls-solve-configuration-complexity-eee8f124e13a
[2]HCL:https://github.com/hashicorp/hcl
[3]Dhall:https://dhall-lang.org/
[4]Jsonnet:https://jsonnet.org/
[5]Starlark:https://github.com/bazelbuild/starlark
[6]CUE:https://cuelang.org/
[7]Nickel:https://nickel-lang.org/
[8]KCL:https://www.kcl-lang.io/
[9]Pkl:https://pkl-lang.org/
[10]其他:https://github.com/rix0rrr/gcl
[11]15種:https://xkcd.com/927/
[12]YAMLScript:https://yamlscript.org/
[13]KCL 項目的這篇文章:https://blog.stackademic.com/10-ways-for-kubernetes-declarative-configuration-management-3538673fd0b5
[14]比較了 KCL 與許多這些語言:https://www.kcl-lang.io/docs/user_docs/getting-started/intro
[15]確保它們不會產生副作用:https://sre.google/workbook/configuration-specifics/
[16]根據自己的喜好塑造語言:https://ruudvanasseldonk.com/2024/a-reasonable-configuration-language
[17]為特定工具創建的:https://www.reddit.com/r/ProgrammingLanguages/comments/gzqsxj/the_future_of_general_purpose_configuration/
[18]Bazel:https://bazel.build/extending/config
[19]Nix 包管理器:https://www.tweag.io/blog/2023-01-24-nix-with-with-nickel/
[20]KusionStack:https://www.kusionstack.io/docs/
[21]使用熟悉的通用語言:/generating-configuration-using-general-purpose-programming-languages-19230a2c2573
[22]CUE:https://cuelang.org/docs/introduction/
[23]Jsonnet:https://jsonnet.org/
[24]通過不允許覆蓋來更容易確定最終值設置的位置:https://cuelang.org/docs/concept/the-logic-of-cue/#relation-to-inheritance
[25]Dhall 的一個目標是使導入安全:https://dhall-lang.org/
[26]可嵌入的 Python 方言:https://github.com/bazelbuild/starlark/?tab=readme-ov-file#design-principles
[27]JSON 的超集:https://jsonnet.org/articles/design.html
[28]Nickel 中的類型是可選的:https://nickel-lang.org/user-manual/introduction
[29]Pkl:https://www.youtube.com/watch?v=N7zmsHUiTkM
[30]Kubernetes 提供程序:https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment#example-usage
[31]官方示例:https://github.com/dhall-lang/dhall-kubernetes/blob/master/examples/deployment.dhall
[32]更多Kubernetes示例:https://jsonnet.org/articles/kubernetes.html
[33]Kubernetes 教程:https://github.com/cue-labs/cue-by-example/tree/main/003_kubernetes_tutorial#controlling-kubernetes-with-cue
[34]示例:https://github.com/apple/pkl-k8s-examples/tree/main/pkl
[35]示例:https://github.com/tweag/nickel-kubernetes/blob/master/examples/redis-replication-controller.ncl
[36]示例:https://github.com/kcl-lang/examples
[37]示例:https://github.com/cruise-automation/isopod/blob/master/examples/ingress.ipd
[38]語法:https://github.com/lightbend/config/blob/master/HOCON.md#syntax
[39]對熟悉 Haskell 的人來說更熟悉:https://pv.wtf/posts/taming-the-beast#dhall
[40]這種抽象被削弱:/the-tension-between-flexibility-and-simplicity-in-infrastructure-as-code-6cec841e3d16
[41]使配置更可重用適得其反:https://medium.com/itnext/how-software-engineering-instincts-clash-with-infrastructure-as-code-6b18a9cd9cef
[42]配置復雜性時鐘:https://mikehadlow.blogspot.com/2012/05/configuration-complexity-clock.html
[43]冗長辯論:https://news.ycombinator.com/item?id=22787332
[44]Pkl 更強調其集成:https://github.com/apple/pkl/discussions/7
[45]用 YAML 包裝它的用途:https://github.com/AppsFlyer/terra-crust
[46]Helm 的模板語法:/kubernetes-configuration-in-2024-434abc7a5a1b
[47]之前提到的:/fundamental-challenges-with-infrastructure-as-code-imply-the-language-doesnt-matter-41030475c296
[48]IaC 的根本問題:/the-12-anti-factors-of-infrastructure-as-code-acb52fba3ae0
[49]LinkedIn:https://www.linkedin.com/in/bgrant0607/
[50]X/Twitter:https://x.com/bgrant0607
[51]Bluesky:https://bsky.app/profile/bgrant0607.bsky.social
[52]基礎設施即代碼和聲明式配置系列:https://medium.com/@bgrant0607/list/infrastructure-as-code-and-declarative-configuration-8c441ae74836