私有化倉庫的 GO 模組使用實踐

又拍雲發表於2021-06-24

本文以又拍雲團隊私有化模組處理的實踐案例為基礎,介紹如何使用私有化模組,以及 go get 工具背後的細節,其中包括如何讓 go 正確的源獲私有化 gitlab 上原始碼以及認證等問題。文章根據又拍雲資深開發工程師劉雲鵬在 Open Talk 公開課直播分享進行整理,回放視訊請下拉文末點選“閱讀原文”。

關於 Open Talk:由又拍雲發起的綜合性技術沙龍,秉承又拍雲“讓創業更簡單”的初衷,以全乾貨的形式為技術開發者提供包括技術、運維、產品、創業等多維度的知識分享,幫助企業成員提升專業技能,推動企業更好更快地發展。

研發背景

GO 在 1.11 版本開始引入 Module 的特性;1.13 版本引入 Module 校驗和檢查,加強了 Module 的安全性;現在的 1.16 版本已經預設使用 Module 模式。日前 GO 團隊在部落格上表明,將在 1.17 版本時刪除對 GOPAHT 的支援,如果現在還沒有使用 GO MODULE,趕緊抓緊時間試試 GOMDULE 吧。

GOMODULE 和 GOPATH 的主要區別在於私有化模組的使用。公有化模組使用是相同的,都是通過 go get 直接獲取模組。對於私有化模組 GOPAHT 可以直接將模組程式碼丟在 GOPAHT 目錄下,而 GO Module 不行,它有自己的程式碼管理方式,下面我們簡單介紹下。

GO 如何獲取 Module

GO 獲取模組通常是使用 go get 工具獲取模組,當前 go get 支援兩種方式:

第一種是通過傳統的 VCS 去程式碼託管平臺上拉取程式碼,以 git 為主,還支援 svn、hg、等其他平臺。

第二種是通過 1.12 版本開始支援的 GOPROXY 協議,go 在 GOPROXY 伺服器上獲取程式碼歸檔檔案。

從 1.13 起 GO 還使用校驗和檢查—— GO SUM ,所有模組下載後都會檢查其校驗和。它會將下載模組的雜湊值與 Google 線上資料庫中的雜湊值進行比對,防止模組被篡改,只有驗證通過後的模組才能正常安裝使用。

VCS 獲取模組的方式

GO 支援很多的版本管理工具。首先需要判斷使用什麼版本管理工具去獲取模組。判斷方式大致分成三類,不依賴其他的兩種靜態匹配方式和一種動態匹配方式。

靜態匹配方式

字首匹配:比如 github 、谷歌的 bitbuket 和 apache、openstack 等程式碼託管平臺,會內製在 go get 的工具鏈中,會去判斷模組的字首當字首匹配上則使用對應的版本管理工具。圖中左方的一例子,github.com/eamaple/pkg 模組會匹配字首,並與 github 相匹配,同時能知道 github 使用 git 工具。

正則匹配:正則的方式是給模組加上字尾,字尾名可以是前文介紹的五種版本管理工具( git,svn ,hg ,bzr,fossil )之一的字尾。字尾的匹配是通過正規表示式實現的。上圖中兩個例子都是以 .git 作為字尾,通過正規表示式的匹配會得到裡面的子分組,即 VCS 子分組會匹配到模組是使用 git 進行管理的。

動態匹配方式

當字首和正規表示式都匹配不上,則會採用動態判斷的方式。go get 會傳送一個 HTTP 請求,URL為模組帶上協議頭和引數( go-get=1 )。go get 期待伺服器返回模組相應資訊來幫助go get 進一步的操作。GO 預設會傳送 HTTPS 請求,如果伺服器想用 HTTP 協議,可以通過環境變數 GOINSECURE 來處理,當 GOINSECURE 為 1 時,GO 就會使用 HTTP 協議。

Go get 預期的返回體是一個 HTML 文件,其中對 GO 有意義的是要帶 name="go-import" 屬性的 meta 標籤。該 meta 標籤會通過 content 屬性告訴 GO 怎麼去獲取模組。

content 的內容有三部分:第一部分 root-path,指模組的名字;第二部分 vcs 代表需要使用的管理工具,比如說 git、svn。;第三部分 repo-url 指的是模組原始碼存放在哪個倉庫下面,該倉庫就需要是協議加倉庫地址的形式。

上圖以 GO 的子包為例,通過 curl 模擬傳送 go get 請求,golang.org/x/net 伺服器返回了一個 html 文件,文件有用的是紅圈框起來的部分,裡面是 meta 標記,content 第一部分是 GO 模組名稱 golang.org/x/net ;第二部分是 git,代表需要使用 git 來獲取原碼;第三部分是模組託管的地址,表示託管在模組包的地址 googlesource.com/net 上。需要注意 meta 標記只能放在 head 裡面,go get 解析會從頭開始,當遇到 head 的結束標籤或者 body 的開始標籤時停止解析。

GIT 在 GO GET 中的應用

git 支援 HTTP 協議和 SSH 協議,GO呼叫 git 時預設只使用 HTTP 協議,呼叫過程中會禁用 git 的互動過程。例如 git 使用 HTTP 協議去克隆私有倉庫需要輸入使用者名稱和密碼,但是 GO 呼叫 git 時不能通過互動輸入使用者名稱和密碼會導致獲取模組失敗。互動是通過環境變數 GIT_TERMINAL_PROMPT 控制,如果手動將變數強行更改為 1,就可以啟用互動從而手動輸入使用者名稱和密碼。

那麼該怎樣將使用者名稱和密碼無感知的傳遞給 git 呢?事實上在 git 裡,如果是使用 HTTP 協議都可以通過 netrc 檔案來傳遞使用者名稱和密碼,該檔案在 HOME 目錄下,有兩種檔案格式:

  • 第一種:通過伺服器名和使用者名稱密碼的方式去定義伺服器的使用者名稱和密碼;

  • 第二種:不指定伺服器,把所有的伺服器都指定相同的使用者名稱和密碼。

配置檔案示例

如上圖所示,第一條中配置了 gitlab.com ,使用者名稱為 root,密碼是 admin。通過 git 去克隆 gitlab 的私有倉庫時,可以把使用者名稱 root 和 admin 傳遞給 git,讓 git 無感知獲取到使用者名稱和密碼,從而就不會再要求輸入密碼了。第二條中通過 default 給所有的伺服器都設定預設的使用者名稱和密碼,設定的使用者名稱為 guest,密碼是 123456,表示除了 gitlab.com 之外的所有伺服器需要認證時,都會把將 guest 和 123456 作為使用者名稱和密碼傳遞給需要的程式。

go 呼叫 git 時也支援 SSH 協議,但預設不會使用。只有在動態獲取的時候顯示指定,才可以使用 SSH 協議。如果通過靜態匹配方式(字首匹配或正則匹配),能匹配上使用的模組資訊,都只能使用 HTTPS 協議。

上圖中的模組是 example.com/pkg,倉庫地址是 gitlab.example.com/example/pkg。meta 標記的content 裡包含了完整的模組資訊,首先 第一部分是模組的名字,這和前面 module 的名字定義是相同的;緊接著是 git,代表使用 git 去獲取程式碼,最後一部分是倉庫地址這裡就顯示指定了 SSH 協議,同時還有 git 的使用者名稱和伺服器 SSH 服務埠號。

Git ssh 認證是基於金鑰對實現的,如果沒有金鑰對,可以通過 SSH 工具套件 ssh-keygen 生成金鑰。上圖中列舉了常用的引數 -t,該引數可以指定金鑰的型別。其中 RSA 金鑰可能是最常用的,而本人比較喜歡使用 ED25519,它有個明顯的優點就是金鑰長度非常短,公鑰和私鑰都只有 32 位元組,安全性也可以和 RSA 金鑰 3000 位左右的相媲美,能夠保證安全性,金鑰長度又短,因此會經常使用 ED25519 作為金鑰。

