問題
這套 CI 模板從來沒在真的 runner 上跑過——gitlab.com 的免費額度用完了。把 GitLab runner 用官方 Helm chart 裝進 Kubernetes(Kubernetes executor)之後,又連續卡了四次才第一次全綠。全綠之前,還順手挖出兩個早就存在、但因為 CI 從沒跑過而沒人發現的缺陷。
四個讓 job 跑不起來或跑錯地方的坑
| 症狀 | 原因 | 修法 |
|---|---|---|
所有 job 7 秒內紅,ImagePullBackOff,no match for platform in manifest | Apple Silicon 上的 kind 節點是 arm64,runner 預設抓 x86_64 的 helper image | [runners.kubernetes.node_selector] 寫 "kubernetes.io/arch" = "arm64",runner 會依此改挑 arm64 helper |
| runner 顯示 online,job 卻不是它跑的 | 每月 1 日額度重置,gitlab.com 共用 runner 又開始接 job,搶在自建 runner 前面 | 群組設定 shared_runners_setting=disabled_and_unoverridable,底下專案一併強制關閉 |
runner 啟動時警告 Long polling issues detected | request_concurrency=1,兩個 job 同時排隊時會被長輪詢卡住 | 設成跟 concurrent 一樣(這裡是 2),改完警告消失 |
| 模板自測時變數無效,跑去找衍生專案才有的路徑 | GitLab CI 的 job 層 variables 優先於 pipeline 層;模板在 job 層給了預設值,自測在 pipeline 層的覆寫被蓋掉 | 預設值只放在給衍生專案 include 的入口檔 pipeline 層,job 層不給 |
每一條對應的官方說法:
- helper image 架構:Kubernetes executor 文件寫明預設作業系統與架構是
linux/amd64,runner 會讀node_selector裡的kubernetes.io/os、kubernetes.io/arch來決定 helper image 的 OS 與架構;affinity之類的設定只影響排程,不影響這個選擇(Kubernetes executor)。 - 共用 runner:Groups API 的
disabled_and_unoverridable是「Disables instance runners for all projects and subgroups in this group, and prevents subgroups from overriding this setting」(Groups API)。 - long polling:
request_concurrency的定義是限制向 GitLab 要新 job 的並行請求數,預設1;Long polling issues detected這個警告會出現在 runner log,並附上針對情況的建議(Advanced configuration)。 - 變數優先序:GitLab 的 CI/CD 變數優先序裡,job 內的
variables排在.gitlab-ci.yml頂層預設variables之上(CI/CD variable precedence)。
CI 第一次真的跑,挖出的兩筆舊帳
這兩件不是 runner 的問題,而是 CI 從來沒跑過才一直沒浮現:
- chart 的
values.yaml有一整段重複的 key。 yamllint 的key-duplicates規則抓到的。YAML 遇到重複 key 會讓後者靜默蓋掉前者,所以平常完全看不出來。照理說本機的完整驗證(含 yamllint)也該抓到,為什麼沒人報,我沒有查清楚——推測是加進重複段落那次之後沒跑完整驗證(未查證)。 - git 歷史裡有 3 筆已刪除的佔位憑證。 CI 用 gitleaks 掃全歷史才抓到,本機只掃工作目錄所以一直是綠的。
Runner token 怎麼處理
用 GitLab API 的 POST /user/runners(runner_type=group_type)直接建 group runner。官方教學說明,回應裡的 token 只會出現這一次,要存到安全的地方或秘密管理系統(Automate runner creation and registration)。這個 token 是 runner authentication token,前綴是 glrt-(GitLab token overview)。
我的做法是讓程式直接把 API 回應裡的 token 寫進 repo 以外、權限只有自己可讀的檔案,不讓它出現在畫面上。原則是三個「不」:
- 不經過螢幕輸出
- 不寫進 Helm values 檔
- 不出現在指令列參數(建 Kubernetes Secret 時用暫存檔+
--from-file,而不是--from-literal)
為什麼這樣解
架構用 node_selector,不直接寫死 helper_image。 node_selector 同時保證 job pod 排到對的節點,helper 也跟著自動挑對。寫死 helper image 的話,之後升級 runner 版本時要記得一起改,遲早會忘。
關掉共用 runner,而不是加 tags。 模板裡所有 job 都沒有 tags:,加 tags 要改每個衍生專案。關掉共用 runner 之後,CI 的行為不再隨額度變化;Pod Security 這類只在自建 runner 上才成立的設定,也才驗得到。
job 層不給預設。 同名變數 job 層一定贏,模板自己就沒辦法在自測時覆寫。預設值放 pipeline 層,衍生專案 include 時照樣拿得到。
怎麼預防再次發生
自建 runner 第一次上線時,照這個順序檢查:
- runner pod 的 log 沒有 WARNING
- job log 裡的 runner 欄位是自己那台,不是
saas-*開頭的共用 runner - 節點架構跟 helper image 的架構一致
寫 CI 模板時,預設值一律放在入口檔的 pipeline 層;隱藏模板(.xxx)的 job 層只放「衍生專案不會想改」的值。
最後一條是心態上的:「CI 沒跑過」本身就是一種假綠。 本機檢查過不代表 CI 會過。runner 一裝好,先讓既有 pipeline 原封不動跑一次,看它挖出什麼,再開始改東西。
相關
- 相關主題:同一輪把 CI 的 image build 從 dind 換成 rootless BuildKit
- 相關主題:gitleaks 本機用
--no-git與 CI 掃全歷史的差異