Kubernetes-Operator in Python ohne Frameworks und SDK
Go hat derzeit das Monopol auf die Programmiersprachen, die Menschen zum Schreiben von Anweisungen für Kubernetes wählen. Dafür gibt es objektive Gründe, wie zum Beispiel:
Es gibt ein leistungsstarkes Framework für die Entwicklung von Operatoren in Go – Betreiber-SDK.
Bahnbrechende Anwendungen wie Docker und Kubernetes werden in Go geschrieben. Wenn Sie Ihren Operator in Go schreiben, sprechen Sie mit dem Ökosystem dieselbe Sprache.
Hohe Leistung von Go-Anwendungen und einfache Tools für die Arbeit mit Parallelität sofort einsatzbereit.
NB: Übrigens, wie man seine eigene Aussage in Go schreibt, wir bereits beschrieben in einer unserer Übersetzungen ausländischer Autoren.
Was aber, wenn Sie aus Zeitmangel oder einfach aus Motivation daran gehindert werden, Go zu lernen? Der Artikel liefert ein Beispiel dafür, wie Sie eine gute Erklärung in einer der beliebtesten Sprachen schreiben können, die fast jeder DevOps-Ingenieur kennt – Python.
Lernen Sie kennen: Kopierer – Kopierer!
Betrachten Sie beispielsweise die Entwicklung einer einfachen Anweisung, die dazu dient, eine ConfigMap zu kopieren, entweder wenn ein neuer Namespace erscheint oder wenn sich eine von zwei Entitäten ändert: ConfigMap und Secret. Aus praktischer Sicht kann der Operator für die Massenaktualisierung von Anwendungskonfigurationen (durch Aktualisierung der ConfigMap) oder für die Aktualisierung geheimer Daten nützlich sein – beispielsweise Schlüssel für die Arbeit mit der Docker-Registrierung (beim Hinzufügen von Secret zum Namespace).
Der Operator ist konfigurierbar. Dazu verwenden wir Befehlszeilenflags und Umgebungsvariablen.
Der Build des Docker-Containers und des Helm-Charts ist so konzipiert, dass Benutzer den Operator einfach (im wahrsten Sinne des Wortes mit einem Befehl) in ihrem Kubernetes-Cluster installieren können.
CRD
Damit der Bediener weiß, nach welchen Ressourcen er suchen 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 dieses CRD haben?
Ressourcentyp, nach dem wir suchen werden (ConfigMap oder Secret).
Liste der Namensräume, in dem sich die Ressourcen befinden sollen.
Wähler, mit dem wir nach Ressourcen im Namespace suchen.
Bereit! Jetzt müssen wir irgendwie Informationen über unsere Herrschaft bekommen. Lassen Sie mich gleich einen Vorbehalt machen, dass wir selbst keine Anfragen an den Cluster-API-Server schreiben. Dazu verwenden wir eine 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 der Ausführung dieses Codes erhalten wir Folgendes:
Großartig: Wir haben es geschafft, eine Regel für den Operator zu bekommen. Und was am wichtigsten ist: Wir haben das gemacht, was man den Kubernetes-Weg nennt.
Umgebungsvariablen oder Flags? Wir nehmen alles!
Kommen wir zur Hauptbetreiberkonfiguration. Es gibt zwei grundlegende Ansätze zum Konfigurieren von Anwendungen:
Verwenden Sie Befehlszeilenoptionen.
Umgebungsvariablen verwenden.
Mit Befehlszeilenoptionen können Sie Einstellungen flexibler lesen, mit Datentypunterstützung und -validierung. Die Standardbibliothek von Python verfügt über ein Modul argparser, die wir verwenden werden. Einzelheiten und Beispiele seiner Fähigkeiten finden Sie in amtliche Dokumentation.
In unserem Fall würde ein Beispiel für die Einrichtung von Lese-Befehlszeilen-Flags wie folgt aussehen:
Andererseits können Sie mithilfe von Umgebungsvariablen in Kubernetes problemlos Serviceinformationen über den Pod innerhalb des Containers übertragen. Informationen über den Namespace, in dem der Pod läuft, können wir beispielsweise mit folgendem Aufbau erhalten:
Um zu verstehen, wie man Methoden für die Arbeit mit ConfigMap und Secret trennt, verwenden wir spezielle Maps. Dann können wir verstehen, welche Methoden wir benötigen, um das Objekt zu verfolgen und zu erstellen:
Als nächstes müssen Sie Ereignisse vom API-Server empfangen. Lassen Sie es uns wie folgt implementieren:
def handle(specs):
kubernetes.config.load_incluster_config()
v1 = kubernetes.client.CoreV1Api()
# Получаем метод для слежения за объектами
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)
Nachdem wir das Ereignis erhalten haben, gehen wir zur Hauptlogik seiner 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 fertig! Jetzt müssen wir das alles in ein Python-Paket packen. Wir bereiten die Datei vor setup.py, schreibe dort Metainformationen zum 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='[email protected]',
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 Client-Versionen und Kubernetes-Versionen finden Sie in Kompatibilitätsmatrizen.
Nun sieht unser Projekt so aus:
copyrator
├── copyrator
│ ├── cli.py # Логика работы с командной строкой
│ ├── constant.py # Константы, которые мы приводили выше
│ ├── load_crd.py # Логика загрузки CRD
│ └── operator.py # Основная логика работы оператора
└── setup.py # Оформление пакета
Docker und Helm
Die Docker-Datei wird unglaublich einfach sein: Nehmen Sie das Basis-Python-Alpine-Image und installieren Sie unser Paket. Verschieben wir die Optimierung auf bessere Zeiten:
FROM python:3.7.3-alpine3.9
ADD . /app
RUN pip3 install /app
ENTRYPOINT ["copyrator"]
Auch die Bereitstellung für den Betreiber ist denkbar einfach:
So konnten wir ohne Angst, Vorwürfe oder das Erlernen von Go unseren eigenen Operator für Kubernetes in Python erstellen. Natürlich hat es noch Raum für Wachstum: In Zukunft wird es in der Lage sein, mehrere Regeln zu verarbeiten, in mehreren Threads zu arbeiten, Änderungen in seinen CRDs unabhängig zu überwachen ...
Um Ihnen einen genaueren Blick auf den Code zu ermöglichen, haben wir ihn eingefügt öffentliches Repository. 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 richten (erste и zweite).
PS Und wenn Sie zu faul sind, sich mit Kubernetes-Ereignissen auseinanderzusetzen, oder Sie einfach eher an die Verwendung von Bash gewöhnt sind, haben unsere Kollegen in der Form eine fertige Lösung vorbereitet Shell-Operator (Wir angekündigt es im April).