萬字長文詳解宣告式配置發展歷程

Peefy發表於2023-03-16

零、前言

文字僅用於澄清宣告式配置技術概述,KCL 概念以及核心設計,以及與其他配置語言的對比。

一、宣告式配置概述

1.1 配置的重要性

  • 軟體不是一成不變的,每天有成千上萬的配置更新,並且配置本身也在逐漸演進,對規模化效率有較高的訴求

  • 配置更新越來越頻繁:配置提供了一種改變系統功能的低開銷方式,不斷髮展的業務需求、基礎設施要求和其他因素意味著系統需要不斷變化。

  • 配置規模越來越大:一份配置往往要分發到不同的雲站點、不同的租戶、不同的環境等。

  • 配置場景廣泛:應用配置、資料庫配置、網路配置、監控配置等。

  • 配置格式繁多:JSON, YAML, XML, TOML, 各種配置模版如 Java Velocity, Go Template 等。

  • 配置的穩定性至關重要,系統當機或出錯的一個最主要原因是有大量工程師進行頻繁的實時配置更新,表 1 示出了幾個由於配置導致的系統出錯事件。

時間 事件
2021 年 7 月 中國 Bilibili 公司由於 SLB Lua 配置計算出錯陷入死迴圈導致網站當機
2021 年 10 月 韓國 KT 公司由於路由配置錯誤導致在全國範圍內遭受重大網路中斷

表 1 配置導致的系統出錯事件

1.2 宣告式配置分類

雲原生時代帶來了如雨後春筍般的技術發展,出現了大量面向終態的宣告式配置實踐,如圖 1 所示,宣告式配置一般可分為如下幾種方式。 圖 1 宣告式配置方式分類

1.2.1 結構化 (Structured) 的 KV

結構化的 KV 可以滿足最小化資料宣告需求,比如數字、字串、列表和字典等資料型別,並且隨著雲原生技術快速發展應用,宣告式 API 可以滿足 X as Data 發展的訴求,並且面向機器可讀可寫,面向人類可讀。其優劣如下:

  • 優勢

  • 語法簡單,易於編寫和閱讀

  • 多語言 API 豐富

  • 有各種 Path 工具方便資料查詢,如 XPath, JsonPath 等

  • 痛點

  • 冗餘資訊多:當配置規模較大時,維護和閱讀配置很困難,因為重要的配置資訊被淹沒在了大量不相關的重複細節中

  • 功能性不足

  • 約束校驗能力

  • 複雜邏輯編寫能力

  • 測試、除錯能力

  • 不易抽象和複用

  • Kustomize 的 Patch 比較定製,基本是透過固定幾種 Patch Merge 策略

結構化 KV 的代表技術有

  • JSON/YAML:非常方便閱讀,以及自動化處理,不同的語言均具有豐富的 API 支援。
  • Kustomize:提供了一種無需模板DSL 即可自定義 Kubernetes 資源基礎配置和差異化配置的解決方案,本身不解決約束的問題,需要配合大量的額外工具進行約束檢查如 Kube-linterCheckov 等檢查工具,圖 2 示出了 Kustomize 的典型工作方式。

image.png
圖 2 Kustomize 典型工作方式

1.2.3 模版化 (Templated) 的 KV

模版化 (Templated) 的 KV 賦予靜態配置資料動態引數的能力,可以做到一份模版+動態引數輸出不同的靜態配置資料。其優劣如下:

  • 優勢

  • 簡單的配置邏輯,迴圈支援

  • 支援外部動態引數輸入模版

  • 痛點

  • 容易落入所有配置引數都是模版引數的陷阱

  • 當配置規模變大時,開發者和工具都難以維護和分析它們

模版化代表技術有:

  • Helm:Kubernetes 資源的包管理工具,透過配置模版管理 Kubernetes 資源配置。圖 3 示出了一個 Helm Jekins Package ConfigMap 配置模版,可以看出這些模版本身都十分短小,可以書寫簡單的邏輯,適合 Kubernetes 基礎元件固定的一系列資源配置透過包管理+額外的配置引數進行安裝。相比於單純的模版化的 KV,Helm 一定程度上提供了模版儲存/引用和語義化版本管理的能力相比於 Kustomize 更適合管理外部 Charts, 但是在多環境、多租戶的配置管理上不太擅長。

