Day 7:把儲存接上——叢集裡的東西怎麼讀到你的資料
這是什麼、解決什麼問題
你的資料在叢集外面——在 NAS、在物件儲存、在某台機器上。 而 workbench、pipeline、模型服務都在叢集裡面。
Connection 就是中間那份「怎麼連過去」的設定:位址加憑證。
在 OpenShift AI 裡它不是一個 CRD,是一個貼了特定 label 的 Secret:
oc get crd | grep -i connection
# (沒有東西)
知道這件事很重要,因為它決定了你怎麼建、怎麼查、怎麼除錯。
什麼時候你會用到
第一次要讓叢集裡的東西讀到叢集外的資料時。 通常依序發生:
- workbench 要讀訓練資料
- pipeline 要存 checkpoint(pod 會消失,產物不能只留在裡面)
- 模型服務要抓權重——因為權重不進 image
第 3 點通常是逼你走到這一步的那個:模型換一次就重 build 一次 image,很快受不了。
前置條件
- 一個 S3 相容的儲存(AWS S3、MinIO、Ceph RGW 都可以)
- 那個儲存的 endpoint、access key、secret key、bucket 名稱
- 叢集連得到那個位址(離線環境要先確認防火牆)
步驟一:看有哪些連線型別
ODH 內建三種(3.5 和 3.6 都是這三種):
oc get cm -n opendatahub -l opendatahub.io/connection-type=true
# oci-v1 ← OCI registry
# s3 ← S3 相容物件儲存
# uri-v1 ← 單一 URI
s3 這型要填哪些欄位,定義就在那個 ConfigMap 裡:
oc get cm s3 -n opendatahub -o jsonpath='{.data.fields}' \
| jq -r '.[] | "\(.envVar)\trequired=\(.required)"'
AWS_ACCESS_KEY_ID required=true
AWS_SECRET_ACCESS_KEY required=true
AWS_S3_ENDPOINT required=true
AWS_DEFAULT_REGION required=false
AWS_S3_BUCKET required=false
三個必填。 這比翻文件快,而且不會過期——定義就在你的叢集上。
步驟二:建一個
apiVersion: v1
kind: Secret
metadata:
name: minio-connection
namespace: llm-serve-demo
labels:
opendatahub.io/dashboard: "true" # ← 沒有這個,dashboard 看不到
opendatahub.io/managed: "true"
annotations:
opendatahub.io/connection-type-ref: s3 # ← 指向上面那個型別
openshift.io/display-name: "MinIO(模型與 pipeline 產物)"
type: Opaque
stringData:
AWS_ACCESS_KEY_ID: <key>
AWS_SECRET_ACCESS_KEY: <secret>
AWS_S3_ENDPOINT: http://minio.example:9000
AWS_DEFAULT_REGION: us-east-1
AWS_S3_BUCKET: models
也可以在 dashboard 上按「Create connection」用表單填,結果一樣。
驗證這一步:
oc get secret -n <ns> -l opendatahub.io/dashboard=true
# minio-connection
⚠️ 這只證明「建出來了」,不證明「能用」。 真正的驗證在步驟四。
步驟三:給不同的東西用
同一份 Connection,三種消費方式不一樣:
workbench:整包注入環境變數
spec:
template:
spec:
containers:
- name: my-workbench
envFrom:
- secretRef: { name: minio-connection } # ← 一行搞定
進去之後直接讀環境變數:
import os, boto3
s3 = boto3.client("s3",
endpoint_url=os.environ["AWS_S3_ENDPOINT"],
aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"])
pipeline:用 kfp 的 helper 注入
from kfp import kubernetes
kubernetes.use_secret_as_env(
task, secret_name="minio-connection",
secret_key_to_env={"AWS_ACCESS_KEY_ID": "AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY": "AWS_SECRET_ACCESS_KEY"})
憑證不要進 image。 image 會進 registry、會被複製、會被掃描工具展開—— 任何進了 image layer 的東西都要當成已經公開。
模型服務(KServe):走 annotation,而且要掛在 ServiceAccount 上
⚠️ KServe 用的是另一組慣例——不是 label,是貼在 Secret 上的 annotation:
apiVersion: v1
kind: Secret
metadata:
name: minio-s3
namespace: llm-serve-demo
annotations:
serving.kserve.io/s3-endpoint: minio.example:9000 # ← 不含 http://
serving.kserve.io/s3-usehttps: "0"
serving.kserve.io/s3-region: us-east-1
type: Opaque
stringData:
AWS_ACCESS_KEY_ID: <key>
AWS_SECRET_ACCESS_KEY: <secret>
但光有這個 Secret 沒用。 KServe 不會去掃 namespace 裡的 Secret,
它是從 InferenceService 用的那個 ServiceAccount 上去找:
apiVersion: v1
kind: ServiceAccount
metadata:
name: llm-sa
secrets:
- name: minio-s3 # ← 這一行才是接線
# InferenceService
spec:
predictor:
serviceAccountName: llm-sa # ← 指到上面那個 SA
model:
storageUri: s3://models/sklearn-demo/ # ← 要抓的路徑
這條鏈少一環就抓不到權重,而錯誤訊息只會說連不到 endpoint, 不會告訴你是 SA 沒掛 Secret。串起來是:
InferenceService → serviceAccountName → ServiceAccount.secrets → 有 annotation 的 Secret
所以同一個叢集上,S3 設定有三套並存的慣例: dashboard 讀 label、KServe 讀 annotation + ServiceAccount、你的程式讀 data key。
我自己的 lab 最後是開兩份 Secret:
minio-connection(給 dashboard 和 workbench, 五個欄位齊全)、minio-s3(給 KServe,只有 access key 兩個欄位 + annotation; Day 9 的 pipeline server 也是指到這一份)。 一份 Secret 要同時滿足三套當然做得到——貼齊 label、annotation、五個 key,再掛上 SA—— 但拆兩份的好處是給 KServe 的那份只帶它需要的兩個欄位。 代價是盤點時要記得有兩份,這正是 Day 6 講的 「Connections 頁不是憑證的唯一來源」。
步驟四:⭐ 確認它真的能用
這一步不能省。 dashboard 顯示一份 connection 存在, 只代表格式正確——label 有、必填欄位填了。 它不會拿那組憑證去連一次。
我自己就踩過:憑證填錯了,dashboard 上一切正常,
直到在 workbench 裡真的去用才拿到 SignatureDoesNotMatch。
最快的驗證方式——在 workbench 裡跑三行:
import os, boto3
s3 = boto3.client("s3", endpoint_url=os.environ["AWS_S3_ENDPOINT"],
aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"])
print([o["Key"] for o in s3.list_objects_v2(Bucket=os.environ["AWS_S3_BUCKET"]).get("Contents", [])][:5])
列得出東西才算設好。
沒有 workbench 的話,用一個一次性的 pod 也行。
⚠️ oc run 沒有 --env-from(只有 --env,要一個一個列,
等於把金鑰打在指令列上留進 shell 紀錄)。整包注入要用 --overrides:
oc run s3check --rm -i --restart=Never -n <ns> --image=quay.io/minio/mc \
--overrides='{"spec":{"containers":[{"name":"s3check","image":"quay.io/minio/mc",
"command":["sh","-c","mc alias set t $AWS_S3_ENDPOINT $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY >/dev/null && mc ls t/$AWS_S3_BUCKET"],
"envFrom":[{"secretRef":{"name":"minio-connection"}}]}]}}'
跑起來會直接把 bucket 內容列出來:
[2026-08-30 14:34:03 UTC] 2.8KiB STANDARD registry.json
[2026-09-05 09:20:19 UTC] 0B llm/
[2026-09-05 09:20:19 UTC] 0B sklearn-demo/
pod "s3check" deleted
憑證從頭到尾只存在於 Secret 和 pod 裡,沒有進過你的指令列。
怎麼確認做對了
| 檢查 | 指令 | |
|---|---|---|
| 1 | Secret 建出來了 | oc get secret -l opendatahub.io/dashboard=true |
| 2 | dashboard 看得到 | 打開 Connections 分頁 |
| 3 | 憑證真的能連 | 上面那三行 python |
| 4 | 誰在用它 | Connections 頁的 Connected resources 欄 |
第 4 項容易被忽略但很有用:如果那一欄是 --,代表沒有東西在用這份 connection。
而如果同時你的 pipeline 又在跑,那就表示它用的是另一份你在 UI 上看不到的憑證。
常見問題
Q:pipeline 明明在用 S3,dashboard 上卻沒有 connection。
A:那份 Secret 沒貼 opendatahub.io/dashboard label。
執行期讀的是 Secret 的內容,dashboard 讀的是 label——兩邊看的是同一個物件的不同部位。
盤點的時候要看實際掛進 pod 的:
oc get pods -n <ns> -o json | jq -r '
.items[].spec.containers[].envFrom[]?.secretRef.name,
.items[].spec.volumes[]?.secret.secretName
| select(. != null)' | sort -u
⚠️ 這行也抓不到 KServe 走 ServiceAccount 那條路的憑證—— 那份 Secret 不會出現在 pod spec 裡。SA 那邊要另外查:
oc get sa -n <ns> -o json | jq -r '.items[] | select(.secrets) |
"\(.metadata.name) -> \(.secrets[].name)"'
Q:SignatureDoesNotMatch。
A:帳號密碼錯。注意 AWS_S3_ENDPOINT 有沒有多寫或少寫 http://——
boto3 要完整 URL,KServe 的 annotation 不要 scheme。
Q:改了 Secret,但服務還是用舊的。
A:環境變數是 pod 啟動時注入的,改 Secret 不會自動生效,要重建 pod:
oc delete pod -l <你的 selector>。
Q:可以用 PVC 取代嗎? A:可以,資料很大或本來就在企業儲存上時更合適。 但 PVC 掛進來的東西不受平台版本控制——別人改了那個檔案, 你的訓練結果就變了,而且沒有紀錄。
你們的訓練資料和模型放哪?S3、PVC,還是 NFS?
🧪 這篇的實驗環境與 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-* 那一套)。
指令的邏輯可以照用,字串要自己對一次。
留言與指正
我特別想知道:你們的訓練資料和模型放哪?S3、PVC,還是 NFS?