Kubernetes-Operator in Python ohne Frameworks und SDK

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:

  1. Es gibt ein leistungsstarkes Framework für die Entwicklung von Operatoren in Go – Betreiber-SDK.
  2. Bahnbrechende Anwendungen wie Docker und Kubernetes werden in Go geschrieben. Wenn Sie Ihren Operator in Go schreiben, sprechen Sie mit dem Ökosystem dieselbe Sprache.
  3. 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).

somit Was ein guter Bediener haben sollte:

  1. Die Interaktion mit dem Bediener erfolgt über Benutzerdefinierte Ressourcendefinitionen (im Folgenden CRD genannt).
  2. Der Operator ist konfigurierbar. Dazu verwenden wir Befehlszeilenflags und Umgebungsvariablen.
  3. 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?

  1. Ressourcentyp, nach dem wir suchen werden (ConfigMap oder Secret).
  2. Liste der Namensräume, in dem sich die Ressourcen befinden sollen.
  3. Wähler, mit dem wir nach Ressourcen im Namespace suchen.

Beschreiben wir das 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 wir erstellen es sofort einfache Regel – um im Namensraum mit dem Namen zu suchen default alle ConfigMap mit Labels wie copyrator: "true":

apiVersion: flant.com/v1
kind: CopyratorRule
metadata:
  name: main-rule
  labels:
    module: copyrator
ruleType: configmap
selector:
  copyrator: "true"
namespace: default

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:

{'ruleType': 'configmap', 'selector': {'copyrator': 'true'}, 'namespace': ['default']}

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:

  1. Verwenden Sie Befehlszeilenoptionen.
  2. 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:

   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()

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:

env:
- name: NAMESPACE
  valueFrom:
     fieldRef:
         fieldPath: metadata.namespace 

Operatorlogik

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:

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. 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:

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

Abschließend müssen Sie noch eine entsprechende Rolle für den Operator mit den nötigen Rechten anlegen:

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 }}

Ergebnis

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).

PPS

Lesen Sie auch auf unserem Blog:

Source: habr.com

Kommentar hinzufügen