image.png
圖 3 Helm Jekins Package ConfigMap 配置模版

  • 其他各種配置模版:Java Velocity, Go Template 等文字模板引擎非常適合 HTML 編寫模板。但是在配置場景中使用時,存在所有配置欄位即模版引數的風險,開發者和工具都難以維護和分析它們。

1.2.3 程式碼化 (Programmable) 的 KV

Configuration as Code (CaC), 使用程式碼產生配置,就像工程師們只需要寫高階 GPL 程式碼,而不是手工編寫容易出錯而且難以理解的伺服器二進位制程式碼一樣。配置變更同程式碼變更同樣嚴肅地對待,同樣可以執行單元測試、整合測試等。程式碼模組化和重用是維護配置程式碼比手動編輯 JSON/YAML 等配置檔案更容易的一個關鍵原因。其優劣如下:

  • 優勢

  • 必要的程式設計能力(變數定義、邏輯判斷、迴圈、斷言等)

  • 程式碼模組化與抽象(支援定義資料模版,並用模版得到新的配置資料)

  • 可以抽象配置模版+並使用配置覆蓋

  • 痛點

  • 型別檢查不足

  • 執行時錯誤

  • 約束能力不足

程式碼化 KV 的代表技術有:

  • GCL:一種 Python 實現的宣告式配置程式語言,提供了必要的言能力支援模版抽象,但編譯器本身是 Python 編寫,且語言本身是解釋執行,對於大的模版例項 (比如 K8s 型) 效能較差。
  • HCL:一種 Go 實現結構化配置語言,原生語法受到 libuclnginx 配置等的啟發,用於建立對人類和機器都友好的結構化配置語言,主要針對 devops 工具、伺服器配置及 Terraform 中定義資源配置等。
  • Jsonnet:一種 C++ 實現的資料模板語言,適用於應用程式工具開發人員,可以生成配置資料並且無副作用組織、簡化、統一管理龐大的配置。

1.2.4 型別化 (Typed) 的 KV

型別化的 KV,基於程式碼化 KV,多了型別檢查和約束的能力,其優劣如下:

  • 優勢

  • 配置合併完全冪等,天然防止配置衝突

  • 豐富的配置約束語法用於編寫約束

  • 將型別和值約束編寫抽象為同一種形式,編寫簡單

  • 配置順序無關

  • 痛點

  • 圖合併和冪等合併等概念複雜,使用者理解成本較高

  • 型別和值混合定義提高抽象程度的同時提升了使用者的理解成本,並且所有約束在執行時進行檢查,大規模配置程式碼下有效能瓶頸

  • 對於想要配置覆蓋、修改的多租戶、多環境場景難以實現

  • 對於帶條件的約束場景,定義和校驗混合定義編寫使用者介面不友好

型別化 KV 的代表技術有:

  • CUE:CUE 解決的核心問題是“型別檢查”,主要應用於配置約束校驗場景及簡單的雲原生配置場景

1.2.5 模型化 (Structural) 的 KV

模型化的 KV 在程式碼化和型別化 KV 的基礎上以高階語言建模能力為核心描述,期望做到模型的快速編寫與分發,其優劣如下:

  • 優勢

  • 引入可分塊、可擴充套件的 KV 配置塊編寫方式

  • 類高階程式語言的編寫、測試方式

  • 語言內建的強校驗、強約束支援

  • 面向人類可讀可寫,面向機器部分可讀可寫

  • 不足

  • 擴充套件新模型及生態構建需要一定的研發成本,或者使用工具對社群中已有的 JsonSchema 和 OpenAPI 模型進行模型轉換、遷移和整合。

模型化 KV 的代表技術有:

  • KCL:一種 Rust 實現的宣告式配置策略程式語言,把運維類研發統一為一種宣告式的程式碼編寫,可以針對差異化應用交付場景抽象出使用者模型並新增相應的約束能力,期望藉助可程式設計 DevOps 理念解決規模化運維場景中的配置策略編寫的效率和可擴充套件性等問題。圖 4 示出了一個 KCL 編寫應用交付配置程式碼的典型場景

image.png
圖 4 使用 KCL 編寫應用交付配置程式碼

1.3 不同宣告式配置方式的選擇標準與最佳實踐

  • 配置的規模:對於小規模的配置場景,完全可以使用 YAML/JSON 等配置,比如應用自身的簡單配置,CI/CD 的配置。此外對於小規模配置場景存在的多環境、多租戶等需求可以藉助 Kustomize 的 overlay 能力實現簡單配置的合併覆蓋等操作。
  • 模型抽象與約束的必要性:對於較大規模的配置場景特別是對多租戶、多環境等有配置模型和運維特性研發和沉澱迫切需求的,可以使用程式碼化、型別化和模型化的 KV 方式。

此外,從不同宣告式配置方式的使用場景出發

  • 如果需要編寫結構化的靜態的 K-V,或使用 Kubernetes 原生的技術工具,建議選擇 YAML
  • 如果希望引入程式語言便利性以消除文字(如 YAML、JSON) 模板,有良好的可讀性,或者已是 Terraform 的使用者,建議選擇 HCL
  • 如果希望引入型別功能提升穩定性,維護可擴充套件的配置檔案,建議選擇 CUE 之類的資料約束語言
  • 如果希望以現代語言方式編寫複雜型別和建模,維護可擴充套件的配置檔案,原生的純函式和策略,和生產級的效能和自動化,建議選擇 KCL

不同於社群中的其他同型別領域語言,KCL 是一種面向應用研發人員並採納了現代語言設計和技術的靜態強型別編譯語言

注意,本文將不會討論通用語言編寫配置的情況,通用語言一般是 Overkill 的,即遠遠超過了需要解決的問題,通用語言存在各式各樣的安全問題,比如能力邊界問題 (啟動本地執行緒、訪問 IO, 網路,程式碼死迴圈等不安全隱患),比如像音樂領域就有專門的音符去表示音樂,方便學習與交流,不是一般文字語言可以表述清楚的。

此外,通用語言因為本身就樣式繁多,存在統一維護、管理和自動化的成本,通用語言一般用來編寫客戶端執行時,是服務端執行時的一個延續,不適合編寫與執行時無關的配置,最終被編譯為二進位制從程式啟動,穩定性和擴充套件性不好控制,而配置語言往往編寫的是資料,再搭配以簡單的邏輯,描述的是期望的最終結果,然後由編譯器或者引擎來消費這個期望結果。

二、KCL 的核心設計與應用場景

Kusion 配置語言(KCL)是一個開源的基於約束的記錄及函式語言。KCL 透過成熟的程式語言技術和實踐來改進對大量繁雜配置的編寫,致力於構建圍繞配置的更好的模組化、擴充套件性和穩定性,更簡單的邏輯編寫,以及更快的自動化整合和良好的生態延展性。

KCL 的核心特性是其建模約束能力,KCL 核心功能基本圍繞 KCL 這個兩個核心特性展開,此外 KCL 遵循以使用者為中心的配置理念而設計其核心特性,可以從兩個方面理解:

  • 以領域模型為中心的配置檢視:藉助 KCL 語言豐富的特性及 KCL OpenAPI 等工具,可以將社群中廣泛的、設計良好的模型直接整合到 KCL 中(比如 K8s 資源模型),使用者也可以根據自己的業務場景設計、實現自己的 KCL 模型 (庫) ,形成一整套領域模型架構交由其他配置終端使用者使用。
  • 以終端使用者為中心的配置檢視:藉助 KCL 的程式碼封裝、抽象和複用能力,可以對模型架構進行進一步抽象和簡化(比如將 K8s 資源模型抽象為以應用為核心的 Server 模型),做到最小化終端使用者配置輸入,簡化使用者的配置介面,方便手動或者使用自動化 API 對其進行修改。

不論是以何為中心的配置檢視,對於程式碼而言(包括配置程式碼)都存在對配置資料約束的需求,比如型別約束、配置欄位必選/可選約束、範圍約束、不可變性約束等,這也是 KCL 致力於解決的核心問題之一。綜上,KCL 是一個開源的基於約束和宣告的函式式語言,KCL 主要包含如圖 5 所示的核心特性:

圖 5 KCL 核心特性

  • 簡單易用:源於 Python、Golang 等高階語言,採納函數語言程式設計語言特性,低副作用
  • 設計良好:獨立的 Spec 驅動的語法、語義、執行時和系統庫設計
  • 快速建模:以 Schema 為中心的配置型別及模組化抽象
  • 功能完備:基於 ConfigSchemaLambdaRule 的配置及其模型、邏輯和策略編寫
  • 可靠穩定:依賴靜態型別系統約束自定義規則的配置穩定性
  • 強可擴充套件:透過獨立配置塊自動合併機制保證配置編寫的高可擴充套件性
  • 易自動化CRUD APIs多語言 SDK語言外掛 構成的梯度自動化方案
  • 極致效能:使用 Rust & C,LLVM 實現,支援編譯到原生程式碼和 WASM 的高效能編譯時和執行時
  • API 親和:原生支援 OpenAPI、 Kubernetes CRD, Kubernetes YAML 等 API 生態規範
  • 開發友好語言工具 (Format,Lint,Test,Vet,Doc 等)、 IDE 外掛 構建良好的研發體驗
  • 安全可控:面向領域,不原生提供執行緒、IO 等系統級功能,低噪音,低安全風險,易維護,易治理
  • 多語言APIGo, PythonREST API 滿足不同場景和應用使用需求
  • 生產可用:廣泛應用在螞蟻集團平臺工程及自動化的生產環境實踐中

圖 6 KCL 語言核心設計

更多語言設計和能力詳見 KCL 文件,儘管 KCL 不是通用語言,但它有相應的應用場景,如圖 6 所示,研發者可以透過 KCL 編寫配置(config)模型(schema)函式(lambda)規則(rule) ,其中 Config 用於定義資料,Schema 用於對資料的模型定義進行描述,Rule 用於對資料進行校驗,並且 Schema 和 Rule 還可以組合使用用於完整描述資料的模型及其約束,此外還可以使用 KCL 中的 lambda 純函式進行資料程式碼組織,將常用程式碼封裝起來,在需要使用時可以直接呼叫。

對於使用場景而言,KCL 可以進行結構化 KV 資料驗證、複雜配置模型定義與抽象、強約束校驗避免配置錯誤、分塊編寫及配置合併能力、自動化整合和工程擴充套件等能力,下面針對這些功能和使用場景進行闡述。

2.1 結構化 KV 資料驗證

如圖 7 所示,KCL 支援對 JSON/YAML 資料進行格式校驗。作為一種配置語言,KCL 在驗證方面幾乎涵蓋了 OpenAPI 校驗的所有功能。在 KCL 中可以透過一個結構定義來約束配置資料,同時支援透過 check 塊自定義約束規則,在 schema 中書寫校驗表示式對 schema 定義的屬性進行校驗和約束。透過 check 表示式可以非常清晰簡單地校驗輸入的 JSON/YAML 是否滿足相應的 schema 結構定義與 check 約束。

圖 7 KCL 中結構化 KV 校驗方式

基於此,KCL 提供了相應的校驗工具直接對 JSON/YAML 資料進行校驗。此外,透過 KCL schema 的 check 表示式可以非常清晰簡單地校驗輸入的 JSON 是否滿足相應的 schema 結構定義與 check 約束。此外,基於此能力可以構建如圖 8 所示的 KV 校驗視覺化產品。

圖 8 基於 KCL 結構化 KV 校驗能力構建的視覺化產品介面

2.2 複雜配置模型定義與抽象

如圖 9 所示,藉助 KCL 語言豐富的特性及 KCL OpenAPI 等工具,可以將社群中廣泛的、設計良好的模型直接整合到 KCL 中(比如 K8s 資源模型 CRD),使用者也可以根據自己的業務場景設計、實現自己的 KCL 模型 (庫) ,形成一整套領域模型架構交由其他配置終端使用者使用。

圖 9 KCL 複雜配置建模的一般方式

基於此,可以像圖 10 示出的那樣用一個大的 Konfig 倉庫 管理全部的 KCL 配置程式碼,將業務配置程式碼 (應用程式碼)、基礎配置程式碼 (核心模型+底層模型)在一個大庫中,方便程式碼間的版本依賴管理,自動化系統處理也比較簡單,定位唯一程式碼庫的目錄及檔案即可,程式碼互通,統一管理,便於查詢、修改、維護,可以使用統一的 CI/CD 流程進行配置管理(此外,大庫模式也是 Google 等頭部網際網路公司內部實踐的模式)。

圖 10 使用 KCL 的語言能力整合領域模型並抽象使用者模型並使用

2.3 強約束校驗避免配置錯誤

如圖 11 所示,在 KCL 中可以透過豐富的強約束校驗手段避免配置錯誤:

圖 11 KCL 強約束校驗手段

  • KCL 語言的型別系統被設計為靜態的,型別和值定義分離,支援編譯時型別推導和型別檢查,靜態型別不僅僅可以提前在編譯時分析大部分的型別錯誤,還可以降低後端執行時的動態型別檢查的效能損耗。此外,KCL Schema 結構的屬性強制為非空,可以有效避免配置遺漏。
  • 當需要匯出的 KCL 配置被宣告之後,它們的型別和值均不能發生變化,這樣的靜態特性保證了配置不會被隨意篡改。
  • KCL 支援透過結構體內建的校驗規則進一步保障穩定性。比如對於如圖 12 所示的 KCL 程式碼,,在 App 中定義對 containerPortservicesvolumes 的校驗規則,目前校驗規則在執行時執行判斷,後續 KCL 會嘗試透過編譯時的靜態分析對規則進行判斷從而發現問題。

image.png
圖 12 帶規則約束的 KCL 程式碼校驗

2.4 分塊編寫及配置合併

KCL 提供了配置分塊編寫及自動合併配置的能力,並且支援冪等合併、補丁合併和唯一配置合併等策略。冪等合併中的多份配置需要滿足交換律,並且需要開發人員手動處理基礎配置和不同環境配置衝突。 補丁合併作為一個覆蓋功能,包括覆蓋、刪除和新增。唯一的配置要求配置塊是全域性唯一的並且未修改或以任何形式重新定義。 KCL 透過多種合併策略簡化了使用者側的協同開發,減少了配置之間的耦合。

如圖 13 所示,對於存在基線配置、多環境和多租戶的應用配置場景,有一個基本配置 base.k。 開發和 SRE 分別維護生產和開發環境的配置 base.k 和 prod.k,他們的配置互不影響,由 KCL 編譯器合併成一個 prod 環境的等效配置程式碼。

image.png

圖 13 多環境場景配置分塊編寫例項

2.5 自動化整合

在 KCL 中提供了很多自動化相關的能力,主要包括工具和多語言 API。 透過 package_identifier : key_identifier的模式支援對任意配置鍵值的索引,從而完成對任意鍵值的增刪改查。比如圖 14 所示修改某個應用配置的映象內容,可以直接執行如下指令修改映象,修改前後的 diff 如下圖所示。

image.png

圖 14 使用 KCL CLI/API 自動修改應用配置映象

此外,可以基於 KCL 的自動化能力實現如圖 15 所示的一鏡交付及自動化運維能力並整合到 CI/CD 當中。

圖 15 典型 KCL 自動化整合鏈路

三、KCL 與其他宣告式配置的對比

3.1 vs. JSON/YAML

YAML/JSON 配置等適合小規模的配置場景,對於大規模且需要頻繁修改的雲原生配置場景,比較適合 KCL 比較適合,其中涉及到主要差異是配置資料抽象與展開的差異:

  • 對於 JSON/YAML 等靜態配置資料展開的好處是:簡單、易讀、易於處理,但是隨著靜態配置規模的增加,當配置規模較大時,JSON/YAML 等檔案維護和閱讀配置很困難,因為重要的配置資訊被淹沒在了大量不相關的重複細節中。
  • 對於使用 KCL 語言進行配置抽象的好處是:對於靜態資料,抽象一層的好處這意味著整體系統具有部署的靈活性,不同的配置環境、配置租戶、執行時可能會對靜態資料具有不同的要求,甚至不同的組織可能有不同的規範和產品要求,可以使用 KCL 將最需要、最常修改的配置暴露給使用者,對差異化的配置進行抽象,抽象的好處是可以支援不同的配置需求。並且藉助 KCL 語言級別的自動化整合能力,還可以很好地支援不同的語言,不同的配置 UI 等。

3.2 vs. Kustomize

Kustomize 的核心能力是其 Overlay 能力,並 Kustomize 支援檔案級的覆蓋,但是存在會存在多個覆蓋鏈條的問題,因為找到具體欄位值的宣告並不能保證這是最終值,因為其他地方出現的另一個具體值可以覆蓋它,對於複雜的場景,Kustomize 檔案的繼承鏈檢索往往不如 KCL 程式碼繼承鏈檢索方便,需要仔細考慮指定的配置檔案覆蓋順序。此外,Kustomize 不能解決 YAML 配置編寫、配置約束校驗和模型抽象與開發等問題,較為適用於簡單的配置場景,當配置元件增多時,對於配置的修改仍然會陷入大量重複不相關的配置細節中,並且在 IDE 中不能很好地顯示配置之間的依賴和覆蓋關係情況,只能透過搜尋/替換等批次修改配置。

在 KCL 中,配置合併的操作可以細粒度到程式碼中每一個配置欄位,並且可以靈活的設定合併策略,並不侷限於資源整體,並且透過 KCL 的 import 可以靜態分析出配置之間的依賴關係。

3.3 vs. HCL

3.3.1 功能對比

HCL KCL
建模能力 透過 Terraform Go Provider Schema 定義,在使用者介面不直接感知,此外編寫複雜的 object 和必選/可選欄位定義時使用者介面較為繁瑣 透過 KCL Schema 進行建模,透過語言級別的工程和部分物件導向特性,可以實現較高的模型抽象
約束能力 透過 Variable 的 condition 欄位對動態引數進行約束,Resource 本身的約束需要透過 Go Provider Schema 定義或者結合 Sentinel/Rego 等策略語言完成,語言本身的完整能力不能自閉環,且實現方式不統一 以 Schema 為核心,在進行建模的同時定義其約束,在 KCL 內部自閉環並一統一方式實現,支援多種約束函式編寫,支援可選/必選欄位定義
擴充套件性 Terraform HCL 透過分檔案進行 Override, 模式比較固定,能力受限。 KCL 可以自定義配置分塊編寫方式和多種合併策略,可以滿足複雜的多租戶、多環境配置場景需求
語言化編寫能力 編寫複雜的物件定義和必選/可選欄位定義時使用者介面較為繁瑣 複雜的結構定義、約束場景編寫簡單,不借助其他外圍 GPL 或工具,語言編寫自閉環

3.3.2 舉例

Terraform HCL Variable 約束校驗編寫 vs. KCL Schema 宣告式約束校驗編寫

  • HCL
variable "subnet_delegations" {
type = list(object({
name = string
service_delegation = object({
name = string
actions = list(string)
})
}))
default = null
validation {
condition = var.subnet_delegations == null ? true : alltrue([for d in var.subnet_delegations : (d != null)])
}
validation {
condition = var.subnet_delegations == null ? true : alltrue([for n in var.subnet_delegations.*.name : (n != null)])
}
validation {
condition = var.subnet_delegations == null ? true : alltrue([for d in var.subnet_delegations.*.service_delegation : (d != null)])
}
validation {
condition = var.subnet_delegations == null ? true : alltrue([for n in var.subnet_delegations.*.service_delegation.name : (n != null)])
}
}
  • KCL
schema SubnetDelegation:
name: str
service_delegation: ServiceDelegation

schema ServiceDelegation:
name: str
actions?: [str] # 使用 ? 標記可選屬性

subnet_delegations: [SubnetDelegation] = option("subnet_delegations")

此外,KCL 還可以像高階語言一樣寫型別,寫繼承,寫內建的約束,這些功能是 HCL 所不具備的

Terraform HCL 函式 vs. KCL Lambda 函式編寫

add_func = lambda x: int, y: int -> int {
x + y
}
two = add_func(1, 1) # 2

HCL 刪除 null 值與 KCL 使用 -n 編譯引數刪除 null 值

  • HCL
variable "conf" {
type = object({
description = string
name = string
namespace = string
params = list(object({
default = optional(string)
description = string
name = string
type = string
}))
resources = optional(object({
inputs = optional(list(object({
name = string
type = string
})))
outputs = optional(list(object({
name = string
type = string
})))
}))
results = optional(list(object({
name = string
description = string
})))
steps = list(object({
args = optional(list(string))
command = optional(list(string))
env = optional(list(object({
name = string
value = string
})))
image = string
name = string
resources = optional(object({
limits = optional(object({
cpu = string
memory = string
}))
requests = optional(object({
cpu = string
memory = string
}))
}))
script = optional(string)
workingDir = string
}))
})
}

locals {
conf = merge(
defaults(var.conf, {}),
{ for k, v in var.conf : k => v if v != null },
{ resources = { for k, v in var.conf.resources : k => v if v != null } },
{ steps = [for step in var.conf.steps : merge(
{ resources = {} },
{ for k, v in step : k => v if v != null },
)] },
)
}
  • KCL (編譯引數新增 -n 忽略 null 值)
schema Param:
default?: str
name: str

schema Resource:
cpu: str
memory: str

schema Step:
args?: [str]
command?: [str]
env?: {str:str}
image: str
name: str
resources?: {"limits" | "requests": Resource}
script?: str
workingDir: str

schema K8sManifest:
name: str
namespace: str
params: [Param]
results?: [str]
steps: [Step]

conf: K8sManifest = option("conf")

綜上可以看出,在 KCL 中,透過 Schema 來宣告方式定義其型別和約束,可以看出相比於 Terraform HCL, 在實現相同功能的情況下,KCL 的約束可以編寫的更加簡單 (不需要像 Terraform 那樣重複地書寫 validation 和 condition 欄位),並且額外提供了欄位設定為可選的能力 (?運算子,不像 Terraform 配置欄位預設可空,KCL Schema 欄位預設必選),結構更加分明,並且可以在程式碼層面直接獲得型別檢查和約束校驗的能力。

3.4 vs. CUE

3.4.1 功能對比

CUE KCL
建模能力 透過 Struct 進行建模,無繼承等特性,當模型定義之間無衝突時可以實現較高的抽象。由於 CUE 在執行時進行所有的約束檢查,在大規模建模場景可能存在效能瓶頸 透過 KCL Schema 進行建模,透過語言級別的工程和部分物件導向特性(如單繼承),可以實現較高的模型抽象。 KCL 是靜態編譯型語言,對於大規模建模場景開銷較小
約束能力 CUE 將型別和值合併到一個概念中,透過各種語法簡化了約束的編寫,比如不需要泛型和列舉,求和型別和空值合併都是一回事 KCL 提供了跟更豐富的 check 宣告式約束語法,編寫起來更加容易,對於一些配置欄位組合約束編寫更加簡單(能力上比 CUE 多了 if guard 組合約束,all/any/map/filter 等集合約束編寫方式,編寫更加容易)
分塊編寫能力 支援語言內部配置合併,CUE 的配置合併是完全冪等的,對於滿足複雜的多租戶、多環境配置場景的覆蓋需求可能無法滿足 KCL 可以自定義配置分塊編寫方式和多種合併策略,KCL 同時支援冪等和非冪等的合併策略,可以滿足複雜的多租戶、多環境配置場景需求
語言化編寫能力 對於複雜的迴圈、條件約束場景編寫複雜,對於需要進行配置精確修改的編寫場景較為繁瑣 複雜的結構定義、迴圈、條件約束場景編寫簡單

3.4.2 舉例

CUE 約束校驗編寫 vs. KCL Schema 宣告式約束校驗編寫及配置分塊編寫能力

CUE (執行命令 cue export base.cue prod.cue)

  • base.cue
// base.cue
import "list"

#App: {
domainType: "Standard" | "Customized" | "Global",
containerPort: >=1 & <=65535,
volumes: [...#Volume],
services: [...#Service],
}

#Service: {
clusterIP: string,
type: string,

if type == "ClusterIP" {
clusterIP: "None"
}
}

#Volume: {
container: string | *"*" // The default value of `container` is "*"
mountPath: string,
_check: false & list.Contains(["/", "/boot", "/home", "dev", "/etc", "/root"], mountPath),
}

app: #App & {
domainType: "Standard",
containerPort: 80,
volumes: [
{
mountPath: "/tmp"
}
],
services: [
{
clusterIP: "None",
type: "ClusterIP"
}
]
}
  • prod.cue
// prod.cue
app: #App & {
containerPort: 8080, // error: app.containerPort: conflicting values 8080 and 80:
}

KCL (執行命令 kcl base.k prod.k)

  • base.k
# base.k
schema App:
domainType: "Standard" | "Customized" | "Global"
containerPort: int
volumes: [Volume]
services: [Service]

check:
1 <= containerPort <= 65535

schema Service:
clusterIP: str
$type: str

check:
clusterIP == "None" if $type == "ClusterIP"

schema Volume:
container: str = "*" # The default value of `container` is "*"
mountPath: str

check:
mountPath not in ["/", "/boot", "/home", "dev", "/etc", "/root"]

app: App {
domainType = "Standard"
containerPort = 80
volumes = [
{
mountPath = "/tmp"
}
]
services = [
{
clusterIP = "None"
$type = "ClusterIP"
}
]
}
  • prod.k
# prod.k
app: App {
# 可以使用 = 屬性運算子對 base app 的 containerPort 進行修改
containerPort = 8080
# 可以使用 += 屬性運算子對 base app 的 volumes 進行新增
# 此處表示在 prod 環境增加一個 volume, 一共兩個 volume
volumes += [
{
mountPath = "/tmp2"
}
]
}

此外由於 CUE 的冪等合併特性,在場景上並無法使用類似 kustomize 的 overlay 配置覆蓋和 patch 等能力,比如上述的 base.cue 和 prod.cue 一起編譯會報錯。

