Wer sich mit Kubernetes beschäftigt, wird sich irgendwann auch mit persistenten Storage auseinandersetzen. In diesem Blogpost zeigt euch Sebastian, wie mit Hilfe von rook.io Cloud-Native Storage auf einem Kubernetes Cluster betrieben wird.

Doch was ist eigentlich Rook? Rook wird auf der eigenen Website wie folgt beschrieben:
A Storage Orchestrator for Kubernetes
Rook turns distributed storage systems into self-managing, self-scaling, self-healing storage services. It automates the tasks of a storage administrator: deployment, bootstrapping, configuration, provisioning, scaling, upgrading, migration, disaster recovery, monitoring, and resource management.
https://rook.io/
Rook uses the power of the Kubernetes platform to deliver its services: cloud-native container management, scheduling, and orchestration.
Rook ermöglicht es, mehrere Storage Provider zu verwenden. Darunter befinden sich Ceph und EdgeFS, aber auch Datenbanken wie CockroachDB und Cassandra. In diesem Beitrag werde ich mit Rook einen Ceph Cluster bauen und dann sowohl Block- wie auch File-Storage davon beziehen.
Übrigens: Auch RedHat setzt mit RedHat OpenShift Container Storage auf Rook.
Kubernetes Cluster
Für dieses Setup habe ich zuerst einen neuen Kubernetes Cluster erstellt. Dafür verwende ich unsere Rancher Installation und den cloudscale.ch Rancher UI Driver, um automatisch bei unserem Cloud-Provider einen neuen Kubernetes Cluster zu provisionieren. Damit direkt im UI Driver zusätzlichen Storage an die VM’s angehängt werden kann, musste ich aber zuerst den UI Driver etwas weiterentwickeln. Rancher verwendet Docker Machine Driver, um VM’s für die Kubernetes Cluster zu erstellen. Cloudscale.ch hat ihren Docker Machine Driver vor kurzem aktualisiert und ermöglicht nun zusätzliche Volumes (SSD oder Bulk) direkt beim Erzeugen der VM’s anzuhängen. Somit konnte ich den UI Driver um diese Funktionalität ergänzen. Neu können direkt im Node-Template weitere Volumes der VM hinzugefügt werden:
Der Pull Request dafür ist bereits offen, das Feature sollte bald verfügbar sein.
Damit habe ich einen Kubernetes Cluster mit 3 Master Nodes, 3 Storage Nodes und 2 Worker Nodes erstellt. Die Storage Nodes wurden je mit einem zusätzlich 500 GB SSD Volume erstellt, die ich dann für Rook verwenden will. Die 3 Storage Nodes haben ausserdem ein Taint und ein Node Label gesetzt, damit diese später dediziert für Rook und den Ceph Cluster verwendet werden können. Der normale Container Workload wird somit nicht durch Storage Workload beinträchtigt (und umgekehrt).
$ kubectl get node NAME STATUS ROLES AGE VERSION k8s-test-master1 Ready controlplane,etcd 17h v1.17.2 k8s-test-master2 Ready controlplane,etcd 17h v1.17.2 k8s-test-master3 Ready controlplane,etcd 17h v1.17.2 k8s-test-storage1 Ready worker 17h v1.17.2 k8s-test-storage2 Ready worker 17h v1.17.2 k8s-test-storage3 Ready worker 17h v1.17.2 k8s-test-worker1 Ready worker 17h v1.17.2 k8s-test-worker2 Ready worker 15h v1.17.2
Ceph Cluster mit dem rook-ceph Operator
Rook verwendet einen Operator um Ceph Cluster zu erstellen – den rook-ceph Operator. Für die Installation, verwende ich das Ceph Operator Helm Chart, das von Rook zur Verfügung gestellt wird. Das Chart habe ich via Rancher App Catalog installiert und ich verwende folgende Values:
--- agent: flexVolumeDirPath: "/var/lib/kubelet/volumeplugins" nodeSelector: storage: "true" tolerations: - key: storage operator: Exists discover: tolerations: - key: storage operator: Exists nodeAffinity: storage=true
Der flexVolumeDirPath muss angepasst werden, da Rancher diesen nicht am Standardpfad hat. Allerdings verwenden wir später sowieso CSI und nicht flexvolumes. Weiter sind Node-Affinity/Node-Selector und Tolerations gesetzt, damit der Ceph Operator und auch die Disk Discovery Pods nur auf den zuvor erwähnten Storage Nodes erstellt werden. Sobald die App installiert ist, sehen wir drei Pods und diese wie gewünscht nur auf Storage Nodes:
kubectl get pod -o wide NAME READY STATUS NODE rook-ceph-operator-6775969454-xgf44 1/1 Running k8s-test-storage2 rook-discover-hjfbn 1/1 Running k8s-test-storage3 rook-discover-hrn2j 1/1 Running k8s-test-storage1 rook-discover-lg2tl 1/1 Running k8s-test-storage2
Der Ceph-Operator läuft nun und wartet darauf, dass wir einen Ceph Cluster erstellen. Rook hat dafür diverse CRD’s, unter anderem eines das CephCluster.ceph.rook.io/v1 heisst. Um den Ceph Cluster zu erstellen, müssen wir also eine Kubernetes Ressource dieses Typs erstellen:
--- apiVersion: ceph.rook.io/v1 kind: CephCluster metadata: name: rook-ceph namespace: rook-ceph spec: cephVersion: image: ceph/ceph:v14.2.6 dataDirHostPath: /var/lib/rook dashboard: enabled: true placement: all: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: storage operator: In values: - "true" tolerations: - key: storage operator: Exists mon: count: 3 allowMultiplePerNode: true storage: useAllNodes: false useAllDevices: true nodes: - name: k8s-test-storage1 - name: k8s-test-storage2 - name: k8s-test-storage3
Darin wird definiert, wie unser Ceph Cluster auszusehen hat. Besonders interessant sind wieder die Node-Affinity und Tolerations. Damit wird gesteuert auf welchen Kubernetes Nodes die Komponenten des Ceph Cluster laufen sollen. Weitere definieren wir explizit, welche Nodes als Storage Backend verwendet werden sollen. Es wäre auch möglich, useAllNodes: true zu verwenden. Dank den Node-Affinity / Tolerations würden trotzdem nur die drei Storage Nodes verwendet werden. Damit könnte später aber durch Hinzufügen weiteree Storage Node, automatisch der Ceph Cluster erweitert werden (ohne Anpassen der CephCluster Ressource).
Der Ceph Operator deployet nun alle Komponenten, die für den Ceph Cluster benötigt werden. Wir sehen nun diverse neue Pods mit allen Ceph Komponenten (mgt, mon, osd’s).
Sobald alle ready sind, ist auch das Ceph Dashboard verfügbar. Um darauf zuzugreifen müssen wir den Port mit kubectl forwarden und das admin Passwort aus einem Secret auslesen:
# Admin Password des Dashboard kubectl -n rook-ceph get secret rook-ceph-dashboard-password -o jsonpath="{['data']['password']}" | base64 --decode && echo # Port-Forwarding für Dashboard kubectl port-forward svc/rook-ceph-mgr-dashboard 7000:7000
Anstelle des Portforwardings könnten wir natürlich auch einen Ingress einrichten und darüber dann von überall zugreifen. Der Ceph Cluster ist nun ready und kann verwendet werden.
Block Storage
Wir wollen zuerst Block Storage für Kubernetes zur Verfügung stellen. Dazu benötigen wir einen neuen CephBlockPool. Dieser wird mit einer weiteren CRD definiert, aus dem der Ceph-Operator dann automatisch ein Storage Pool in Ceph erstellt:
apiVersion: ceph.rook.io/v1 kind: CephBlockPool metadata: name: replicapool namespace: rook-ceph spec: failureDomain: host replicated: size: 3
Um nun Block-Storage aus diesem Storage Pool in Kubernetes zu beziehen, erstellen wir eine Kubernetes StorageClass. Diese sagt Kubernetes, wie und wer aus PVCs dynamisch Storage provisioniert und dann die Informationen zum Beziehen dieses Storage in ein PV ablegt:
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: rook-ceph-block parameters: clusterID: rook-ceph csi.storage.k8s.io/fstype: xfs csi.storage.k8s.io/node-stage-secret-name: rook-csi-rbd-node csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph csi.storage.k8s.io/provisioner-secret-name: rook-csi-rbd-provisioner csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph imageFeatures: layering imageFormat: "2" pool: replicapool provisioner: rook-ceph.rbd.csi.ceph.com reclaimPolicy: Delete volumeBindingMode: Immediate allowVolumeExpansion: false
Mit einem PVCs, welches diese StorageClass verwendet, erstellt dann der csi-rdbplugin-provisioner automatisch ein Block Image:
Jedes PV ist auch im Ceph Dashboard als Image Block sichtbar.
File Storage
Um FileStorage von Ceph zu beziehen, existiert die CephFilesystem CRD. Analog zu vorher bei Block-Storage können wir also ein Objekt diese CephFilesystem Typ erstellen:
apiVersion: ceph.rook.io/v1 kind: CephFilesystem metadata: name: myfs namespace: rook-ceph spec: metadataPool: replicated: size: 3 dataPools: - replicated: size: 3 preservePoolsOnDelete: true metadataServer: activeCount: 1 activeStandby: true
Der Ceph-Operator konfiguriert dann wiederum automatisch ein Ceph Filesystem und den dazugehörigen Ceph Block Pools. Um mit PVC’s dynamisch PV’s zu erstellen, wird wiederum eine StorageClass benötigt:
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: csi-cephfs provisioner: rook-ceph.cephfs.csi.ceph.com parameters: # clusterID is the namespace where operator is deployed. clusterID: rook-ceph # CephFS filesystem name into which the volume shall be created fsName: myfs # Ceph pool into which the volume shall be created pool: myfs-data0 # The secrets contain Ceph admin credentials. These are generated automatically by the operator # in the same namespace as the cluster. csi.storage.k8s.io/provisioner-secret-name: rook-csi-cephfs-provisioner csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph csi.storage.k8s.io/node-stage-secret-name: rook-csi-cephfs-node csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph reclaimPolicy: Delete
Die PVs, die davon erstellt werden, unterstützen nun als accessMode ReadWriteMany und können daher von mehreren Pod’s gleichzeitig gemounted werden. Ceph stellt ein shared Filesystem (CephFS) zur Verfügung.
Object Storage
Mit dem Rook ceph-operator kann analog zu Block- und File-Storage auch Object-Storage bezogen werden. Das Vorgehen ist sehr ähnlich wie zuvor. Ich verzichte hier aber auf eine Anleitung. Mehr Infos dazu ist in der Rook Dokumentation zu finden.