terraform validate 檢查語法,terraform plan 預覽變更,但兩者都無法告訴你部署的基礎設施是否真的能正常運作。Terratest 填補了這個缺口——它部署真實的基礎設施,對其執行斷言,然後銷毀,全部透過 Go 標準的 testing 套件完成。
Terratest 是什麼,為什麼需要它
Terraform 內建的 terraform test 指令(v1.6 引入)針對模擬或臨時配置執行測試,適合對個別模組邏輯做單元測試,但並非設計來驗證跨真實 AWS、GCP 或 Azure 資源的端對端行為。
Terratest 採取不同立場:全部用真實環境部署,打實際的 endpoint、查詢真實 API、驗證輸出,然後全部拆掉。這意味著:
- S3 bucket 測試會真正建立 bucket、確認版本控制已啟用、確認 policy,然後刪除 bucket。
- EC2 測試會真正啟動 instance、等待它回應 HTTP,然後終止。
- VPC 測試會真正建立網路、確認路由表存在,然後移除所有資源。
代價是時間與金錢——真實基礎設施需要幾分鐘且會產生費用。回報是對模組在真實環境中確實可運作的信心,而非僅在模擬中通過。
Terratest 由 Gruntwork 維護,Apache 2.0 授權,目前版本為 v0.56.0(2026 年 2 月),需要 Go >= 1.21.1。
Terratest vs terraform test
| 面向 | terraform test | Terratest |
|---|---|---|
| 語言 | HCL | Go |
| 基礎設施 | 模擬或臨時 | 真實雲端資源 |
| 範疇 | 模組單元測試 | 整合 / 端對端 |
| HTTP 驗證 | 無 | 有(http_helper) |
| AWS/GCP API 檢查 | 無 | 有(aws、gcp 套件) |
| 重試邏輯 | 有限 | 第一等公民(retry 套件) |
| 平行測試 | 有限 | 原生 t.Parallel() |
| 階段跳過 | 無 | 有(test_structure) |
對模組邏輯做快速、低成本的單元檢查用 terraform test。需要證明已部署的系統行為正確時用 Terratest。
安裝與專案設定
從 go.dev/dl 安裝 Go >= 1.21.1,然後建立如下的目錄結構:
| |
在 test/ 目錄內初始化 Go module:
| |
go.mod 看起來會像:
| |
執行測試時需要延長 timeout——基礎設施操作需要時間:
| |
Go 預設 timeout 是 10 分鐘。大多數基礎設施測試需要 15–30 分鐘,請務必明確設定 -timeout。
核心模式:部署、驗證、銷毀
每個 Terratest 測試都遵循相同的三階段結構:
| |
defer terraform.Destroy(...) 這行至關重要。在 Go 中,defer 的呼叫會在外層函式回傳時執行——不論是正常回傳或因測試失敗而回傳。在部署前就註冊清理動作,能確保即使 InitAndApply 或任何斷言發生 panic,destroy 依然會執行。
terraform 套件
terraform 套件包裝了 Terraform CLI。每個函式都有兩種變體:普通變體在錯誤時呼叫 t.Fatal(),加 E 後綴的變體回傳 error 供明確處理。
terraform.Options
| |
WithDefaultRetryableErrors 為 options 加上一組常見的暫時性錯誤,Terratest 遇到這些錯誤會自動重試,例如「connection reset by peer」或「Provider produced inconsistent result after apply」。
InitAndApply
| |
Output
| |
Plan
| |
Destroy
| |
http_helper 套件
http_helper 套件處理帶有內建重試邏輯的 HTTP 驗證——這是必要的,因為新部署的伺服器需要時間才能就緒。
HttpGetWithRetry
| |
這會每 5 秒輪詢 instanceURL,最多 30 次(共 2.5 分鐘)。若伺服器返回 200 且 body 包含「Hello, World」,測試通過。若重試次數耗盡,測試失敗並附上描述性訊息。
HttpGetWithRetryWithCustomValidation
當你需要對什麼算是有效回應有更多控制時:
| |
跳過 TLS 驗證(自簽憑證)
| |
僅在你掌控基礎設施的測試環境中使用。
aws 套件
aws 套件將 AWS SDK 呼叫包裝成測試友善的函式。
Region 選擇
| |
AMI 查詢
| |
EC2 instance 類型選擇
| |
S3 bucket 檢查
| |
EC2 查詢
| |
其他 AWS 服務
| |
retry 套件
Terratest 的 retry 套件提供獨立於特定基礎設施 helper 的明確重試控制。
DoWithRetry
| |
若 action 回傳任何非 nil 的 error,Terratest 會等待後重試。若所有重試都耗盡,測試失敗。
FatalError:跳過重試
| |
DoWithRetryE
當你想自行處理「所有重試都耗盡」的情況,而非讓測試直接失敗時:
| |
DoWithRetryableErrors
僅針對特定錯誤模式(以 regex 匹配)重試:
| |
不符合任何模式的錯誤會立即被包裝成 FatalError,測試失敗且不重試。
背景輪詢
| |
test_structure 套件
長時間執行的基礎設施測試在反覆迭代時很令人痛苦。如果測試需要 20 分鐘,而在驗證步驟失敗,你不會希望每次都從頭重新部署。test_structure 套件透過將測試拆分為可獨立跳過的命名階段來解決這個問題。
RunTestStage 與 SKIP_ 環境變數
| |
只重新執行驗證階段(部署已完成):
| |
RunTestStage 會檢查 SKIP_<stageName> 環境變數,若設定了任意非空值,該階段的主體就會被跳過。
儲存和載入測試資料
| |
資料以 JSON 檔案形式寫入 workingDir,路徑格式為 <workingDir>/.test-data/<name>.json。
CopyTerraformFolderToTemp
多個平行測試共用同一個 Terraform 目錄時,會在 .terraform/ 和 terraform.tfstate 上衝突。解決方案是為每個測試將模組複製到暫存目錄:
| |
設定了任何 SKIP_* 變數時,CopyTerraformFolderToTemp 會跳過複製並回傳原始路徑——在反覆迭代執行之間保留快取狀態。
平行測試
平行執行測試能大幅縮短有多個獨立模組需要測試時的 CI 總時間。
t.Parallel()
| |
命名空間隔離,避免衝突
在同一個 AWS 帳號中平行執行測試時,資源名稱必須唯一:
| |
random.UniqueId() 產生適合作為資源名稱後綴的短隨機字串。
表格驅動的平行子測試
| |
每個子測試平行執行,部署各自獨立的 bucket。
實作範例:端對端測試 S3 模組
這是一個完整可執行的範例,測試帶有版本控制、bucket policy 和伺服器存取日誌的 Terraform S3 模組。
Terraform 模組(examples/s3-module/main.tf)
| |
測試檔案(test/s3_module_test.go)
| |
執行測試
| |
實作範例:EC2 + HTTP 驗證
這個範例部署一個提供網頁服務的 EC2 instance,然後驗證 HTTP 回應。
| |
錯誤處理慣例
每個 Terratest 函式都遵循相同的慣例:
terraform.InitAndApply(t, opts)— 錯誤時呼叫t.Fatal(),測試立即停止。terraform.InitAndApplyE(t, opts)— 回傳(string, error),由你決定如何處理。
普通變體適用於大多數測試,因為部署出錯時快速且明確地失敗是正確行為。當你需要條件邏輯,或預期部分失敗且想繼續時,才使用 E 變體。
| |
小結
Terratest 涵蓋了 Terraform 模組的完整測試面向:
| 套件 | 功能 |
|---|---|
terraform | 包裝 Terraform CLI:init、apply、plan、destroy、outputs |
http_helper | 帶重試和自訂驗證的 HTTP GET 函式 |
aws | AWS SDK 包裝:S3、EC2、AMI、RDS、Lambda、SSM、Secrets Manager |
retry | 通用重試邏輯,支援 timeout、fatal error 和可重試錯誤模式 |
test_structure | 基於階段的測試執行,在階段間持久化狀態 |
random | 用於資源命名空間隔離的唯一 ID 產生 |
基本工作流程永遠不變:defer Destroy,然後 InitAndApply,然後斷言。其他一切都是將這些基本組件與重試邏輯、AWS API 呼叫和 HTTP 檢查組合在一起,以驗證模組所承諾提供的特定行為。
