Kubernetes Operator Pythonban keretrendszerek és SDK-k nélkül

Kubernetes Operator Pythonban keretrendszerek és SDK-k nélkül

A Go jelenleg a Kubernetes operátorok írására választott programozási nyelvek monopolistája. Ezt számos objektív ok indokolja:

  1. Van egy erőteljes keretrendszer az operátorok Go nyelven történő fejlesztésére — Operator SDK.
  2. A Go nyelven írták az olyan "átformáló" alkalmazásokat, mint a Docker és a Kubernetes. Saját operátor írása Go nyelven azt jelenti, hogy egy nyelven kommunikálunk az ökoszisztémával.
  3. A Go alkalmazások magas teljesítménye és az előre beépített egyszerű eszközök a párhuzamosság kezelésére.

NB: Egyébként, hogyan lehet megírni a saját operátorát Go nyelven, azt már leírtuk az egyik külföldi szerzők fordításában.

De mi van, ha az idő hiánya vagy egyszerűen csak a motiváció hiánya gátolja az Ön Go nyelvű tanulmányait? A cikk példát mutat arra, hogyan lehet jó operátort írni, használva az egyike a legnépszerűbb nyelveknek, amelyet szinte minden DevOps mérnök ismer — Python.

Itt van: a Kópirovat - a másoló operátor!

Példaként nézzük meg egy egyszerű operátor fejlesztését, amely ConfigMap másolására szolgál új namespace megjelenésekor, vagy az egyik két entitás, a ConfigMap és a Secret módosulásakor. Praktikus alkalmazás szempontjából az operátor hasznos lehet az alkalmazások konfigurációinak tömeges frissítésére (a ConfigMap frissítésével) vagy a titkos adatok frissítésére — például a Docker Registry-hez szükséges kulcsokra (amikor Secret-et adunk hozzá a namespace-hez).

Tehát, mi legyen egy jó operátorban:

  1. Az operátorral való interakció a Custom Resource Definitions (továbbiakban — CRD) révén történik.
  2. Az operátor konfigurálható. Ehhez parancssori kapcsolókat és környezeti változókat fogunk használni.
  3. A Docker-konténer és a Helm-diagramos összeállításokat úgy dolgozzuk ki, hogy a felhasználók könnyen (szinte egyetlen parancs segítségével) telepíthessék az operátort a Kubernetes klaszterükbe.

CRD

Ahhoz, hogy az operátor tudja, milyen erőforrásokat és hol kell keresnie, szabályt kell megadnunk számára. Minden szabályt egy CRD objektum formájában fogunk megjeleníteni. Milyeneknek kell lennie ennek a CRD-nek a mezői?

  1. Erőforrás típusa, amelyet keresni fogunk (ConfigMap vagy Secret).
  2. Namespace-ek listája, ahol az erőforrásoknak lennie kell.
  3. Kiválasztó, amellyel az erőforrásokat a namespace-ben keresni fogjuk.

Írjuk le a CRD-t:

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

És már azonnal létrehozunk egyszerű szabályt — a névterület keresésére a következő névvel alapértelmezett minden ConfigMap-ot az alábbi címkékkel copyrator: "true":

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

Kész! Most valahogy információt kell szereznünk a szabályunkról. Azonnal szeretném megjegyezni, hogy nem fogunk saját API Server lekérdezéseket írni a klaszterhez. Ehhez kész Python könyvtárat fogunk használni 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')}

A kód futása után a következő eredményt kapjuk:

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

Remek: sikerült megszereznünk a szabályt az operátortól. És ami a legfontosabb — ezt úgy tettük, ahogy a Kubernetes-ben kell.

Környezeti változók vagy zászlók? Mindent elviszünk!

Átérünk az operátor fő konfigurációjára. Két alapvető megközelítés van az alkalmazások konfigurálásához:

  1. parancssori argumentumok használata;
  2. környezeti változók használata.

A parancssori argumentumok rugalmasabbá teszik a beállítások olvasását, támogatva a típusok ellenőrzését is. A Python standard könyvtárában van egy modul, argparser, amelyet használni fogunk. Részletek és példák a lehetőségeiről elérhetőek hivatalos dokumentációban.

