Dieses Jahr fand Europas führende Kubernetes-Konferenz, KubeCon + CloudNativeCon Europe 2020, virtuell statt. Diese Formatänderung hinderte uns jedoch nicht daran, den lange geplanten Bericht „Go? Bash! Meet the Shell-operator“ zu präsentieren, der unserem Open Source-Projekt gewidmet ist. .
Dieser auf dem Vortrag basierende Artikel stellt einen Ansatz zur Vereinfachung des Prozesses zur Erstellung von Operatoren für Kubernetes vor und zeigt, wie Sie mit minimalem Aufwand mithilfe des Shell-Operators Ihre eigenen Operatoren erstellen können.

Vorstellen (~23 Minuten auf Englisch, deutlich informativer als der Artikel) und die Hauptzusammenfassung davon in Textform. Lass uns gehen!
Bei Flant optimieren und automatisieren wir ständig alles. Heute sprechen wir über ein weiteres faszinierendes Konzept. Treffen: Cloud-natives Shell-Scripting!
Beginnen wir jedoch mit dem Kontext, in dem all dies geschieht: Kubernetes.
Kubernetes-API und -Controller
Man kann sich die API in Kubernetes als einen Dateiserver mit Verzeichnissen für jeden Objekttyp vorstellen. Objekte (Ressourcen) auf diesem Server werden durch YAML-Dateien dargestellt. Darüber hinaus verfügt der Server über eine grundlegende API, mit der Sie drei Dinge tun können:
- bekommen Ressource nach Art und Name;
- ändern Ressource (in diesem Fall speichert der Server nur „richtige“ Objekte – alle falsch formatierten oder für andere Verzeichnisse bestimmten Objekte werden verworfen);
- folgen für die Ressource (in diesem Fall erhält der Benutzer sofort die aktuelle/aktualisierte Version).
Kubernetes fungiert also als eine Art Dateiserver (für YAML-Manifeste) mit drei grundlegenden Methoden (ja, es gibt tatsächlich noch andere, aber die überspringen wir vorerst).

Das Problem besteht darin, dass der Server nur Informationen speichern kann. Damit es funktioniert, brauchen Sie Controller – das zweitwichtigste und grundlegendste Konzept in der Kubernetes-Welt.
Es gibt zwei Haupttypen von Controllern. Der erste nimmt Informationen von Kubernetes, verarbeitet sie gemäß der verschachtelten Logik und gibt sie an K8s zurück. Der zweite Typ übernimmt Informationen von Kubernetes, ändert aber im Gegensatz zum ersten Typ den Status einiger externer Ressourcen.
Schauen wir uns den Prozess der Erstellung eines Deployments in Kubernetes genauer an:
- Deployment Controller (enthalten in
kube-controller-manager) ruft Informationen zum Deployment ab und erstellt ein ReplicaSet. - ReplicaSet erstellt basierend auf diesen Informationen zwei Replikate (zwei Pods), aber diese Pods sind noch nicht geplant.
- Der Scheduler plant Pods und fügt ihren YAMLs Knoteninformationen hinzu.
- Kubelets nehmen Änderungen an einer externen Ressource vor (z. B. Docker).
Anschließend wird der gesamte Ablauf in umgekehrter Reihenfolge wiederholt: Kubelet prüft die Container, berechnet den Status des Pods und sendet ihn zurück. Der ReplicaSet-Controller empfängt den Status und aktualisiert den Zustand des Replikatsatzes. Dasselbe passiert mit dem Deployment Controller und der Benutzer erhält schließlich den aktualisierten (aktuellen) Status.

Shell-Betreiber
Es stellt sich heraus, dass Kubernetes auf der gemeinsamen Arbeit verschiedener Controller basiert (Kubernetes-Operatoren sind auch Controller). Es stellt sich die Frage, wie Sie mit minimalem Aufwand Ihren eigenen Operator erstellen können. Und hier kommt das von uns entwickelte Tool zur Rettung . Es ermöglicht Systemadministratoren, mit vertrauten Methoden eigene Operatoren zu erstellen.
Einfaches Beispiel: Geheimnisse kopieren
Schauen wir uns ein einfaches Beispiel an.
Nehmen wir an, wir haben einen Kubernetes-Cluster. Es hat einen Namespace. default mit einigen Geheimnissen mysecret. Darüber hinaus gibt es im Cluster noch weitere Namespaces. An einigen von ihnen ist ein spezielles Etikett angebracht. Unser Ziel ist es, Secret in Namespaces mit einem Label zu kopieren.
Das Problem wird dadurch verkompliziert, dass im Cluster neue Namespaces erscheinen können und einige davon dieses Label haben können. Andererseits sollte beim Löschen des Labels auch „Secret“ gelöscht werden. Darüber hinaus kann sich auch das Geheimnis selbst ändern: In diesem Fall muss das neue Geheimnis in alle Namespaces mit Labels kopiert werden. Wenn ein Geheimnis in einem beliebigen Namespace versehentlich gelöscht wird, muss unser Betreiber es sofort wiederherstellen.
Nachdem die Aufgabe nun formuliert wurde, ist es an der Zeit, mit der Implementierung mithilfe des Shell-Operators zu beginnen. Doch zunächst lohnt es sich, ein paar Worte über den Shell-Operator selbst zu verlieren.
So funktioniert der Shell-Operator
Wie andere Workloads in Kubernetes läuft der Shell-Operator in seinem eigenen Pod. In diesem Pod im Verzeichnis /hooks ausführbare Dateien gespeichert sind. Dies können Skripte in Bash, Python, Ruby usw. sein. Wir nennen solche ausführbaren Dateien Hooks (Haken).

Der Shell-Operator abonniert Kubernetes-Ereignisse und führt diese Hooks als Reaktion auf die von uns benötigten Ereignisse aus.

Woher weiß der Shell-Operator, welcher Hook wann ausgeführt werden soll? Die Sache ist, dass jeder Haken zwei Phasen hat. Beim Start führt der Shell-Operator alle Hooks mit dem Argument aus --config – dies ist die Konfigurationsphase. Und danach werden die Hooks auf die normale Weise gestartet – als Reaktion auf die Ereignisse, an die sie angehängt sind. Im letzteren Fall erhält der Hook einen Bindungskontext (Bindungskontext) – Daten im JSON-Format, auf die wir weiter unten näher eingehen werden.
Erstellen eines Operators in Bash
Jetzt sind wir bereit für die Umsetzung. Dazu müssen wir zwei Funktionen schreiben (übrigens empfehlen wir Bibliothek , was das Schreiben von Hooks in Bash erheblich vereinfacht):
- das erste wird für die Konfigurationsphase benötigt – es gibt den Bindungskontext aus;
- Der zweite enthält die Hauptlogik des Hooks.
#!/bin/bash
source /shell_lib.sh
function __config__() {
cat << EOF
configVersion: v1
# BINDING CONFIGURATION
EOF
}
function __main__() {
# THE LOGIC
}
hook::run "$@"
Im nächsten Schritt müssen wir entscheiden, welche Objekte wir benötigen. In unserem Fall müssen wir Folgendes verfolgen:
- Quellgeheimnis für Änderungen;
- alle Namespaces im Cluster, um zu wissen, welchen ein Label zugeordnet ist;
- Zielgeheimnisse, um sicherzustellen, dass sie alle mit dem Quellgeheimnis synchronisiert sind.
Abonnieren Sie die geheime Quelle
Die Bindungskonfiguration dafür ist recht einfach. Wir geben an, dass wir an Secret mit dem Namen interessiert sind mysecret im Namensraum default:

