
Go ist derzeit ein Monopolist unter den Programmiersprachen, mit denen Leute Aussagen für Kubernetes schreiben. Es gibt objektive Gründe wie:
- Es gibt ein leistungsfähiges Framework für die Entwicklung von Operatoren im Go- Operator SDK .
- Go hat verkehrte Anwendungen wie Docker und Kubernetes geschrieben. Um Ihren eigenen Operator in Go zu schreiben, sprechen Sie dieselbe Sprache mit dem Ökosystem.
- Hochleistungsanwendungen für unterwegs und einfache Tools für die sofortige Arbeit mit Parallelität.
NB : Übrigens haben wir bereits in einer unserer Übersetzungen ausländischer Autoren beschrieben, wie Sie Ihren eigenen Operator auf Go schreiben können.Aber was ist, wenn das Lernen von Go durch Zeitmangel oder triviale Motivation verhindert wird? Der Artikel enthält ein Beispiel dafür, wie Sie einen soliden Operator mit einer der beliebtesten Sprachen schreiben können, die fast jeder DevOps-Ingenieur kennt -
Python .
Treffen Sie: Texter - Kopierer!
Betrachten Sie beispielsweise die Entwicklung eines einfachen Operators zum Kopieren von ConfigMap, wenn ein neuer Namespace angezeigt wird oder wenn sich eine der beiden Entitäten ändert: ConfigMap und Secret. Aus Sicht der praktischen Anwendung kann der Bediener nützlich sein, um Anwendungskonfigurationen massenweise zu aktualisieren (durch Aktualisieren von ConfigMap) oder um geheime Daten zu aktualisieren - beispielsweise Schlüssel für die Arbeit mit der Docker-Registrierung (beim Hinzufügen von Secret zum Namespace).
Was sollte ein guter Bediener haben :
- Die Interaktion mit dem Bediener erfolgt über benutzerdefinierte Ressourcendefinitionen (im Folgenden: CRD).
- Der Bediener kann angepasst werden. Zu diesem Zweck verwenden wir Befehlszeilenflags und Umgebungsvariablen.
- Die Zusammenstellung des Docker-Containers und des Helm-Diagramms wird ausgearbeitet, damit Benutzer den Operator einfach (buchstäblich mit einem Befehl) in ihrem Kubernetes-Cluster installieren können.
CRD
Damit der Bediener weiß, welche Ressourcen und wo er suchen muss, müssen wir eine Regel für ihn festlegen. Jede Regel wird als einzelnes CRD-Objekt dargestellt. Welche Felder sollte diese CRD haben?
- Die Art der Ressource, nach der wir suchen (ConfigMap oder Secret).
- Eine Liste der Namespaces, in denen sich Ressourcen befinden sollen.
- Selektor , mit dem wir nach Ressourcen im Namespace suchen.
Wir beschreiben die CRD:
apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: copyrator.flant.com spec: group: flant.com versions: - name: v1 served: true storage: true scope: Namespaced names: plural: copyrators singular: copyrator kind: CopyratorRule shortNames: - copyr validation: openAPIV3Schema: type: object properties: ruleType: type: string namespaces: type: array items: type: string selector: type: string
Und erstellen Sie sofort eine
einfache Regel - um im Namespace mit dem
default
allen ConfigMap mit Bezeichnungen wie
copyrator: "true"
:
apiVersion: flant.com/v1 kind: CopyratorRule metadata: name: main-rule labels: module: copyrator ruleType: configmap selector: copyrator: "true" namespace: default
Fertig! Jetzt müssen Sie irgendwie Informationen über unsere Regel erhalten. Ich muss sofort reservieren, dass wir keine Anfragen an die Cluster-Server-API schreiben. Dazu verwenden wir die vorgefertigte Python-Bibliothek
kubernetes-client :
import kubernetes from contextlib import suppress CRD_GROUP = 'flant.com' CRD_VERSION = 'v1' CRD_PLURAL = 'copyrators' def load_crd(namespace, name): client = kubernetes.client.ApiClient() custom_api = kubernetes.client.CustomObjectsApi(client) with suppress(kubernetes.client.api_client.ApiException): crd = custom_api.get_namespaced_custom_object( CRD_GROUP, CRD_VERSION, namespace, CRD_PLURAL, name, ) return {x: crd[x] for x in ('ruleType', 'selector', 'namespace')}
Als Ergebnis dieses Codes erhalten wir Folgendes:
{'ruleType': 'configmap', 'selector': {'copyrator': 'true'}, 'namespace': ['default']}
Hervorragend: Wir haben es geschafft, eine Regel für den Betreiber zu bekommen. Und vor allem haben wir das gemacht, was man Kubernetes nennt.
Umgebungsvariablen oder Flags? Wir nehmen alles!
Wir gehen zur Hauptkonfiguration des Bedieners über. Es gibt zwei grundlegende Ansätze zum Konfigurieren von Anwendungen:
- Verwenden Sie Befehlszeilenoptionen
- Umgebungsvariablen verwenden.
Mit den Befehlszeilenoptionen können Sie Einstellungen flexibler lesen und Datentypen unterstützen und validieren. Die Python-Standardbibliothek verfügt über ein
argparser
Modul, das wir verwenden werden. Details und Beispiele seiner Funktionen finden Sie in der
offiziellen Dokumentation .
So sieht das Beispiel für das Setzen des Lesens von Befehlszeilenflags für unseren Fall aus:
parser = ArgumentParser( description='Copyrator - copy operator.', prog='copyrator' ) parser.add_argument( '--namespace', type=str, default=getenv('NAMESPACE', 'default'), help='Operator Namespace' ) parser.add_argument( '--rule-name', type=str, default=getenv('RULE_NAME', 'main-rule'), help='CRD Name' ) args = parser.parse_args()
Auf der anderen Seite können Sie mithilfe von Umgebungsvariablen in Kubernetes problemlos Serviceinformationen über den Pod in den Container übertragen. Beispielsweise können wir Informationen über den Namespace abrufen, in dem der Pod mit der folgenden Konstruktion ausgeführt wird:
env: - name: NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace
Bedienerlogik
Um zu verstehen, wie die Methoden für die Arbeit mit ConfigMap und Secret getrennt werden, verwenden wir spezielle Karten. Dann können wir verstehen, welche Methoden wir benötigen, um das Objekt zu verfolgen und zu erstellen:
LIST_TYPES_MAP = { 'configmap': 'list_namespaced_config_map', 'secret': 'list_namespaced_secret', } CREATE_TYPES_MAP = { 'configmap': 'create_namespaced_config_map', 'secret': 'create_namespaced_secret', }
Als Nächstes müssen Sie Ereignisse vom API-Server empfangen. Wir implementieren es wie folgt:
def handle(specs): kubernetes.config.load_incluster_config() v1 = kubernetes.client.CoreV1Api()
Nach dem Empfang des Ereignisses fahren wir mit der Hauptlogik seiner Verarbeitung fort:
Grundlegende Logik ist fertig! Jetzt müssen Sie all dies in ein Python-Paket packen. Wir führen die
setup.py
und schreiben dort Metainformationen über das Projekt:
from sys import version_info from setuptools import find_packages, setup if version_info[:2] < (3, 5): raise RuntimeError( 'Unsupported python version %s.' % '.'.join(version_info) ) _NAME = 'copyrator' setup( name=_NAME, version='0.0.1', packages=find_packages(), classifiers=[ 'Development Status :: 3 - Alpha', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', ], author='Flant', author_email='maksim.nabokikh@flant.com', include_package_data=True, install_requires=[ 'kubernetes==9.0.0', ], entry_points={ 'console_scripts': [ '{0} = {0}.cli:main'.format(_NAME), ] } )
NB : Der kubernetes-Client für Python verfügt über eine eigene Versionierung. Weitere Informationen zur Kompatibilität zwischen Clientversionen und Kubernetes-Versionen finden Sie in der Kompatibilitätsmatrix .Jetzt sieht unser Projekt so aus:
copyrator ├── copyrator │ ├── cli.py # │ ├── constant.py # , │ ├── load_crd.py # CRD │ └── operator.py # └── setup.py #
Docker und Helm
Das Dockerfile wird unglaublich einfach sein: Nehmen Sie das grundlegende Python-Alpine-Image und installieren Sie unser Paket. Wir werden die Optimierung auf bessere Zeiten verschieben:
FROM python:3.7.3-alpine3.9 ADD . /app RUN pip3 install /app ENTRYPOINT ["copyrator"]
Die Bereitstellung für den Bediener ist ebenfalls sehr einfach:
apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Chart.Name }} spec: selector: matchLabels: name: {{ .Chart.Name }} template: metadata: labels: name: {{ .Chart.Name }} spec: containers: - name: {{ .Chart.Name }} image: privaterepo.yourcompany.com/copyrator:latest imagePullPolicy: Always args: ["--rule-type", "main-rule"] env: - name: NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace serviceAccountName: {{ .Chart.Name }}-acc
Schließlich müssen Sie die entsprechende Rolle für den Bediener mit den erforderlichen Rechten erstellen:
apiVersion: v1 kind: ServiceAccount metadata: name: {{ .Chart.Name }}-acc --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: name: {{ .Chart.Name }} rules: - apiGroups: [""] resources: ["namespaces"] verbs: ["get", "watch", "list"] - apiGroups: [""] resources: ["secrets", "configmaps"] verbs: ["*"] --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: name: {{ .Chart.Name }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: {{ .Chart.Name }} subjects: - kind: ServiceAccount name: {{ .Chart.Name }}-acc
Zusammenfassung
Ohne Angst, Vorwurf und Lernen von Go konnten wir unseren eigenen Operator für Kubernetes in Python zusammenstellen. Natürlich hat er noch Raum zum Wachsen: In Zukunft wird er in der Lage sein, mehrere Regeln zu verarbeiten, in mehreren Threads zu arbeiten und Änderungen in seiner CRD unabhängig zu überwachen ...
Um den Code genauer zu betrachten, haben wir ihn in ein
öffentliches Repository gestellt . Wenn Sie Beispiele für ernsthaftere Operatoren wünschen, die mit Python implementiert wurden, können Sie Ihre Aufmerksamkeit auf zwei Operatoren für die Bereitstellung von Mongodb lenken (den
ersten und den
zweiten ).
PS Und wenn Sie zu faul sind, um mit Kubernetes-Ereignissen umzugehen, oder wenn Sie sich mit Bash nur wohler fühlen, haben unsere Kollegen eine vorgefertigte Lösung in Form eines
Shell-Operators vorbereitet (wir
haben dies im April
angekündigt ).
PPS
Lesen Sie auch in unserem Blog: