前言
Terraform 是 Hashicorp 公司開源的一種多雲資源編排工具。使用者通過一種特定的配置語言(HCL, Hashicorp Configuration Language)來描述基礎設施,由 Terraform 工具統一解析,構建資源之間的關係,生成執行計劃,並通過調用各家雲廠商的具體實現來完成整個基礎設施生命週期的管理。
相對於其它的雲上資源管理方式,Terraform 的主要特點有:
- 基於 IaC(基礎設施即代碼,Infrastructure as Code)的設計,可以將基礎設施以一種領域特定語言描述出來,消除了在基礎設施自動化時描述語義上的歧義,同時減輕了人爲因素造成的不確影響。
- Terraform 在執行編排動作前,會生成一份可讀性良好的執行計劃,關鍵基礎設施的變更可以得到充分審查,保證了基礎設施的可靠性。
- 基於 DAG(有向無環圖,Directed Acyclic Graph)描述資源與資源之間的關係,由於 DAG 良好的拓撲性質,當資源屬性與資源關係發生改變時,變更動作將被充分並行地執行。
在UCloud,我們最終選擇了 Terraform 來編寫 UCloud 基礎設施代碼,並配合 UCloud CLI、Ansible 等工具,進一步拓展了 Terraform 的功能,實現基礎設施可編程。
本文將詳細闡述 Terraform 的整個生命週期,從 Provider 開發者的視角,介紹 Terraform 在安全、效率和狀態一致性三個方面的內部機理與具體實現。
技術實現解析
生命週期
以首次執行 Terraform 創建 UCloud 雲上資源爲例,這一資源編排動作的生命週期如下圖所示:
圖中立方體所示分別爲:
- Terraform 核心進程:負責資源定義文件,構建有向無環圖,管理狀態存儲;
- Provider 進程:即提供資源編排能力的進程,包括由雲廠商實現的能力(比如 UCloud),和應用程序提供的能力(比如 TLS)等;
- Provisioner 進程:即提供資源編排後處理操作的進程,比如執行 Shell 命令,上傳文件等;
以中央的有向無環圖爲分界線,左側的部分是 Terraform 本身提供的能力,右側是由雲廠商提供的能力。
當執行 Terraform 命令首次編排雲上資源時:
- Terraform首先喚醒核心進程,初始化 Backend(即狀態管理組件);
- 解析用戶編寫的資源定義文件,同步最新的資源狀態,並與當前的資源定義作對比;
- 初次構建 DAG 時,資源尚未被初始化,所以資源狀態爲空,用戶的資源實例都將作爲 DAG 中新增的節點被創建。
在並行構建資源時:
- 並行遍歷 DAG;
- 當遇到 Provider 節點時,Terraform 核心進程喚醒 UCloud Provider 進程;
- 將所有的編排動作依次發給 UCloud Provider;
- Provider 調用 UCloud OpenAPI 管理雲上資源;
- 返回的結果由 Terraform 核心進程寫回狀態存儲。
進程管理
隨着雲計算的普及,以及人們對於數據安全性和可用性上的考量,越來越多的企業開始意識到,不能把雞蛋放在一個籃子裏,基礎設施的中立和非綁定是雲服務商十分關鍵的屬性。
Terraform 中每一個雲廠商的實現(Provider)都是一個獨立的進程,進程間使用 RPC 通信的方式下發指令和交換數據,這樣設計有什麼好處呢?
- 安全性:多雲環境下,進程隔離雲廠商的實現,防止共享內存帶來的安全性問題。
- 擴展性:插件式的設計使得特性的增加更加容易,而官方插件倉庫使得特性的質量更有保障。
- 穩定性:核心與插件分離,保證了核心簡單可靠,測試充分。單一插件的 Bug 不會擴散到全局。
而從使用者的角度來看,Terraform 多進程模型的重中之重,是多雲環境下廠商隔離帶來的安全性問題。安全性是多雲編排的基石,如果無法保證雲廠商之間的隔離性和安全性,多雲編排則無從談起。
Terraform 使用插件(Plugin = Provider + Provisioner)來抽象出各個雲廠商之間的差異,並相互隔離。
在一次編排任務的生命週期中,Provider 將會基於 Terraform 提供的能力,完成靜態檢查(Validate)、資源狀態同步(Read/Refresh)、生成執行計劃(Plan)、執行編排(Apply)等操作。
依賴管理
軟件工程的實踐表明,高層次的抽象,可以簡化問題,讓複雜的問題變得可以測試。而對於依賴關係的抽象,業界最通行的做法即使用有向無環圖(DAG,Directed Acyclic Graph)來描述事務間的依賴關係。有向無環圖上的點即事物本身,邊則是事物與事物之間的聯繫。
業界對於 DAG 的使用極爲廣泛,比較典型的是各種大數據工作流引擎,比如 Oozie,Airflow 等。在這些引擎中,批處理任務作爲 DAG 上的節點,而任務間的依賴作爲 DAG 上的邊。
Terraform 將所有的資源構建爲一張有向無環圖(DAG),計算它們的依賴關係,並行地去創建和修改相互間沒有依賴的那些資源。因此整個基礎設施的構建過程是高效且嚴格有序的。
下面我們將舉例介紹它的內部原理和實現。
注意:文中的圖與描述爲了呈現效果,分別有所簡略,僅供參考
圖構建
假設一個場景,一臺主機與一個公網彈性 IP 綁定,且客戶使用自有的第三方 DNS 服務,通過 A 記錄指向該主機的公網 IP。
這個場景展現了 Terraform 對於資源拓撲關係的描述能力,以及對於外部服務的集成能力。它背後的工作原理是什麼樣的呢?
所有云上資源,都抽象爲 DAG 的一個節點,而資源與資源之間的關係,則有兩種抽象方式:
- 一種是抽象爲邊,將兩個資源節點連接在一起,例如從 dnssimple_record 到 ucloud_eip 的箭頭表示將 DNS 指向彈性 IP(eip);
- 另一種是抽象爲一個單獨的資源,例如 eip_association 資源將彈性 IP(eip)和雲主機(instance)綁定在一起。
圖變換
在圖構建的過程中,Terraform 需要對 DAG 進行若干次變換(Transform)操作,如:
- 添加輔助節點,如 Config、Variable、Local、Provider Node、Root Node 等;
- 附加輔助信息到 Resource Node;
- 添加清理操作節點,作爲圖遍歷過程中末端的節點,例如 CloseProvider/Provisioner;
- 進行圖化簡操作,例如做 transitive reduction 簡化多餘的邊,減少編排成本。
圖遍歷
最後對整張圖進行遍歷,對每一個資源節點分別執行資源編排操作,比如讀取、創建、更新和刪除等。
從根節點開始,Terraform 並行地去編排整個資源拓撲,遍歷整個有向無環圖,直到所有資源都被成功編排,並執行清理操作。
可以看出,由於有向無環圖出色的拓撲性質,整個遍歷過程,存在着充分的局部並行化,編排時間跟基礎設施複雜度有顯著關係,而同構基礎設施的規模則對編排時間影響較小,保證了 Terraform 在大規模水平擴展時擁有較好的性能。
狀態管理
Terraform 引入了面向資源的設計,將資源的狀態描述爲一個狀態的集合,並支持若干種不同類型的狀態存儲。
默認情況下,在 Terraform 的執行目錄下,會存儲一個本地的資源狀態文件,並在每次編排開始時,從遠程同步狀態到本地,比較該狀態與用戶定義的資源之間的差異,從而生成編排計劃。
定義
在這一抽象中,Terraform 官方給出了幾個基本的定義:
從上文中的定義可以看出,執行計劃(Plan)本質上就是 Diff 格式化輸出的結果,而執行編排就是應用這個 Diff 的過程。
Backend
Terraform 將對資源狀態的管理抽象出了一個統一的狀態管理層(Backend),使得基於 Terraform 的資源編排系統可以保持基礎設施的一致性。
想象一個場景,如果 A 同學在操作基礎設施的變更,B 同學此時也想執行變更,這個變更會執行麼?
答案顯然是不會的,任何一個成熟的系統都應該對這樣的問題提出解決方案。
Terraform Backend 通過對狀態加鎖來解決資源的競態問題。A 在操作資源的時候狀態會被鎖定,此時 B 執行的任何變更行爲都將被拒絕。
其中,consul、etcd 和 http 是比較推薦的擴展:
- consul、etcd 提供了鎖機制,且基於 Raft 協議保證了數據的強一致性;
- http 適用於自行研發的,可擴展的狀態存儲,如雲服務商提供的狀態託管服務,可選支持鎖機制。
Terraform 對 Backend 的抽象增強了狀態存儲的可擴展性,同時提供了可選的鎖機制擴展,基於此雲廠商可以定義自己的遠程狀態存儲,用於託管用戶的資源狀態,併爲用戶提供可靠的併發安全保障。
時效性
Terraform 的殺手級特性之一 —— 執行計劃,允許導出執行計劃,延後執行,提供了在 CI/CD 環境下人工審查執行計劃的可能,對關鍵基礎設施變更的安全性提供了保障。所以,導出的執行計劃是否過期是生產環境中最常見的問題。
Terraform 如何保證已經失效的執行計劃不再被執行?Terraform 使用多版本快照(Multi-Version Snapshot)的方式來實現。可以類比於常規的 MVCC(多版本併發控制)來理解,下圖是一個最小化的 MVCC 實現:
進程 P1 和 P2 依次讀到了序號爲 1 的數據,並且都想進行寫操作,P1 先修改數據,自增序號爲 2 並寫入成功,此時 P2 進行寫操作時,由於修改後的序號同樣爲 2,此時應拋出寫失敗,P2 需要主動重新讀取最新的數據再次修改,才能成功寫入。
由於 Terraform 可以執行一個已導出的執行計劃,一個事務的時間被極大延長了,所以版本衝突的可能被無限放大。
基於此,Terraform 同樣選擇該方式,通過一個序號來標識狀態的版本,當執行計劃的狀態序號小於當前狀態的序號時,直接丟棄過時的執行計劃:
基於這樣的原理,Terraform 保證了導出的執行計劃是有時效性的。例如一個用戶導出了一份執行計劃,將雲主機從 1 臺水平擴容到 3 臺,但在該執行計劃審查通過之前,另一個用戶已經擴容到 5 臺雲主機,此時這份執行計劃執行時會 Abort,而不會從 5 臺降爲 3 臺,從而保證關鍵基礎設施的變更是安全的。
狀態升級
在軟件構建或產品設計中,不可避免的會出現一些破壞性的變更,而這些破壞性的變更又不可避免地會影響資源狀態。
常見的在線服務(比如 HTTP API)設計中,通常在 HTTP Header 甚至 URL 加一個版本號來區分新老版本。但 Terraform 作爲一個二進制分發的軟件,且在用戶的本地存儲有一份資源狀態,如果進行破壞性的升級,則必須同時考慮存量用戶的正常使用,以及舊版狀態文件的原地升級。
Terraform 的解決方案是標定資源的 Schema Version,默認 Version 爲 0,當資源的 Schema 有破壞性的更改時,作爲 Provider 的雲廠商必須爲此提供一個原地升級函數。
假設 UCloud 的 EIP 共有 3 個版本,0,1,2,則須提供 2 個升級函數。
當一個使用版本 0 的用戶升級到最近版本 2 執行編排的時候,他的狀態文件將會依次經過兩個升級函數,將資源狀態原地升級至最新版本的狀態,而無需任何額外操作。
總結
總體而言,Terraform 是一個安全的,可擴展的,有紮實的理論基礎,也有漸進式工程實踐的資源編排工具。Terraform 的關鍵特性:基礎設施即代碼、多雲編排、執行計劃與過程分離、統一的資源狀態管理,是我們在新一代資源編排系統實踐中的重要保障。
作者簡介
李宇飛,UCloud 後臺研發工程師,參與資源編排等接入產品項目研發,專注於雲計算,DevOps,分佈式系統等領域。