當生成祕鑰對之後,會在 HOME 下的 .ssh 資料夾中生成金鑰隊的檔案碼,包含私鑰和公鑰。".pub" j結尾的檔案是公鑰檔案,需要把公鑰檔案配置在 gitlab 或 github 等程式碼託管平臺上。右邊是 gitlab 的截圖,圖中使用的金鑰就是 ED25519 格式的金鑰,可以看到長度真的非常短。

GOPROXY 獲取模組

GO 支援通過 GOPROXY 協議獲取 GO 模組。模組是基於 HTTP 協議的,只會使用 HTTP 的 get 請求,並且使用標準的 HTTP 狀態碼進行呼叫。當使用的公共 GOPROXY 協議,其 GOPROXY 代理伺服器預設都是沒有使用者名稱和密碼的。但實際上如果需要搭建私有的,是可以支援 HTTP 基礎授權,方式與前面一樣,通過 .netrc 檔案去配置使用者名稱和密碼。另外 GOPROXY 還有兩點特性:

  • 第一:比起使用 VCS 方式直接去克隆,GOPROXY 獲取模組的速度會更快,原因後面會詳細說明。

  • 第二:可以解決模組不能訪問的問題,比如 Golang 域名訪問不了等問題,通過第三方搭建好的代理伺服器即可訪問下載到這些模組。

GOPROXY 使用

GOPROXY 的配置是通過 GOPROXY 環境變數來控制,配置的是代理伺服器URL。代理伺服器 URL 可以配置多個,通過逗號和管道符來進行分割,管道符和逗號的區別後面會舉例講解。

通過固定的字串 off 和 direct 可以代替 URL。off 禁止從任何來源去下載模組,把 GOPROXY 設定為 off 會禁止下載模組,只能使用本地模組,無論從 gitlab、github或其他地方的模組都不能下載。direct 代表直接從VCS上拉取,一般會作為備選方案。

圖中展示了兩個例子:

  • 第一個是 Linux 環境變數的語法,通過 export 來設定環境變數。前面配置了proxy.golang.org,這是 google 官方的 goproxy 的伺服器,逗號之後指定了備選方案 direct。在 GOPROXY 伺服器返回403和410狀態碼時,表示找不到模組。以逗號為分隔指定備選方案時只有當伺服器返回了403或410狀態碼時,go get 會嘗試使用備選方案,這裡是從版本管理平臺上去下載程式碼。

  • 第二個使用了另一種語法配置,go env -w 語法是 GO 自帶的,GO1.13版本開始支援。它是可以跨平臺使用的,通過這種語法,沒有作業系統的差異,在 windows、Linux、max 上面,都可以通過該方式去配置 GO 相關的環境變數。示例中設定成了國內常用的 proxy 的地址:goproxy.cn。這裡使用了管道符指定備選方案,管道符的意義是無論代理伺服器返回了什麼錯誤,即便不是 HTTP 的錯誤,如 GOproxy 伺服器掛了返回500的錯誤,或者網路錯誤。都會嘗試使用備選方案去下載模組。

GOPROXY 實現

GOPROXY 的實現很簡單,官方定義只有五個介面。

URL 中的三個變數意義如下:

  • base 代表是 GOPROXY 伺服器的 URL 地址;

  • module 表示需要需獲取模組的名字;

  • version 是模組的版本。

大小寫編碼問題

在 HTTP 的 URL 定義上是不區分大小寫的,當 module 或 version 出現大寫字母時,在某些系統中可能會出現混淆的問題。為了避免此問題,需要進行大小寫的編碼,把大寫字母轉換成感嘆號加小寫字母的編碼。

  • 第一個介面是獲取所有版本列表;

  • 第二個介面是獲取指定版本的資訊;

  • 第三個介面是獲取指定模組,指定版本的 mod 檔案;

  • 第四個介面是獲取模組的最新版本。這是可選的介面,不提供與實現該介面,GOPROXY 仍然可以正常工作;

  • 最後一個介面是下載模組指定版本的 zip 檔案。

GOPROXY LIST

上圖是 list 介面示例, proxy.golang.org 是代理伺服器的地址, golang.org/x/text 是要獲取的模組名字,@v 為固定的字串,list 是要呼叫的 list 介面。可以看到該介面返回了 text 包的所有版本,圖中 GO 獲取了所有版本後可以通過版本語義推斷出模組的最新版本。

GOPROXY INFO & LATEST

如上圖所示, INFO 介面和 LATEST 介面返回的內容是一樣的。 Version:固定版本字串的版本號, Time 是 fc3339 時間格式的字串,為可選項,代表版本的提交時間。

GOPROXY MOD & ZIP

最後是 MOD 和 ZIP 介面。MOD 介面就是返回指定版本的 mod 檔案,上圖示例中獲取了最新版本的 mod 檔案, text 包只依賴了 tools 模組。ZIP 檔案介面就是獲取模組指定版本的 ZIP 檔案,當它把版本的所有原檔案打包成 ZIP 檔案,go get 最終通過介面去下載的就是這個版本的模組。

前面提到通過 GOPROXY 去獲取原始碼會比通過 VCS 獲取要更快,通過 zip 去下載只會下載當前版本的所有檔案不會包含歷史的版本資訊,如果是通過 VCS 比如 git 去克隆倉庫,就會獲取所有的歷史版本資訊;因此通過 GOPROXY zip 介面獲取檔案的體積會更小,下載也會更快,需要注意的是 GOPROXY 定義了模組 zip 檔案的大小和其所有檔案的未壓縮總限制為 500 MiB,go.mod 檔案和 LICENSE 檔案大小限制為 16 MiB。

module 驗證

Go1.13 版本開始加入模組 SUM 驗證機制,預設所有 go 模組下載後都會驗證其 hash 是否與線上( 預設:sum.golang.org 國內:sum.golang.google.cn)記錄的一致。

驗證的過程可以通過環境變數 GONOSUMDB 和 GOSUMDB 來控制:首先來看 GOSUMDB 的配置,它指定了需要使用的線上資料庫地址。因為預設使用的 sum.golang.org 在國內無法訪問,上圖中配置使用的是 google 搭建的國內映象,還可以配置為 off,代表禁用校驗,即下載模組不進行雜湊值的校驗,徹底拋棄這個過程。使用中我不建議這樣做,可以使用 GONOSUMDB 的環境變數去配置不需要驗證的模組,比如私有模組肯定是不能通過驗證的。GONOSUMDE 是通過字首匹配的方式執行的。圖中配置了 gitlab.com,那麼所有以 gitlab.com 開頭的包都不會進行 GO 的校驗和檢查。

下面來梳理下常見的變數:

  • GONOPROXY,基於字首的匹配方式執行,上圖中指定了gitlab.com,也就是所有 gitlab.com 上的程式碼,不從 GOPROXY 伺服器上去獲取,全部通過傳統 VCS 方式,直接去原始碼伺服器上拉取;

  • GONOSUMDB,可以讓字首匹配上的模組跳過安全性檢查;

  • GOPRIVATE,相當於前面兩個環境變數的集合,配置了 GOPRIVATE 就相當於把前面的兩個環境變數一起配置了;

  • GOVCS ,這是 GO1.16 版本才新增的,其主要作用是指定哪些模組使用哪些 VCS。

又拍雲的業務實踐

私有包的使用

下面介紹一下如何使用私有模組。一般公司內使用較多的是私有化搭建的 gitlab 服務,gitlab 本身是支援響應 go get 的 HTTP 請求。通過 go get 獲取包時,客戶端會傳送 HTTP 請求到 gitlab 伺服器上,伺服器收到請求後會返回響應中包含 meta 標記。該標記會告訴客戶端,模組使用 git 通過 HTTP 協議獲取原始碼。gitlab 預設使用 HTTPS 協議。客戶端收到 gitlab 伺服器響應結果後,能正確的使用 git 去拉取模組的原始碼。模組下載通過後,同樣會有校驗和檢查的過程,可以在 GOPRIVATE 變數加上 gitlab.com,告知 go gitlabc.com 相關的模組都是私有模組跳過校驗和檢查。

在又拍雲內部實踐中,情況有些不同,又拍雲內部所有使用的 HTTP 服務都需要經過 google 的二次驗證。所有發往內部 gitlab 伺服器的請求都會預先檢查是否有 google 授權的 head,如果沒有會被直接攔截掉並返回403錯誤。這樣會導致所有的簡單 HTTP 請求都不能到達 gitlab 伺服器直接被攔截。go 傳送的 HTTP 請求同樣也會被攔截掉,將導致 go 不能正確的獲取模組資訊。這時雖然可以直接通 ssh 協議 clone 伺服器上原始碼,但由於 go get 沒有這些資訊,導致請求失敗。因此下圖中灰線表示的請求實際上是發不出來的。

那麼該如何解決呢?方法是採用額外的 http 服務來處理 go get 的 HTTP 請求。額外 HTTP 服務沒有驗證過程,請求通過後會 go get能正確的獲取到需要的 meta 資訊。meta 中必須指定使用 ssh 協議,因為 gitlab http 服務有二次認證,沒有認證的請求都不能通過,因此只能使用 ssh 協議。許可權認證可以由 SSH 金鑰對完成,進行無感知進行授權。go get 引導 http 服務不會管理授權相關問題,所有的授權處理都交給 gitlab。作為私有模組,如果沒有對應的響應程式,授權認證都交給 gitlab 處理。

go get 請求指引

採用額外的服務去引導 go get 是怎麼做的呢?這需要對模組包的命名進行修改,需要基於 gitlab 命名的規則修改。

gitlab.com/lyp256/pkg

域名 倉庫名

一個完整的模組有幾部分組成,首先是域名 gitlab.com,lyp256 是所有者,pkg 是模組的專案名字。對於單個 gitlab平臺重要的是後面兩段,也就是指定這個模組所有者和專案名,域名肯定是固定的可以忽略。

基於這樣的規則我實現了一個簡單的小服務,來解決 go get http 請求的處理。程式碼如下:

Gitlab CI 實踐

Gitlab CI 時會起一個空的容器,圖中示例使用的是 golang alpine 的映象。這個映象裡除了 golang 沒有其他的東西。我們需要安裝相關依賴和注入 SSH 認證相關內容。script 中定義如下:

第一步: 使用 mikdir -p,在 cache 下建立目錄,這個目錄是我們 CI 機器上的快取掛載的是物理盤上的一塊空間可以保留資料,用來快取 go mod 減少模組下載。

第二步:安裝基礎環境、工具軟體包等。圖中示例安裝了 git 和 g++,g++ 是 go 編譯所需要的依賴,openssh 是 ssh 的工具鏈 git 需要用到。

第三步:處理 SSH 祕鑰。這裡有兩步,信任 gitlab 伺服器祕鑰和匯入認證私鑰。私鑰是通過環境變數 $DEPLOY_SSH_KEY 匯入,只需要保留該環境變數中的內容到對應的祕鑰檔案就可以了。gitlab 伺服器祕鑰使用 ssh-keyscan 來獲取並儲存到 known_hosts 檔案。通過 gitlab SI 的配置把能訪問 git 專案的私鑰放在環境變數 $DEPLOY_SSH_KEY 裡面,把私鑰放在相應的 ssh 私鑰檔案並且授予正確的許可權。

最後還需要配置 GOPRIVATE 變數定義所有 go.holdcloud.com 相關的模組為 PRIVATE 模組不要使用代理和檢驗和檢查。

到此已基本完成所有準備工作,後面的 go test 是正常的 ci 測驗邏輯,可以根據實際情況來寫。

總結

  • GO 在 1.17 版本會刪除對 GOPATH 的支援,建議儘快遷移至 GOMDULE;

  • GO 的校驗和檢查可以感知到程式碼的變化提高安全與可用性,建議不要關閉;

  • 建議保留 vendor ,防止依賴模組被刪除。

相關文章