Operátor Kubernetes v Pythone bez rámcov a SDK

Operátor Kubernetes v Pythone bez rámcov a SDK

Go má v súčasnosti monopol na programovacie jazyky, ktoré si ľudia vyberajú na písanie príkazov pre Kubernetes. Má to objektívne dôvody, ako napr.

  1. V Go existuje výkonný rámec pre vývoj operátorov - SDK operátora.
  2. Aplikácie, ktoré menia hru, ako sú Docker a Kubernetes, sú napísané v Go. Napísať svojho operátora v Go znamená hovoriť rovnakým jazykom s ekosystémom.
  3. Vysoký výkon Go aplikácií a jednoduché nástroje na prácu so súbežnosťou hneď po vybalení.

NB: Mimochodom, ako napísať svoje vlastné vyhlásenie v Go, my už popísané v jednom z našich prekladov zahraničných autorov.

Čo ak vám však v učení Go bráni nedostatok času alebo jednoducho povedané motivácia? Článok poskytuje príklad, ako môžete napísať dobré vyhlásenie pomocou jedného z najpopulárnejších jazykov, ktorý pozná takmer každý inžinier DevOps - Pytón.

Zoznámte sa: Kopírka - operátor kopírovania!

Ako príklad zvážte vytvorenie jednoduchého príkazu určeného na skopírovanie mapy ConfigMap buď vtedy, keď sa objaví nový priestor názvov, alebo keď sa zmení jedna z dvoch entít: ConfigMap a Secret. Z praktického hľadiska môže byť operátor užitočný pri hromadnej aktualizácii konfigurácií aplikácií (aktualizáciou ConfigMap) alebo pri aktualizácii tajných údajov – napríklad kľúčov pre prácu s Docker Registry (pri pridávaní Secret do menného priestoru).

Takže, čo by mal mať dobrý operátor:

  1. Interakcia s operátorom sa vykonáva pomocou Definície vlastných zdrojov (ďalej len CRD).
  2. Operátor je možné konfigurovať. Na tento účel použijeme príznaky príkazového riadku a premenné prostredia.
  3. Zostavenie kontajnera Docker a grafu Helm je navrhnuté tak, aby používatelia mohli jednoducho (doslova jedným príkazom) nainštalovať operátora do svojho klastra Kubernetes.

CRD

Aby operátor vedel, aké zdroje má hľadať a kde hľadať, musíme mu nastaviť pravidlo. Každé pravidlo bude reprezentované ako jeden objekt CRD. Aké polia by malo obsahovať toto CRD?

  1. Typ zdroja, ktorý budeme hľadať (ConfigMap alebo Secret).
  2. Zoznam menných priestorov, v ktorej by sa mali zdroje nachádzať.
  3. selektor, pomocou ktorého budeme hľadať zdroje v mennom priestore.

Opíšme 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

A hneď ho vytvoríme jednoduché pravidlo — vyhľadávanie v priestore mien s menom default všetky ConfigMap s menovkami ako copyrator: "true":

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

Pripravený! Teraz musíme nejakým spôsobom získať informácie o našom pravidle. Dovoľte mi hneď urobiť rezerváciu, že nebudeme sami zapisovať požiadavky na server klastra API. Na to nám poslúži hotová knižnica Python kubernetes-klient:

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

V dôsledku spustenia tohto kódu dostaneme nasledovné:

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

Skvelé: podarilo sa nám získať pravidlo pre operátora. A čo je najdôležitejšie, urobili sme to, čomu sa hovorí Kubernetesova cesta.

Premenné prostredia alebo príznaky? Berieme všetko!

Prejdime ku konfigurácii hlavného operátora. Existujú dva základné prístupy ku konfigurácii aplikácií:

  1. použite možnosti príkazového riadku;
  2. používať premenné prostredia.

Možnosti príkazového riadka vám umožňujú flexibilnejšie čítať nastavenia s podporou a overovaním typov údajov. Štandardná knižnica Pythonu má modul argparser, ktorý budeme používať. Podrobnosti a príklady jeho schopností sú k dispozícii v oficiálna dokumentácia.

V našom prípade by takto vyzeral príklad nastavenia čítania príznakov príkazového riadku:

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

Na druhej strane, pomocou premenných prostredia v Kubernetes môžete ľahko prenášať servisné informácie o podu vnútri kontajnera. Napríklad môžeme získať informácie o mennom priestore, v ktorom modul beží, pomocou nasledujúcej konštrukcie:

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

Logika operátora

Aby sme pochopili, ako oddeliť metódy práce s ConfigMap a Secret, použijeme špeciálne mapy. Potom môžeme pochopiť, aké metódy potrebujeme na sledovanie a vytváranie objektu:

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

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

Ďalej musíte prijímať udalosti zo servera API. Implementujme to nasledovne:

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)

Po prijatí udalosti prejdeme k hlavnej logike jej spracovania:

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

Hlavná logika je pripravená! Teraz to všetko musíme zabaliť do jedného balíka Python. Pripravíme súbor setup.py, napíšte tam meta informácie o projekte:

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: Klient kubernetes pre Python má svoje vlastné verzovanie. Viac informácií o kompatibilite medzi klientskymi verziami a verziami Kubernetes nájdete v matice kompatibility.

Teraz náš projekt vyzerá takto:

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

Docker a Helm

Dockerfile bude neuveriteľne jednoduchý: vezmite základný obraz python-alpine a nainštalujte náš balík. Odložme jeho optimalizáciu na lepšie časy:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Nasadenie pre operátora je tiež veľmi jednoduché:

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

Nakoniec musíte vytvoriť vhodnú rolu pre operátora s potrebnými právami:

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

Celkový

Takto sme si bez strachu, výčitiek alebo učenia sa Go dokázali postaviť vlastného operátora pre Kubernetes v Pythone. Samozrejme, stále má priestor na rast: v budúcnosti bude schopný spracovávať viacero pravidiel, pracovať vo viacerých vláknach, nezávisle sledovať zmeny vo svojich CRD...

Aby sme vám poskytli bližší pohľad na kód, vložili sme ho verejné úložisko. Ak chcete príklady serióznejších operátorov implementovaných pomocou Pythonu, môžete obrátiť svoju pozornosť na dva operátory na nasadenie mongodb (первый и druhý).

PS A ak ste príliš leniví na to, aby ste riešili udalosti Kubernetes alebo ste jednoducho viac zvyknutí používať Bash, naši kolegovia pripravili hotové riešenie vo forme shell-operátor (My oznámil to v apríli).

PPS

Prečítajte si aj na našom blogu:

Zdroj: hab.com

Pridať komentár