
Derzeit ist Go das dominierende Programmierlanguage, das Entwickler für das Schreiben von Operatoren für Kubernetes wählen. Dafür gibt es objektive Gründe wie:
- Es gibt ein leistungsstarkes Framework zur Entwicklung von Operatoren in Go — .
- So beeindruckende Anwendungen wie Docker und Kubernetes sind in Go geschrieben. Einen eigenen Operator in Go zu schreiben, bedeutet, mit der Community dieselbe Sprache zu sprechen.
- Die hohe Leistung von Go-Anwendungen und die einfachen, sofort einsatzbereiten Werkzeuge für die Arbeit mit Concurrency.
NB: Übrigens haben wir bereits beschrieben, wie man seinen eigenen Operator in Go schreibt, Doch was, wenn Ihnen das Lernen von Go durch Zeitmangel oder einfach mangelnde Motivation im Weg steht? In diesem Artikel wird ein Beispiel gezeigt, wie man einen soliden Operator schreiben kann, indem man eine der beliebtesten Programmiersprachen verwendet, die fast jeder DevOps-Ingenieur kennt —
Hier ist: Kopierator — der Kopier-Operator! Python.
Präsentieren Sie: Kopiator — der Kopieroperator!
Nehmen wir als Beispiel die Entwicklung eines einfachen Operators, der dazu dient, ConfigMaps zu kopieren, entweder beim Erstellen eines neuen Namespaces oder beim Ändern einer der beiden Entitäten: ConfigMap und Secret. Praktisch kann dieser Operator nützlich sein, um Konfigurationen von Anwendungen massenhaft zu aktualisieren (durch das Aktualisieren von ConfigMaps) oder um geheime Daten zu aktualisieren — beispielsweise Schlüssel für den Zugriff auf das Docker-Registry (wenn ein Secret im Namespace hinzugefügt wird).
Also, was einen guten Operator auszeichnet:
- Die Interaktion mit dem Operator erfolgt über (nachfolgend CRD).
- Der Operator kann konfiguriert werden. Hierzu werden wir Kommandozeilenflags und Umgebungsvariablen verwenden.
- Der Aufbau des Docker-Containers und des Helm-Charts wird so gestaltet, dass Benutzer den Operator problemlos (wörtlich mit einem einzigen Befehl) in ihrem Kubernetes-Cluster installieren können.
CRD
Damit der Operator weiß, welche Ressourcen er suchen soll und wo, müssen wir eine Regel für ihn definieren. Jede Regel wird durch ein einzelnes CRD-Objekt dargestellt. Welche Felder sollte dieses CRD haben?
- Ressourcentyp, das wir suchen werden (ConfigMap oder Secret).
- Liste der Namespaces, in denen die Ressourcen untergebracht sein sollen.
- Selector, nach dem wir die Ressourcen im Namespace suchen werden.
Lassen Sie uns die CRD beschreiben:
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 sofort erstellen wir eine einfache Regel — um im Namespace mit dem Namen zu suchen default alle ConfigMaps mit Labels der Form copyrator: "true":
apiVersion: flant.com/v1
kind: CopyratorRule
metadata:
name: main-rule
labels:
module: copyrator
ruleType: configmap
selector:
copyrator: "true"
namespace: defaultFertig! Jetzt müssen wir irgendwie Informationen über unsere Regel erhalten. Ich möchte gleich klarstellen, dass wir keine eigenen Anfragen an den API-Server des Clusters schreiben werden. Dafür nutzen wir eine fertige Python-Bibliothek :
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')}Das Ergebnis dieses Codes wird Folgendes sein:
{'ruleType': 'configmap', 'selector': {'copyrator': 'true'}, 'namespace': ['default']}Großartig: Wir konnten die Regel für den Operator abrufen. Und das Wichtigste ist, dass wir es auf die sogenannte Kubernetes-Art gemacht haben.
Umgebungsvariablen oder Flags? Wir nehmen alles!
Gehen wir zur Hauptkonfiguration des Operators über. Es gibt zwei grundlegende Ansätze zur Konfiguration von Anwendungen:
- Verwendung von Befehlszeilenparametern;
- Verwendung von Umgebungsvariablen.
Befehlszeilenparameter ermöglichen es, die Einstellungen flexibler zu lesen, mit Unterstützung und Validierung von Datentypen. In der Standardbibliothek von Python gibt es das Modul argparser, das wir nutzen werden. Details und Beispiele für seine Möglichkeiten sind verfügbar unter .
So würde ein Beispiel für die Einstellung des Lesens von Befehlszeilenflags in unserem Fall aussehen:
parser = ArgumentParser(
description='Copyrator - Kopieroperator.',
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()Andererseits können Sie mit Umgebungsvariablen in Kubernetes problemlos Betriebsinformationen über den Pod in den Container übertragen. Zum Beispiel können wir die Informationen über den Namespace, in dem der Pod läuft, mit der folgenden Konstruktion abrufen:
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace Logik des Operators
Um zu verstehen, wie man die Methoden für die Arbeit mit ConfigMap und Secret trennt, nutzen wir spezielle Karten. So können wir nachvollziehen, welche Methoden für das Überwachen und Erstellen von Objekten erforderlich sind:
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 wir Ereignisse vom API-Server empfangen. Implementieren wir dies wie folgt:
def handle(specs):
kubernetes.config.load_incluster_config()
v1 = kubernetes.client.CoreV1Api()
# Wir erhalten die Methode zur Überwachung von Objekten
method = getattr(v1, LIST_TYPES_MAP[specs['ruleType']])
func = partial(method, specs['namespace'])
w = kubernetes.watch.Watch()
for event in w.stream(func, _request_timeout=60):
handle_event(v1, specs, event)Nach dem Empfang des Ereignisses gehen wir zur Hauptlogik der Verarbeitung über:
# Типы событий, на которые будем реагировать
ALLOWED_EVENT_TYPES = {'ADDED', 'UPDATED'}
def handle_event(v1, specs, event):
if event['type'] not in ALLOWED_EVENT_TYPES:
return
object_ = event['object']
labels = object_['metadata'].get('labels', {})
# Ищем совпадения по selector'у
for key, value in specs['selector'].items():
if labels.get(key) != value:
return
# Получаем активные namespace'ы
namespaces = map(
lambda x: x.metadata.name,
filter(
lambda x: x.status.phase == 'Active',
v1.list_namespace().items
)
)
for namespace in namespaces:
# Очищаем метаданные, устанавливаем namespace
object_['metadata'] = {
'labels': object_['metadata']['labels'],
'namespace': namespace,
'name': object_['metadata']['name'],
}
# Вызываем метод создания/обновления объекта
methodcaller(
CREATE_TYPES_MAP[specs['ruleType']],
namespace,
object_
)(v1) Die Hauptlogik ist bereit! Jetzt müssen wir das Ganze in ein Python-Paket verpacken. Wir erstellen die Datei setup.py, und fügen dort die Metainformationen zum Projekt ein:
from sys import version_info
from setuptools import find_packages, setup
if version_info[:2] < (3, 5):
raise RuntimeError(
'Nicht unterstützte Python-Version %s.' % '.'.join(version_info)
)
_NAME = 'copyrator'
setup(
name=_NAME,
version='0.0.1',
packages=find_packages(),
classifiers=[
'Entwicklungsstatus :: 3 - Alpha',
'Programmiersprache :: Python',
'Programmiersprache :: Python :: 3',
'Programmiersprache :: Python :: 3.5',
'Programmiersprache :: Python :: 3.6',
'Programmiersprache :: 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 hat seine eigene Versionierung. Mehr über die Kompatibilität zwischen Client-Versionen und Kubernetes-Versionen erfahren Sie aus der .
Unser Projekt sieht jetzt so aus:
copyrator
├── copyrator
│ ├── cli.py # Logik für die Arbeit mit der Befehlszeile
│ ├── constant.py # Konstanten, die wir zuvor besprochen haben
│ ├── load_crd.py # Logik zum Laden von CRD
│ └── operator.py # Hauptlogik für den Operator
└── setup.py # PaketgestaltungDocker und Helm
Die Dockerfile wird extrem einfach sein: Wir nehmen das Basis-Image python-alpine und installieren unser Paket. Die Optimierung lassen wir für später.
FROM python:3.7.3-alpine3.9
ADD . /app
RUN pip3 install /app
ENTRYPOINT ["copyrator"]Das Deployment für den Operator 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 }}-accSchließlich ist es notwendig, die entsprechende Rolle für den Operator mit den erforderlichen Berechtigungen zu 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 }}Zusammenfassung
So haben wir, ohne Angst, Vorurteile oder das Lernen von Go, unseren eigenen Operator für Kubernetes in Python erstellt. Natürlich gibt es noch viel Potenzial zur Verbesserung: Zukünftig könnte er mehrere Regeln verarbeiten, in mehreren Threads arbeiten und selbstständig Änderungen seiner CRDs überwachen.
Um einen näheren Einblick in den Code zu erhalten, haben wir ihn in . Wenn Sie an Beispielen für ernsthaftere Operatoren interessiert sind, die mit Python umgesetzt wurden, schauen Sie sich gerne zwei Operatoren zum Deployment von mongodb an ( und ).
P.S. Falls Sie keine Lust haben, sich mit Kubernetes-Events auseinanderzusetzen oder einfach lieber Bash nutzen — unsere Kollegen haben eine fertige Lösung vorbereitet in Form von (wir haben es im April).
P.P.S.
Lesen Sie auch in unserem Blog:
- «»;
- «»;
- «»;
- «»;
- «».
Quelle: habr.com
