Kubernetes Operator v Pythone bez frameworkov a SDK

Kubernetes Operator v Pythone bez frameworkov a SDK

Go je v súčasnosti monopolom medzi programovacími jazykmi, ktoré si ľudia vyberajú na písanie operátorov pre Kubernetes. Existujú na to objektívne dôvody, ako napríklad:

  1. Existuje mocný rámec na vývoj operátorov v Go — Operator SDK.
  2. Aplikácie ako Docker a Kubernetes, ktoré «prevrátili hru», sú napísané v Go. Písať svoj operátor v Go znamená hovoriť s ekosystémom jedným jazykom.
  3. Vysoký výkon aplikácií v Go a jednoduché nástroje na prácu s concurrency «z krabice».

Poznámka: Mimochodom, ako napísať svoj operátor v Go, sme už opísali v jednom z našich prekladov zahraničných autorov.

Ale čo ak vám bráni v štúdiu Go nedostatok času alebo, jednoducho, motivácie? V článku je uvedený príklad toho, ako napísať kvalitný operátor, pričom sa využíva jeden z najpopulárnejších jazykov, ktorý pozná takmer každý DevOps inžinier — Python.

Predstavujeme: Kopírovateľ — kopírovací operátor!

Pre príklad si vezmeme vývoj jednoduchého operátora, ktorý je určený na kopírovanie ConfigMap buď pri vytvorení nového namespace, alebo pri zmene jednej z dvoch entít: ConfigMap a Secret. Z praktického hľadiska môže byť operátor užitočný na hromadné aktualizácie konfigurácií aplikácie (aktualizovaním ConfigMap) alebo na aktualizáciu tajných údajov — napríklad kľúčov pre prácu s Docker Registry (pri pridaní Secretu do namespace).

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

  1. Interakcia s operátorom sa uskutočňuje pomocou Custom Resource Definitions, (ďalej — CRD).
  2. Operátor môže byť konfigurovaný. Na to budeme používať príkazové riadkové príznaky a premenné prostredia.
  3. Súbor Docker kontajnera a Helm chartu sú vypracované tak, aby používatelia mohli jednoducho (doslova jedným príkazom) nainštalovať operátor do svojho Kubernetes klastrov.

CRD

Aby operátor vedel, aké zdroje a kde má hľadať, musíme mu určiť pravidlo. Každé pravidlo bude reprezentované jedným objektom CRD. Aké polia by malo mať toto CRD?

  1. Typ zdroja, ktoré budeme hľadať (ConfigMap alebo Secret).
  2. Zoznam namespace’ov, v ktorých by mali byť zdroje.
  3. Selector, podľa ktorého budeme hľadať zdroje v namespace.

Opíšeme 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 okamžite vytvoríme jednoduché pravidlo — na vyhľadávanie v namespace s názvom default všetkých ConfigMap s labelami vo forme copyrator: "true":

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

Hotovo! Teraz potrebujeme získať informácie o našom pravidle. Okamžite dodávam, že nebudeme písať požiadavky na API Server klastru. Na to využijeme hotovú knižnicu Pythonu 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ýsledkom tohto kódu dostaneme nasledujúce:

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

Výborne: podarilo sa nám získať pravidlo pre operátor. A čo je najdôležitejšie — urobili sme to, čo sa nazýva, Kubernetes way.

Premenné prostredia alebo vlajky? Berieme všetko!

Prechádzame k základnej konfigurácii operátora. Existujú dva základné prístupy k konfigurácii aplikácií:

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

Parametre príkazového riadku umožňujú čítať nastavenia flexibilnejšie, s podporou a validáciou typov údajov. V štandardnej knižnici Pythonu existuje modul argparser, ktorý využijeme. Podrobnosti a príklady jeho možností sú dostupné v oficiálnej dokumentácii.

Takto bude pre náš prípad vyzerať príklad nastavenia čítania vlajok 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 environmentálnych premenných v Kubernetes môžeme ľahko preniesť servisné informácie o pod’e do kontajnera. Napríklad, informáciu o namespace, v ktorom je pod spustený, môžeme získať nasledujúcou konštrukciou:

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

Logika fungovania operátora

Aby sme pochopili, ako rozdeliť metódy na prácu s ConfigMap a Secret, využijeme špeciálne mapy. Potom budeme schopní pochopiť, aké metódy potrebujeme na sledovanie a vytvorenie 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',
}

Následne je potrebné prijímať udalosti z API servera. Realizujeme to nasledovne:

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

    # Získavame metódu na sledovanie objektov
    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 prechádzame 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 hotová! Teraz treba zabaliť všetko do jedného Python balíka. Vytvárame súbor setup.py, píšeme 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(
        'Nepodporovaná verzia 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),
        ]
    }
)

Poznámka: Klient kubernetes pre Python má svoje verzionovanie. Podrobnosti o kompatibilite verzií klienta a verzií Kubernetes sa dajú zistiť z matrice kompatibility.

Teraz náš projekt vyzerá takto:

copyrator
├── copyrator
│   ├── cli.py # Logika práce s príkazovým riadkom
│   ├── constant.py # Konštanty, ktoré sme uviedli vyššie
│   ├── load_crd.py # Logika načítania CRD
│   └── operator.py # Hlavná logika práce operátora
└── setup.py # Upravovanie balíka

Docker a Helm

Dockerfile bude neobvykle jednoduchý: vezmeme základný obraz python-alpine a nainštalujeme náš balík. Jeho optimalizáciu odložíme na neskôr:

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 je potrebné vytvoriť zodpovedajúcu rolu pre operátora s potrebnými oprávneniami:

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

Záver

Takto, bez strachu, výčitiek a štúdia Go, sme dokázali zostaviť vlastného operátora pre Kubernetes na Pythone. Samozrejme, má ešte veľa priestoru na rast: v budúcnosti bude schopný spracovávať niekoľko pravidiel, pracovať v niekoľkých vláknach, sám monitorovať zmeny svojich CRD...

Aby ste sa mohli lepšie oboznámiť s kódom, zverejnili sme ho v verejnom repozitári. Ak máte záujem o príklady serióznejších operátorov implementovaných pomocou Pythona, môžete sa pozrieť na dva operátory pre nasadenie mongodb (prvý a druhý).

P.S. A ak sa vám nechce zaoberať udalosťami Kubernetes alebo ak je pre vás pohodlnejšie používať Bash — naši kolegovia pripravili hotové riešenie vo forme shell-operator (publikovali sme ho v apríli). ohlásili Go je zatiaľ.

P.P.S.

Prečítajte si tiež na našom blogu:

Zdroj: habr.com

Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster