萬字長文詳解宣告式設定發展歷程

2023-03-17 06:01:33

零、前言

文字僅用於澄清宣告式設定技術概述,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 的典型工作方式。


圖 2 Kustomize 典型工作方式

1.2.3 模版化 (Templated) 的 KV

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

  • 優勢

  • 簡單的設定邏輯,迴圈支援

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

  • 痛點

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

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

模版化代表技術有:

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


圖 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 編寫應用交付設定程式碼的典型場景


圖 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 會嘗試通過編譯時的靜態分析對規則進行判斷從而發現問題。


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

2.4 分塊編寫及設定合併

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

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

圖 13 多環境場景設定分塊編寫範例

2.5 自動化整合

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

圖 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 網站

五、參考