Kubernetes Operator v Pythonu bez frameworků a SDK

Kubernetes Operator v Pythonu bez frameworků a SDK

Go má v současné době monopol na programovací jazyky, které se lidé rozhodnou psát příkazy pro Kubernetes. Má to objektivní důvody, např.

  1. V Go existuje výkonný rámec pro vývoj operátorů - SDK operátora.
  2. Aplikace, které mění hru, jako je Docker a Kubernetes, jsou napsány v Go. Napsat svého operátora v Go znamená mluvit stejným jazykem s ekosystémem.
  3. Vysoký výkon aplikací Go a jednoduché nástroje pro práci se souběžností ihned po vybalení.

NB: Mimochodem, jak napsat vlastní prohlášení v Go, my již popsáno v jednom z našich překladů zahraničních autorů.

Co když vám ale v učení Go brání nedostatek času nebo jednoduše řečeno motivace? Článek poskytuje příklad toho, jak můžete napsat dobré prohlášení pomocí jednoho z nejpopulárnějších jazyků, který zná téměř každý inženýr DevOps - PYTHON.

Seznamte se: Kopírka - kopírovací operátor!

Jako příklad zvažte vytvoření jednoduchého příkazu určeného ke zkopírování mapy ConfigMap, buď když se objeví nový jmenný prostor, nebo když se změní jedna ze dvou entit: ConfigMap a Secret. Z praktického hlediska může být operátor užitečný pro hromadnou aktualizaci konfigurací aplikací (aktualizací ConfigMap) nebo pro aktualizaci tajných dat – například klíčů pro práci s Docker Registry (při přidávání Secret do jmenného prostoru).

To znamená, co by měl mít dobrý operátor:

  1. Interakce s operátorem se provádí pomocí Definice vlastních zdrojů (dále jen CRD).
  2. Operátor lze konfigurovat. K tomu použijeme příznaky příkazového řádku a proměnné prostředí.
  3. Sestavení kontejneru Docker a Helm chart je navrženo tak, aby uživatelé mohli snadno (doslova jedním příkazem) nainstalovat operátora do svého clusteru Kubernetes.

CRD

Aby operátor věděl, jaké zdroje má hledat a kde hledat, musíme mu nastavit pravidlo. Každé pravidlo bude reprezentováno jako jeden objekt CRD. Jaké obory by toto CRD mělo obsahovat?

  1. Typ zdroje, který budeme hledat (ConfigMap nebo Secret).
  2. Seznam jmenných prostorů, ve kterém by se měly zdroje nacházet.
  3. Selector, pomocí kterého budeme hledat zdroje ve jmenném prostoru.

Popiš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 hned ho vytvoříme jednoduché pravidlo — pro vyhledávání ve jmenném prostoru se jménem default všechny ConfigMap s popisky jako copyrator: "true":

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

Připraveno! Nyní potřebujeme nějakým způsobem získat informace o našem pravidle. Dovolte mi, abych si hned zarezervoval, že nebudeme sami zapisovat požadavky na cluster API Server. K tomu nám poslouží již hotová Python knihovna 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 spuštění tohoto kódu získáme následující:

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

Skvělé: podařilo se nám získat pravidlo pro operátora. A co je nejdůležitější, udělali jsme to, čemu se říká Kubernetesův způsob.

Proměnné prostředí nebo příznaky? Bereme všechno!

Přejděme ke konfiguraci hlavního operátora. Existují dva základní přístupy ke konfiguraci aplikací:

  1. používat možnosti příkazového řádku;
  2. používat proměnné prostředí.

Možnosti příkazového řádku umožňují flexibilnější čtení nastavení s podporou datových typů a ověřováním. Standardní knihovna Pythonu má modul argparser, který budeme používat. Podrobnosti a příklady jeho schopností jsou k dispozici v oficiální dokumentace.

V našem případě by takto vypadal příklad nastavení čtení příznaků příkazového řádku:

   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 druhou stranu pomocí proměnných prostředí v Kubernetes můžete snadno přenášet servisní informace o podu uvnitř kontejneru. Například můžeme získat informace o jmenném prostoru, ve kterém modul běží, pomocí následující konstrukce:

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

Logika operátora

Abychom pochopili, jak oddělit metody pro práci s ConfigMap a Secret, použijeme speciální mapy. Pak můžeme pochopit, jaké metody potřebujeme ke sledování a vytváření 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',
}

Dále musíte přijímat události ze serveru API. Implementujeme to následovně:

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 přijetí události přejdeme k hlavní logice jejího zpracování:

# Типы событий, на которые будем реагировать
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 připravena! Nyní to vše musíme zabalit do jednoho balíčku Pythonu. Připravíme soubor setup.py, napište tam meta informace o projektu:

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 pro Python má své vlastní verzování. Další informace o kompatibilitě mezi klientskými verzemi a verzemi Kubernetes naleznete v matice kompatibility.

Nyní náš projekt vypadá takto:

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

Docker a Helm

Dockerfile bude neuvěřitelně jednoduchý: vezměte základní obraz python-alpine a nainstalujte náš balíček. Odložme jeho optimalizaci na lepší časy:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Nasazení pro operátora je také velmi 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

Nakonec musíte vytvořit vhodnou roli pro operátora s potřebnými právy:

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ý

Tak jsme si bez strachu, výčitek nebo učení Go dokázali postavit vlastního operátora pro Kubernetes v Pythonu. Samozřejmě má stále kam růst: v budoucnu bude schopen zpracovávat více pravidel, pracovat ve více vláknech, nezávisle sledovat změny ve svých CRD...

Abychom vám poskytli bližší pohled na kód, vložili jsme jej veřejné úložiště. Pokud chcete příklady serióznějších operátorů implementovaných pomocí Pythonu, můžete obrátit svou pozornost na dva operátory pro nasazení mongodb (první и druhý).

PS A pokud jste příliš líní řešit události Kubernetes nebo jste prostě více zvyklí používat Bash, naši kolegové připravili hotové řešení ve formě shell-operátor (My oznámil to v dubnu).

PPS

Přečtěte si také na našem blogu:

Zdroj: www.habr.com

Přidat komentář