Operatore Kubernetes in Python senza framework e SDK

Operatore Kubernetes in Python senza framework e SDK

Go attualmente ha il monopolio sui linguaggi di programmazione che le persone scelgono per scrivere istruzioni per Kubernetes. Ci sono ragioni oggettive per questo, come ad esempio:

  1. Esiste un potente framework per lo sviluppo di operatori in Go - SDK dell'operatore.
  2. Le applicazioni rivoluzionarie come Docker e Kubernetes sono scritte in Go. Scrivere il tuo operatore in Go significa parlare la stessa lingua con l'ecosistema.
  3. Elevate prestazioni delle applicazioni Go e strumenti semplici per lavorare con la concorrenza fuori dagli schemi.

NB: A proposito, come scrivere la tua dichiarazione in Go, we già descritto in una delle nostre traduzioni di autori stranieri.

Ma cosa succede se ti viene impedito di imparare il Go per mancanza di tempo o, in poche parole, di motivazione? L'articolo fornisce un esempio di come scrivere una buona dichiarazione utilizzando uno dei linguaggi più popolari che quasi tutti gli ingegneri DevOps conoscono: Python.

Incontra: Copier - operatore di copia!

Ad esempio, considera lo sviluppo di una semplice istruzione progettata per copiare una ConfigMap quando appare un nuovo spazio dei nomi o quando cambia una delle due entità: ConfigMap e Secret. Da un punto di vista pratico, l'operatore può essere utile per l'aggiornamento in blocco delle configurazioni dell'applicazione (aggiornando ConfigMap) o per l'aggiornamento dei dati segreti, ad esempio le chiavi per lavorare con il registro Docker (quando si aggiunge Secret allo spazio dei nomi).

Così, cosa dovrebbe avere un buon operatore:

  1. L'interazione con l'operatore viene effettuata utilizzando Definizioni di risorse personalizzate (di seguito CRD).
  2. L'operatore può essere configurato. Per fare ciò, utilizzeremo i flag della riga di comando e le variabili di ambiente.
  3. La build del contenitore Docker e del grafico Helm è progettata in modo che gli utenti possano facilmente (letteralmente con un comando) installare l'operatore nel proprio cluster Kubernetes.

CRD

Affinché l'operatore sappia quali risorse cercare e dove cercare, dobbiamo impostargli una regola. Ciascuna regola verrà rappresentata come un singolo oggetto CRD. Quali campi dovrebbe avere questo CRD?

  1. Tipo di risorsa, che cercheremo (ConfigMap o Secret).
  2. Elenco degli spazi dei nomi, in cui dovrebbero essere collocate le risorse.
  3. Selettore, mediante il quale cercheremo le risorse nello spazio dei nomi.

Descriviamo il 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

E lo creeremo subito semplice regola — per cercare nello spazio dei nomi con il nome default tutti i ConfigMap con etichette come copyrator: "true":

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

Pronto! Ora dobbiamo in qualche modo ottenere informazioni sulla nostra regola. Consentitemi subito di prenotare che non scriveremo noi stessi le richieste al server API del cluster. Per fare ciò, utilizzeremo una libreria Python già pronta client-kubernetes:

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

Come risultato dell'esecuzione di questo codice, otteniamo quanto segue:

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

Ottimo: siamo riusciti a ottenere una regola per l'operatore. E, cosa più importante, abbiamo adottato quello che viene chiamato il metodo Kubernetes.

Variabili o flag d'ambiente? Prendiamo tutto!

Passiamo alla configurazione principale dell'operatore. Esistono due approcci di base per la configurazione delle applicazioni:

  1. utilizzare le opzioni della riga di comando;
  2. utilizzare variabili d'ambiente.

Le opzioni della riga di comando consentono di leggere le impostazioni in modo più flessibile, con supporto e convalida del tipo di dati. La libreria standard di Python ha un modulo argparser, che utilizzeremo. Dettagli ed esempi delle sue capacità sono disponibili in documentazione ufficiale.

Nel nostro caso, ecco come apparirebbe un esempio di impostazione della lettura dei flag della riga di comando:

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

D'altra parte, utilizzando le variabili di ambiente in Kubernetes, puoi trasferire facilmente le informazioni di servizio sul pod all'interno del contenitore. Ad esempio, possiamo ottenere informazioni sullo spazio dei nomi in cui è in esecuzione il pod con la seguente costruzione:

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

Logica dell'operatore

Per capire come separare i metodi per lavorare con ConfigMap e Secret, utilizzeremo mappe speciali. Quindi possiamo capire di quali metodi abbiamo bisogno per tracciare e creare l'oggetto:

LIST_TYPES_MAP = {
    'configmap': 'list_namespaced_config_map',
    'secret': 'list_namespaced_secret',
}

CREATE_TYPES_MAP = {
    'configmap': 'create_namespaced_config_map',
    'secret': 'create_namespaced_secret',
}

Successivamente, devi ricevere eventi dal server API. Implementiamolo come segue:

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)

Dopo aver ricevuto l'evento, si passa alla logica principale della sua elaborazione:

# Типы событий, на которые будем реагировать
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)

La logica principale è pronta! Ora dobbiamo racchiudere tutto questo in un unico pacchetto Python. Prepariamo il fascicolo setup.py, scrivi lì le meta informazioni sul progetto:

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: il client Kubernetes per Python ha il proprio controllo delle versioni. Ulteriori informazioni sulla compatibilità tra le versioni client e le versioni Kubernetes sono disponibili in matrici di compatibilità.

Ora il nostro progetto si presenta così:

copyrator
├── copyrator
│   ├── cli.py # Логика работы с командной строкой
│   ├── constant.py # Константы, которые мы приводили выше
│   ├── load_crd.py # Логика загрузки CRD
│   └── operator.py # Основная логика работы оператора
└── setup.py # Оформление пакета

Docker e timone

Il Dockerfile sarà incredibilmente semplice: prendi l'immagine base python-alpine e installa il nostro pacchetto. Rimandiamo la sua ottimizzazione a tempi migliori:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Anche l'implementazione per l'operatore è molto semplice:

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

Infine, è necessario creare un ruolo appropriato per l'operatore con i diritti necessari:

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

risultato

È così che, senza paura, rimprovero o apprendimento di Go, siamo stati in grado di creare il nostro operatore per Kubernetes in Python. Certo, ha ancora spazio per crescere: in futuro sarà in grado di elaborare più regole, lavorare su più thread, monitorare in modo indipendente i cambiamenti nei suoi CRD...

Per darti uno sguardo più da vicino al codice, lo abbiamo inserito deposito pubblico. Se desideri esempi di operatori più seri implementati utilizzando Python, puoi rivolgere la tua attenzione a due operatori per la distribuzione di mongodb (prima и secondo).

PS E se sei troppo pigro per gestire gli eventi Kubernetes o semplicemente sei più abituato a usare Bash, i nostri colleghi hanno preparato una soluzione già pronta sotto forma operatore di shell (Noi annunciato ad aprile).

PPS

Leggi anche sul nostro blog:

Fonte: habr.com

Aggiungi un commento