
Go ist derzeit der dominierende Programmiersprache, die Menschen für die Entwicklung von Operatoren für Kubernetes wählen. Dafür gibt es objektive Gründe, wie:
- Es gibt ein mächtiges Framework zur Entwicklung von Operatoren in Go — .
- In Go wurden solche ‚spielverändernden‘ Anwendungen wie Docker und Kubernetes entwickelt. Einen eigenen Operator in Go zu schreiben bedeutet, mit der Ökosystem in einer Sprache zu kommunizieren.
- Die hohe Leistung von Anwendungen in Go und einfache Werkzeuge für die Arbeit mit Concurrency ‚out of the box‘.
NB: Übrigens, wie man seinen eigenen Operator in Go schreibt, haben wir in einem unserer Übersetzungen ausländischer Autoren.
Aber was ist, wenn dir die Zeit oder einfach die Motivation zum Lernen von Go fehlt? In diesem Artikel wird ein Beispiel vorgestellt, wie man einen anständigen Operator mit einer der bekanntesten Sprachen schreiben kann, die fast jeder DevOps-Ingenieur kennt — Python.
Einführung: Kopyrator — der Kopieroperator!
Als Beispiel betrachten wir die Entwicklung eines einfachen Operators, der dazu bestimmt ist, ConfigMap entweder beim Erstellen eines neuen Namespace oder bei der Änderung einer der beiden Entitäten: ConfigMap und Secret zu kopieren. Praktisch kann dieser Operator nützlich sein für die Massenaktualisierung von Anwendungskonfigurationen (durch das Aktualisieren der ConfigMap) oder zur Aktualisierung von geheimen Daten — beispielsweise Schlüsseln für die Verwendung mit Docker Registry (wenn ein Secret im Namespace hinzugefügt wird).
Also, was ein guter Operator haben sollte:
- Die Interaktion mit dem Operator erfolgt über (im Folgenden — CRD).
- Der Operator kann konfiguriert werden. Dazu verwenden wir Befehlszeilenflags und Umgebungsvariablen.
- Der Aufbau des Docker-Containers und des Helm-Charts wird so gestaltet, dass Benutzer den Operator einfach (mit nur einem Befehl) in ihrem Kubernetes-Cluster installieren können.
CRD
Damit der Operator weiß, welche Ressourcen und wo er sie suchen soll, müssen wir eine Regel für ihn festlegen. Jede Regel wird in Form eines einzelnen CRD-Objekts dargestellt. Welche Felder sollte dieses CRD haben?
- Ressourcentyp, den wir suchen werden (ConfigMap oder Secret).
- Liste der Namespaces, in denen sich die Ressourcen befinden sollen.
- Selector, mit dem wir die Ressourcen im Namespace suchen werden.
Lass uns das 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 — zur Suche im Namespace mit dem Namen default aller ConfigMap mit Labels vom Typ 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 die Anfragen an den API-Server des Clusters nicht selbst schreiben werden. Dafür verwenden wir die 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')}Als Ergebnis dieses Codes erhalten wir Folgendes:
{'ruleType': 'configmap', 'selector': {'copyrator': 'true'}, 'namespace': ['default']}Super: Wir konnten die Regel für den Operator abrufen. Und das Wichtigste — wir haben es auf die sogenannte Kubernetes-Art gemacht.
Umgebungsvariablen oder Flags? Wir nehmen alles!
Kommen wir zur Hauptkonfiguration des Operators. Es gibt zwei grundlegende Ansätze zur Konfiguration von Anwendungen:
- Verwendung von Befehlszeilenparametern;
- verwendung von Umgebungsvariablen.
Befehlszeilenparameter ermöglichen es, Einstellungen flexibler und mit Typenvalidierung auszulesen. In der Standardbibliothek von Python gibt es das Modul argparser, das wir verwenden werden. Details und Beispiele seiner Möglichkeiten sind in der .
So sieht für unseren Fall ein Beispiel für das Auslesen von Befehlszeilenflags aus:
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 kann man mit Umgebungsvariablen in Kubernetes die Metainformationen über das Pod einfach in den Container übertragen. Zum Beispiel können wir folgende Konstruktion verwenden, um die Informationen über den Namespace, in dem das Pod läuft, zu erhalten:
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace Die 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 erkennen, welche Methoden wir zur Überwachung und Erstellung von Objekten benötigen:
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. Wir implementieren dies wie folgt:
def handle(specs):
kubernetes.config.load_incluster_config()
v1 = kubernetes.client.CoreV1Api()
# Wir erhalten die Methode zur Überwachung der Objekte
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 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 bereit! Jetzt müssen wir alles in einem Python-Paket verpacken. Wir gestalten die Datei setup.py, und schreiben dort die 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 hat seine eigene Versionierung. Mehr über die Kompatibilität zwischen Client- und Kubernetes-Versionen erfahren Sie aus .
Derzeit sieht unser Projekt so aus:
copyrator
├── copyrator
│ ├── cli.py # Logik zur Arbeit mit der Kommandozeile
│ ├── constant.py # Konstanten, die wir zuvor aufgeführt haben
│ ├── load_crd.py # Logik zum Laden von CRD
│ └── operator.py # Hauptlogik des Operators
└── setup.py # Gestaltung des PaketsDocker und Helm
Das Dockerfile wird erfreulich einfach sein: Wir verwenden ein Basis-Image von python-alpine und installieren unser Paket. Seine Optimierung verschieben wir auf später:
FROM python:3.7.3-alpine3.9
ADD . /app
RUN pip3 install /app
ENTRYPOINT ["copyrator"]Die Bereitstellung 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, eine entsprechende Rolle für den Operator mit den erforderlichen Rechten 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 }}Fazit
So konnten wir ohne Angst, Vorurteile und das Lernen von Go unseren eigenen Operator für Kubernetes in Python erstellen. Natürlich gibt es noch viel zu tun: In Zukunft könnte er mehrere Regeln verarbeiten, in mehreren Threads arbeiten und Änderungen an seinen CRDs selbst überwachen...
Um einen besseren Einblick in den Code zu bekommen, haben wir ihn in gelegt. Wenn Sie an Beispielen ernsthafterer Operatoren, die mit Python implementiert wurden, interessiert sind, können Sie sich zwei Operatoren zur Bereitstellung von MongoDB ansehen ( und ).
P.S. Wenn Sie keine Lust haben, sich mit Kubernetes-Ereignissen auseinanderzusetzen, oder es Ihnen einfach angenehmer ist, Bash zu verwenden — unsere Kollegen haben eine fertige Lösung in Form von (wir im April).
P.P.S.
Lesen Sie auch in unserem Blog:
- «»;
- «»;
- «»;
- «»;
- «».
Quelle: habr.com
