Kubernetes hat sich als führende Plattform für containerisierte Anwendungen etabliert. Allerdings bringt deren Betrieb, insbesondere im Bereich der Datenverwaltung und -sicherung, spezielle Herausforderungen mit sich, die sich von denen in virtuellen Maschinen unterscheiden. Hier setzt das Open-Source-Tool Kanister an, das von Kasten entwickelt wurde und inzwischen zu Veeam gehört. In diesem Artikel erklären wir die Grundlagen des Werkzeugs und zeigen anhand von Beispielen, wie es sich bei der Sicherung in Kubernetes einsetzen lässt.
Für sein Datenmanagement-Tool Kanister [1] hat sich Veeam eine Reihe von Zielen gesetzt. Im Vordergrund steht eine applikationszentrische Sicherung. Konkret bedeutet dies, dass vor allem die Nutzer von Fachanwendungen im Fokus stehen und nicht nur systemnahe Infrastruktur-Admins. Des Weiteren war den Entwicklern ein auf APIs fußender Programmieransatz wichtig – alle ansprechbaren Tasks werden durch eine wohldefinierte API abstrahiert, die darüber hinaus noch sehr gut erweiterbar ist.
ActionSets und Blueprints
Wie bei vielen anderen Kubernetes-Produkten basiert Kanister in seiner Implementierung auf dem Operator-Prinzip. Damit ermöglicht Veeam auf einfache Art und Weise die Paketierung, Bereitstellung und Verwaltung von Kanister und stellt so eine Reihe von Kubernetes-Resource-Definitionen bereit. Insgesamt besteht Kanister aus drei Hauptkomponenten: einem Controller und zwei Custom Resources – und zwar ActionSets und Blueprints. Der Workflow sieht wie folgt aus:
1. In einem ersten Schritt gilt es, ein ActionSet zu erzeugen. Wie die meisten Manifeste ist auch dieses deklarativ. Ein ActionSet beschreibt eine Menge von Aktionen, die zur Laufzeit auf Kubernetes-Ressourcen ausgeführt werden sollen. Dabei ist jede Aktion mit einem Kubernetes-Objekt verknüpft und referenziert einen Blueprint mit den notwendigen Informationen.
Für sein Datenmanagement-Tool Kanister [1] hat sich Veeam eine Reihe von Zielen gesetzt. Im Vordergrund steht eine applikationszentrische Sicherung. Konkret bedeutet dies, dass vor allem die Nutzer von Fachanwendungen im Fokus stehen und nicht nur systemnahe Infrastruktur-Admins. Des Weiteren war den Entwicklern ein auf APIs fußender Programmieransatz wichtig – alle ansprechbaren Tasks werden durch eine wohldefinierte API abstrahiert, die darüber hinaus noch sehr gut erweiterbar ist.
ActionSets und Blueprints
Wie bei vielen anderen Kubernetes-Produkten basiert Kanister in seiner Implementierung auf dem Operator-Prinzip. Damit ermöglicht Veeam auf einfache Art und Weise die Paketierung, Bereitstellung und Verwaltung von Kanister und stellt so eine Reihe von Kubernetes-Resource-Definitionen bereit. Insgesamt besteht Kanister aus drei Hauptkomponenten: einem Controller und zwei Custom Resources – und zwar ActionSets und Blueprints. Der Workflow sieht wie folgt aus:
1. In einem ersten Schritt gilt es, ein ActionSet zu erzeugen. Wie die meisten Manifeste ist auch dieses deklarativ. Ein ActionSet beschreibt eine Menge von Aktionen, die zur Laufzeit auf Kubernetes-Ressourcen ausgeführt werden sollen. Dabei ist jede Aktion mit einem Kubernetes-Objekt verknüpft und referenziert einen Blueprint mit den notwendigen Informationen.
2. Danach liest der Kanister-Controller das übergebene ActionSet aus und lädt die referenzierten Blueprints. Blueprints definieren Befehle zum Ausführen auf Kubernetes-Ressourcen, wie zum Beispiel das Einfrieren einer Datenbank.
3. Nachdem der Controller die entsprechenden Blueprints gefunden und geladen hat, macht er sich an die Arbeit und führt mittels KubeExec / KubeTask die entsprechenden Befehle aus den Blueprints aus.
4. Nach Beendigung aller Aktionen im ActionSet wird eine Statusmeldung erzeugt und im ActionSet gespeichert.
Das Zusammenspiel zwischen Controller, ActionSets und Blueprints in Kanister.
Einfacher Beispiel-Workflow
Wie bereits angesprochen, verwendet Kanister im Hintergrund die Custom Resources ActionSets, Action und Blueprint. Im Folgenden wollen wir an einer Beispielapplikation zeigen, wie Kanister funktioniert. Danach gilt es, die Applikation immer weiter zu verfeinern. Als einfaches Exempel ließe sich folgendes Deployment in Ihrem Kubernetes-Cluster bereitstellen:
cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: time-logger
spec:
replicas: 1
template:
metadata:
labels:
app: time-logger
spec:
containers:
- name: test-container
image: amazon/aws-cli
command: ["sh", "-c"]
args: ["while true; do for x in $(seq 1200); do date >> /var/log/time.log; sleep 1; done; truncate /var/log/time.log --size 0; done"]
EOF
Das Deployment ist dabei simpel gehalten: Mittels eines Shell-Kommandos schreiben Sie das aktuelle Datum in regelmäßigen Abständen in eine Logdatei. Nun können Sie sich an das Verfassen eines Blueprints machen. In vielen Fällen ist dies zwar gar nicht notwendig, weil Kanister bereits eine Vielzahl an fertigen Blueprints in seinem GitHub-Repository mitliefert, etwa für Datenbanken wie PostgreSQL, MySQL, MongoDB, ElasticSearch und viele andere. Ein initialer Blueprint sieht wie folgt aus:
cat <<EOF | kubectl create -f -
apiVersion: cr.kanister.io/v1alpha1
kind: Blueprint
metadata:
name: time-log-bp
namespace: kanister
actions:
backup:
phases:
- func: KubeExec
name: backupToS3
args:
namespace: "{{ .Deployment.Namespace }}"
pod: "{{ index
.Deployment.Pods 0 }}"
container: test-container
command:
- sh
- -c
- echo /var/log/time.log
EOF
Der Blueprint enthält nur eine Aktion mit dem Namen "Backup". Diese führt mittels KubeExec einen Pod aus und übergibt einen Befehl – in der ersten Iteration nur die Ausgabe des Logprotokolls. Nachdem Sie den Blueprint angelegt haben, können Sie sich per kubectl-Befehl vergewissern, dass dies auch funktioniert hat:
Im nächsten Schritt kümmern wir uns um ein ActionSet. Wie bereits erwähnt bedarf es eines ActionSets, um zur Laufzeit Aktionen, die in Blueprints enthalten sind, auszuführen. Die Bereitstellung kann wiederum über kubectl erfolgen:
cat <<EOF | kubectl create -f -
apiVersion: cr.kanister.io/v1alpha1
kind: ActionSet
metadata:
generateName: s3backup-
namespace: kanister
spec:
actions:
- name: backup
blueprint: time-log-bp
object:
kind: Deployment
name: time-logger
namespace: default
EOF
Nach der Ausführung lässt sich der aktuelle Status wie folgt abfragen:
kubectl --namespace kanister get actionsets.cr.kanister.io -o yaml
kubectl --namespace kanister get pod -l app=kanister-operator
Backup verfeinern
An dieser Stelle haben wir nun bereits einen einfachen Kanister-Workflow. Allerdings ist dieser noch nicht sehr generisch. Wollen wir dies ändern und den Workflow konfigurierbar machen, verpacken wir die Konfigurationseinstellungen am besten in eine Kubernetes-ConfigMap. Diese lässt sich dann wiederum aus einem Blueprint auslesen. Mit dem nächsten Befehl hinterlegen wir in der ConfigMap einen entsprechenden Datenpfad, in dem ein Backup erstellt worden soll – den Pfad müssen Sie natürlich an Ihre Umgebung anpassen:
cat <<EOF | kubectl create -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: s3-location
namespace: kanister
data:
path: s3://it-admin-bucket/tutorial
EOF
Jetzt erweitern Sie den Blueprint so, dass er einen Input namens "location" für ConfigMaps entgegennimmt:
cat <<EOF | kubectl apply -f -
apiVersion: cr.kanister.io/v1alpha1
kind: Blueprint
metadata:
name: time-log-bp
namespace: kanister
actions:
backup:
configMapNames:
- location
phases:
- func: KubeExec
name: backupToS3
args:
namespace: "{{
.Deployment.Namespace }}"
pod: "{{ index .Deploy
ment.Pods 0 }}"
container: test-container
command:
- sh
- -c
- |
echo /var/log/time.log
echo "{{ .ConfigMaps. location.Data.path }}"
EOF
An dieser Stelle lässt sich nun ein neues ActionSet erzeugen, in dem wir die erzeugte ConfigMap referenzieren und an den eben definierten Blueprint übergeben:
cat <<EOF | kubectl create -f -
apiVersion: cr.kanister.io/v1alpha1
kind: ActionSet
metadata:
generateName: s3backup-
namespace: kanister
spec:
actions:
- name: backup
blueprint: time-log-bp
object:
kind: Deployment
name: time-logger
namespace: default
configMaps:
location:
name: s3-location
namespace: kanister
EOF
Mit Amazon S3 verzahnen
Nun ist noch die Frage, wie Kanister mit S3 kommunizieren kann, etwa zur Übergabe der für den Zugriff auf S3 benötigten Credentials. Dafür sieht Kubernetes das Anlegen eines Secrets vor. Bei AWS braucht es für den Zugriff einen Access- und einen Secret-Key. Wollen wir diese in einem Secret zugänglich machen, müssen wir dessen Werte als erstes Base64 codieren. Dies kann mittels des Echo-Befehls für beide AWS-Variablen geschehen:
echo -n "YOUR_KEY" | base64
$ cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Secret
metadata:
name: aws-creds
namespace: kanister
type: Opaque
data:
aws_access_key_id: XXXX
aws_secret_access_key: XXXX
EOF
Sobald das Secret im Kubernetes-Cluster angelegt ist, ändern Sie den Blueprint so, dass sich die Credentials aus dem Secret auslesen und durch den Befehl aws s3 cp nutzen lassen. Dafür modifizieren Sie den Command-Abschnitt im Blueprint wie im Listing 1 gezeigt. Wenn Sie im ActionSet beim Antriggern noch das Secret übergeben, gelingt das Backup Richtung S3:
Der bisherige Workflow konnte bereits Daten auf dem Objektspeicher S3 ablegen. Mit der derzeitigen Ausführung geht aber nach dem Durchlauf des ActionSets das Wissen über das Anlegen eines Objekts in S3 verloren. Wie bei anderen Pipelines, etwa GitLab-CI-Pipelines, gibt es auch bei Kanister die Möglichkeit, Objekte dauerhaft als Artefakte zu erzeugen, um später wieder darauf zuzugreifen. Artefakte werden intern als Key-Value-Pärchen gespeichert und sind in einem Blueprint definierbar:
Das obige Beispiel ergänzt den bisherigen Blueprint und definiert ein Output-Artefakt namens "timeLog". Nach Ausführung des ActionSets ist es möglich, sich das entsprechende Artefakt auch im Status anzeigen zu lassen. Neben Output- gibt es auch Input-Artefakte. Diese sind vor allem bei Restore-Vorgängen interessant. Das folgende ActionSet zeigt, wie Sie eine Referenz auf ein Input-Artefakt übergeben:
Wie bisher kommt die mitgelieferte Funktion "KubeExec" zur Ausführung. Diese nutzt den aktuellen Namespace samt dem Pod des Deployments, um den Befehl aws s3 cp auszuführen – nur diesmal in die andere Richtung.
CLI-Befehle vereinfachen
Bisher haben wir alle Befehle für Kanister mittels kubectl ausgeführt. Um die Nutzung zu vereinfachen, hat Veeam jedoch ein dediziertes Kommandozeilen-Tool namens "kanctl", mit dessen Hilfe Sie CLI-Befehle in kürzerer Zeit absetzen können. Das Aufspielen gelingt recht einfach mit folgendem Befehl – eine Installation von Go wird jedoch vorausgesetzt:
Nachdem die Grundlagen von Kanister nun bekannt sind, wollen wir beispielhaft zeigen, wie sich eine PostgreSQL-Datenbank sichern lässt. Damit die Sicherung auf S3 erfolgen kann, müssen Sie den Bucket-Namen zuerst in einer ConfigMap definieren und diese dann in Kubernetes erzeugen. Dies geht wie folgt:
apiVersion: v1
kind: ConfigMap
metadata:
name: postgres-s3-location
data:
bucket: s3://<Bucket-Name>
Entsprechend verfahren Sie mit einem Secret – in das Sie die Base64-codierten AWS-Credentials wie oben bereits gezeigt ablegen:
apiVersion: v1
kind: Secret
metadata:
name: aws-creds
type: Opaque
# Note: the aws keys below must be base64 encoded:
# echo -n "YOUR_KEY" | base64
data:
aws_access_key_id: <XXXX>
aws_secret_access_key: <XXXX>
Der größte Teil ist im Blueprint – siehe Listing 3 – zu erledigen. Das Skript ist auf den ersten Blick recht lang, aber auf den zweiten Blick zeigt sich, dass viele Elemente bekannt sein dürften. Von Relevanz sind vor allem die Aufrufe mit psql, durch die Sie KubeTask-Function kapseln.
Mit Kanister steht ein leistungsstarkes, Kubernetes-natives und Open-Source-basiertes Tool zur Verfügung, das auf die Datensicherung in containerisierten Umgebungen spezialisiert ist. Mit einer Vielzahl vorgefertigter Blueprints für gängige Datenbanken und Messaging-Systeme erleichtert es die Implementierung komplexer Backup-Workflows. Zudem ist es flexibel genug, um sich mühelos an individuelle Bedürfnisse anzupassen.