Íme, hogyan fog kinézni az argumentumok parancssori olvasására szolgáló beállítás példa a mi esetünkben:

   parser = ArgumentParser(
        description='Copyrator - másoló operátor.',
        prog='copyrator'
    )
    parser.add_argument(
        '--namespace',
        type=str,
        default=getenv('NAMESPACE', 'default'),
        help='Operátor névtere'
    )
    parser.add_argument(
        '--rule-name',
        type=str,
        default=getenv('RULE_NAME', 'main-rule'),
        help='CRD név'
    )
    args = parser.parse_args()

Másrészt a Kubernetes környezeti változóin keresztül könnyen átvitelhetjük a pod-hoz kapcsolódó információt a konténerbe. Például, a namespace információt, ahol a pod fut, a következő konstrukcióval kaphatjuk meg:

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

Az operátor működésének logikája

Ahhoz, hogy megértsük, hogyan különböztessük meg a ConfigMap és a Secret kezelési módszereit, használjunk speciális térképeket. Így megérthetjük, hogy milyen módszerekre van szükségünk az objektumok figyeléséhez és létrehozásához:

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

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

Ezután szükséges eseményeket kapni az API szerverről. Ezt a következőképpen valósítjuk meg:

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

    # Megkapjuk az objektumok figyeléséhez szükséges módszert
    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)

Az esemény fogadása után áttérünk a feldolgozás fő logikájára:

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

A fő logika készen áll! Most mindezt egy Python csomagba kell csomagolnunk. Elkészítjük a fájlt setup.py, és beleírjuk a projekt metaadatait:

from sys import version_info

from setuptools import find_packages, setup

if version_info[:2] < (3, 5):
    raise RuntimeError(
        'Nem támogatott python verzió %s.' % '.'.join(version_info)
    )


_NAME = 'copyrator'
setup(
    name=_NAME,
    version='0.0.1',
    packages=find_packages(),
    classifiers=[
        'Fejlesztési állapot :: 3 - Alpha',
        'Programozási nyelv :: Python',
        'Programozási nyelv :: Python :: 3',
        'Programozási nyelv :: Python :: 3.5',
        'Programozási nyelv :: Python :: 3.6',
        'Programozási nyelv :: 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: A Python-hoz készült kubernetes kliens verziója szigorúan követi a verziózást. A kliens verziója és a Kubernetes verzióival kapcsolatos kompatibilitásról az alábbiakban tájékozódhatunk kompatibilitási mátrix.

Jelenleg a projektünk így néz ki:

copyrator
├── copyrator
│   ├── cli.py # A parancssoros logika
│   ├── constant.py # Az előbb említett konstansok
│   ├── load_crd.py # A CRD betöltési logikája
│   └── operator.py # Az operátor fő működési logikája
└── setup.py # A csomag kialakítása

Docker és Helm

A Dockerfile elég egyszerű lesz: alapként vesszük a python-alpine képet, és telepítjük a csomagunkat. Az optimalizációt későbbre halasztjuk:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

A teleoperator számára a telepítés szintén nagyon egyszerű:

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

Végül létre kell hozni a megfelelő szerepkört az operátor számára a szükséges jogokkal:

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

Result

Így, félelem, szemrehányás és Go tanulmányozása nélkül, sikerült összeraknunk a saját műveletünket a Kubernetes számára Pythonban. Persze, még van hova fejlődni: a jövőben több szabályt fog tudni kezelni, több szálon dolgozni, és önállóan figyelni a CRD-i változásait...

Annak érdekében, hogy közelebbi kapcsolatba kerüljünk a kóddal, összegyűjtöttük egy nyilvános tárolóban. Ha komolyabb operátor példákat szeretnél Python segítségével, akkor figyelj két mongodb telepítő operátor irányába (első és második).

P.S. Ha pedig nem akarod magad belemélyedni a Kubernetes eseményeibe, vagy egyszerűen jobban szeretsz Bash-t használni – kollégáink kész megoldásként szolgáltattak egy Stolon; (ezt announced áprilisban készítettük).

P.P.S.

Olvasson további információkat a blogunkban:

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster