Kubernetes Operator na Pythonu bez frameworků a SDK

Kubernetes Operator na Pythonu bez frameworků a SDK

Go je v současnosti monopolním jazykem, který lidé volí pro psaní operatorů pro Kubernetes. Existují k tomu objektivní důvody, jako:

  1. Existuje mocný framework pro vývoj operatorů v Go — Operator SDK.
  2. Na Go byly napsány aplikace, které «změnily hru», jako Docker a Kubernetes. Psát svého operátora v Go znamená hovořit s ekosystémem jedním jazykem.
  3. Vysoký výkon aplikací v Go a jednoduché nástroje pro práci s concurrency „přímo z krabice“.

NB: Mimochodem, jak napsat svého operátora v Go, jsme již popsali v jednom z našich překladů zahraničních autorů.

Ale co když vám ve studiu Go brání nedostatek času nebo, banálně, motivace? V článku je uveden příklad, jak napsat solidního operátora s použitím jednoho z nejpopulárnějších jazyků, který zná prakticky každý DevOps inženýr — CouchDB.

Představujeme: Kopirátor — kopírovací operátor!

Pro příklad se zaměříme na vývoj jednoduchého operatora určeného k kopírování ConfigMap buď při vzniku nového namespace, nebo při změně jedné ze dvou entit: ConfigMap a Secret. Z praktického hlediska může být operator užitečný pro masivní aktualizaci konfigurací aplikace (aktualizací ConfigMap) nebo pro aktualizaci tajných údajů — například klíčů pro práci s Docker Registry (při přidání Secret do namespace).

Takže, co by měl mít dobrý operator:

  1. Interakce s operátorem probíhá pomocí Custom Resource Definitions (dále — CRD).
  2. Operator může být konfigurován. K tomu použijeme příznaky příkazového řádku a proměnné prostředí.
  3. Sestavení Docker kontejneru a Helm chartu je navrženo tak, aby uživatelé mohli snadno (doslova jedním příkazem) nainstalovat operátora do svého Kubernetes clusteru.

CRD

Aby operator věděl, jaké zdroje a kde hledat, musíme mu stanovit pravidlo. Každé pravidlo bude reprezentováno jako jeden objekt CRD. Jaká pole by toto CRD měla mít?

  1. Typ zdroje, které budeme hledat (ConfigMap nebo Secret).
  2. Seznam namespace'ů, ve kterých by měly být zdroje.
  3. Selector, podle kterého budeme hledat zdroje v namespace.

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 vytvoříme jednoduché pravidlo — na vyhledávání v namespace s názvem default všech ConfigMap s labely ve formátu copyrator: "true":

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

Hotovo! Teď potřebujeme nějak získat informace o našem pravidle. Hned na začátek podotýkám, že nebudeme psát dotazy na API server clusteru sami. K tomu využijeme hotovou Python knihovnu 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')}

V důsledku běhu tohoto kódu získáme následující:

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

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

Přenvěné proměnné nebo příznaky? Bereme vše!

Přejdeme k hlavní konfiguraci operátora. Existují dva základní přístupy k konfiguraci aplikací:

  1. použít parametry příkazového řádku;
  2. použít proměnné prostředí.

Parametry příkazového řádku umožňují číst nastavení flexibilněji, s podporou a validací datových typů. Ve standardní knihovně Python existuje modul argparser, který využijeme. Podrobnosti a příklady jeho funkcí jsou k dispozici v oficiální dokumentaci.

Takto vypadá příklad pro naši situaci, jak nastavit č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='Operátor Namespace'
    )
    parser.add_argument(
        '--rule-name',
        type=str,
        default=getenv('RULE_NAME', 'main-rule'),
        help='CRD Název'
    )
    args = parser.parse_args()

Na druhé straně lze pomocí proměnných prostředí v Kubernetes snadno přenést provozní informace o podu dovnitř kontejneru. Například informace o jmenném prostoru, ve kterém je pod spuštěn, můžeme získat následující konstrukcí:

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

Logika fungování operátora

Abychom porozuměli, jak oddělit metody pro práci s ConfigMap a Secret, využijeme speciální mapy. Tím získáme přehled o metodách potřebných k 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 je třeba přijímat události od API serveru. Realizujeme to následujícím způsobem:

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

    # Získáváme metodu pro sledování objektů
    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 obdržení události přecházíme 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 hotova! Nyní je třeba zabalit to všechno do jednoho Python balíčku. Upravujeme soubor setup.py, píšeme tam meta informace o projektu:

from sys import version_info

from setuptools import find_packages, setup

if version_info[:2] < (3, 5):
    raise RuntimeError(
        'Nepodporovaná verze pythonu %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: Klient kubernetes pro Python má své verzionování. Podrobnosti o kompatibilitě verzí klienta a verzí Kubernetes lze nalézt v kompatibilní matici.

Nyní náš projekt vypadá takto:

copyrator
├── copyrator
│   ├── cli.py # Logika práce s příkazovým řádkem
│   ├── constant.py # Konstanty, které jsme uváděli výše
│   ├── load_crd.py # Logika načítání CRD
│   └── operator.py # Hlavní logika fungování operátora
└── setup.py # Úprava balíčku

Docker a Helm

Dockerfile bude neuvěřitelně jednoduchý: vezmeme základní obraz python-alpine a nainstalujeme náš balíček. Jeho optimalizaci odložíme 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 je třeba vytvořit odpovídající 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 }}

Výsledek

Takto, bez strachu, výčitek a studia Go, jsme dokázali sestavit vlastního operátora pro Kubernetes v Pythonu. Samozřejmě, že má ještě co zdokonalovat: v budoucnu bude moci zpracovávat více pravidel, pracovat v několika vláknech, autonomně sledovat změny svých CRD...

Abychom se s kódem mohli lépe seznámit, shromáždili jsme ho v veřejném repozitáři. Pokud hledáte příklady vážnějších operátorů vytvořených pomocí Pythonu, můžete se zaměřit na dva operátory pro nasazení mongodb (první a druhé).

P.S. A pokud se vám nechce rozumět událostem Kubernetes nebo vám jednoduše vyhovuje používat Bash — naši kolegové připravili hotové řešení v podobě shell-operator (my oznámili to udělali v dubnu).

P.P.S.

Přečtěte si také náš blog:

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster