爲什麼配置模式令人抓狂?嘗試用編程語言來寫吧

雲棲號資訊:【點擊查看更多行業資訊
在這裏您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!

本文將試着解釋爲什麼大多數配置格式用起來都不太舒服,作者建議大家嘗試使用一門真正的編程語言(例如,像 Python 這樣的通用編程語言)來編寫配置,通常這是一種可行的選擇,且使用過程更感愉悅。

大多數現代配置格式都很糟糕

本節,我主要針對 JSON/YAML/TOML/ini 文件,這是我遇到過最常見的配置格式。

我們暫將這種配置稱爲常見配置(如果有更好的名字,歡迎在評論中留言,謝謝)。

大家可能遇到過如下情況:

  • JSON 沒有註釋,設計如此
  • 大量配置無法重用

例如,雖然 YAML 在理論上支持重用 / 引用配置(他們稱之爲錨),但有些軟件(如 Github Actions )卻並不支持。通常,開發者無法重用配置的一部分,必須複製粘貼。

  • .gitconfig 使用一個自定義語法來合併這些配置
  • 不能包含任何邏輯

很多人認爲這是一種積極的做法,但我認爲,如果不能定義臨時變量、輔助函數、替換字符串或連接列表,那就有點差勁。變通方法(如果有的話)通常也不好用,因爲它們額外增加了認知開銷。於是,出現了一批重新發明的編程語言:

  • 變量和字符串插值:Ansible 使用 Jinja 模板(!) 進行變量操作。
  • Github Actions 爲此使用了自定義語法

此外,他們有自己的一套函數來處理變量。你得爲此學習一門從來都未曾想過要學習的新語言。

  • 範圍
    例如,在 Github 操作中有幾個針對於 env 指令的自定義作用域。
  • 控制流
  1. 循環:構建矩陣和“排除”總是讓人頭疼不已

if 語句:例如,CircleCI 中的 when

  • 無法被校驗。可以校驗配置語法本身(例如,檢查 JSON 串的正確性),但無法做語義檢查。這是因爲在配置文件中沒有邏輯。通常情況下,你必須編寫一個輔助程序來檢查配置,並在傳遞給程序之前調用。很少有程序會遇到這個問題,通常,使用簡單的類型系統就可以發現程序中的細小錯誤。
  • YAML 的隱式轉換和可移植性問題非常突出。這一點已經飽受非議,所以在此只提供一個相關鏈接,供感興趣的讀者自行了解:“ YAML:可能沒那麼好”

總結:我們在花時間學習沒什麼用處的語法,而不是在富有成效地完成工作。

解決方法

當遇到這些問題時會出現什麼情況呢?通常最終會使用一種“真正的”(即通用的、圖靈完備的)編程語言來解決問題:

  • 編寫一個過濾自定義註釋語法的程序;
  • 編寫一個合併配置或使用模板引擎的程序;
  • 編寫一個“evaluate”配置的程序,在此過程中,常常需要爲一門簡單的函數式語言重新實現一個解釋器;
  • 編寫一個校驗配置的程序。

在大多數情況下,它就是類型檢查的樣板文件。你不僅要處理已解決的問題,而且得到的錯誤消息質量也不高,所有這些事情都會分散你在主要目標上的注意力。

使用一門真正的編程語言

其思想是用目標編程語言編寫配置。這裏我將使用 Python,但是,這一思想也適用於其他語言,只要足夠動態即可(比如 Javascript、Ruby 等等)。這樣,只需 import 或 evaluate 配置文件就可完成。
一個小例子:

config.py

from typing import NamedTuple
class Person(NamedTuple):
    name: str
    age: int
PEOPLE = [
    Person('Ann'  , 22),
    Person('Roger', 15),
    Person('Judy' , 49),
]

使用這個配置(如果你想知道爲什麼我使用 exec 而不是 import,請看看這個回覆):

from pathlib import Path
config = {}
exec(Path('config.py').read_text(), config)
people = config['PEOPLE']
print(people)
[Person(name='Ann', age=22), Person(name='Roger', age=15), Person(name='Judy', age=49)]

我覺得它很簡潔。讓我們看看如何解決上文所述問題:

  • 註釋:很明顯,不需贅述
  • 包含:很簡單,使用 import

你甚至可以 import 正在配置的包,可以針對配置定義一個 DSL,它將在配置文件中進行導入和使用。

邏輯
你可以使用語言的語法和庫。例如,單獨使用像 pathlib 之類的可以節省大量重複配置。
當然,隨意亂用可能會讓人難以理解。就我個人而言,我寧願接受語言被濫用,也不願受限制。

校驗
你可以將邏輯校驗保留在配置中,以便在加載時進行檢查。成熟的靜態分析工具(如 JS flow、eslint、pylint、mypy)對此可以有所幫助。

缺點
互操作性

如果程序是用 Python 編寫的,那沒什麼問題。但如果不是,或者稍後將以另一種語言(比如 C++ 之類的編譯語言)重寫它,該怎麼辦呢?

將來,軟件是否無需解釋器即可運行?現代的 FFI 很是繁瑣,鏈接配置將相當棘手。

我們特別以 Python 爲例,大多數現代 OS 發行版中都有它。那麼,你可以按以下方式來做:

1.使 Python 配置可執行
2.在 main() 函數中構建配置,轉換爲 JSON 串並輸出到 stdout

由於 Python 是動態的,所以無需樣板文件即可執行此步驟。

3.在代碼中執行 Python 配置(比如,使用 popen()),讀取原生的 JSON 串並予以處理。仍然需要手動在代碼中將配置反序列化,但這至少不像只使用 JSON 並手動編輯它那麼糟。

通用編程語言很難推理

這多少有點主觀。就我個人而言,我更有可能被一個過於冗長的普通文本配置搞得不知所措,我一直都更喜歡簡潔的 DSL。其中一個重要因素是代碼風格:我確信你可以使配置文件在幾乎任何編程語言中都具有可讀性,甚至根本不熟悉該語言的人也能夠看得懂,最大的問題可能是安全性和終止檢查。

安全性

例如,如果配置可以執行任意代碼,那麼它可能會竊取密碼、格式化硬盤等。

如果配置是由你不信任的第三方提供的,那麼,我認爲普通文本配置更安全。然而,通常並非如此,一般都是用戶自己控制自己的配置。
此外,也可以通過沙箱解決這一問題,是否值得這樣做取決於項目的性質,但是如果你使用像 CI executor 之類的東西,無論如何都需要它。
另外要注意,使用普通文本的配置格式不一定能躲過這些麻煩。參見“ YAML:一般並不安全”。

終止檢查

即使不關心安全性,也不希望配置會掛起程序。我個人從來沒有遇到過這樣的問題,但這裏有一些潛在的解決方法可供參考:

  • 爲加載配置指定顯式的超時時間
  • 有些語言能夠有所幫助,例如, Bazel Skylark

有人知道在通用語言中檢查終止的保守的靜態分析工具的例子嗎?注意,使用普通文本配置並不意味着它不會無限循環,參閱 "Accidentally Turing complete" .

配置會花很多時間去 evaluate,雖然技術上需要在有限的時間內完成,請參閱 "Why Dhall advertises the absence of Turing-completeness" 。雖然 Ackermann 函數是一個人爲設計的例子,但它表明如果你真的關心惡意輸入,那麼無論如何都要做沙箱處理。

爲什麼是 Python?

我發現出於以下原因,大家都特別喜歡用 Python 來編寫配置文件:

  • 幾乎所有的現代操作系統中都有 Python
  • 大家認爲 Python 語法很簡單(不是件壞事),所以 Python 配置很有可能不會比普通配置更難理解
  • 數據類、函數和生成器構成了精簡的 DSL 的基礎
  • 類型標註同時用作文檔和校驗

其實,你可以在大多數現代編程語言中獲得類似的愉快體驗(只要它們足夠動態)。

還有誰在做這件事?

一些項目允許用代碼作爲配置:

  • Webpack ,Web 模塊打包器,使用 Javascript 作爲配置
  • setuptools ,安裝 Python 包的標準方法

允許同時使用 setup.cfg 和 setup.py 文件。這樣的話,如果你不能以普通文本配置完成你的需求,那麼可以在 setup.py 中進行調整,從而使你可以在聲明式和靈活性之間取得平衡。

  • Jupiter ,交互式計算工具
    使用一個 python 文件配置輸出。
  • Emacs :大家都知道使用 Elisp 進行它的配置
    雖然我一點也不喜歡 Elisp,但它確實使 Emacs 非常靈活,可以實現你想要的任何配置。另一方面,如果你曾經讀過其他人的 Emacs 設置,那麼你可以發現,當你允許使用通用語言進行配置時,有些事情可能很難操控。
  • Surfingkeys 瀏覽器擴展:使用 Javascript DSL 進行配置
  • Gradle 支持以 Groovy 和 Kotlin DSL 來編寫構建文件
  • Awesome Window Manager 使用 Lua 進行配置
  • Guix 包管理器:使用 Guile Scheme 進行配置