function __config__() {
cat << EOF
configVersion: v1
kubernetes:
- name: src_secret
apiVersion: v1
kind: Secret
nameSelector:
matchNames:
- mysecret
namespace:
nameSelector:
matchNames: ["default"]
group: main
EOF
Infolgedessen wird der Hook ausgelöst, wenn sich das Quellgeheimnis ändert (src_secret) und erhalten Sie den folgenden Bindungskontext:

Wie Sie sehen, enthält es den Namen und das gesamte Objekt.
Namespaces im Auge behalten
Jetzt müssen Sie Namespaces abonnieren. Dazu geben wir folgende Bindungskonfiguration an:
- name: namespaces
group: main
apiVersion: v1
kind: Namespace
jqFilter: |
{
namespace: .metadata.name,
hasLabel: (
.metadata.labels // {} |
contains({"secret": "yes"})
)
}
group: main
keepFullObjectsInMemory: false
Wie Sie sehen, ist in der Konfiguration ein neues Feld mit dem Namen erschienen jqFilter. Wie der Name schon sagt, jqFilter filtert alle unnötigen Informationen heraus und erstellt ein neues JSON-Objekt mit den für uns interessanten Feldern. Ein Hook mit einer solchen Konfiguration erhält den folgenden Bindungskontext:

Es enthält ein Array filterResults für jeden Namespace im Cluster. Boolesche Variable hasLabel Gibt an, ob das Label diesem Namespace zugeordnet ist. Wähler keepFullObjectsInMemory: false gibt an, dass es nicht notwendig ist, vollständige Objekte im Speicher zu behalten.
Geheime Ziele verfolgen
Wir abonnieren alle Geheimnisse, die einen Annotationssatz haben managed-secret: "yes" (das sind unsere Ziele dst_secrets):
- name: dst_secrets
apiVersion: v1
kind: Secret
labelSelector:
matchLabels:
managed-secret: "yes"
jqFilter: |
{
"namespace":
.metadata.namespace,
"resourceVersion":
.metadata.annotations.resourceVersion
}
group: main
keepFullObjectsInMemory: false
In diesem Fall jqFilter filtert alle Informationen außer dem Namespace und den Parametern heraus resourceVersion. Der letzte Parameter wurde bei der Erstellung des Geheimnisses an die Annotation übergeben: Er ermöglicht es Ihnen, Versionen von Geheimnissen zu vergleichen und auf dem neuesten Stand zu halten.
Ein auf diese Weise konfigurierter Hook erhält bei der Ausführung die drei oben beschriebenen Bindungskontexte. Man kann sie als eine Art Momentaufnahme betrachten (Schnappschuss)-Cluster.

Basierend auf all diesen Informationen kann ein grundlegender Algorithmus entwickelt werden. Es durchläuft alle Namespaces und:
- wenn
hasLabelAngelegenheitentruefür den aktuellen Namespace:- vergleicht das globale Geheimnis mit dem lokalen:
- wenn sie gleich sind, passiert nichts;
- wenn sie unterschiedlich sind - führt
kubectl replaceodercreate;
- vergleicht das globale Geheimnis mit dem lokalen:
- wenn
hasLabelAngelegenheitenfalsefür den aktuellen Namespace:- stellt sicher, dass Secret nicht im angegebenen Namespace liegt:
- wenn ein lokales Geheimnis vorhanden ist - löscht es mit
kubectl delete; - Wenn das lokale Geheimnis nicht gefunden wird, geschieht nichts.
- wenn ein lokales Geheimnis vorhanden ist - löscht es mit
- stellt sicher, dass Secret nicht im angegebenen Namespace liegt:

Sie können es von unserem herunterladen .
So konnten wir mit 35 Zeilen YAML-Konfigurationen und etwa der gleichen Menge Bash-Code einen einfachen Kubernetes-Controller erstellen! Die Aufgabe des Shell-Operators besteht darin, sie miteinander zu verknüpfen.
Das Kopieren von Geheimnissen ist jedoch nicht der einzige Anwendungsbereich des Dienstprogramms. Hier sind noch ein paar Beispiele, die zeigen, was er kann.
Beispiel 1: Änderungen an ConfigMap vornehmen
Sehen wir uns eine Bereitstellung an, die aus drei Pods besteht. Pods verwenden ConfigMap, um einige Konfigurationen zu speichern. Als die Pods gestartet wurden, befand sich die ConfigMap in einem bestimmten Zustand (nennen wir sie v.1). Dementsprechend verwenden alle Pods diese Version von ConfigMap.
Nehmen wir nun an, dass sich die ConfigMap geändert hat (v.2). Pods verwenden jedoch die vorherige Version von ConfigMap (v.1):

Wie kann ich sie dazu bringen, auf die neue ConfigMap (v.2) umzusteigen? Die Antwort ist einfach: Verwenden Sie eine Vorlage. Fügen wir dem Abschnitt eine Prüfsummenannotation hinzu template Bereitstellungskonfigurationen:

Als Ergebnis wird diese Prüfsumme in alle Pods geschrieben und sie ist dieselbe wie die der Bereitstellung. Jetzt müssen Sie nur noch die Anmerkung aktualisieren, wenn sich die ConfigMap ändert. Und der Shell-Operator ist in diesem Fall sehr nützlich. Alles was Sie brauchen, ist es zu programmieren ein Hook, der die ConfigMap abonniert und die Prüfsumme aktualisiert.
Nimmt der Benutzer Änderungen an der ConfigMap vor, bemerkt der Shell-Operator diese und berechnet die Prüfsumme neu. An diesem Punkt kommt die Magie von Kubernetes ins Spiel: Der Orchestrator beendet den Pod, erstellt einen neuen und wartet, bis er Ready, und fahren Sie mit dem nächsten fort. Infolgedessen wird die Bereitstellung synchronisiert und auf die neue Version von ConfigMap migriert.

Beispiel 2: Arbeiten mit benutzerdefinierten Ressourcendefinitionen
Wie Sie wissen, können Sie mit Kubernetes benutzerdefinierte Objekttypen (Arten) erstellen. Sie können beispielsweise eine Art erstellen MysqlDatabase. Nehmen wir an, dieser Typ hat zwei Metadatenparameter: name и namespace.
apiVersion: example.com/v1alpha1
kind: MysqlDatabase
metadata:
name: foo
namespace: bar
Wir haben einen Kubernetes-Cluster mit verschiedenen Namespaces, in denen wir MySQL-Datenbanken erstellen können. In diesem Fall kann der Shell-Operator zur Überwachung von Ressourcen verwendet werden MysqlDatabase, ihre Verbindung zum MySQL-Server und die Synchronisierung der gewünschten und beobachteten Zustände des Clusters.

Beispiel 3: Überwachung eines Cluster-Netzwerks
Wie Sie wissen, ist die Verwendung von Ping die einfachste Möglichkeit, ein Netzwerk zu überwachen. In diesem Beispiel zeigen wir, wie eine solche Überwachung mithilfe des Shell-Operators implementiert wird.
Zunächst müssen Sie die Knoten abonnieren. Der Shell-Operator benötigt den Namen und die IP-Adresse jedes Knotens. Mit ihrer Hilfe wird er diese Knoten anpingen.
configVersion: v1
kubernetes:
- name: nodes
apiVersion: v1
kind: Node
jqFilter: |
{
name: .metadata.name,
ip: (
.status.addresses[] |
select(.type == "InternalIP") |
.address
)
}
group: main
keepFullObjectsInMemory: false
executeHookOnEvent: []
schedule:
- name: every_minute
group: main
crontab: "* * * * *"
Parameter executeHookOnEvent: [] verhindert, dass der Hook als Reaktion auf ein beliebiges Ereignis ausgelöst wird (d. h. als Reaktion auf das Ändern, Hinzufügen oder Entfernen von Knoten). Er wird gestartet (und aktualisieren Sie die Knotenliste) geplant - jede Minute, wie es das Feld vorschreibt schedule.
Nun stellt sich die Frage, wie wir genau über Probleme wie Paketverlust Bescheid wissen. Werfen wir einen Blick auf den Code:
function __main__() {
for i in $(seq 0 "$(context::jq -r '(.snapshots.nodes | length) - 1')"); do
node_name="$(context::jq -r '.snapshots.nodes['"$i"'].filterResult.name')"
node_ip="$(context::jq -r '.snapshots.nodes['"$i"'].filterResult.ip')"
packets_lost=0
if ! ping -c 1 "$node_ip" -t 1 ; then
packets_lost=1
fi
cat >> "$METRICS_PATH" <<END
{
"name": "node_packets_lost",
"add": $packets_lost,
"labels": {
"node": "$node_name"
}
}
END
done
}
Wir durchlaufen die Liste der Knoten, ermitteln ihre Namen und IP-Adressen, pingen sie an und senden die Ergebnisse an Prometheus. Shell-Operator kann Metriken nach Prometheus exportieren, und speichern Sie sie in einer Datei, die sich im in der Umgebungsvariablen angegebenen Pfad befindet $METRICS_PATH.
Sie können einen Operator für die einfache Netzwerküberwachung in einem Cluster erstellen.
Warteschlangenmechanismus
Dieser Artikel wäre nicht vollständig, ohne einen weiteren wichtigen Mechanismus zu beschreiben, der in den Shell-Operator integriert ist. Stellen Sie sich vor, es führt als Reaktion auf ein Ereignis im Cluster einen Hook aus.
- Was passiert, wenn gleichzeitig ein Problem im Cluster auftritt? noch eine Sache Ereignis?
- Wird der Shell-Operator eine weitere Instanz des Hooks ausführen?
- Was passiert, wenn beispielsweise fünf Ereignisse gleichzeitig in einem Cluster auftreten?
- Wird der Shell-Operator sie parallel verarbeiten?
- Was ist mit verbrauchten Ressourcen wie Speicher und CPU?
Glücklicherweise verfügt der Shell-Operator über einen integrierten Warteschlangenmechanismus. Alle Ereignisse werden in eine Warteschlange gestellt und nacheinander verarbeitet.
Lassen Sie uns dies anhand von Beispielen veranschaulichen. Nehmen wir an, wir haben zwei Haken. Das erste Ereignis geht an den ersten Hook. Sobald die Verarbeitung abgeschlossen ist, wird die Warteschlange fortgesetzt. Die nächsten drei Ereignisse werden an den zweiten Hook weitergeleitet – sie werden aus der Warteschlange gezogen und in einem „Batch“ an ihn gesendet. Das heißt Der Hook empfängt eine Reihe von Ereignissen – oder genauer gesagt, eine Reihe von Bindungskontexten.
Auch diese Veranstaltungen können zu einer großen Veranstaltung zusammengefasst werden. Verantwortlich hierfür ist der Parameter group in der Bindungskonfiguration.

Sie können eine beliebige Anzahl von Warteschlangen/Hooks und alle möglichen Kombinationen davon erstellen. Beispielsweise kann eine Warteschlange mit zwei Hooks arbeiten oder umgekehrt.

Sie müssen lediglich das Feld entsprechend einrichten. queue in der Bindungskonfiguration. Wenn kein Warteschlangenname angegeben ist, wird der Hook in der Standardwarteschlange ausgeführt (default). Dieser Warteschlangenmechanismus ermöglicht es uns, alle Ressourcenverwaltungsprobleme bei der Arbeit mit Hooks vollständig zu lösen.
Fazit
Wir haben erklärt, was ein Shell-Operator ist, gezeigt, wie man damit schnell und einfach Kubernetes-Operatoren erstellen kann, und mehrere Beispiele für seine Verwendung gegeben.
Detaillierte Informationen zum Shell-Operator sowie eine Kurzanleitung zur Verwendung finden Sie im entsprechenden . Bei Fragen können Sie uns gerne kontaktieren: Wir beraten Sie gerne in einem (auf Russisch) oder in (auf Englisch).
Und wenn es Ihnen gefallen hat, freuen wir uns immer über neue Issues/PRs/Stars auf GitHub, wo Sie übrigens auch andere finden können . Unter ihnen ist hervorzuheben , der ältere Bruder des Shell-Operators. Dieses Dienstprogramm verwendet Helm-Diagramme zum Installieren von Add-Ons, kann Updates bereitstellen und verschiedene Diagrammparameter/-werte überwachen, den Diagramminstallationsprozess steuern und diese auch als Reaktion auf Ereignisse im Cluster ändern.

Videos und Folien
Video von der Aufführung (~23 Minuten):

Präsentation des Berichts:
PS
Lesen Sie auch auf unserem Blog:
- «";
- «";
- «";
- «.
Source: habr.com
