GO 語言基於容器的 CI 實踐
什麼是 CI,解決了什麼問題
CI 即持續整合(Continuous Integration),沒有 CI 之前,將新增的程式碼改動合併到主幹是一件危險的事情,通常是定期合併,合併之前進行人工 review & 測試,確認無誤後才執行合併。
簡單來說 CI 是一個自動化流程,方便我們頻繁地合併程式碼。CI 通常在程式碼合併之前自動執行,主要步驟包括:構建、程式碼靜態檢查、單元測試等。有了 CI,我們合併程式碼的信心大大增強。
Go 語言的 CI
主要關注以下幾點:
-
程式碼生成:在 CI 流程中重新執行程式碼生成,有助於發現生成的程式碼被篡改,或生成程式碼不一致(生成工具版本差異)諸如此類的問題。重新生成程式碼後,可以通過
git diff
-
程式碼格式化:沒有經過格式化的程式碼不允許合入主幹。
go fmt
是 Go 工具鏈內建的程式碼格式化命令,但是不支援對 import 分類&排序,官方維護的golang.org/x/tools/cmd/goimports
是一個很好的代替。進行程式碼格式化後,可以通過git diff
命令判斷程式碼是否有變動,如果有直接 fail CI。 -
靜態檢查:
- BUG 探測:
go vet
是 Go 工具鏈內建的強力命令,能發現一些常規的 BUG。 - 風格檢查:
golang.org/x/lint/golint
可以根據 Go 官方的風格指南《 - goreportcard、golangci-lint、staticcheck 也是可以納入考慮的選項
- BUG 探測:
-
單元測試:官方的測試框架
go test
已經夠用了,要有什麼補充的話,考慮 BDD 測試框架 ginkgo。
執行 CI 的環境
通常用 Shell 指令碼來實現 CI 流程,但 Shell 指令碼的環境依賴是一個深坑,不同作業系統的 Shell 環境差異很大,隨手列舉幾個:
bash
還是sh
(POSIX)bash
的版本差異- BSD Toolchain (MacOS)還是 GNU Toolchain(Linux)
嘗試用一套 Shell 指令碼去相容所有的作業系統環境是不可行的,會陷入深不見底的泥潭。
sh + docker 作為 CI 的執行環境
sh (POSIX) 在大多數作業系統下都能找到,docker 也被大多數作業系統支援,使用 sh 作為 docker 容器的啟動器,將專案程式碼掛入到容器內,在容器內執行 bash + GNU Toolchain 執行我們的 CI 指令碼,這個方案能解決 99.99% 的環境問題,我們只需要選定容器內 bash + GNU Toolchain 的版本,不再需要考慮 Shell 指令碼相容的問題。
docker volume 的許可權問題
將專案程式碼以 volume 的方式掛入到容器後,在容器內檢視檔案的 owner (uid:gid) 和容器外是一樣的,如果uid對應的 user 或 gid 對應的 group 在容器內不存在,則在容器內直接顯示為 uid 或 gid 的數值。也就是說,檔案的 owner 與 user 名或 group 名無關,與具體的 uid 和 gid 有關。
docker 容器內預設的使用者為 root,如果在 docker 容器內往 volume 寫入新檔案,那麼新檔案的 owner 為 (root:root) 對應 uid=0 gid=0,這個 owner(uid=0 gid=0)通常在容器外也是 root,這會造成:如果我們在本地用 sh + docker 執行 CI 指令碼,可能會在專案程式碼目錄內產生需要 root 許可權才能讀寫的檔案——這是不合理的。可以在 docker run 時通過 --user
選項指定執行 CI 指令碼的 uid 和 gid,設定為和容器外一致,就可以避免這個問題。
在 docker 容器內使用 sudo
docker run 時通過 --user
選項指定非 root 使用者後,ci 指令碼便無法使用需要 root 許可權的命令:例如安裝軟體包。雖然這樣的需求在 CI 中並不常見,通常在 CI 的 Docker 映象構建時已經預裝了需要的軟體包,但考慮到一套 CI 指令碼可能被多個專案複用,可能出現各種各樣的需求,如果能在容器內支援 sudo 命令那就更好了。
sudo 的使用者白名單本質上是 uid 和 gid 的白名單,在 CI 映象打包時,我們不能確定未來執行容器時要指定的 uid 和 gid。
這裡提供一個變通的方法:
- 構建 CI 映象時,用命令預先建立 user 和 group 作為佔位,例如:ci 使用者 ci 組,uid 和 gid 隨意。
docker run
執行 sleep 命令 —— 啥也不幹。docker exec
(root 使用者)修改容器內 ci 使用者的 uid 為容器外的 uid(usermod 命令),修改容器內 ci 組的 gid 為容器外的 gid(groupmod命令)。docker exec --user ci:ci
(ci 使用者)真正執行 CI 指令碼。
CI 指令碼和專案程式碼分離
通用的 CI 指令碼應該和專案程式碼保持鬆耦合,分開維護,這樣通用 CI 指令碼也可以被多個專案複用。可以只在專案內放置一個簡單的 sh(posix)指令碼,呼叫 curl 獲取真正執行的 CI 指令碼程式碼,通過 eval
命令解釋執行指令碼程式碼。最終在指令碼程式碼裡再執行 docker 容器執行更復雜更高階的 CI 指令碼……
Go 快取
可以通過環境變數 GOMODCACHE 指定 Go Module 的快取(下載)目錄,將其指向 docker volume 中的某個目錄,CI 完成時儲存這個目錄,下次 CI 時裝載這個目錄(需要配合 CI 系統提供的 cache 機制),這樣可以節省每次 CI 下載 Go Module 的時間。
GOCACHE 環境變數對應 Go Build 的快取目錄,可以作類似的快取處理,加速 go build 命令。