Operatore Kubernetes in Python senza framework e SDK

Operatore Kubernetes in Python senza framework e SDK

Go è attualmente un monopolista tra i linguaggi di programmazione che le persone scelgono per scrivere operatori per Kubernetes. Ci sono motivi oggettivi come:

  1. Esiste un potente framework per lo sviluppo di operatori in Go — Operator SDK.
  2. In Go sono scritte applicazioni che hanno "cambiato il gioco" come Docker e Kubernetes. Scrivere il proprio operatore in Go significa comunicare con l'ecosistema nella stessa lingua.
  3. L'alta performance delle applicazioni in Go e gli strumenti semplici per la gestione della concorrenza "out of the box".

NB: A proposito, come scrivere il proprio operatore in Go, lo abbiamo già descritti in una delle nostre traduzioni di autori stranieri.

Ma cosa succede se la mancanza di tempo o, banalmente, la mancanza di motivazione ti ostacolano nello studio di Go? L'articolo offre un esempio di come scrivere un operatore di qualità, utilizzando uno dei linguaggi più popolari, conosciuto praticamente da ogni ingegnere DevOps, — Python.

Presentiamo: Copiatore — un operatore di copia!

Prendiamo ad esempio lo sviluppo di un semplice operatore destinato a copiare un ConfigMap o all'insorgere di un nuovo namespace, oppure quando una delle due entità cambia: ConfigMap e Secret. Dal punto di vista pratico, l'operatore può essere utile per l'aggiornamento massivo delle configurazioni delle applicazioni (aggiornando il ConfigMap) o per l'aggiornamento dei dati segreti — ad esempio, delle chiavi per lavorare con il Docker Registry (con l'aggiunta di un Secret nel namespace).

Quindi, cosa deve avere un buon operatore:

  1. L'interazione con l'operatore avviene attraverso Custom Resource Definitions (di seguito — CRD).
  2. L'operatore può essere configurabile. Utilizzeremo per questo i flag delle righe di comando e le variabili d'ambiente.
  3. La costruzione del contenitore Docker e del chart Helm è progettata in modo che gli utenti possano facilmente (letteralmente con un solo comando) installare l'operatore nel proprio cluster Kubernetes.

CRD

Affinché l'operatore sappia quali risorse cercare e dove trovarle, dobbiamo definire una regola per esso. Ogni regola sarà rappresentata come un singolo oggetto CRD. Quali campi deve avere questo CRD?

  1. Tipo di risorsa, che stiamo cercando (ConfigMap o Secret).
  2. Elenco dei namespace, in cui devono trovarsi le risorse.
  3. Selettore, con cui cercheremo le risorse nel namespace.

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 subito creiamo una regola semplice — per cercare nel namespace con nome default tutti i ConfigMap con label del tipo copyrator: "true":

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

Fatto! Ora dobbiamo in qualche modo ottenere informazioni sulla nostra regola. Preciso subito che non scriveremo richieste al API Server del cluster da soli. Useremo una libreria Python pronta 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')}

Il risultato dell'esecuzione di questo codice sarà il seguente:

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

Ottimo: siamo riusciti a ottenere la regola per l'operatore. E la cosa più importante è che l'abbiamo fatto, come si suol dire, nel modo Kubernetes.

Variabili d'ambiente o flag? Prendiamo tutto!

Passiamo alla configurazione principale dell'operatore. Ci sono due approcci fondamentali per configurare le applicazioni:

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

I parametri della riga di comando consentono di leggere le impostazioni in modo più flessibile, supportando la convalida dei tipi di dati. Nella libreria standard di Python c'è un modulo argparser, di cui faremo uso. I dettagli e alcuni esempi delle sue funzionalità sono disponibili in documentazione ufficiale.

Ecco come apparirà un esempio di configurazione della lettura dei flag della riga di comando nel nostro caso:

   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, tramite le variabili d'ambiente in Kubernetes è possibile trasferire facilmente le informazioni di servizio del pod all'interno del container. Ad esempio, possiamo ottenere le informazioni sul namespace in cui è stato avviato il pod con la seguente struttura:

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

La logica di funzionamento dell'operatore

Per capire come separare i metodi per lavorare con ConfigMap e Secret, utilizzeremo delle mappe speciali. In questo modo potremo comprendere quali metodi sono necessari per il monitoraggio e la creazione dell'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, è necessario ricevere eventi dal server API. Realizziamo ciò nel seguente modo:

def handle(specs):
    kubernetes.config.load_incluster_config()
    v1 = kubernetes.client.CoreV1Api()

    # Otteniamo il metodo per monitorare gli oggetti
    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, passiamo alla logica principale del suo trattamento:

# Типы событий, на которые будем реагировать
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 impacchettare tutto questo in un unico pacchetto Python. Componiamo il file setup.py, scriviamo lì le meta-informazioni del progetto:

from sys import version_info

from setuptools import find_packages, setup

if version_info[:2] < (3, 5):
    raise RuntimeError(
        'Versione python non supportata %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: Il client kubernetes per Python ha la sua versioning. Maggiori dettagli sulla compatibilità tra le versioni del client e le versioni di Kubernetes possono essere trovati nella matrice di compatibilità.

Adesso il nostro progetto appare così:

copyrator
├── copyrator
│   ├── cli.py # Logica per il funzionamento della riga di comando
│   ├── constant.py # Costanti che abbiamo fornito sopra
│   ├── load_crd.py # Logica di caricamento CRD
│   └── operator.py # Logica principale del funzionamento dell'operatore
└── setup.py # Compilazione del pacchetto

Docker e Helm

Il Dockerfile sarà indebitamente semplice: prenderemo un'immagine base python-alpine e installeremo il nostro pacchetto. L'ottimizzazione verrà rimandata a tempi migliori:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Il deployment per l'operatore è altrettanto 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 il ruolo corrispondente 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ì, senza paura, rimprovero e senza dover studiare Go, siamo riusciti a costruire il nostro operatore per Kubernetes in Python. Certo, c'è ancora molto da fare: in futuro sarà in grado di gestire più regole, lavorare in parallelo e monitorare autonomamente le modifiche ai suoi CRD...

Per poter conoscere meglio il codice, lo abbiamo raccolto in un repository pubblico. Se desideri esempi di operatori più avanzati realizzati con Python, puoi dare un'occhiata a due operatori per il deployment di mongodb (primo e secondo).

P.S. Se ti sembra faticoso districarti tra gli eventi di Kubernetes o ti è più comodo usare Bash, i nostri colleghi hanno preparato una soluzione pronta sotto forma di shell-operator (noi hanno annunciato l'abbiamo presentata ad aprile).

P.P.S.

Leggi anche nel nostro blog:

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster