離線鏡像:operator 宣告 2 顆,實際跑起來要 76 顆
1. 問題長什麼樣
企業內網裝東西的標準流程:跟廠商要一份 image 清單 → 鏡進內網 registry → 設定叢集去那裡拉 → 安裝。
聽起來很直接。問題出在第一步:那份清單是怎麼來的。
2. ⭐ 我在叢集上數的
Operator 的 CSV 有一個 relatedImages 欄位,那是官方宣告的相依 image 清單:
oc get csv opendatahub-operator.v3.5.0 -n openshift-operators \
-o jsonpath='{.spec.relatedImages}' | jq -r '.[].name'
odh-kube-auth-proxy-image
odh-kube-rbac-proxy-image
兩顆。
現在數實際跑起來需要幾顆:
oc get pods -n opendatahub --field-selector=status.phase=Running \
-o jsonpath='{range .items[*].status.containerStatuses[*]}{.image}{"\n"}{end}' \
| sed 's|@sha256.*||' | sort -u | wc -l
22
光是 opendatahub 這一個 namespace 就 22 顆。
整個叢集(含 OCP 自己的):
oc get pods -A --field-selector=status.phase=Running \
-o jsonpath='{range .items[*].status.containerStatuses[*]}{.image}{"\n"}{end}' \
| sed 's|@sha256.*||' | sort -u | wc -l
76
來源分佈:
37 quay.io
31 registry.redhat.io
2 docker.io
5 私有 registry(我自己的)
1 registry.access.redhat.com
68 顆來自外網。
3. 為什麼 relatedImages 靠不住
它宣告的是「operator 這個程式自己需要的」,不是「這個平台跑起來需要的」。
差在這幾類:
| 類別 | 例子 | 為什麼不在清單上 |
|---|---|---|
| 元件 image | dashboard、kserve controller、notebook controller、model controller | 那是 operator 部署出來的東西,不是它自己 |
| 注入式 sidecar | kube-rbac-proxy、storage-initializer |
執行期才被注入,靜態清單看不到 |
| runtime image | notebook 的 Jupyter image、serving runtime | 使用者選的,數量隨你開幾種而變 |
| 你自己的 | 你的模型服務 image | 廠商當然不知道 |
第二類最陰:storage-initializer 不在任何元件清單上,
但它是模型上線的必要條件——沒有它,InferenceService 起不來。
4. 怎麼拿到真的清單
唯一可靠的方法:在一個能連外網的環境把它跑起來,然後反查。
# 所有 Running pod 實際用的 image(含 sidecar 與 init container)
oc get pods -A --field-selector=status.phase=Running -o jsonpath='
{range .items[*].status.containerStatuses[*]}{.imageID}{"\n"}{end}
{range .items[*].status.initContainerStatuses[*]}{.imageID}{"\n"}{end}' \
| sed 's|@.*||' | sort -u
⚠️ 用 .imageID 不要用 .image——前者是實際拉到的 digest,
後者可能是 tag。離線環境一律用 digest,tag 會漂移。
而且要把功能都跑過一遍再數:只裝不用的話, 很多 image(notebook runtime、pipeline 的 launcher、driver)根本不會被拉下來。
5. ⚠️ 清單裡可能有不存在的 image
這個我今天才踩到。ODH 3.5 內建的 ImageStream 裡有這種東西:
jupyter-rocm-minimal:3.6
→ quay.io/opendatahub/odh-workbench-jupyter-minimal-rocm-py312-ubi9:3.6_ea1-v1.47
→ not found
3.5 的 tag 匯入正常,3.6 的 tag 在 quay.io 上根本不存在。
在連得到外網的環境,這只是 DSC 上一行警告,沒人在意。 在離線環境,你會照著一份含有不存在 image 的清單去鏡像—— 然後在內網花時間查為什麼少東西。
所以鏡像之前要先驗證清單本身:每一顆都 skopeo inspect 得到,才算數。
while read -r img; do
skopeo inspect "docker://$img" >/dev/null 2>&1 || echo "✗ $img"
done < image-list.txt
5.5 ⭐ 我後來真的踩到了,而且是最糟的形式
上面那條「清單裡可能有不存在的 image」,我原以為只是理論上的風險。 幾天後我自己撞到了,而且比想像的更陰。
我要建一個 workbench,pod 卡在 ImagePullBackOff。錯誤訊息:
Failed to pull image "quay.io/opendatahub/odh-workbench-jupyter-datascience-cpu-py312-ubi9@sha256:8ab465…":
(Mirrors also failed: [100.117.49.79:8088/odh/…@sha256:8ab465…:
reading manifest sha256:8ab465… : artifact not found])
注意:Harbor 裡「有」那個 repo。 IDMS 也「有」設而且生效。 但拉不到。
去比對 digest:
| digest | |
|---|---|
imagestream 的 3.5 tag 要 |
sha256:8ab465… |
imagestream 的 3.6 tag 要 |
sha256:6f0e62… |
| Harbor 裡實際鏡的 | sha256:6f0e62… ← 那是 3.6 |
我鏡的是 3.6,而 pod 要的是 3.5。
完整因果鏈:
① 我鏡像那天,抓的是 3.6 的 digest
② 上游後來把 3.6 的 tag 撤掉了(3.6_ea1-v1.47 → not found)
③ imagestream 現在解析到 3.5
④ IDMS 把來源改導向我的 Harbor
⑤ Harbor 裡只有 3.6 那顆 → manifest not found
⑥ ImagePullBackOff
為什麼這種失敗最難查
- Harbor 裡有那個 repo,名字完全正確 ✅
- IDMS 設了而且生效 ✅
oc get pod只說ImagePullBackOff,不會說「你鏡的是另一個 digest」
而且它會誤導你往「網路慢」的方向想。我一開始也以為是慢—— 分辨方法是直接去問你的 registry:
skopeo inspect --tls-verify=false \
docker://<你的registry>/<repo>@sha256:<pod要的那個digest>
回 artifact not found 就是沒鏡到,不是慢。
慢的話你會看到 Pulling 持續進行而沒有 Failed。
補鏡的成本(實測)
skopeo copy \
docker://quay.io/opendatahub/…@sha256:8ab465… ← 來源給 digest
docker://<你的registry>/…:3.5 ← 目的地給 tag
17 分 26 秒,1,698 MB。一顆。
⚠️ 來源一定要給 digest——這樣鏡過去的那顆 digest 才會跟來源一致, IDMS 才對得上。如果來源也用 tag,你無法保證鏡到的是同一顆—— 而那正是這個坑的成因。
兩個帶得走的結論
① 上游會撤 tag。 你鏡的時候存在的東西,幾天後可能就不在了。 鏡像清單需要定期重驗,不是鏡完就結束。
② 正式環境的時間成本要先算。 一顆 1.7 GB 的 image 要 17 分鐘。幾十顆就是好幾小時—— 而且往往是在你發現少東西的當下才開始傳,也就是最不想等的時候。
6. IDMS ≠ insecureRegistries
拿到 image 之後,要讓叢集去內網拉。這裡有一個常見的混淆:
| 做什麼 | |
|---|---|
| IDMS(ImageDigestMirrorSet) | 改寫拉取來源:看到 quay.io/x 就去 my-registry/x 拿 |
| insecureRegistries | 允許用 HTTP 或自簽憑證連某個 registry |
兩件事完全獨立。
只設 insecureRegistries:叢集還是會去 quay.io 拉,只是允許你的私有 registry 不用 TLS。
離線環境下 quay.io 連不到,一樣失敗。
只設 IDMS:來源改對了,但如果你的私有 registry 是 HTTP 或自簽憑證, 會卡在 TLS 驗證。
⚠️ IDMS 用的是 digest 比對。 如果你的 pod spec 寫的是 tag 不是 digest, IDMS 不會生效——要用 ITMS(ImageTagMirrorSet)。這是另一個常見的「設了但沒作用」。
7. 關鍵指標
| 「跑完了」 | ⭐「做對了」 | |
|---|---|---|
| 離線鏡像 | 內網 registry 裡有東西、安裝成功 | 斷網之後,把所有功能再跑一次都不失敗 |
安裝成功不代表鏡完整。 很多 image 是「用到某個功能時才拉」的—— 你不建 workbench,就不會發現 notebook image 沒鏡到。
驗收方式:斷網,然後把驗收清單從頭走一遍。
8. 給驗收的三個要求
- 要 digest 清單,不要 tag 清單(
name@sha256:...) - 要求清單來自「跑起來的環境反查」,不是 operator bundle 的
relatedImages——問一句「這份清單怎麼產的」就知道 - 明確問:sidecar 與 init container 有沒有涵蓋
——
kube-rbac-proxy、storage-initializer這兩顆特別容易漏
而最有效的一句是:
「請在斷網的環境,從 dashboard 建一個 workbench 並開啟它。」
那一個動作會同時驗到:元件 image、notebook runtime image、 sidecar、以及 IDMS 有沒有真的生效。
你們的離線鏡像清單是怎麼來的?廠商給的,還是自己從跑起來的環境反查的?
🧪 這篇的實驗環境與 lab 檔案(最後更新 2026-08-30)
叢集
- CRC 2.63.0 · OpenShift 4.22.7 · Kubernetes v1.35.6
- 單節點:13 vCPU / 40 GiB RAM / 120 GB disk
- 宿主:Framework Laptop 16(Ryzen AI 7 350 · 8C/16T · 64 GB RAM · 1 TB NVMe · RTX 5070 顯卡模組)
- ⚠️ 叢集內看不到 GPU(CRC 是 VM,RTX 5070 未 passthrough)
Operator
opendatahub-operator.v3.5.0← 即 RHOAI 3.x 的上游開源版cert-manager-operator.v1.20.0(3.x 的必要相依;2.x 不需要)openshift-pipelines-operator-rh.v1.23.2、openshift-gitops-operator.v1.21.3
DataScienceCluster 開啟的元件
kserve、aipipelines、dashboard、workbenches、modelregistry、kueue(Unmanaged)- 其餘(
ray/trustyai/feast/aigateway…)為Removed
叢集外的依賴(跑在宿主的 podman 上,crc start 不會帶起來)
- Harbor v2.15.2(私有 registry)· MinIO(S3)
模型端
- Python 3.12.13 · PyTorch 2.11.0+cu128 · FastAPI + uvicorn · prometheus-client
lab 檔案
- YAML/Containerfile/腳本:github.com/ryanGTR/openshift-ai-30days
(含 Day 對照表;主機名是佔位符,跑
set-lab-host.sh換成你自己的) - 服務的那個模型:llm-from-scratch(從零手刻的小 GPT)
⚠️ ODH ≠ RHOAI:元件同源,但 namespace 與部分名稱不同
(我這裡是 opendatahub,商用版是 redhat-ods-* 那一套)。
指令的邏輯可以照用,字串要自己對一次。
留言與指正
我特別想知道:你們的離線鏡像清單是怎麼來的?廠商給的,還是自己從跑起來的環境反查的?