問題

這套 CI 模板從來沒在真的 runner 上跑過——gitlab.com 的免費額度用完了。把 GitLab runner 用官方 Helm chart 裝進 Kubernetes(Kubernetes executor)之後,又連續卡了四次才第一次全綠。全綠之前,還順手挖出兩個早就存在、但因為 CI 從沒跑過而沒人發現的缺陷。

四個讓 job 跑不起來或跑錯地方的坑

症狀原因修法
所有 job 7 秒內紅,ImagePullBackOff,no match for platform in manifestApple 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 detectedrequest_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 第一次上線時,照這個順序檢查:

  1. runner pod 的 log 沒有 WARNING
  2. job log 裡的 runner 欄位是自己那台,不是 saas-* 開頭的共用 runner
  3. 節點架構跟 helper image 的架構一致

寫 CI 模板時,預設值一律放在入口檔的 pipeline 層;隱藏模板(.xxx)的 job 層只放「衍生專案不會想改」的值。

最後一條是心態上的:「CI 沒跑過」本身就是一種假綠。 本機檢查過不代表 CI 會過。runner 一裝好,先讓既有 pipeline 原封不動跑一次,看它挖出什麼,再開始改東西。

相關

  • 相關主題:同一輪把 CI 的 image build 從 dind 換成 rootless BuildKit
  • 相關主題:gitleaks 本機用 --no-git 與 CI 掃全歷史的差異