[應用][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 自動建立
```
- **建立結果**


- **解除安裝**
```
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 %}