Man sollte meinen, dass es inzwischen viele etablierte und gut funktionierende Storage-Lösungen für Kubernetes gibt. Aber das wäre wohl zu einfach.
Wir mussten jedenfalls so einiges anstellen, um eine lauffähige Lösung zu erhalten!
Doch der Reihe nach…
Begrifflichkeiten
Einer der entscheidenden Punkte ist, wie Kubernetes (oder besser gesagt die PODs) auf den Storage zugreifen können. Dazu gibt es in Kubernetes drei Modi: ReadWriteOnce
, ReadOnlyMany
und ReadWriteMany
.
Da wir mit ReadOnlyMany
im Allgemeinen nicht viel anfangen können, fällt der schon mal raus. Bleiben noch zwei.
ReadWriteOnce
bedeutet, dass das so angebundene Volume nur von einem Node des Kubernetes-Cluster aus angesprochen werden kann. Nun besteht so ein Kubernetes-Cluster aber normalerweise aus mindestens drei Nodes. Und eine Applikation wird und soll ja auch horizontal skaliert werden, also auf mehrere PODs aufgeteilt, die sich wiederum auf die einzelnen Nodes verteilen. Das passt also nicht zusammen.
Und entsprechend wollen wir also die Volumes mit ReadWriteMany
anbinden – was nicht viele Möglichkeiten offen lässt: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#access-modes
Von den 20 aufgeführten Volume-Plugins unterstützen nur 6 ReadWriteMany
(8 wenn man Glück hat).
Aber bevor wir die im einzelnen durch gehen…
Was muss so eine Storage-Lösung denn noch so alles bieten?
dynamic provisioning
Wo wir gerade noch bei Kubernetes sind, wäre da natürlich die Anforderung von dynamischer Provisionierung. Will heißen, dass ich in der Kubernets-Config meiner App nur einen PersistentVolumeClaim
mit der passenden StorageClass
anlege, und der Rest passiert automatisch. Die Alternative wäre, dass ich jedes Mal händisch noch ein dazugehöriges Volume anlegen muss – und zwar sowohl in Kubernetes als auch auf dem Storage-Cluster.
Das ist nicht nur lästig sondern bremst auch ungemein aus. Weil die Entwickler sollen und wollen ja nicht auf dem Storage-Server rum hantieren. Dann müssten sie ein Ticket auf machen, damit sich jemand darum kümmert, das dauert entsprechend – und das alles nur um mal schnell eine App zu testen, die hinterher wieder weg kann. Womit der ganze Kram auch händisch wieder entfernt werden müsste – ihr könnt euch vorstellen, wer das dann macht …niemand! Vermüllung ahoi!
Also wir konstatieren: dynamic provisioning ist ein must-have!
Filemodes und Owner
Das fällt meistens erst auf, wenn die Applikation Fehler wirft oder zumindest logged. Dann sieht man: hoppla, die Files gehören alle root und haben O777 als File-Mode …und das lässt sich auch nicht ändern. Scheiße!
In vielen Fällen ist das erst mal kein großes Problem, aber meistens ist es nur eine Frage der Zeit, bis es zu einem wird – und dann steht man da.
Besitzer und File-Mode richtig/individuell setzen zu können, ist also auch eine wichtige Anforderung – für uns sogar essentiell.
Von mehreren Quellen aus nutzbar
Die Anforderung, die den meisten Lösungen das Genick bricht! Wir wollen den Storage-Cluster nicht nur von und auf einem Kubernetes-Cluster aus nutzen! Sondern von mehreren …und noch von ein paar old-school VMs!
Konkret bedeutet das: es ist egal wie gut ein In-Cluster-Lösung ist – der selbe Storage-Cluster muss auch von einem anderen k8s-Cluster aus genutzt werden können, ohne dort einen eigenen Aufsetzen zu müssen. Und dann ist es auch egal ob der Storage-Cluster in Kubernetes läuft oder nicht – er muss „von außen“ benutzbar sein. Und es braucht einen entsprechenden „Standalone“-Treiber in Kubernetes um einen externen Storage-Cluster ansprechen zu können.
Performance
Vermutlich das Thema, dass die meisten umtreibt, bevor sie sich der oben stehenden Probleme/Anforderungen bewusst werden. Bis zu dem Punkt, wo diese Anforderung komplett in den Hintergrund tritt, und man einfach nur noch will, dass der Scheiß überhaupt läuft. Einen schönen Überblick liefert aber bspw. folgende Seite:
https://medium.com/volterra-io/kubernetes-storage-performance-comparison-9e993cb27271
Auch wenn das im Endeffekt nicht von Belang ist, da die meisten der dort verglichenen Lösungen den oben stehenden Anforderungen nicht gerecht wird!
AKS
Eine Anforderung, die für uns noch essentiell war/ist: das ganze muss in Azure laufen.
AWS konnte ich nicht testen (bzw. wollte ich nicht, da wir es ohnehin nicht hätten nutzen können) – vielleicht seid ihr damit fein raus …kA.
Welche Lösungen wir getestet haben …und warum die unzureichend waren
Wie weiter oben bereits erwähnt, ist die Auswahl an Lösungen, die ReadWriteMany
bieten, recht beschränkt.
Gehen wir also der Reihe nach durch.
AzureFile
AzureFile hat den großen Vorteil, dass es bei AKS out-of-the-box nutzbar ist, ohne das dazu noch irgendwas installiert oder gar maintained werden müsste. Aber neben der recht überschaubaren Performance, ist das Hauptproblem, dass Owner und File-Mode immer auf „root“ und „O777“ stehen und nicht geändert werden können!
Zudem gestaltet sich die Einbindung der Volumes in andere Cluster oder VMs sehr schwierig. Es ist möglich, aber nichts für den täglichen Einsatz.
Damit ist diese Lösung raus.
CephFS
Hier sei zunächst angemerkt, dass es einige HowTo’s gibt, bei denen ein Ceph-Cluster via RBD angebunden wird! Wie aus der oben verlinkten Übersicht der Volume-Plugins hervorgeht, unterstützt RBD nur ReadWriteOnce
– ist also unbrauchbar.
Noch mehr Guides promoten die Verwendung von https://github.com/kubernetes-retired/external-storage. Wie am URL-Pfad retired zu erkennen ist: das Projekt ist tot! Und das verlinkte Docker-Image wurde zuletzt im August 2018 aktualisiert: https://quay.io/repository/external_storage/cephfs-provisioner?tab=tags
Dass es vor Sicherheitslücken nur so strotzt, brauche ich wohl nicht extra zu erwähnen! Wie man in 2020 noch Guides schreiben kann, wo dessen Verwendung empfohlen wird, erschließt sich mir nicht so richtig. Offenbar gibt es da draußen einen Haufen Ignoranten, denen das Thema Security am Allerwertesten vorbei geht, und die sich aber hinterher lauthals über „die bösen Hacker“ echauffieren.
Wenn ihr das Thema nicht ernst nehmt und nur als lästigen Kostenfaktor betrachtet, dann gibt es im Schadensfall nur eine Instanz über die ihr euch zu recht ärgern dürft: euch selbst!
Aber genug der guten Laune – zurück zum eigentlichen Thema.
Im Zusammenhang mit Ceph taucht nämlich fast immer ein weiteres Projekt auf: Rook
Dabei handelt es sich um eine In-Cluster-Lösung – da wird also der Ceph-Cluster direkt in Kubernetes aufgesetzt.
Allerdings soll es seit geraumer Zeit auch möglich sein, damit externe Ceph-Cluster anzusprechen. Theoretisch zumindest. Die Dokumentation dazu ist nämlich alles: nur nicht Zielführend. Passende Adjektive wären eher: „verwirrend“, „unvollständig“ oder schlicht „falsch“.
Ich will da jetz nicht ins Detail gehen, sondern lediglich feststellen: nach zwei Tagen frustrierendem Herumprobieren hab ich es aufgegeben.
Falls ihr das hinbekommen habt: Hut ab – und schickt mir den Link zu eurer Doku/Blogpost!
Jetzt werded ihr sagen „Aber ganz oben steht doch, ihr habt eine lauffähige Lösung mit Ceph!“ – Ja! Haben wir auch – dazu komme ich später. Zunächst will ich hier aber erst noch über die Unzulänglichkeiten der anderen Standard-Lösungen lästern …ähm, berichten 😉
GlusterFS
GlusterFS ist eigentlich ein schönes Projekt. Performance-technisch vielleicht nicht top-notch, aber einfach aufzusetzen und zu bedienen. Es macht einfach einen runden Eindruck …bis es an die Integration in Kubernetes geht. Die Volumes händisch anzulegen geht ja noch – aber wir hatten ja schon festgestellt, dass wir dynamic provisioning wollen.
Das bietet GlusterFS selbst nicht. Allerdings gibt es da ein Projekt namens heketi. Schon ein Blick auf die Projektseite trübt aber die Euphorie:
Due to resource limits on the current project maintainers and general lack of contributions we are considering placing Heketi into a near-maintenance mode. While we plan to continue to fix issues, communicate with users, review PRs, and possibly even make small Quality-of-Life enhancements it is unlikely that any major new development will occur.
Zugegeben, das ist immer noch besser als beim „external-storage“-Plugin für Ceph, aber dennoch alles andere als Ideal. Auch das Setup ist deutlich umfangreicher als das von GlusterFS selbst. Umso frustrierender, dass wir am Ende feststellen mussten: funktioniert nicht! Es ist auf AKS ums verrecken nicht zum Laufen zu kriegen. Und das geht offenbar nicht nur uns so – das ist auch in ein paar anderen Artikeln erwähnt. Unter anderem in dem oben verlinkten Performance-Vergleich.
Damit ist GlusterFS leider auch raus!
Quobyte
Das haben wir nicht getestet, da es sich um eine recht teure, proprietäre Lösung handelt.
NFS
NFS war unser erster Versuch eine bessere Storage-Lösung als AzureFile aufzusetzen. Es ist allerdings beim Versuch geblieben.
Das Plugin ist offenbar reichlich verbugged! Mehr als ein NFS-Share/Volume anbinden? Ist leider nicht möglich: https://github.com/kubernetes/kubernetes/issues/83039
Oh, und lasst euch nicht davon irritieren, dass der Bug geschlossen ist: „Rotten issues close after 30d of inactivity.“ -.-
NFS war also schon an dem Punkt raus.
PortworxVolume
Auch hierbei handelt es sich um eine teure, proprietäre In-Cluster-Lösung.
Falls ihr Erfahrung mit dieser Lösung gesammelt habt und etwas zu sagen könnt, ob sie den oben stehenden Anforderungen gerecht wird: lasst es uns wissen!
Azure Netapp Files + Netapp Trident
Diese Lösung steht nicht auf der Liste in der Kuberetes-Doku. Allerdings wurde sie uns von unseren Ansprechpartnern bei Microsoft empfohlen.
Laut Doku sollte diese Kombination unseren Anforderungen gerecht werden.
Es handelt sich dabei um eine nicht ganz billige Hardware-Lösung, für die man sich dediziert registrieren und freischalten lassen muss. Das kann schon mal ein paar Tage dauern – um das zu testen braucht man also Gedult. Ein guter Guide, wie das eingerichtet wird, kann hier gefunden werden: https://seanluce.com/2020/11/azure-netapp-files-trident-dynamic-and-persistent-storage-for-kubernetes/
Was aber weder dort, noch in den sample-files von Trident selbst vorhanden ist: eine passende storageclass.yaml! Aus diversen Github-Issues hab ich geschlossen, dass die etwa so aussehen muss:
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: anf-csi provisioner: csi.trident.netapp.io parameters: backendType: "azure-netapp-files"
Nach dem Einrichten und anlegen eines PersistentVolumeClaim, wollte das ganze aber noch nicht so recht funktionieren. Er hat zwar ein Volume auf der Netapp angelegt, aber der PVC blieb mit Fehlermeldungen auf „pending“. Bevor ich das aber näher untersuchen konnte, war er dann aber plötzlich da – so nach ca. 15 Minuten. Keine Ahnung was da schief gelaufen war oder ob das normal ist. Probleme beim Ansprechen des Volume ergaben sich dann aber auch, als ich Daten drauf kopiert habe. Es kam dabei immer wieder zu freezes – kann aber auch an der Einbindung via NFSv3 zu tun haben, da hab ich jetzt nicht noch mit Finetuning begonnen, denn:
Am problematischsten ist ohnehin die Tatsache, dass es nicht möglich ist, Volumes anzulegen, die kleiner als 100 Gigabyte sind! Wenn im PVC weniger angefragt werden, wird dennoch mindestens 100GB für das Volume auf der Netapp reserviert. Wenn also viele kleine Volumes benötigt werden, ist das definitiv keine Lösung (vor allem bei dem Preis).
Dann bauen wir uns eben unseren eigenen Provisioner…
Wenn wir uns die Liste so anschauen und die proprietären Lösungen mal außen vor lassen, dann gibt es quasi keine Lösung, die unseren Anforderungen gerecht wird.
Allerdings ist CephFS dicht dran! Wenn nur der Provisioner nicht hoffnungslos veraltet und tot wäre.
Also dachten wir uns, das kann ja kein Hexerwerk sein (weil wir die Kräuterprobe eh nicht bestehen würden ^^).
Von dem ganzen Gedöns im „external-storage“-Plugin interessiert uns ohnehin nur der cephfs-provisioner – und dessen Umfang ist doch überschaubar: ein go-Modul und eins in Python, das ganze verpackt in ein knackiges Dockerfile.
Die verwendeten Libraries wurden ja glücklicherweise in ein separates Projekt ausgelagert, welches durchaus noch gepflegt wird: https://github.com/kubernetes-sigs/sig-storage-lib-external-provisioner
go …away
Der erste Schritt (nach dem Herauslösen des cephfs Moduls) war also, das go-File zum Kompilieren zu kriegen.
Der passende Befehl dazu stand zwar im Makefile – die Dependencies aber nicht.
Erschwerend kam hinzu, dass Dependencies bei go inzwischen über go-modules eingebunden werden – ich werde an dieser Stelle darauf verzichten, mich darüber auszulassen, was für ein dummer Bockmist das ist!
Nachdem das dann halbwegs lief, ging es darum die erwähnte Library zu aktualisieren (referenziert war noch Version 1) – bis Version 3 ist der Code noch kompatibel, also hab ich diese erst einmal eingetragen. Zu diesem Zeitpunkt musste ich das ja überhaupt erst mal ans Laufen kriegen – da wollte ich mich nicht noch damit aufhalten, den Code zur neuesten Version kompatibel umzuschreiben.
Schlangen!? Ich hasse Schlangen!
Dann ging es dem Python-File an den Kragen. Das war nämlich noch Python2-Code! Für alle die nicht in/mit Python entwickeln: Python2 ist EOL: https://blog.python.org/2020/04/python-2718-last-release-of-python-2.html
Ein paar Stellen mussten da also entsprechend angepasst werden. Und zwar nicht nur die offensichtlichen. Beim Testen fiel dann auf, dass an einer Stelle eine Variable, die eigentlich ein String sein sollte, als bytes an den Server geschickt wurde. Der konnte damit nicht viel anfangen und lieferte einen Fehler zurück.
Ein
if type(namespace) is bytes: namespace = namespace.decode('UTF-8')
später, funktionierte aber auch das.
Docker
Dann musste noch das Dockerfile aktualisiert werden. Bei der Gelegenheit haben wir das von CentOS auf Ubuntu umgestellt – einfach auch weil das die Pflege vereinfacht …CentOS setzen wir nirgends ein.
k8s
Zu guter Letzt, galt es noch die yaml-Files für die Kubernetes-Config anzupassen. Also allem vorran das referenzierte Dockerimage auf unseres zeigen zu lassen.
Und noch ein Hinweis bzgl. des „ceph-admin-secret“: das muss genau diesen Namen haben! Im go-Module ist der Name des Secret, nachdem er sucht, mehr oder weniger hardcoded!
Fazit
Zunächst einmal sei Angemerkt, dass wir die beschriebene „Eigenlösung“ (noch) nicht im Produktiveinsatz haben. Umfangreichere Tests stehen erst noch aus.
Immerhin mussten wir ja erst einmal etwas finden, das überhaupt funktioniert.
Eine Odyssee, bei der mehr als einmal der Gedanke aufkam „Wir können doch unmöglich die einzigen sein, die dieses Problem haben!?“
Aber vielleicht sind wir auch einfach zu anspruchsvoll.
Der angepasste cephfs-provisioner ist auf unserer Github-Projektseite zu finden: https://github.com/hmg-dev/cephfs-provisioner
Inklusive einer Buildpipeline für Azure Devops. Nach dem Forken das Anpassen der Docker-Registry in der Pipeline und im k8s-yaml nicht vergessen!
Kommentare von Martin Drößler