Container bieten eine große Flexibilität. Doch müssen die enthaltenen Daten unter Umständen ebenso gesichert werden wie in anderen Umgebungen auch. Applikationen mit einem "State" speichern ihre Informationen dabei in einem "Persistent Volume". Wer diese Daten in einem Backup sichern möchte, dem stehen verschiedene, aber nicht immer einfache Wege bereit.
Das ursprüngliche Konzept von Scale-out-Anwendungen in Containern sollte auch die Art und Weise ändern, wie Applikationen ihre Daten speichern. So arbeiten Anwendungen verteilt über mehrere Knoten und müssen dabei keine persistenten Daten auf irgendwelche Laufwerke schreiben. Die Datenbestände sind redundant auf Gruppen von Containern verteilt und lagern dort lediglich in temporären Verzeichnissen. Fällt ein Container aus, verliert er zwar den kompletten Datenbestand, doch die redundante Verteilung verhindert den Datenverlust.
Ein neu gestarteter Container synchronisiert sich mit den bestehenden und übernimmt den Anteil der redundanten Daten, über den der zuvor ausgefallene Container verfügte. Die Sicherung der Files soll dabei über Objekte passieren, die die Applikation im laufenden Cluster regelmäßig in S3-Buckets oder vergleichbaren Objektspeichern ablegt. Alle "High-Level"-Speicherfunktionen wie eine zurückrollbare Historie von Objektänderungen und Backups übernimmt dabei das Objektspeichersystem. Frühe Versionen von Container-Clustern verfügten daher auch noch gar nicht über Funktionen, um einem Container ein persistentes Volume zur Datenspeicherung zuzuweisen.
So weit die Theorie der Scale-out-Welt. In der Praxis bedeutet die Umstellung auf eine konsequente Scale-out-Architektur mit statuslosen Containern jedoch, dass Anwender ihre bestehenden Applikationen neu schreiben müssen – nicht unbedingt die Applikationslogik, sondern die Datenspeicherung und -struktur. Bestehende Datenbanktechnologien wie SQL beherrschen nur mit großen Einschränkungen einen statuslosen Scale-out-Betrieb. Dafür wären Objektdatenbanken viel besser geeignet, was aber wiederum ein ziemlich umfangreiches Redesign bestehender Applikationen mit sich bringt. Diese Problematik hatten wir bereits in IT-Administrator 09/2022 [1] ausführlich beschrieben.
Das ursprüngliche Konzept von Scale-out-Anwendungen in Containern sollte auch die Art und Weise ändern, wie Applikationen ihre Daten speichern. So arbeiten Anwendungen verteilt über mehrere Knoten und müssen dabei keine persistenten Daten auf irgendwelche Laufwerke schreiben. Die Datenbestände sind redundant auf Gruppen von Containern verteilt und lagern dort lediglich in temporären Verzeichnissen. Fällt ein Container aus, verliert er zwar den kompletten Datenbestand, doch die redundante Verteilung verhindert den Datenverlust.
Ein neu gestarteter Container synchronisiert sich mit den bestehenden und übernimmt den Anteil der redundanten Daten, über den der zuvor ausgefallene Container verfügte. Die Sicherung der Files soll dabei über Objekte passieren, die die Applikation im laufenden Cluster regelmäßig in S3-Buckets oder vergleichbaren Objektspeichern ablegt. Alle "High-Level"-Speicherfunktionen wie eine zurückrollbare Historie von Objektänderungen und Backups übernimmt dabei das Objektspeichersystem. Frühe Versionen von Container-Clustern verfügten daher auch noch gar nicht über Funktionen, um einem Container ein persistentes Volume zur Datenspeicherung zuzuweisen.
So weit die Theorie der Scale-out-Welt. In der Praxis bedeutet die Umstellung auf eine konsequente Scale-out-Architektur mit statuslosen Containern jedoch, dass Anwender ihre bestehenden Applikationen neu schreiben müssen – nicht unbedingt die Applikationslogik, sondern die Datenspeicherung und -struktur. Bestehende Datenbanktechnologien wie SQL beherrschen nur mit großen Einschränkungen einen statuslosen Scale-out-Betrieb. Dafür wären Objektdatenbanken viel besser geeignet, was aber wiederum ein ziemlich umfangreiches Redesign bestehender Applikationen mit sich bringt. Diese Problematik hatten wir bereits in IT-Administrator 09/2022 [1] ausführlich beschrieben.
Sorgenkind Backup und Restore
Also begannen Anwender, bestehende Applikationen, die sich eigentlich nicht für eine Scale-out-Architektur eignen, in Container zu verlagern. Dementsprechend mussten die Cluster Funktionen einführen, um Containern persistente Speicher zuweisen zu können. Doch mit diesen Persistent Volumes (PVs) kehren alte Probleme zurück, die Plattformen wie Kubernetes eigentlich abschaffen wollten: Backup und Restore. Die Mehrzahl der aktuellen Applikationen hat sich folglich nicht von ihrem SQL-Backend gelöst. Helm-Charts und Operatoren rollen daher das Gros der Applikationen als "Stateful Set" aus – auf einer Architektur, die speziell für Stateless-Anwendungen entwickelt wurde –, zu dessen Kernbestandteilen eine SQL-Datenbank mit lediglich einem Container nebst persistentem Speicher gehören.
Jetzt könnte ein Ansatz darin bestehen, die alten Backuptechnologien gleich mit in die Container zu migrieren, doch so einfach ist es nicht. Gerade weil Container kein vollständiges OS-Image mit Init-System und eigenen Dateisystemtools benutzen, um Dienste im Hintergrund mitlaufen und Dateiänderungen beobachten zu lassen, funktionieren klassische Client-Server-Backups gar nicht. Würden Anwender ihre Container dazu passend umbauen, dann könnten sie eigentlich gleich bei monolithischen Diensten in virtuellen Maschinen bleiben und sich den ganzen Kubernetes-Aufwand sparen. Also müssen Admins sich andere Methoden und Wege ausdenken, um die persistenten Daten ihrer Kubernetes-Applikationen zu sichern.
Nachfolgend stellen wir Ihnen einige Ansätze und Methoden vor, wie Sie Backups ihrer Kubernetes-Anwendungen, bevorzugt SQL-Datenbanken, anfertigen können. Dabei geht es vor allem darum, dass sich diese Datensicherungen bei einem Ausfall auch wieder zügig zurücksichern lassen. Wir beschränken uns dabei auf die Architekturen, da Sie den Code dann ohnehin an Ihre Umgebung und Ihre Applikationen anpassen müssen.
Persistant Volumes sichern
Um Daten von einem Persistenten Volume (PV) in Kubernetes zu sichern, ist es wichtig zu verstehen, wie ein solches PV funktioniert. Da es innerhalb eines Containers keine Dateisystemtools gibt, kann und soll der Container selbst nicht auf externe Speicherressourcen zugreifen. Vielmehr blendet der Host, auf dem der Container arbeitet, den Storage über einen Bind-Mount in das Container-Dateisystem ein. Das ist im Grunde genommen dasselbe Prinzip, wenn Sie bei Docker oder Podman via "--volume /host/dir:/container/dir" ein Unterverzeichnis des lokalen Dateisystems in den Container einblenden. Kurzum: Der Container weiß eigentlich nichts von den externen Volumes, mit denen er arbeitet, sie sind einfach nur ein Teil des Dateisystems.
Im Prinzip kennt Kubernetes zwei Typen von PVs: Block- und Filesystem-PVs. Erstere können Netzwerklaufwerke via iSCSI, rbd oder Fibre Channel sein. In simplen Single-Node-Setups genügen dazu auch lokale Volumes via LVM (Logical Volume Management). Bei Block-PVs erzeugt der Kubernetes-Knoten ein Dateisystem wie XFS auf dem Laufwerk und blendet es dann in den Container ein. Alternativ kommen Filesystem-PVs zum Einsatz, beispielsweise via NFS, GlusterFS oder CephFS. PVs auf Basis eines Netzwerk-Dateisystems lassen sich theoretisch einfacher verwalten als Block-PVs.
Allerdings arbeiten Netzwerkdateisysteme stets ohne Schreibcache und sind daher bei Write-IOs langsamer als Block-PVs – womit sie sich speziell für den Betrieb mit einer Datenbank weniger eignen. Block-Volumes wiederum profitieren davon, dass der Kubernetes-Host, der das Volume mountet und das lokale Dateisystem verwaltet, Schreibzugriffe cachen kann. Das ist gut für die Performance, aber schlecht fürs Backup. Woher genau die Volumes für die PVs stammen, organisieren die Kubernetes-Storageklassen und Treiber, die es von diversen Hard- und Softwareherstellern passend zu ihren Storage-Systemen gibt. Dabei herrschen große Funktionsunterschiede zwischen Storage-Treibern. TopoLVM beispielsweise offeriert nur wenige Funktionen und beschränkt sich auf Single-Node-Setups. Rook auf der anderen Seite verwaltet Ceph-Storage-Cluster mit rbd-Blockdevices oder CephFS-Dateisystemen und beherrscht Funktionen wie Snapshots.
Damit ein Pod auf Kubernetes überhaupt Zugang zu einem oder mehreren PVs erhält, muss es dieses erst einmal mit einem Persistent Volume Claim (PVC) quasi bestellen. Der PVC gibt die Parameter und Zugriffsarten des PV vor. Die häufigste Klasse sind Volumes des Typs "Read-Write-Once" oder RWO für Volumes, die nur einem Container exklusiv zur Verfügung stehen. Solche kommen in der Regel bei Datenbanken zum Einsatz.
Daneben gibt es Read-Only-Many-Volumes, auf die mehrere PODs gleichzeitig zugreifen, aber eben nur mit Lesezugriff. Diese sind vor allem bei Scale-out-Webservern beliebt und liefern den statischen HTML-Inhalt. Der dritte Modus ist entsprechend Read-Write-Many und erlaubt, dass mehrere Container schreibend und lesend auf das Volume zugreifen. Dieser Modus birgt natürlich gewisse Risiken und wird auch nicht von allen Storage-Treibern unterstützt. In der Regel beherrschen Filesystem-PVs diesen Modus.
Erst stoppen, dann sichern
Nach wie vor kursiert das Märchen vom simplen und sicheren Storage-Snapshot als Backup in Foren und Anleitungen. Demnach sollen Anwender einfach einen LVM- oder Filesystem-Snapshot ihrer Datenpartition machen und hätten damit eine zuverlässige Sicherung. Das ist heute genau so falsch wie schon vor 20 Jahren. Ein laufender Datenbankserver – egal ob MySQL, MS-SQL, PostgreSQL oder sogar SQLite – hat immer Dateien für Schreibzugriffe geöffnet. Snapshots von Dateisystemen mit offenen Dateien sind potenziell unbrauchbar, je nachdem, in welchem Zustand die Datei während des Snapshots war.
Bei virtuellen Maschinen mit Datenbanken läuft ein zuverlässiges Snapshot-Backup wie folgt ab: Ein Skript stoppt den DB-Dienst, der alle Daten im Cache (der Datenbank) in den Tablespace schreibt. Das Skript synchronisiert den Cache des Dateisystems (sync) und erst dann löst es den Snapshot aus. Unter Kubernetes muss der Prozess ähnlich ablaufen: Der Anwender stoppt den DB-Pod (und löscht ihn dabei), weist dann den Snapshot des PV via Storage-Treiber an und erzeugt danach einen neuen DB-Pod mit Verbindung zum passenden PV. Ein Restore wäre hier denkbar einfach: Den letzten funktionierenden Snapshot zum vollwertigen PV aufwerten und einen neuen DB-Pod mit diesem PV starten. Das wiederum geht jedoch nur, wenn der PV-Treiber Snapshots beherrscht und ein Backup auf diese Art Sinn ergibt.
Maintanance-Container
Snapshots haben ihre Grenzen. Setzt ein Administrator mehrere Kubernetes-Cluster mit unterschiedlichen Storage-Backends ein, funktioniert ein PV-Snapshot nicht als Backup-Target. Daher gibt es natürlich auch andere Möglichkeiten für eine transportable Sicherung: Den Maintanance Container. Dieser verfügt über Tools, um den Datenbestand einer PV an ein Backup zu schreiben. Dazu nutzt er gegebenenfalls ein zweites PV, das als Backupziel fungiert. Das kann dann schon ein so simpler Vorgang sein wie ein rsync /<quelle> /<ziel> oder tar --czf <backup-datei.tgz> /volume/*. Für Datenbanken empfiehlt sich auch ein vollständiger Dump mit Tools wie mysqldump. Das Backup-PV des Maintanance-Containers kann dabei ein RWM-Filesystem-Volume sein, auf das in der Folge des Dumps eine externe Backupsoftware zugreift und die Sicherungsdaten weiter verarbeitet.
Der Ablauf ist hier ebenso einfach: Der Anwender terminiert erst den Datenbank-Pod, startet den Maintanance-Pod mit Quell- und Ziel-PV und führt das Backuptool darin aus. Anschließend terminiert der Maintanance-Pod und ein neuer DB-Pod übernimmt. Der Haken an dieser Lösung: Je länger das Backup benötigt, desto länger steht die Anwendung still. Der Maintanance-Container muss zudem die passenden Restore-Tools beherbergen, sodass er nach einem Ausfall mit Datenbeschädigung auch die gesicherten Bestände im korrekten Format auf ein frisches PV schreiben kann. Bei anwendungsspezifischen Sicherungstools muss der Anwender ferner auf die Versionen achten. Die Werkzeuge müssen zur Version der verwendeten Software im Applikationscontainer passen.
Fortlaufende Sicherung
Ein weiterer Ansatz wäre das Continuous-Backup. Hier leitet eine Anwendung oder ein DB-Server jede Transaktion reversibel an eine Backupinstanz oder ein Tansaktionslog weiter. Das setzt jedoch voraus, dass die Applikation diese Form der Datenreplikation unterstützt. Zum Glück beherrschen eigentlich alle klassischen SQL-Datenbankserver einen Spiegelmodus. Der aktive Datenbankserver schickt dabei alle schreibenden Änderungen direkt an eine zweite Instanz und nutzt dazu sein eigenes SQL-Protokoll.
Der Anwender kann dann auf dem Backupknoten weitere Tools wie einen DB-Dump starten, ohne dabei die Funktion und Performance des primären DB-Servers negativ zu beeinträchtigen. Reagiert der Backupserver nicht sofort, cached der Quellserver die ankommenden Dateien und leistet sie dann zur Not zeitverzögert an die Sicherungsinstanz.
Ein Continuous-Backup erlaubt zwar, jede einzelne Transaktion in einer Datenbank zu tracken. Aber genau das erschwert auch den Restore. Kommt es zu einem Datenverlust in der laufenden Datenbank, beispielsweise indem durch einen Fehler die DB-Anwedung die Daten unerwünscht löscht oder durch einen Hacker-Angriff, der Datenbestände verschlüsselt und/oder überschreibt, muss der Anwender Schritt- für Schritt durch das Transaktionslog gehen und die letzte "gute" Transaktion finden.
Auf der anderen Seite bedeutet der Mehraufwand, dass der Anwender erst im Nachhinein bestimmen kann, bis zu welchem Zeitpunkt er die Datenbenstände wiederherstellt. Bei Datenbanken mit zahlreichen Schreib- und Änderungszugriffen belegt ein Transaktionslog zudem große Mengen an Speicher. Daher empfiehlt sich hier die Kombination aus "Point-in-Time"-Full-Backups und dem Transaktionslog zwischen dem Stand der laufenden DB und dem letzten Full-Backup.
Tools und Konfigurationen für Kubernetes
In vielen Fällen muss der Administrator bei der Konfiguration eines solchen Continuous-Backup-Szenarios gar nicht allzu kreativ werden. Es gibt eine ganze Reihe vorgefertigter Kubernetes-Rollouts für gespiegelte Datenbanken. Crunchydata beispielsweise offeriert einen sehr leistungsstarken Kubernetes-Operator für PostgreSQL-Datenbanken. Dieser kann mit wenigen Kommandos gespiegelte Cluster aufsetzen und liefert zudem vorgefertigte Pods für das Backup und den Restore von PostgreSQL-Clustern. Die Open-Source-Version des Crunchy-Operators gibt es kostenfrei auf GitHub [2]. Zudem offeriert der Hersteller kostenpflichtige Werkzeuge mit Support für produktive Umgebungen.
Kubernetes kennt verschiedene Methoden, um Applikationen mit Verbindung zu einer PV auszurollen. Ein "Kind", also der Typ eines Kubernetes-API-Funktionsaufrufes ist das "StatefulSet". Dieses beschreibt neben den Pods der Applikation auch eine Vorlage für einen Persistent Volume Claim. Startet ein Anwender ein StatefulSet, generiert dieses dynamisch den PVC der Anwendung. Das heißt im Gegenzug: Löscht der Anwender das StatefulSet, löscht er damit auch den PVC und gegebenenfalls das zugehörige PV. Für Sicherungssetups wie die beschriebene Methode mit dem Maintanance-Container möchten Sie jedoch das Applikations-Rollout löschen, ohne dabei jedoch das PV mit den Daten oder den zugehörigen Claims zu gefährden.
Für solche Szenarien empfiehlt es sich daher, das Rollout mit separaten Definitionsdateien durchzuführen. Das passende "Kind" hierfür ist das "Deployment". Wie das StatefulSet beschreibt es die Applikations-Pods, verzichtet jedoch auf das eingebettete PVC-Template. Stattdessen verweist es auf einen separaten PV-Claim, den sie getrennt vom Deployment anlegen. Eine bereits bestehende Definition eines StatefulSets lässt sich einfach zu einem Deployment umschreiben, indem Sie den Block "volmeClaimTemplate" entfernen und stattdessen in den "volumeMounts" der Pods auf den in einer anderen Datei beschriebenen PVC verweisen. Sie deklarieren dann zwei Deployments: Eines für die Applikation selbst, ein anderes für den Maintanance-Container und in beiden verweisen Sie auf den separat angelegten PVC.
Der komplette Backupvorgang sieht wie folgt aus: Datenbank-Pod löschen, Maintanance-Pod starten und im Anschluss den Datenbank-Pod erneut ausrollen. Für eine Automatisierung eignet sich natürlich ein Tool wie Ansible. Im Internet kursieren zu diesem Thema leider Unmengen schlechter Lösungsansätze, die per shell: kubectl <irgendwas> versuchen, Kubernetes-Ressourcen zu steuern und deren Status abzufragen. Bevorzugt nutzen Sie die Module der Collection "kubernetes.core" und verzichten auf fehleranfällige shell-Aufrufe.
Kubernetes-Ressourcen steuern Sie über das Ansible-Modul "kubernetes.core.k8s". Dabei können Sie den Inhalt ihrer "Kind"-Definition für Kubernetes direkt als "definition:" in den Ansible-Code hineinschreiben, dabei aber Ansible-Variablen einsetzen. Alternativ bauen Sie ihre bestehenden Kubernetes-YML-Deklarationen in Jinja2-Templates um und integrieren diese im Ansible Code, via definition: "{{ lookup('template', 'kubernetes_deployment.j2') }}.
Um ein Deployment zu entfernen, nutzen Sie entsprechend "state: absent". Informationen zu laufenden Pods, Deployments oder Werten einer Config-Map können Sie mit dem Modul "kubernetes.core.k8s_info" abrufen und via "register" in eine Ansbile-Variable packen. Zusammen mit der Loop-Anweisung "until" generieren Sie so eine Warteschleife in Ihrem Backup-Playbook, die nach dem Start des Maintanance-Pods auf dessen Fertigstellung wartet nach dem Schema: register pod state, until status = "Completed". Das komplette Backup-Playbook löscht zuerst das Datenbank-Deployment, startet den Backup-Pod, wartet bis dieser seine Arbeit getan hat und startet dann das Datenbank-Deployment erneut.
Fazit
Ein zuverlässiges Backup für Kubernetes erfordert einiges an Vorbereitung. Aber das sind Kubernetes-Anwender bereits gewohnt, denn schon die Migration ihrer bestehenden Applikationen von virtuellen Maschinen in von Kubernetes gesteuerte Pods hat etlichen Aufwand bereitet. Wer unabhängig von eventuellen Storage-Backups sein möchte, nutzt bevorzugt einen Maintanance-Pod zur Datensicherung. Auch die kontinuierliche Variante hat ihren Reiz, bedeutet aber nochmal einen Mehraufwand. Aber ganz ohne Aufwand nur mit einem Knopfdruck auf einen Snapshot-Button gibt es nun einmal kein verlässliches Backup.