Phase 3 — Rook-Ceph

範圍

連線我自建的外部 Ceph cluster,透過 Rook-Ceph v1.17 的 External Cluster 模式建立 StorageClassVolumeSnapshotClass,讓 K8s workload 可以直接使用外部 Ceph 提供的 RBD 儲存。


Phase 3-1:安裝 Rook-Ceph Operator

mansion-kubevirt-master 執行

Step 3-1-1:部署 Rook-Ceph CRD 與 Operator

💡 理解這三個檔案的角色(大樓維修比喻):

  • crds.yaml:大樓的新規則手冊(定義了什麼叫 CephCluster)。
  • common.yaml:成立「電梯維修部」(Namespace)、製作「員工工號」(ServiceAccount)、並發給他們「萬能鑰匙和維修授權書」(RBAC)。
  • operator.yaml:真正聘請進來的「維修技師」(Operator Pod)。
ROOK_VERSION=v1.17.0

# 部署 CRD
kubectl apply -f \
  https://raw.githubusercontent.com/rook/rook/${ROOK_VERSION}/deploy/examples/crds.yaml

# 部署 common(namespace、RBAC、SA)
kubectl apply -f \
  https://raw.githubusercontent.com/rook/rook/${ROOK_VERSION}/deploy/examples/common.yaml

# 部署 Operator
kubectl apply -f \
  https://raw.githubusercontent.com/rook/rook/${ROOK_VERSION}/deploy/examples/operator.yaml

Step 3-1-2:驗證 Operator 就緒

kubectl get pods -n rook-ceph -l app=rook-ceph-operator
# 預期:rook-ceph-operator-* 1/1 Running

Phase 3-2:建立 External Cluster 設定

以下步驟需要你的外部 Ceph cluster admin 金鑰與 monitor 位址

Step 3-2-1:準備 external cluster 輸入

# 替換為你的 Ceph cluster 資訊
# 注意:這裡要用 rook 需要的 mon data 格式(monID=ip:port),不是單純逗號分隔 endpoint
CEPH_MON_DATA="a=<mon1_ip>:6789,b=<mon2_ip>:6789,c=<mon3_ip>:6789"
CEPH_CLUSTER_FSID="<fsid>"     # ceph fsid
CEPH_ADMIN_KEY="<admin_key>"   # ceph auth get-key client.admin

Step 3-2-2:建立 ceph-external-cluster namespace 與 secret

💡 為什麼需要這麼多個 Secret?(功能分離與最小權限原則) 雖然在 Lab 中我們都填入 client.admin 的金鑰,但 Rook 設計了不同的「門禁卡」位置,讓生產環境可以針對不同組件設定不同權限:

  • ConfigMap (mon-endpoints):外部叢集的「通訊錄」,告訴 Rook 去哪裡找 Monitor。
  • Secret (mon):外部叢集的「身分證」,包含 FSID 與基礎連線資訊。
  • Secret (operator-creds):Operator 的「管理員權限」,用來查詢叢集狀態與建立資源。
  • Secret (csi-rbd-*):CSI 驅動的「作業權限」,專門用來建立 (Provisioner) 與掛載 (Node) 磁碟。
kubectl create namespace ceph-external-cluster

# 1. 外部叢集通訊錄 (Address Book)
# maxMonId: 告訴 Rook 最後一個 Mon 的編號 (a=0, b=1, c=2),供程式邏輯追蹤用。
kubectl create configmap rook-ceph-mon-endpoints \
  --from-literal=data="${CEPH_MON_DATA}" \
  --from-literal=maxMonId="2" \
  --from-literal=mapping="{}" \
  -n ceph-external-cluster

# 2. 叢集基礎資訊與連線 Secret
kubectl create secret generic rook-ceph-mon \
  --type="kubernetes.io/rook" \
  --from-literal=cluster-name=ceph-external-cluster \
  --from-literal=fsid="${CEPH_CLUSTER_FSID}" \
  --from-literal=admin-secret=admin-secret \
  --from-literal=mon-secret=mon-secret \
  --from-literal=ceph-username=client.admin \
  --from-literal=ceph-secret="${CEPH_ADMIN_KEY}" \
  -n ceph-external-cluster

# 3. Operator 管理員憑證 (Admin Identity)
kubectl create secret generic rook-ceph-operator-creds \
  --type="kubernetes.io/rook" \
  --from-literal=userID=client.admin \
  --from-literal=userKey="${CEPH_ADMIN_KEY}" \
  -n ceph-external-cluster

# 4. CSI 儲存驅動憑證 (Worker Identities)
# 分為 Node (掛載) 與 Provisioner (建立/刪除),分開是為了未來能做精細權限控管。
kubectl create secret generic rook-csi-rbd-node \
  --type="kubernetes.io/rook" \
  --from-literal=userID=client.admin \
  --from-literal=userKey="${CEPH_ADMIN_KEY}" \
  -n ceph-external-cluster

kubectl create secret generic rook-csi-rbd-provisioner \
  --type="kubernetes.io/rook" \
  --from-literal=userID=client.admin \
  --from-literal=userKey="${CEPH_ADMIN_KEY}" \
  -n ceph-external-cluster

rook-ceph namespace 繼續保留給 operator;External Cluster CR 與對應 secret 放在 ceph-external-cluster

Step 3-2-3:部署 CephCluster(External 模式)

💡 連動機制說明(約定優於配置): 你會發現 YAML 裡並沒有指定前面建立的 Secret 或 ConfigMap 名稱。這是因為當 external.enable 設為 true 時,Rook Operator 會自動在該 namespace 下尋找固定名稱的資源:

  1. 看到 external: true → 觸發外部模式邏輯。
  2. 自動去該 Namespace 找名為 rook-ceph-mon-endpoints 的 ConfigMap。
  3. 自動去該 Namespace 找名為 rook-ceph-monrook-ceph-operator-creds 的 Secret。

結論:名稱絕對不能打錯,因為 Operator 是靠「約定名稱」來自動對號入座的。

# /tmp/ceph-external-cluster.yaml
apiVersion: ceph.rook.io/v1
kind: CephCluster
metadata:
  name: rook-ceph-external
  namespace: ceph-external-cluster
spec:
  external:
    enable: true
  crashCollector:
    disable: true
  healthCheck:
    daemonHealth:
      mon:
        interval: 45s
kubectl apply -f /tmp/ceph-external-cluster.yaml

Step 3-2-4:驗證 External Cluster 連線

kubectl get cephcluster -n ceph-external-cluster
# 預期:rook-ceph-external  Connected  ...  True

# operator 仍在 rook-ceph
kubectl get pods -n rook-ceph -l app=rook-ceph-operator
# 預期:rook-ceph-operator Running

💡 成功連線後的自動化成果(自動生成的 Secret): 當狀態變為 Connected 後,Operator 會自動「加工」原始資料並生出以下 Secret,這是驗證連線成功的關鍵指標:

  • rook-ceph-config:自動生成的 ceph.confkeyring,供 K8s 內部 Pod 直接連線 Ceph 使用。
  • rook-ceph-exporter-keyring:監控專用憑證,讓 Prometheus 能抓取外部 Ceph 的數據。
  • rook-csi-cephfs-*:Operator 的「貼心服務」,即便你只設了 RBD,它也會自動幫你準備好 CephFS 的 CSI 憑證。
# 確認所有資源(含自動生成的)已存在
kubectl get configmap -n ceph-external-cluster
kubectl get secret -n ceph-external-cluster

Phase 3-3:建立 StorageClass(RBD)

使用 Ceph RBD 作為 ReadWriteOnce 的 Block storage

Step 3-3-1:建立 RBD StorageClass

# /tmp/ceph-rbd-sc.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ceph-rbd
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: rook-ceph.rbd.csi.ceph.com
parameters:
  clusterID: ceph-external-cluster
  pool: k8s_rbd_pool            # 替換為你的 RBD pool 名稱
  imageFormat: "2"
  imageFeatures: layering
  csi.storage.k8s.io/provisioner-secret-name: rook-csi-rbd-provisioner
  csi.storage.k8s.io/provisioner-secret-namespace: ceph-external-cluster
  csi.storage.k8s.io/controller-expand-secret-name: rook-csi-rbd-provisioner
  csi.storage.k8s.io/controller-expand-secret-namespace: ceph-external-cluster
  csi.storage.k8s.io/node-stage-secret-name: rook-csi-rbd-node
  csi.storage.k8s.io/node-stage-secret-namespace: ceph-external-cluster
reclaimPolicy: Delete
allowVolumeExpansion: true
kubectl apply -f /tmp/ceph-rbd-sc.yaml
kubectl get sc
# 預期:ceph-rbd (default)

Phase 3-4:建立 VolumeSnapshotClass

Step 3-4-0:安裝 Snapshot CRD 與 Controller(若尚未安裝)

# 安裝 VolumeSnapshot CRDs
kubectl apply -k "github.com/kubernetes-csi/external-snapshotter/client/config/crd?ref=v6.3.3"

# 安裝 snapshot-controller
kubectl apply -k "github.com/kubernetes-csi/external-snapshotter/deploy/kubernetes/snapshot-controller?ref=v6.3.3"

# 驗證
kubectl get crd | grep volumesnapshot
kubectl -n kube-system get deploy snapshot-controller

Step 3-4-1:建立 VolumeSnapshotClass

若出現 no matches for kind "VolumeSnapshotClass",表示前一步 CRD 尚未安裝完成或尚未就緒。

# /tmp/ceph-rbd-snapclass.yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
  name: ceph-rbd-snapclass
driver: rook-ceph.rbd.csi.ceph.com
parameters:
  clusterID: ceph-external-cluster
  csi.storage.k8s.io/snapshotter-secret-name: rook-csi-rbd-provisioner
  csi.storage.k8s.io/snapshotter-secret-namespace: ceph-external-cluster
deletionPolicy: Delete
kubectl apply -f /tmp/ceph-rbd-snapclass.yaml
kubectl get volumesnapshotclass
# 預期:ceph-rbd-snapclass   rook-ceph.rbd.csi.ceph.com

Phase 3-5:端對端驗證

# /tmp/test-rbd-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-rbd-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: ceph-rbd
kubectl apply -f /tmp/test-rbd-pvc.yaml
kubectl get pvc test-rbd-pvc
# 預期:STATUS = Bound

# 清除測試資源
kubectl delete pvc test-rbd-pvc

踩坑記錄(Phase 3)

問題 原因 解法
csi-rbdplugin-provisioner / csi-cephfsplugin-provisioner 有 1 個 Pod 長期 Pending provisioner 需要 2 副本且有 anti-affinity;但 cluster 只有 worker 可排程(master/infra 有 taint) 透過 patch 設定 CSI_PROVISIONER_TOLERATIONSCSI_PLUGIN_TOLERATIONS (見下方指令)
monitoring PVC 掛載失敗:driver name rook-ceph.rbd.csi.ceph.com not found csi-rbdplugin 沒有在掛載目標節點(特別是 infra)運行 同上,透過 patch 讓 DaemonSet 也能跑在 infra 與 master 節點

解決方案:補上 CSI Tolerations

執行以下指令,讓 CSI 相關 Pod 能夠進入 masterinfra 節點:

kubectl -n rook-ceph patch configmap rook-ceph-operator-config --type merge -p '{
  "data": {
    "CSI_PROVISIONER_TOLERATIONS": "[{\"key\":\"node-role.kubernetes.io/control-plane\",\"operator\":\"Exists\",\"effect\":\"NoSchedule\"},{\"key\":\"node-role.kubernetes.io/infra\",\"operator\":\"Exists\",\"effect\":\"NoSchedule\"}]",
    "CSI_PLUGIN_TOLERATIONS": "[{\"key\":\"node-role.kubernetes.io/control-plane\",\"operator\":\"Exists\",\"effect\":\"NoSchedule\"},{\"key\":\"node-role.kubernetes.io/infra\",\"operator\":\"Exists\",\"effect\":\"NoSchedule\"}]"
  }
}'