[應用][storage] rancher / local-path-provisioner === ###### tags: `K8s / app` ###### tags: `Kubernetes`, `k8s`, `StorageClass`, `app`, `rancher`, `local-path-provisioner`, `NFD`, `pv`, `pvc`, `volume`, `Retain`, `Delete` <br> [TOC] <br> ## Best practice ### helm install ```bash git clone https://github.com/rancher/local-path-provisioner.git cd local-path-provisioner helm install local-path-storage \ --create-namespace \ --namespace local-path-storage \ ./deploy/chart/local-path-provisioner/ \ --set storageClass.provisionerName=rancher.io/local-path \ --set storageClass.defaultClass=true \ --set storageClass.name=default-local \ --set storageClass.reclaimPolicy=Delete \ --set sharedFileSystemPath="/mnt/k8s-local-path" \ --set-json 'nodePathMap=[]' \ --set-string configmap.teardown=$'#!/bin/sh\nset -eu\necho "[TJ][debug] VOL_DIR=$VOL_DIR"\n# 檢查路徑中是否包含 retain 標記\ncase "$VOL_DIR" in\n */ns-*-retain/*)\n # e.g. VOL_DIR=/mnt/k8s-local-path/ns-tj-retain/tj-data\n echo "[Retain] Keeping volume data at $VOL_DIR"\n exit 0\n ;;\n *)\n # e.g. VOL_DIR=/mnt/k8s-local-path/ns-tj-delete/tj-data\n echo "[Delete] Removing volume data at $VOL_DIR"\n rm -rf "$VOL_DIR"\n ;;\nesac\n#rm -rf "$VOL_DIR"' ``` ### `default-local-retain.yaml` ```yaml= # Usage: # $ kubectl apply -f default-local-retain.yaml # $ kubectl get sc -o yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: default-local-retain annotations: storageclass.kubernetes.io/is-default-class: "false" # 先不要設成預設 provisioner: rancher.io/local-path parameters: # full-path: /mnt/k8s-local-path/ns-{{ .PVC.Namespace }}-retain/{{ .PVC.Name }} nodePath: /mnt/k8s-local-path pathPattern: "ns-{{ .PVC.Namespace }}-retain/{{ .PVC.Name }}" allowVolumeExpansion: true reclaimPolicy: Delete volumeBindingMode: WaitForFirstConsumer ``` ### `default-local-delete.yaml` ```yaml= # Usage: # $ kubectl apply -f default-local-delete.yaml # $ kubectl get sc -o yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: default-local-delete annotations: storageclass.kubernetes.io/is-default-class: "false" # 先不要設成預設 provisioner: rancher.io/local-path parameters: # full-path: /mnt/k8s-local-path/ns-{{ .PVC.Namespace }}-delete/{{ .PVC.Name }} nodePath: /mnt/k8s-local-path pathPattern: "ns-{{ .PVC.Namespace }}-delete/{{ .PVC.Name }}" allowVolumeExpansion: true reclaimPolicy: Delete volumeBindingMode: WaitForFirstConsumer ``` <br> --- --- <br> ## Intro ### 背景知識 - [[Hackmd][元件][儲存] StorageClass](/l44G0WeKQjuKIf9NmlfT0Q) ### 官方資訊 - [github](https://github.com/rancher/local-path-provisioner/tree/master?tab=readme-ov-file) <br> --- ## 安裝方式 (from yaml) (stable) > https://github.com/rancher/local-path-provisioner/tree/master?tab=readme-ov-file#deployment - yaml https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.31/deploy/local-path-storage.yaml <br> ## 安裝方式 (from source) (客製化參數) > 參數:[Configuration](https://github.com/rancher/local-path-provisioner/tree/master/deploy/chart/local-path-provisioner#configuration) ```bash git clone https://github.com/rancher/local-path-provisioner.git cd local-path-provisioner # 安裝 helm install local-path-storage \ --create-namespace \ --namespace local-path-storage \ ./deploy/chart/local-path-provisioner/ \ --set storageClass.provisionerName=rancher.io/local-path \ --set storageClass.defaultClass=true \ --set storageClass.name=default-local \ --set-json nodePathMap='[{"node":"DEFAULT_PATH_FOR_NON_LISTED_NODES","paths":["/mnt/k8s-local-path"]}]' # k8s-local-path 資料夾會由 rancher controller 自動建立 ``` - **建立結果** ![](https://hackmd.io/_uploads/rkeVzMdmlg.png) ![](https://hackmd.io/_uploads/BJbK4m_7xx.png) - **解除安裝** ``` helm uninstall local-path-storage --namespace local-path-storage ``` <br> ## 檢查配置狀態 ``` $ kubectl -n local-path-storage get all,sc,cm,daemonset,csidriver,csinode NAME READY STATUS RESTARTS AGE pod/local-path-storage-local-path-provisioner-f5846d78c-vvlsl 1/1 Running 0 26m NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/local-path-storage-local-path-provisioner 1/1 1 1 26m NAME DESIRED CURRENT READY AGE replicaset.apps/local-path-storage-local-path-provisioner-f5846d78c 1 1 1 26m NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE storageclass.storage.k8s.io/default-local (default) rancher.io/local-path Delete WaitForFirstConsumer true 26m NAME DATA AGE configmap/kube-root-ca.crt 1 75m configmap/local-path-config 4 26m NAME DRIVERS AGE csinode.storage.k8s.io/stage-kube01 0 6d1h <-- =0 預期 ``` <br> ## 測試範例 > https://github.com/rancher/local-path-provisioner?tab=readme-ov-file#usage > - https://raw.githubusercontent.com/rancher/local-path-provisioner/master/examples/pvc/pvc.yaml > - https://raw.githubusercontent.com/rancher/local-path-provisioner/master/examples/pod/pod.yaml ```yaml= apiVersion: v1 kind: PersistentVolumeClaim metadata: name: local-path-pvc spec: storageClassName: default-local accessModes: - ReadWriteOnce resources: requests: storage: 128Mi --- apiVersion: v1 kind: Pod metadata: name: volume-test spec: containers: - name: volume-test image: nginx:stable-alpine imagePullPolicy: IfNotPresent volumeMounts: - name: volv mountPath: /data ports: - containerPort: 80 volumes: - name: volv persistentVolumeClaim: claimName: local-path-pvc ``` - 查看狀態 ``` $ kubectl get all,pv NAME READY STATUS RESTARTS AGE pod/volume-test 1/1 Running 0 111s NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE persistentvolume/pvc-eb016313-1852-42c7-963e-3cf60d112095 128Mi RWO Delete Bound default/local-path-pvc default-local <unset> 108s ``` <br> --- <br> ## 部署 SC (StorageClass) 策略:retain / not retain ? ### 建立兩個 sc (storage class): - ### CaseA ``` default-local <-- 預設,刪了 pvc ,自動刪除 pv default-local-retain <-- 刪了 pvc ,保留 pv ``` - ### CaseB ``` default-local-delete <-- 刪了 pvc ,自動刪除 pv default-local-retain <-- 刪了 pvc ,保留 pv ``` <br> ### `default-local-retain.yaml` ```yaml= # File: default-local-retain.yaml # # Usages: # $ kubectl apply -f default-local-retain.yaml # $ kubectl get sc -o yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: default-local-retain annotations: storageclass.kubernetes.io/is-default-class: "false" # 先不要設成預設 provisioner: rancher.io/local-path allowVolumeExpansion: true reclaimPolicy: Retain volumeBindingMode: WaitForFirstConsumer ``` - ### 說明 - `reclaimPolicy: Retain`:刪除 PVC 後,PV 與底層資料都保留(不自動清理)。 - `WaitForFirstConsumer`:等有 Pod 消費者時才決定要綁到哪個節點(適合本機磁碟)。 - `allowVolumeExpansion: true`:之後可線上擴容(視底層類型是否支援)。 <br> --- --- <br> ## 討論 ### 如何規劃出「固定且可預期的目錄結構(`nodePath` + `pathPattern`)」? - ### gpt-5 (2025/09/15) > **目標**:把 local-path-provisioner 動態建立的資料夾,做成**穩定、可讀、可預期**的結構(而不是預設的 `pvc-<uid>_<namespace>_<pvc>` 亂數風格)。 > 核心作法: > > 1. 在 **StorageClass** 的 `parameters` 設定 `nodePath`(根路徑)與 `pathPattern`(子目錄命名模板)。 > 2. 在 **local-path-config**(ConfigMap)把你要用的根路徑加進 `nodePathMap` 白名單。 > 3. 重新部署或重啟 provisioner,並用新 SC 建立 PVC。 ### 我們的兩個常用策略 * **策略 A(當作預設 SC)** * **根路徑**:`/mnt/k8s-local-path/volumes` * **子目錄格式**:沿用官方預設模板 → `{{ .PVName }}_{{ .PVC.Namespace }}_{{ .PVC.Name }}` - **範例**:`/mnt/k8s-local-path/volumes/pvc-20fdc0ac-aafa-4d91-96a3-3ee0cbe8d866_openldap_data-openldap-0/` - **`.PVName`**: `pvc-20fdc0ac-aafa-4d91-96a3-3ee0cbe8d866` - **`.PVC.Namespace`**: `openldap` - **`.PVC.Name`**: `data-openldap-0` * **用途**:一般工作負載,刪除 PVC 同時刪掉 PV 與資料(`Delete`) * **策略 B(保留資料用 SC)** * **根路徑**:`/mnt/k8s-local-path` * **子目錄格式**:`ns-{{ .PVC.Namespace }}/{{ .PVC.Name }}`(更易讀) * **用途**:重視保留的資料卷,刪除 PVC **不**刪 PV 與資料(`Retain`) > **大小寫很重要**:模板欄位請用 `{{ .PVC.Namespace }}` 與 `{{ .PVC.Name }}`(**大寫**),不要寫成小寫。 > **唯一性**:若擔心撞名,可把 `{{ .PVName }}` 也併進去(例如:`{{ .PVC.Name }}_{{ .PVName }}`)。 > **StatefulSet**:PVC 名稱本身會帶序號(`-0/-1/-2`),可直接用來分片。 --- ### 1) ConfigMap:允許要用的根路徑 ```bash kubectl -n local-path-storage edit cm local-path-config ``` - ### 調整 ConfigMap 把 `data.config.json` 調整為(至少要包含你要用的**兩個根路徑**): ```json= { "nodePathMap": [ { "node": "DEFAULT_PATH_FOR_NON_LISTED_NODES", "paths": [ "/mnt/k8s-local-path", "/mnt/k8s-local-path/volumes" ] } ] } ``` > 若有針對特定節點(例如 `stage-kube01`)做條目,請在該節點的 `paths` 也一併加入上述兩個路徑。 - ### 重啟讓設定生效: ```bash kubectl -n local-path-storage rollout restart deploy/local-path-storage-local-path-provisioner kubectl -n local-path-storage rollout status deploy/local-path-storage-local-path-provisioner ``` - ### 如果沒添加新路徑,會出現底下的錯誤訊息: ``` $ kubectl -n slurm describe persistentvolumeclaim/slurm-data ... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning ProvisioningFailed 13s (x2 over 28s) rancher.io/local-path_local-path-storage-local-path-provisioner-f5846d78c-sffxq_ff2c342a-ed9a-47ab-a082-29b69b213f01 failed to provision volume with StorageClass "default-local": config doesn't contain path /mnt/k8s-local-path/volumes on node stage-kube01 ``` - 關鍵資訊:**`config doesn't contain path /mnt/k8s-local-path/volumes`** <br> --- ### 2) 建立兩個 StorageClass - ### (A) 預設 SC(刪 PVC 會刪資料) ```yaml= # File: default-local.yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: default-local annotations: storageclass.kubernetes.io/is-default-class: "true" provisioner: rancher.io/local-path parameters: nodePath: /mnt/k8s-local-path/volumes pathPattern: "{{ .PVName }}_{{ .PVC.Namespace }}_{{ .PVC.Name }}" allowVolumeExpansion: true reclaimPolicy: Delete volumeBindingMode: WaitForFirstConsumer ``` - ### (B) 保留資料 SC(刪 PVC 不刪資料) ```yaml= # File: default-local-retain.yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: default-local-retain annotations: storageclass.kubernetes.io/is-default-class: "false" provisioner: rancher.io/local-path parameters: nodePath: /mnt/k8s-local-path pathPattern: "ns-{{ .PVC.Namespace }}/{{ .PVC.Name }}" allowVolumeExpansion: true reclaimPolicy: Retain volumeBindingMode: WaitForFirstConsumer ``` - ### 注意事項 - **SC 參數不可直接修改**:若要變更 `parameters`,請用新名稱重建 SC,或先刪後建同名 SC。 - 否則在套用時,會遇到底下錯誤: ``` $ kubectl apply -f default-local-retain.yaml The StorageClass "default-local-retain" is invalid: parameters: Forbidden: updates to parameters are forbidden. ``` - **為什麼?** Kubernetes 不允許就地修改 StorageClass 的 `parameters`/(以及像 `provisioner` 等)不可變欄位;要改只能「刪掉重建」或「建立一個新名字的 SC」。 ([[GitHub] The StorageClass "test" is invalid: parameters: Forbidden: updates to parameters are forbidden. #80472](https://github.com/kubernetes/kubernetes/issues/80472?utm_source=chatgpt.com)) <br> --- ### 3) 範例 PVC/Pod 與驗證 ```yaml= # File: pvc-and-pod.yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: slurm-data namespace: slurm-by-tj spec: storageClassName: default-local-retain # 或 default-local accessModes: [ ReadWriteOnce ] resources: requests: storage: 1Gi --- apiVersion: v1 kind: Pod metadata: name: slurm-vol-test namespace: slurm-by-tj spec: containers: - name: app image: nginx:stable-alpine volumeMounts: - name: data mountPath: /data volumes: - name: data persistentVolumeClaim: claimName: slurm-data ``` **驗證**: ```bash kubectl -n slurm-by-tj get pvc,pv -o wide kubectl -n slurm-by-tj describe pvc slurm-data # 觀察 selected-node 與事件;成功後 PV 會 Bound # 到對應節點查看實體路徑: # 策略 A → /mnt/k8s-local-path/volumes/pvc-<uid>_<ns>_<pvc>/ # 策略 B → /mnt/k8s-local-path/ns-<ns>/<pvc>/ ``` --- ### 4) 常見錯誤排除(速查) * **`config doesn't contain path /.../volumes on node ...`** → 沒把該根路徑加進 `local-path-config` 的 `nodePathMap`。加上後重啟 provisioner。 * **`can't evaluate field namespace in type v1.ObjectMeta`** → 模板大小寫錯誤,請改用 `{{ .PVC.Namespace }}` / `{{ .PVC.Name }}`(大寫)。 * **PVC/Pod 一直 Pending**(`WaitForFirstConsumer`) → 先看 `describe pod/pvc` 事件;多半是排程不到節點(taint/資源限制)。可暫用 `Immediate` 測試 SC 判斷瓶頸在「排程」還是「provision」。 --- ### 5) 小結 * 設 **StorageClass** 的 `nodePath`/`pathPattern` 決定資料夾長相。 * 在 **local-path-config** 把根路徑列入白名單,否則不會建立。 * 建議維持兩個 SC:「預設(Delete)」與「保留(Retain)」,依需求選用。 * 需要高可讀性 → 用 `ns-<Namespace>/<PVC.Name>`;需要絕對唯一 → 併入 `{{ .PVName }}`。 --- ### 6) 參考資料 - ### Storage classes > https://github.com/rancher/local-path-provisioner?tab=readme-ov-file#storage-classes ```yaml= apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: ssd-local-path provisioner: rancher.io/local-path parameters: nodePath: /data/ssd pathPattern: "{{ .PVC.Namespace }}/{{ .PVC.Name }}" volumeBindingMode: WaitForFirstConsumer reclaimPolicy: Delete ``` <br> --- --- <br> ### [ConfigMap] 單一/多個 StorageClass 配置差異 - ### Question 底下兩種配置方案,差在哪裡? - ### 方案一 ```json { "sharedFileSystemPath": "/your/nfs/mount/path" } ``` - ### 方案二 ```json { "storageClassConfigs": { "default-local": { "sharedFileSystemPath": "/mnt/nfs/data" }, "model-registry": { "sharedFileSystemPath": "/mnt/nfs/data/models" } } } ``` - ### Claude Sonnet4.5 (2025/11/20) (us.anthropic.claude-sonnet-4-5-20250929-v1:0) ### **配置差異對比** #### **方案一:頂層配置 (簡化版)** ```json { "sharedFileSystemPath": "/your/nfs/mount/path" } ``` #### **方案二:多 StorageClass 配置** ```json { "storageClassConfigs": { "default-local": { "sharedFileSystemPath": "/mnt/nfs/data" }, "model-registry": { "sharedFileSystemPath": "/mnt/nfs/data/models" } } } ``` --- ### **核心差異** | 特性 | 方案一 | 方案二 | |------|--------|--------| | **StorageClass 數量** | 單一 (預設) | 多個 (自定義) | | **路徑配置** | 所有 PV 共用同一路徑 | 每個 StorageClass 可指定不同路徑 | | **適用場景** | 簡單部署 | 需要隔離不同用途的儲存 | | **配置位置** | 頂層 `sharedFileSystemPath` | 每個 StorageClass 內部配置 | --- ### **程式碼處理流程** #### **方案一處理邏輯 (provisioner.go:783-813)** ```go // loadConfigFile() → canonicalizeConfig() func canonicalizeConfig(data ConfigData) (Config, error) { var config Config // ... config.SharedFileSystemPath = data.SharedFileSystemPath // line 813 } ``` #### **方案二處理邏輯 (provisioner.go:796-810)** ```go // canonicalizeStorageClassConfig() for name, classConfig := range data.StorageClassConfigs { cfg := StorageClassConfig{ SharedFileSystemPath: classConfig.SharedFileSystemPath, // ... } config.StorageClassConfigs[name] = cfg } ``` --- ### **實際效果** **方案二的 StorageClass 配置:** ```yaml # 系統會創建兩個 StorageClass apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: default-local provisioner: rancher.io/local-path parameters: # PV 路徑:/mnt/nfs/data/<pv-name> --- apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: model-registry provisioner: rancher.io/local-path parameters: # PV 路徑:/mnt/nfs/data/models/<pv-name> ``` **方案一則只有預設的單一 StorageClass。** --- ### **建議** - **單一用途**:使用方案一 - **需要路徑隔離 (如不同專案、不同資料類型)**:使用方案二 **兩者在 Node Affinity 行為上完全相同** - 只要配置 `sharedFileSystemPath`,就不會綁定特定節點。 <br> --- --- <br> ### [ConfigMap] Local Path Provisioner 避免 Node Affinity 鎖定的設定教學 - ### Claude Sonnet4.5 (2025/11/20) (us.anthropic.claude-sonnet-4-5-20250929-v1:0) ### **問題說明** 當使用本地路徑配置 (`nodePathMap`) 時,PV 會被綁定到特定節點,導致: - 節點當機時 Pod 無法重新調度 - Provisioner 本身也無法在其他節點啟動 ### **解決方案:使用共享檔案系統配置** --- ### **步驟一:準備 NFS 共享儲存** 確保所有 Kubernetes 節點都掛載了相同的 NFS 路徑: ```bash # 在所有節點執行 sudo mkdir -p /mnt/nfs/data sudo mount -t nfs <NFS_SERVER_IP>:/export/path /mnt/nfs/data # 設定開機自動掛載 (編輯 /etc/fstab) echo "<NFS_SERVER_IP>:/export/path /mnt/nfs/data nfs defaults 0 0" | sudo tee -a /etc/fstab ``` --- ### **步驟二:修改 Local Path Provisioner ConfigMap** #### **方案 A:單一 StorageClass (簡化配置)** ```bash kubectl edit configmap local-path-config -n local-path-storage ``` 修改為: ```yaml= apiVersion: v1 kind: ConfigMap metadata: name: local-path-config namespace: local-path-storage data: config.json: |- { "sharedFileSystemPath": "/mnt/nfs/data" } setup: |- #!/bin/sh set -eu mkdir -m 0777 -p "$VOL_DIR" teardown: |- #!/bin/sh set -eu rm -rf "$VOL_DIR" helperPod.yaml: |- apiVersion: v1 kind: Pod metadata: name: helper-pod spec: containers: - name: helper-pod image: busybox imagePullPolicy: IfNotPresent ``` #### **方案 B:多個 StorageClass (進階隔離)** ```yaml= apiVersion: v1 kind: ConfigMap metadata: name: local-path-config namespace: local-path-storage data: config.json: |- { "storageClassConfigs": { "local-path": { "sharedFileSystemPath": "/mnt/nfs/data" }, "local-path-models": { "sharedFileSystemPath": "/mnt/nfs/data/models" }, "local-path-database": { "sharedFileSystemPath": "/mnt/nfs/data/database" } } } setup: |- #!/bin/sh set -eu mkdir -m 0777 -p "$VOL_DIR" teardown: |- #!/bin/sh set -eu rm -rf "$VOL_DIR" helperPod.yaml: |- apiVersion: v1 kind: Pod metadata: name: helper-pod spec: containers: - name: helper-pod image: busybox imagePullPolicy: IfNotPresent ``` --- ### **步驟三:重啟 Provisioner** ```bash kubectl rollout restart deployment local-path-provisioner -n local-path-storage kubectl rollout status deployment local-path-provisioner -n local-path-storage ``` --- ### **步驟四:驗證配置** #### **測試 PVC 創建** ```yaml= apiVersion: v1 kind: PersistentVolumeClaim metadata: name: test-pvc spec: accessModes: - ReadWriteMany # 共享檔案系統支援 RWX storageClassName: local-path resources: requests: storage: 1Gi ``` ```bash kubectl apply -f test-pvc.yaml kubectl get pvc test-pvc kubectl get pv ``` #### **檢查 PV 無 Node Affinity** ```bash kubectl get pv <pv-name> -o yaml | grep -A 10 nodeAffinity ``` **預期結果:** 不應該出現 `nodeAffinity` 欄位 --- ### **步驟五:測試 Pod 可在任意節點啟動** ```yaml apiVersion: v1 kind: Pod metadata: name: test-pod spec: containers: - name: test image: busybox command: ["sh", "-c", "echo 'Hello from any node' > /data/test.txt && sleep 3600"] volumeMounts: - name: data mountPath: /data volumes: - name: data persistentVolumeClaim: claimName: test-pvc ``` ```bash kubectl apply -f test-pod.yaml kubectl get pod test-pod -o wide # 查看運行在哪個節點 # 刪除 Pod 並等待重新調度到其他節點 kubectl delete pod test-pod kubectl apply -f test-pod.yaml kubectl get pod test-pod -o wide # 應該可以調度到不同節點 ``` --- ### **關鍵差異對比** | 配置類型 | Node Affinity | 支援 RWX | 節點失效影響 | |---------|--------------|---------|------------| | **nodePathMap (預設)** | ✅ 綁定特定節點 | ❌ | ❌ Pod 無法重新調度 | | **sharedFileSystemPath** | ❌ 無綁定 | ✅ | ✅ Pod 可調度到任意節點 | --- ### **故障排除** #### **問題:PV 仍有 nodeAffinity** ```bash # 檢查 Provisioner 日誌 kubectl logs -n local-path-storage deployment/local-path-provisioner ``` #### **問題:Pod 仍然 Pending** ```bash # 檢查 PVC 事件 kubectl describe pvc test-pvc # 檢查所有節點 NFS 掛載狀態 kubectl get nodes -o wide # 在每個節點執行: df -h | grep nfs ``` --- ### **完成!** 現在你的 Local Path Provisioner 已配置為使用共享檔案系統,不會再有 Node Affinity 鎖定問題。 <br> --- --- <br> ### [ConfigMap] `config.json` 中的 `helperPod.yaml` 扮演的角色為何? - ### Question ```yaml= $ kubectl -n local-path-storage get cm/local-path-config -o yaml apiVersion: v1 kind: ConfigMap ... data: config.json: |- { "nodePathMap": [ { "node": "DEFAULT_PATH_FOR_NON_LISTED_NODES", "paths": [ "/mnt/k8s-local-path", "/mnt/k8s-local-path/volumes" ] } ] } helperPod.yaml: |- apiVersion: v1 kind: Pod metadata: name: helper-pod namespace: local-path-storage spec: priorityClassName: system-node-critical tolerations: - key: node.kubernetes.io/disk-pressure operator: Exists effect: NoSchedule containers: - name: helper-pod image: busybox:latest imagePullPolicy: IfNotPresent resources: {} setup: |- #!/bin/sh set -eu mkdir -m 0777 -p "$VOL_DIR" teardown: |- #!/bin/sh set -eu rm -rf "$VOL_DIR" ``` `config.json` 中的 `helperPod.yaml` 扮演的角色為何? - ### Claude Sonnet4.5 (2025/11/20) (us.anthropic.claude-sonnet-4-5-20250929-v1:0) `helperPod.yaml` 在 local-path-provisioner 中扮演**臨時工具 Pod 模板**的角色,用於執行 PV(Persistent Volume)的生命週期管理任務。 ### 主要功能 1. **建立 Volume 目錄**:當需要建立新的 PV 時,provisioner 會根據此模板啟動一個 helper pod,執行 `setup` 腳本在節點上建立目錄 2. **清理 Volume 目錄**:當 PV 被刪除時,啟動 helper pod 執行 `teardown` 腳本刪除節點上的目錄 3. **節點本地操作**:透過 hostPath volume mount,讓容器能夠在節點檔案系統上執行操作 ### 設計特點 - **system-node-critical**:高優先級,確保 storage 操作不被阻塞 - **tolerations**:容忍 disk-pressure,即使磁碟壓力也能執行清理任務 - **busybox**:輕量級映像檔,僅需基本 shell 工具 - **臨時性**:每次操作動態建立,完成後即刪除 這是一種常見的 Kubernetes storage provisioner 設計模式,將需要特權操作(檔案系統管理)的任務委託給專門的 helper pod 執行。 <br> --- --- <br> ### [ConfigMap] teardown 支援 onDelete: Retain? - ### Question ### cm/local-path-config ```yaml= $ kubectl -n local-path-storage get cm/local-path-config -o yaml apiVersion: v1 kind: ConfigMap ...(略) data: config.json: |- ...(略) setup: |- #!/bin/sh set -eu mkdir -m 0777 -p "$VOL_DIR" teardown: |- #!/bin/sh set -eu rm -rf "$VOL_DIR" ``` ### tj-local-retain.yaml ```= apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: tj-local-retain annotations: storageclass.kubernetes.io/is-default-class: "false" # 先不要設成預設 provisioner: rancher.io/local-path parameters: # path: /mnt/k8s-local-path/ns-{{ .PVC.Namespace }}/{{ .PVC.Name }} nodePath: /mnt/k8s-local-path pathPattern: "ns-{{ .PVC.Namespace }}/{{ .PVC.Name }}" onDelete: Retain allowVolumeExpansion: true reclaimPolicy: Delete volumeBindingMode: WaitForFirstConsumer ``` 1. 請根據 `StorageClass.parameters.onDelete` 改寫 `teardown` 2. 當 `onDelete=Retain`,則不移除 disk 資料; 3. 當 `onDelete=Delete`,則移除 disk 資料 ``` teardown: |- #!/bin/sh set -eu rm -rf "$VOL_DIR" ``` - 目的:針對重要資料,K8s 自動刪除 pv,但不移除 disk 資料 - 備註: - 如果有更好的方法,請提出 - 當前配置如果有支援,亦請提出 - ### Claude Sonnet4.5 (2025/11/20) (us.anthropic.claude-sonnet-4-5-20250929-v1:0) 根據我對程式碼的分析,目前 `local-path-provisioner` **尚未原生支援** `onDelete` 參數。讓我提供兩個解決方案: ### **分析結果** #### 當前狀況 1. **程式碼層級**:在 `provisioner.go:498-502`,`teardown` script 會在 `PersistentVolumeReclaimPolicy != Retain` 時執行 2. **ConfigMap 層級**:teardown script 目前是固定的 shell script,無法動態根據 StorageClass 參數調整行為 3. **StorageClass 參數**:程式碼只處理 `nodePath` 和 `pathPattern` 參數,沒有處理 `onDelete` --- - ### **解決方案 1:修改 teardown script(建議使用)** 修改 ConfigMap 中的 teardown script,根據環境變數判斷是否刪除: ```yaml teardown: |- #!/bin/sh set -eu # 檢查是否應該保留資料(根據特定路徑模式) case "$VOL_DIR" in */ns-*retain*) echo "Retaining volume data at $VOL_DIR (onDelete=Retain behavior)" exit 0 ;; *) echo "Deleting volume data at $VOL_DIR" rm -rf "$VOL_DIR" ;; esac ``` **使用方式**: - **保留資料的 StorageClass**:在 `pathPattern` 中加入 `retain` 關鍵字 ```yaml parameters: pathPattern: "ns-{{ .PVC.Namespace }}/retain-{{ .PVC.Name }}" ``` - **刪除資料的 StorageClass**:不含 `retain` 關鍵字 ```yaml parameters: pathPattern: "ns-{{ .PVC.Namespace }}/{{ .PVC.Name }}" ``` --- - ### **解決方案 2:修改程式碼支援 onDelete 參數(更完善但需改 code)** 需要修改 Go 程式碼以支援 `onDelete` 參數: #### 步驟: 1. **修改 `provisioner.go:468-517`** - 在 `deleteFor` 函數中讀取 StorageClass 參數 2. **修改 `provisioner.go:583-729`** - 在 `createHelperPod` 函數中傳遞 `onDelete` 參數作為環境變數 3. **修改 teardown script**: ```yaml teardown: |- #!/bin/sh set -eu # 檢查 ON_DELETE 環境變數(由 provisioner 傳入) if [ "${ON_DELETE:-delete}" = "retain" ]; then echo "Retaining volume data at $VOL_DIR (onDelete=Retain)" exit 0 fi echo "Deleting volume data at $VOL_DIR" rm -rf "$VOL_DIR" ``` --- - ### **解決方案 3:使用現有的 reclaimPolicy(最簡單)** K8s 原生支援 `reclaimPolicy: Retain`,這會: - ✅ PV 被刪除後保留磁碟資料 - ✅ 需要手動清理磁碟空間 - ⚠️ PV 本身仍會被刪除(變成 Released 狀態) ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: tj-local-retain provisioner: rancher.io/local-path parameters: nodePath: /mnt/k8s-local-path pathPattern: "ns-{{ .PVC.Namespace }}/{{ .PVC.Name }}" reclaimPolicy: Retain # ← 使用這個! volumeBindingMode: WaitForFirstConsumer ``` **效果**: - 刪除 PVC 時,PV 變成 `Released` 狀態 - **teardown script 不會執行**(參見 `provisioner.go:485`) - 磁碟資料保留在 `/mnt/k8s-local-path/ns-xxx/yyy` --- ### **建議** 根據你的需求「針對重要資料,K8s 自動刪除 pv,但不移除 disk 資料」: #### ✅ **最佳方案:解決方案 1(修改 teardown script + pathPattern)** **優點**: - 無需修改 Go 程式碼 - 可以透過 pathPattern 靈活控制 - 易於維護 **ConfigMap 完整配置**: ```yaml data: teardown: |- #!/bin/sh set -eu # 檢查路徑中是否包含 retain 標記 case "$VOL_DIR" in */retain-*) echo "[Retain] Keeping volume data at $VOL_DIR" exit 0 ;; *) echo "[Delete] Removing volume data at $VOL_DIR" rm -rf "$VOL_DIR" ;; esac ``` **兩個 StorageClass 範例**: ```yaml --- # 保留資料的 StorageClass apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: tj-local-retain provisioner: rancher.io/local-path parameters: nodePath: /mnt/k8s-local-path pathPattern: "retain-{{ .PVC.Namespace }}-{{ .PVC.Name }}" reclaimPolicy: Delete volumeBindingMode: WaitForFirstConsumer --- # 刪除資料的 StorageClass apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: tj-local-delete provisioner: rancher.io/local-path parameters: nodePath: /mnt/k8s-local-path pathPattern: "delete-{{ .PVC.Namespace }}-{{ .PVC.Name }}" reclaimPolicy: Delete volumeBindingMode: WaitForFirstConsumer ``` 這樣可以達到: - **重要資料**:使用 `tj-local-retain` → PV 自動刪除,磁碟資料保留 - **臨時資料**:使用 `tj-local-delete` → PV 自動刪除,磁碟資料也刪除 <br> --- --- <br> ### [StorageClass] 解釋 StorageClass.parameters & StorageClass.reclaimPolicy - ### Question ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass ... provisioner: rancher.io/local-path parameters: defaultMount: /model onDelete: Retain pathPattern: '{{ or (index .PVC.Annotations "models/storage-path") (print .PVC.Namespace "/" .PVC.Name) }}' reclaimPolicy: Delete ``` 說明 parameters 參數底下的各個參數功能,以及 reclaimPolicy 參數 - ### gpt-4.5 (2025/11/20) 先講結論: * `parameters` 底下這三個 key 是「*供 provisioner 使用的自訂參數*」,K8s 核心本身不管內容。 * `reclaimPolicy` 則是 K8s 原生欄位,決定 PVC 被刪掉以後,PV 和背後實體資料的處理策略。 下面逐一說明你這個 `StorageClass` 裡的欄位意義。 --- ### 1. `parameters.defaultMount: /model` 這個 key **不是** upstream `local-path-provisioner` 官方的標準參數,而是你們額外加在 `StorageClass` 裡、供上層系統(例如 Helm chart、自己的 controller 或 operator)使用的「*約定欄位*」。 典型用途會是: * 表示:**「這個 StorageClass 建出來的 volume,設計上預期會掛在容器內的 `/model` 目錄」** * 方便: * 產生工作負載(Deployment / StatefulSet)時,自動帶入 `mountPath: /model` * 或讓你們的平台 UI / API 看到 `SC=model-registry` 就知道「這一類 PVC 的預設掛載點是 `/model`」 > 重點: > > * 對 Kubernetes / local-path-provisioner 來說,`defaultMount` 完全沒有內建語意。 > * 真正會不會掛在 `/model`,還是看 Pod 的 `volumeMounts.mountPath` 怎麼寫。 > * 這個 key 只有你們的工具或 Helm 模板有去讀,它才有實際效果。 --- ### 2. `parameters.onDelete: Retain` 這個欄位的語意是由 **provisioner 實作決定** 的。 在很多 driver(例如 `nfs-subdir-external-provisioner`)與一些 local-path 改版裡,`onDelete` 通常代表: > **當 PV 被刪除時,provisioner 對「實際資料目錄」要做什麼處理。** 常見值大概兩種:([IT Wonder Lab][20251120-A-1]) * `onDelete: delete` * PV 被刪時,**刪掉底層資料夾**(例如 `/some/path/ns/pvc-name` 整個 `rm -rf`)。 * `onDelete: retain` * PV 被刪時,**只刪 PV 物件,不動底層資料夾**(檔案還在,方便你之後手動掛載或做備份)。 在你這個設定裡: ```yaml parameters: onDelete: Retain ... reclaimPolicy: Delete ``` 大致可以解讀成: * `reclaimPolicy: Delete`: * PVC 被刪 → PV 變成 `Released` → K8s 會依照動態配置規則把 PV 直接刪掉(而不是保留在 `Released` 狀態重複使用)。([Kubernetes][20251120-A-2]) * `onDelete: Retain`(由 provisioner 解釋): * **當 PV 要被刪掉時,provisioner 只刪「PV 物件」本身,不會清空底層實體資料夾**。 * 也就是「K8s 資源清掉,但在 Node 上 `/model/...` 的檔案還在」,較適合需要人工/外部工具管理資料備份或回收的情境。 換句話說: > * API 物件層級:PV 會跟著走(`Delete`)。 > * 檔案系統層級:資料盡量保留(`onDelete: Retain`)。 --- ### 3. `parameters.pathPattern` 你現在的設定: ```yaml pathPattern: '{{ or (index .PVC.Annotations "models/storage-path") (print .PVC.Namespace "/" .PVC.Name) }}' ``` `pathPattern` 在這類 provisioner 裡的典型功用是: > **決定 PV 在「實體儲存路徑中的子目錄命名規則」**, > 類似 nfs-subdir-external-provisioner 的 `pathPattern` 或 local-path 改版提供的 template。([GitHub][20251120-A-3]) 這邊用的是 Go template 語法,逐段拆解: 1. `index .PVC.Annotations "models/storage-path"` * 從 PVC 的 annotations 裡讀取 key 為 `"models/storage-path"` 的值。 * 等同:`pvc.metadata.annotations["models/storage-path"]` 2. `print .PVC.Namespace "/" .PVC.Name` * 如果 annotation 沒設定,就 fallback 成字串:`<namespace>/<pvc-name>`。 3. `or (index ...) (print ...)` * Go template 的 `or`:回傳第一個「非空值」,否則回傳第二個。 組起來就是: > * 如果 PVC 有設定: > > ```yaml > metadata: > annotations: > models/storage-path: some/custom/subdir > ``` > > 則資料夾會建立在: > `.../<nodePath>/some/custom/subdir` > > * 如果沒設定 `models/storage-path`: > 則 fallback 為: > `.../<nodePath>/<namespace>/<pvc-name>` 這讓你可以: * **預設**: 每個 PVC 對應 `namespace/pvc-name` 的資料夾,結構清楚可預期。 * **需要客製化時**: 在 PVC 上加 annotation `models/storage-path`,就能指定特別的子目錄(例如 `team-a/project-x/exp-001`)。 --- ### 4. `reclaimPolicy: Delete` 這個是 Kubernetes 原生欄位,語意固定:([Kubernetes][20251120-A-2]) #### 常見值 * `Delete` * **PVC 被刪除** → PV 變成 `Released` → 由控制器(對於動態 provision 的 PV)直接刪除 PV 物件,並呼叫 driver 做底層清理動作。 * `Retain` * PVC 被刪除後,PV 留在 `Released` 狀態,不會自動刪除,也不會自動清空資料。 * 需要管理員手動處理(回收、重新綁定或清空)。 ### 套回你的例子 ```yaml reclaimPolicy: Delete parameters: onDelete: Retain ``` * **K8s 層級**: * PVC 刪除 → PV 會被自動刪除(不會留下 `Released` PV)。 * **storage plugin 層級**(根據 `onDelete: Retain` 的設計): * 雖然 PV 被刪了,但 provisioner 預期「**不會自動 `rm -rf` 實體資料夾**」,由你之後用其他流程處理資料。 如果你改成: ```yaml reclaimPolicy: Retain parameters: onDelete: Retain ``` 那就會變成: * PV 也不刪,資料夾也不清 → 完全手動管理回收流程。 這種通常用在「一定不能自動刪資料」的情境。 --- ### 小結(一句話版本) * `defaultMount`: 你們自訂的 metadata,告訴上層系統「這類 volume 通常會掛在 `/model`」。 * `onDelete: Retain`: 告訴 provisioner「PV 刪除時,不要清空底層資料夾」。 * `pathPattern`: 透過 Go template 決定在 Node 上的子目錄路徑(優先用 `models/storage-path` annotation,否則用 `<namespace>/<pvc-name>`)。 * `reclaimPolicy: Delete`: PVC 刪除時,K8s 自動刪 PV 物件(不保留 PV),但底層實際資料是否被清掉,要再看 `onDelete` 的實作行為。 [20251120-A-1]: https://www.itwonderlab.com/kubernetes-nfs/?utm_source=chatgpt.com "How to use an external NFS Persistent Volume on Kubernetes" [20251120-A-2]: https://kubernetes.io/docs/concepts/storage/storage-classes/?utm_source=chatgpt.com "Storage Classes" [20251120-A-3]: https://github.com/rancher/local-path-provisioner?utm_source=chatgpt.com "rancher/local-path-provisioner: Dynamically provisioning ..." <br> {%hackmd vaaMgNRPS4KGJDSFG0ZE0w %}