Operator Kubernetes în Python fără cadre și SDK

Operator Kubernetes în Python fără cadre și SDK

Go are în prezent monopolul asupra limbajelor de programare pe care oamenii le aleg să scrie declarații pentru Kubernetes. Există motive obiective pentru aceasta, cum ar fi:

  1. Există un cadru puternic pentru dezvoltarea operatorilor în Go - Operator SDK.
  2. Aplicațiile care schimbă jocul precum Docker și Kubernetes sunt scrise în Go. A-ți scrie operatorul în Go înseamnă a vorbi aceeași limbă cu ecosistemul.
  3. Performanță înaltă a aplicațiilor Go și instrumente simple pentru lucrul cu concurență imediată.

NB: Apropo, cum să vă scrieți propria declarație în Go, noi deja descris într-una dintre traducerile noastre de către autori străini.

Dar dacă ești împiedicat să înveți Go din lipsă de timp sau, pur și simplu, de motivație? Articolul oferă un exemplu despre cum puteți scrie o declarație bună folosind una dintre cele mai populare limbi pe care aproape fiecare inginer DevOps le cunoaște - Piton.

Întâlnește: Copiator - operator de copiere!

De exemplu, luați în considerare dezvoltarea unei instrucțiuni simple concepute pentru a copia un ConfigMap fie când apare un nou spațiu de nume, fie când se modifică una dintre cele două entități: ConfigMap și Secret. Din punct de vedere practic, operatorul poate fi util pentru actualizarea în bloc a configurațiilor aplicației (prin actualizarea ConfigMap) sau pentru actualizarea datelor secrete - de exemplu, chei pentru lucrul cu Registrul Docker (la adăugarea Secret în spațiul de nume).

Astfel, ce ar trebui să aibă un operator bun:

  1. Interacțiunea cu operatorul se realizează folosind Definiții personalizate de resurse (denumit în continuare CRD).
  2. Operatorul poate fi configurat. Pentru a face acest lucru, vom folosi steaguri de linie de comandă și variabile de mediu.
  3. Construcția containerului Docker și a diagramei Helm este concepută astfel încât utilizatorii să poată instala cu ușurință (literal cu o singură comandă) operatorul în clusterul lor Kubernetes.

CRD

Pentru ca operatorul să știe ce resurse și unde să caute, trebuie să-i stabilim o regulă. Fiecare regulă va fi reprezentată ca un singur obiect CRD. Ce câmpuri ar trebui să aibă acest CRD?

  1. Tipul de resursă, pe care îl vom căuta (ConfigMap sau Secret).
  2. Lista spațiilor de nume, în care ar trebui să fie amplasate resursele.
  3. Selector de, prin care vom căuta resurse în spațiul de nume.

Să descriem 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

Și îl vom crea imediat regulă simplă — pentru a căuta în spațiul de nume cu numele default toate ConfigMap cu etichete ca copyrator: "true":

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

Gata! Acum trebuie să obținem cumva informații despre regula noastră. Permiteți-mi să fac o rezervare imediat că nu vom scrie noi înșine cereri către serverul API al clusterului. Pentru a face acest lucru, vom folosi o bibliotecă Python gata făcută 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')}

Ca rezultat al rulării acestui cod, obținem următoarele:

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

Grozav: am reușit să obținem o regulă pentru operator. Și cel mai important, am făcut ceea ce se numește modul Kubernetes.

Variabile de mediu sau steaguri? Luăm totul!

Să trecem la configurația principală a operatorului. Există două abordări de bază pentru configurarea aplicațiilor:

  1. utilizați opțiunile din linia de comandă;
  2. utilizați variabile de mediu.

Opțiunile din linia de comandă vă permit să citiți setările mai flexibil, cu suport și validare pentru tipul de date. Biblioteca standard a lui Python are un modul argparser, pe care o vom folosi. Detalii și exemple ale capabilităților sale sunt disponibile în documentație oficială.

Pentru cazul nostru, așa ar arăta un exemplu de configurare a citirii steaguri de linie de comandă:

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

Pe de altă parte, folosind variabilele de mediu în Kubernetes, puteți transfera cu ușurință informații despre serviciu despre pod din interiorul containerului. De exemplu, putem obține informații despre spațiul de nume în care rulează pod-ul cu următoarea construcție:

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

Logica operatorului

Pentru a înțelege cum să separăm metodele de lucru cu ConfigMap și Secret, vom folosi hărți speciale. Apoi putem înțelege ce metode avem nevoie pentru a urmări și a crea obiectul:

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

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

Apoi, trebuie să primiți evenimente de la serverul API. Să o implementăm după cum urmează:

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)

După primirea evenimentului, trecem la logica principală a procesării acestuia:

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

Logica principală este gata! Acum trebuie să împachetăm toate acestea într-un singur pachet Python. Pregătim dosarul setup.py, scrieți acolo meta informații despre proiect:

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: Clientul kubernetes pentru Python are propria sa versiune. Mai multe informații despre compatibilitatea dintre versiunile client și versiunile Kubernetes pot fi găsite în matrice de compatibilitate.

Acum proiectul nostru arată astfel:

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

Docker și Helm

Dockerfile va fi incredibil de simplu: luați imaginea de bază python-alpine și instalați pachetul nostru. Să amânăm optimizarea acestuia până la vremuri mai bune:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

De asemenea, implementarea pentru operator este foarte simplă:

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

În cele din urmă, trebuie să creați un rol adecvat pentru operatorul cu drepturile necesare:

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

Total

Așa, fără teamă, reproș sau fără să învățăm Go, am putut să ne construim propriul operator pentru Kubernetes în Python. Desigur, încă mai are loc să crească: în viitor va putea procesa mai multe reguli, va putea lucra în mai multe fire, va putea monitoriza în mod independent modificările CRD-urilor sale...

Pentru a vă arunca o privire mai atentă asupra codului, l-am introdus depozit public. Dacă doriți exemple de operatori mai serioși implementați folosind Python, vă puteți îndrepta atenția către doi operatori pentru implementarea mongodb (în primul rând и în al doilea rând).

PS Și dacă ești prea lene să faci față evenimentelor Kubernetes sau pur și simplu ești mai obișnuit să folosești Bash, colegii noștri au pregătit o soluție gata făcută sub forma operator-shell (Noi a anunțat este în aprilie).

PPS

Citește și pe blogul nostru:

Sursa: www.habr.com

Adauga un comentariu