GitHub Actions 自託管 Runner 優化——和運營商鬥智鬥勇
在之前的文章 在 Kubernetes 上運行 GitHub Actions Self-hosted Runner[1] 和 關於從 GitHub Actions Self-Hosted Runner 中偷 Secrets/Credentials 的一些安全研究 [2] 中,我們知道已經可以通過各種手段讓 Self Hosted Runner 在我們內部設施上跑起來,加上一個設計合理的專線 + 路由表,基本已經可以流暢地接受 + 處理 GitHub 上使用了 self-hosted
的 CI 任務了,由於語法和 GitHub Actions 官方語法一致,基本使用者都會有類似「太順滑了,幾乎沒有任何的體感差異」,「有效減少了高峯用官方 Runner 排隊的問題」,「成功獲得了 ARM64 環境」,「性能和 RAM 直接翻倍」等等好評。
但是使用一個非海外的基礎設施我們很快就會看到一些地理位置上的缺陷,比如…
爲什麼
actions/setup-go@v2
可以跑這麼久?
那.. 這.. 用的國內三大運營商,這不是很正常麼?(雖然這個包本身並不大,才 120M 左右)
GOPROXY 優化
爲了優化 Golang 做 go mod tidy
等操作,在 Runner 的鏡像中已經顯式地指定好了 GOPROXY ,Dockerfile 類似:
ENV GOPROXY "http://goproxy.nova.moe,https://proxy.golang.org,direct"
這樣在用戶使用 Golang 程序的時候就可以直接走內部 GOPROXY 來加速了,但是這樣依然不夠,因爲要給 Runner 安裝 Go ,還需要使用 actions/setup-go@v2
來安裝。
這個時候,有些小機靈鬼就會說了:「那你把 Go 打在 Image 裏面不就好了麼?」
確實可以,但是這樣對於多版本管理是很不利的,難道你像下圖一樣維護一堆類似 n0vad3v/github-runner:go1130
,n0vad3v/github-runner:go1160
的鏡像,然後手動控制這些鏡像的 Container 數量和 Tag,然後讓用戶去用類 Jenkins 的語法,去手動指定 runs-on: [self-hosted,X64,go1130]
?
所以爲了解決這個問題,我們還是得讓用戶自己去用一個 Step 來安裝 Go,畢竟環境的模塊化組裝(以及 Matrix 的使用)是 GitHub Actions 的一大優勢,不然一堆 if-else 和 Jenkins 有啥區別,更何況現在 Runner 安裝了一次 Go 之後就會緩存下來(除非你啓動的時候指定了 --ephemeral
),在下一次遇到同版本的時候會直接使用緩存。
actions/setup-go
優化
在 Runner 上安裝 Golang,大家一般會使用 actions/setup-go@v2
,用法也很簡單,如下:
- uses: actions/setup-go@v2
with:
go-version: '1.16'
爲了瞭解這個 Action 是如何工作的,在不看代碼,只看代碼結構的角度,我們從 https://github.com/actions/setup-go/blob/main/__tests__/data/versions-manifest.json
文件中可以發現它 ” 背後的數據地址 “ 類似:
https://github.com/actions/go-versions/releases/download/1.12.17-20200616.21/go-1.12.17-darwin-x64.tar.gz
反推得到實際的數據倉庫爲:https://github.com/actions/go-versions/ 的 https://github.com/actions/go-versions/blob/main/versions-manifest.json
, 數據格式類似如下:
[
{
"version": "1.17.5",
"stable": true,
"release_url": "https://github.com/actions/go-versions/releases/tag/1.17.5-1559554870",
"files": [
{
"filename": "go-1.17.5-darwin-x64.tar.gz",
"arch": "x64",
"platform": "darwin",
"download_url": "https://github.com/actions/go-versions/releases/download/1.17.5-1559554870/go-1.17.5-darwin-x64.tar.gz"
},
{
"filename": "go-1.17.5-linux-x64.tar.gz",
"arch": "x64",
"platform": "linux",
"download_url": "https://github.com/actions/go-versions/releases/download/1.17.5-1559554870/go-1.17.5-linux-x64.tar.gz"
},
{
"filename": "go-1.17.5-win32-x64.zip",
"arch": "x64",
"platform": "win32",
"download_url": "https://github.com/actions/go-versions/releases/download/1.17.5-1559554870/go-1.17.5-win32-x64.zip"
}
]
},
]
後來發現其實 README 中有寫:It will first check the local cache for a version match. If version is not found locally, It will pull it from
main
branch of go-versions[3]
在確認了實際下載的包的地址之後我們就可以反推 setup-go
中是如何使用這個地址的了,通過一波 rg
,我們在 src/installer.ts
的 143 行發現:
const releases = await tc.getManifestFromRepo(
'actions',
'go-versions',
auth,
'main'
);
所以現在緩存的思路就很清晰了:
-
下載 https://github.com/actions/go-versions/blob/main/versions-manifest.json 中的包到內網
-
Fork + 修改一份 https://github.com/actions/go-versions 倉庫,把 download_url 中的內容換爲內網地址
-
Fork + 修改一份 setup-go,把它獲取 Manifest 的地址指向 Fork 後的 go-versions 倉庫
-
在 Runner 中調用 Fork 後的 setup-go
下載包到內網
非常容易,Python 可以這麼寫,只要指定一下 HOST_URL
爲內網下載地址,STORE_PATH
爲實際存儲地址,GOLANG_VERSION_LIST
中填上想要緩存的 Golang 版本即可,保存爲一個 download.py
,運行後等着就好:
import requests
from urllib.parse import urlparse
import os
import json
## ENV
HOST_URL = "http://download.nova.moe/download/github-actions/golang/"
STORE_PATH = "/path/to/download/github-actions/golang/"
GOLANG_VERSION_LIST = ['go-1.16','go-1.17','go-1.13']
## END ENV
def process_each_package(package_filename, package_url):
package_path = STORE_PATH + package_filename
if not os.path.isfile(package_path):
print("Downloading: " + package_url)
r = requests.get(package_url)
with open(package_path, 'wb') as f:
f.write(r.content)
return package_path
if __name__ == '__main__':
go_versions_url = 'https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json'
r = requests.get(go_versions_url).json()
return_list = []
golang_package_list = []
for item in r:
package_url = item['files'][1]['download_url']
for version in GOLANG_VERSION_LIST:
if version in package_url:
golang_package_list.append(package_url)
a = urlparse(package_url)
package_filename = os.path.basename(a.path)
process_each_package(package_filename, package_url)
item['files'][1]['download_url'] = HOST_URL + package_filename
return_list.append(item)
with open('versions-manifest.json', 'w') as f:
f.write(json.dumps(return_list, indent=2))
運行結束後所有的 tar.gz 包都會保存到 STORE_PATH
中,同時運行目錄下會生成一個下載地址已經替換爲內網地址的 versions-manifest.json
。
修改 go-versions 和 setup-go
Fork 這兩個倉庫後,將 Fork 後的 go-versions 倉庫下的 versions-manifest.json
替換爲剛剛已經生成好的版本(這個操作過於簡單建議直接用網頁修改,避免浪費拉倉庫使用的本地帶寬)。
由於 setup-go 需要編譯,爲了省事考慮(反正我們只修改兩個變量),直接將 Fork 的 setup-go 中 dist/index.js
的 5037 行
const releases = yield tc.getManifestFromRepo('actions', 'go-versions', auth, "main");
改爲 fork 後的地址,比如:
const releases = yield tc.getManifestFromRepo('n0vad3v', 'go-versions-forked', auth, "master");
修改 Runner
在上面的操作完成之後,我們只需要使用 fork 後的 setup-go ,即可使用到內網的下載速度了,用法類似:
- uses: n0vad3v/setup-go-forked@master
with:
go-version: '1.16'
看看效果?
快到模糊!
小結
由於緩存 Golang 的包的操作看上去是一個 One shot 的操作,基本沒有短時間內持續更新的需求,暫時也就沒有考慮自動化之類的事情,在有了內網緩存之後,整體的 Runner 運行效率一下子就提升了起來,使用體驗又愉快了不少。
這是關於 GitHub Actions Self-hosted Runner 優化的第一篇文章,後續可能還會有一些相關的有趣的分享,同時我也在考慮把相關的組件(比如 關於從 GitHub Actions Self-Hosted Runner 中偷 Secrets/Credentials 的一些安全研究 [4] 中提到的那個假 KMS,以及可用的 Runner 的 Dockerfile)開源出來,不過這些都還沒想好,有興趣的同學可以期待一下~
引用鏈接
[1]
在 Kubernetes 上運行 GitHub Actions Self-hosted Runner: https://nova.moe/run-self-hosted-github-action-runners-on-kubernetes/
[2]
關於從 GitHub Actions Self-Hosted Runner 中偷 Secrets/Credentials 的一些安全研究: https://nova.moe/steal-credentials-from-ci-agents/
[3]
go-versions: https://github.com/actions/go-versions/blob/main/versions-manifest.json
[4]
關於從 GitHub Actions Self-Hosted Runner 中偷 Secrets/Credentials 的一些安全研究: https://nova.moe/steal-credentials-from-ci-agents/
原文鏈接:https://nova.moe/self-hosted-runner-golang-cache/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/pS4RPUHHMscOCrdd5KVf2w