持續測試基礎設施
作者 | 趙志佳
持續測試基礎設施的必要性
基礎設施作為應用程序的支柱,為之提供關鍵的運行環境、網絡連接和資源調度等支持。一旦基礎設施出現故障,整個應用生態系統都可能面臨嚴重的連鎖反應,如性能降低、數據丟失乃至系統崩潰。因此,基礎設施的穩定性和可靠性對于運行在其上的應用程序至關重要。
持續測試可以在基礎設施的整個生命周期中進行檢查,確保一切運行正常,盡早發現并解決潛在問題,減少影響擴散。此外,持續測試通過為團隊提供即時的狀態反饋,有助于提高基礎設施的可維護性和可擴展性,進而支持業務持續增長和變化的需求。
因此,持續測試不僅是持續交付高質量軟件的必要保障,對于基礎設施而言,其價值和影響更為深遠。
本文來分享一下我們團隊是如何對基礎設施進行測試的。
測試的范圍
首先我們要識別出需要測什么。在 IaC(基礎設施即代碼)的實踐中,我們以測試金字塔和敏捷測試四象限為指導原則,適用的測試方案包括:
- 單元測試:對實現中的特殊邏輯,比如環境差異、批量處理等進行部署前的驗證。
- 組件測試:對部署的獨立組件進行驗證,部署結果與預期一致。比如 S3 bucket 部署配置。
- 功能測試:對多個需要串聯合作使用才能達成實現一個功能的組件進行驗證,保證組件間配置的正確性。比如通過公網域名可以訪問到 app。
- 冒煙測試:在服務、組件部署完成之后進行端到端驗證,確保服務基本可用和出入口配置的正確性。
- 安全性測試:驗證各項安全配置是否已經啟用。比如數據庫、域名是否采取了 TLS 且無法在不加密情況下進行連接。其它的如權限控制、代碼漏洞等。
- 可靠性測試:基礎設施的容災耐力、數據,混沌工程等。
- 部署測試:確保應用在發布的過程中,平臺提供了正確可用的部署能力。
- 性能、可用性測試:服務的響應時間、吞吐量、并發用戶數等指標。由于平臺在服務間添加了一些基礎設施級組件和服務,如 Service Mesh、Styra,所以也會受到影響。因此,平臺團隊可以構建一個覆蓋了影響范圍的簡單應用,對其進行驗證。
明確了測試方案,我們就需要識別測試優先級,在不同階段開展相應的測試:
- 首先覆蓋關鍵路徑和高價值,如單元測試、組件測試、功能測試。這些代碼變化而引發的測試在代碼變化時都應該進行。
- 其次是覆蓋代碼變化之外由我們可控因素導致的問題,如證書到期、磁盤空間滿、token 失效等,保證運行時環境相關組件和功能。如冒煙測試、部署測試、可用性測試等??梢栽谄脚_功能上了生產環境后,核心功能交付無壓力時進行。
- 最后是在平臺相較穩定后(即被測功能不會有大的變動時),以提高平臺可靠性為目的的測試。用來驗證在面對代碼之外,不可控的因素導致的問題發生時我們的應對能力。如外部依賴變化、數據恢復能力、容災重建能力等。通常在平臺上的應用服務在生產環境已有真實用戶投入使用后進行。
測試工具的選擇
市面上有很多可以測試工具可以選擇:
- 最基礎和單一的是 Shell 腳本語言,典型如 Bash。
- 之后是應用開發語言的測試框架,如 Bash 的 bats、Ruby 的 RSpec 和 JavaScript 的 Jest。
- 最后是在語言提供的能力上對三方 cli 和 API 進行封裝的測試庫,如 Ruby 的 AWSpec,Go 的 Terratest 等。
比較來看,shell 優點是原生,直接調用服務方提供的 CLI,如 AWS CLI, Kubectl;缺點是面對復雜場景編寫起來費心費力;
使用封裝起來的測試庫看起來很簡單,但開發者日常就要使用 CLI/Curl 命令來進行基礎驗證,而用封裝庫進行開發就需要多學習一套知識;而且在被測服務發布新功能后,平臺想跟進卻發現測試庫沒能跟進,導致最后還得用原生方式來寫。比如 AWSpec 支持 RDS,但是很長時間都沒有支持 Aurora。如果已經寫了很多測試,就只能在 Aurora 這里使用其它方式驗證,最后導致各處驗證方式不統一。
所以我推薦選擇團隊熟悉的應用開發語言的測試框架,優點如下:
- 可以直接通過系統命令調用 CLI,開發者平常工作怎么驗證,測試代碼就怎么寫,拷貝過來能用。
- 相較 shell 來說,良好的測試框架支持。比如在多級 JSON 中驗證部分內容,jq 驗證起來就很麻煩。
- 各種驗證場景統一實現,不用學習多框架或多語言。比如 Terratest 只適合驗證 infra,如果需要想做冒煙測試,還要另起爐灶。
- 如果確實有必要集成測試庫,也可以按需集成。
我的選擇則是 Ruby/RSpec,因為 Ruby 簡潔自然的語法和 RSpec 的強大驗證器,讓測試代碼中很少出現語言自身導致的難懂和多余的代碼。
如何測
組件測試加上人工驗證是交付環境能夠成功部署的主要信心來源,而在有邏輯分支的時候,單元測試可以用來成為對組件測試的補充:組件測試驗證代碼的主干,單元測試在部署前來驗證分支,以實現對代碼的測試全覆蓋。
下面我們基于 Terraform 實現,以單元測試和組件測試為例進行測試。其它 IaC 實現和不依賴外部工具的測試都可以參考來實現。
注意這些由代碼變化產生的測試都應在 Pipeline 的流水線中,而不是手動觸發。任何不攔截在上線必經之路的測試,最終都將無人理睬。
部署前
(1) 單元測試
在 Terraform 中,通常需要人工來驗證 terraform plan 的結果,但是它只能覆蓋當前 state 和配置參數下的結果。當我們代碼中包含邏輯時,我們就需要通過配置 local backend、不同配置和 state 文件來本地驗證對應的 plan 結果。示例:
(2) 檢查 plan 結果
在部署流水線中,通過 terraform plan 加人工驗證。在測試環境中 apply 后,人工測試來保證正確性。驗證完成后,對于后續環境來說在測試環境的 plan 結果就是其它環境的參考輸入,由人工核對確認后進行 apply。
部署后
在資源生成后,我們便可以通過測試腳本調用 CLI/API 請求目標資源,來驗證產生的結果與預期一致。比如服務可以被成功訪問、數據庫確實被創建出來并配有正確的參數,密鑰管理器中被保存下來的數據庫密鑰我們可以成功連接到數據庫等等。與應用測試一樣,任何一條失敗的測試都應讓我們的 Pipeline 變紅,向團隊告警。并確保只有在前一個環境被驗證通過后,我們才向下一個環境前進。
一個測試的范例
我們以 Ruby/RSpec 為例。在一個代碼庫中,以生成的目標資源上下文劃分測試文件。
比如對于 RDS 數據庫的創建,我們可以組織這三個文件:
- rds_spec.rb: 用來驗證 AWS RDS 生成的資源,如 cluster、db parameter。
- db_spec.rb: 用來驗證在 DB 中進行的設置,比如支持動態數據庫憑證所在 DB 中創建的資源,DB 的 extension 被正確啟用。
- vault_spec.rb:用來驗證 Vault 中創建的資源、比如 master 憑證的存儲、支持動態數據庫憑證所需的資源。
一個文件中的組織結構如下:
下面是一個驗證 RDS 的 DB parameter 按預期被創建的例子:
可以看出測試代碼非常的語義化,沒有額外的數據結構定義和難懂的語法??疵靼琢诉@個測試,其它命令行相關的測試也就全都會寫了。平臺開發者們可以專注于業務驗證,而不會因為測試框架帶來額外的負擔。
IaC 可以測試驅動開發嗎?
當然,只需要我們能在編寫功能代碼之前被測內容是什么。我們可以通過各種文檔來識別出被測內容,比如 Kubectl、AWS、Vault 等 CLI,或各種服務的 API。如果我們無法識別出被測內容時,那就需要通過拆解步驟、手動部署資源等方式分析出來。像在其它語言進行測試驅動開發時一樣,小步驗證,紅綠重構。
進行測試驅動在其它語言中帶來的優點,在 IaC 也一樣大部分適用:
- 促進模塊化設計和提交
- 簡化調試過程
- 更快地反饋循環
- 更好地設計決策
- 易于重構
- 減少過度工程
- 保障測試覆蓋率:這點需要單獨提一下,目前還沒有什么好的方案可以檢查 IaC 代碼的測試覆蓋率,所以在測試驅動中「只實現剛好可以通過測試的代碼」對保障覆蓋率很重要。
總結
自動化測試是高代碼質量和穩定開發效率的重要保障,應用服務開發如是,基礎設施因為擔負著更大的使命和責任更是如此。測試驅動能幫助開發者更好的設計和實現。在 IaC 開發過程也同樣適用。在工具選型上,避免選擇編寫成本過高和太復雜的語言和工具,大部分 Ops 們更習慣編寫動態語言的腳本,方便和順手更重要。
希望本文能對你的工程實踐帶來啟發,從下一個 IaC feature 開始測試驅動開發。