有些語言是專門爲配置而設計的:

  • Bazel Skylark 使用 Python 的一個子集來描述構建規則

雖然爲了確保終止檢查和確定性而特意對 Bazel 進行了限制,但是配置 Bazel 比我使用過的任何其他構建系統都要愉快得多。

  • Meson 構建系統:借鑑 Python 的語法
  • Nix :專門爲 Nix 包管理器設計的語言

雖然弄一門全新的語言讓人感覺有點大材小用,但是仍然好過用普通文本來進行配置。

  • Dhall :專門爲配置文件設計的語言

Dhall 宣稱自己是“JSON + 函數 + 類型 + 導入”。的確,它看起來很棒,解決了我上文列出的大部分問題。

  • Jsonnet :JSON + 變量 + 控制流

它們之間的具體區別,請參閱其他配置語言間的比較

這種語言的缺點是還沒有被廣泛使用。如果你沒有綁定目標語言,那麼需要二次解析 JSON。

但是,至少它能使你可以愉快地編寫配置。

然而,如果你的程序是用 Javascript 編寫的,並且不與其他語言交互,那麼爲什麼不直接用 Javascript 編寫配置呢?

如果一個也不選要怎麼辦?

在使用普通文本配置的時候,我找到了一些減少那些問題的方法:

儘量少寫配置文件

這通常適用於 CI 流水線配置(例如 Gitlab、Circle、Github Actions)或 Dockerfiles。通常情況下,這樣的配置使用了大量的 shell 命令,如果不逐行復制,就不可能在本地運行。

是的,的確也有調試的方法,但是它們的反饋週期非常慢。

  • 使用更適合設置本地虛擬環境的工具,如 tox-dev/tox
  • 更多地採用 helper shell 腳本,並從你的流水線中調用它們

這多少有點令人沮喪,因爲它引入了間接而分散的代碼。但是,同時它也是一個優勢,你可以剝離(例如 shellcheck)你的流水線腳本,使它更容易在本地運行。有時,如果你的流水線很短,你可以視情況做出自己的判斷。讓 CI 只負責爲你設置 VM/ 容器、緩存依賴項和發佈構件。

生成而不是手動編寫

這樣做的缺點是,相比於手工編輯而言,生成的配置可能會更分散。

你可以添加警告註釋,提醒該配置是自動生成的,並附上生成器的鏈接,同時將配置文件設置爲只讀,以防止有人手動編輯。
此外,如果你正在實行 CI,可以將一致性檢查作爲流水線本身的一部分。

參考資料

  • (命令行)標記非常適用於配置
    總體上,我同意這一觀點,但是仍然有些情況是不適用於標記的。

它也容易泄露機密(密鑰、令牌、密碼)——無論是在你的 shell 歷史記錄中還是通過 ps 都可以看到。

  • Xmonad :配置文件是可執行文件
    一個有趣的方法,但不一定總是可行的,例如,你可能沒有安裝編譯器。
  • Mage :以 Go 編寫用於 makefile 的工具
  • Dhall wiki:可編程的配置文件
  • 擴展語言的演變:Lua 的歷史——顯然 Lua 已經開始成爲配置語言
  • Cue :定義、生成和驗證數據的語言

我在網站上找了很久才找到一個代碼例子,就在這裏。

  • 配置複雜性時鐘:一個硬編碼的例子

最後的問題

之於現在爲什麼 YAML 成爲一個主流選擇,我還沒有答案。我相信,Ansible/CircleCI 或者 Github Actions 都出自於非常優秀的工程師之手,他們應該考慮過使用 YAML 的利弊。

歡迎大家在評論區留言,分享你在做配置時經受過的痛苦,以及是如何解決它的。

【雲棲號在線課堂】每天都有產品技術專家分享!
課程地址:https://yqh.aliyun.com/zhibo

立即加入社羣,與專家面對面,及時瞭解課程最新動態!
【雲棲號在線課堂 社羣】https://c.tb.cn/F3.Z8gvnK

原文發佈時間:2020-04-29
本文作者:佚名
本文來自:“InfoQ”,瞭解相關信息可以關注“InfoQ

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章