3.5 Performance

在程式碼規模較大或者計算量較高的場景情況下 KCL 比 CUE/Jsonnet/HCL 等語言效能更好 (CUE 等語言受限於執行時約束檢查開銷,而 KCL 是一個靜態編譯型語言)

  • CUE (test.cue)
import "list"

temp: {
for i, _ in list.Range(0, 10000, 1) {
"a(i)": list.Max([1, 2])
}
}
  • KCL (test.k)
a = lambda x: int, y: int -> int {
max([x, y])
}
temp = {"a${i}": a(1, 2) for i in range(10000)}
  • Jsonnet (test.jsonnet)
local a(x, y) = std.max(x, y);
{
temp: {["a%d" % i]: a(1, 2) for i in std.range(0, 10000)},
}
  • Terraform HCL (test.tf, 由於 terraform range 函式只支援最多 1024 個迭代器,將 range(10000) 拆分為 10 個子 range)
output "r1" {
value = {for s in range(0, 1000) : format("a%d", s) => max(1, 2)}
}
output "r2" {
value = {for s in range(1000, 2000) : format("a%d", s) => max(1, 2)}
}
output "r3" {
value = {for s in range(1000, 2000) : format("a%d", s) => max(1, 2)}
}
output "r4" {
value = {for s in range(2000, 3000) : format("a%d", s) => max(1, 2)}
}
output "r5" {
value = {for s in range(3000, 4000) : format("a%d", s) => max(1, 2)}
}
output "r6" {
value = {for s in range(5000, 6000) : format("a%d", s) => max(1, 2)}
}
output "r7" {
value = {for s in range(6000, 7000) : format("a%d", s) => max(1, 2)}
}
output "r8" {
value = {for s in range(7000, 8000) : format("a%d", s) => max(1, 2)}
}
output "r9" {
value = {for s in range(8000, 9000) : format("a%d", s) => max(1, 2)}
}
output "r10" {
value = {for s in range(9000, 10000) : format("a%d", s) => max(1, 2)}
}
  • 執行時間(考慮到生產環境的實際資源開銷,本次測試以單核為準)
環境 KCL v0.4.3 執行時間 (包含編譯+執行時間) CUE v0.4.3 執行時間 (包含編譯+執行時間) Jsonnet v0.18.0 執行時間 (包含編譯+執行時間) HCL in Terraform v1.3.0 執行時間 (包含編譯+執行時間)
OS: macOS 10.15.7; CPU: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz; Memory: 32 GB 2400 MHz DDR4; 不開啟 NUMA 440 ms (kclvm_cli run test.k) 6290 ms (cue export test.cue) 3340 ms (jsonnet test.jsonnet) 1774 ms (terraform plan -parallelism=1)

綜上可以看出:CUE 和 KCL 均可以覆蓋到絕大多數配置校驗場景,並且均支援屬性型別定義、配置預設值、約束校驗等編寫,但是 CUE 對於不同的約束條件場景無統一的寫法,且不能很好地透出校驗錯誤,KCL 使用 check 關鍵字作統一處理,支援使用者自定義錯誤輸出。

另一個複雜的例子

使用 KCL 和 CUE 編寫 Kubernetes 配置

  • CUE (test.cue)
package templates

import (
apps "k8s.io/api/apps/v1"
)

deployment: apps.#Deployment

deployment: {
apiVersion: "apps/v1"
kind: "Deployment"
metadata: {
name: "me"
labels: me: "me"
}
}
  • KCL (test.k)
import kubernetes.api.apps.v1

deployment = v1.Deployment {
metadata.name = "me"
metadata.labels.name = "me"
}
環境 KCL v0.4.3 執行時間 (包含編譯+執行時間) CUE v0.4.3 執行時間 (包含編譯+執行時間)
OS: macOS 10.15.7; CPU: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz; Memory: 32 GB 2400 MHz DDR4; no NUMA 140 ms (kclvm_cli run test.k) 350 ms (cue export test.cue)

四、小結

文字對宣告式配置技術做了整體概述,其中重點闡述了 KCL 概念、核心設計、使用場景以及與其他配置語言的對比,期望幫助大家更好的理解宣告式配置技術及 KCL 語言。更多 KCL 的概念、背景、設計與使用者案例等相關內容,歡迎訪問 KCL 網站

五、參考

相關文章