Kubernetes Operator na Python-u bez framework-ova i SDK-a

Kubernetes Operator na Python-u bez framework-ova i SDK-a

Go je trenutno monopol među programskim jezicima koje ljudi biraju za pisanje operatora za Kubernetes. To ima objektivne razloge, kao što su:

  1. Postoji moćan okvir za razvoj operatora na Go-u — Operator SDK.
  2. Mnoge aplikacije koje su promenile igru, kao što su Docker i Kubernetes, napisane su na Go-u. Pisati sopstveni operator na Go-u znači govoriti jezikom ekosistema.
  3. Visoke performanse aplikacija na Go-u i jednostavni alati za obradu konkurentnosti "iz kutije".

NB: Usput, kako napisati svoj operator na Go-u, mi već smo opisali u jednom od naših prevoda inostranih autora.

Ali šta ako vam nedostatak vremena ili, jednostavno, motivacije, otežava učenje Go-a? U članku je dat primer kako se može napisati kvalitetan operator koristeći jedan od najpopularnijih jezika koji poznaje gotovo svaki DevOps inženjer — Python.

Predstavljamo: Kopyrator — operator za kopiranje!

U ovom primeru razmotrićemo razvoj jednostavnog operatora namenjenog za kopiranje ConfigMap-a ili prilikom stvaranja novog namespace-a, ili pri promeni jedne od dve entiteta: ConfigMap i Secret. Sa praktične tačke gledišta, operator može biti koristan za masovno ažuriranje konfiguracija aplikacije (putem ažuriranja ConfigMap-a) ili za ažuriranje tajnih podataka — na primer, ključeva za rad sa Docker Registry (prilikom dodavanja Secreta u namespace).

Dakle, šta bi trebao imati dobar operator:

  1. Interakcija s operatorom vrši se putem Custom Resource Definitions (dalje — CRD).
  2. Operator može biti konfigurisana. U tu svrhu koristićemo flagove iz komandne linije i promenljive okruženja.
  3. Izgradnja Docker kontejnera i Helm šarta postavljeni su tako da korisnici mogu lako (doslovno jednim komandama) instalirati operator u svoj Kubernetes klaster.

CRD

Da bi operator znao koje resurse i gde da traži, potrebno je postaviti pravilo za njega. Svako pravilo biće predstavljeno kao jedan objekat CRD. Koja polja bi trebala imati ovaj CRD?

  1. Tip resursa, koji ćemo tražiti (ConfigMap ili Secret).
  2. Lista namespace-ova, u kojima bi resursi trebali biti.
  3. Selector, po kojem ćemo tražiti resurse u namespace-u.

Opisaćemo 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

In zdaj bomo ustvarili preprosto pravilo — za iskanje v prostoru imen z imenom default vseh ConfigMap z oznakami, kot so copyrator: "true":

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

Gotovo! Zdaj moramo nekako pridobiti informacije o našem pravilu. Takoj moram povedati, da ne bomo sami pisali poizvedb za API strežnik gruče. Za to bomo uporabili pripravljeno Python knjižnico 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')}

Kot rezultat te kode dobimo naslednje:

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

Odlično: uspelo nam je pridobiti pravilo za operatorja. In kar je najpomembneje — to smo naredili na način Kubernetes.

Okoljske spremenljivke ali zastavice? Vzamemo vse!

Preidemo k osnovni konfiguraciji operatorja. Obstajata dva osnovna pristopa za konfiguracijo aplikacij:

  1. uporaba parametrov ukazne vrstice;
  2. uporaba okoljskih spremenljivk.

Parametri ukazne vrstice omogočajo prilagodljivo branje nastavitev, jajčno podporo in validacijo tipov podatkov. V standardni knjižnici Pythona je modul argparser, ki ga bomo uporabili. Podrobnosti in primeri njegovih možnosti so na voljo v uradni dokumentaciji.

Tako bo naš primer za nastavitev branja zastavic ukazne vrstice izgledal takole:

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

S druge strane, korišćenjem promenljivih okruženja u Kubernetes-u lako možemo preneti servisne informacije o pod-u unutar kontejnera. Na primer, informacije o namespace-u u kojem je pod pokrenut, možemo dobiti sledećom konstrukcijom:

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

Logika rada operatera

Da bismo razumeli kako da podelimo metode za rad sa ConfigMap i Secret, koristićemo specijalne mape. Tako ćemo moći da shvatimo koje su nam metode potrebne za praćenje i kreiranje objekta:

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

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

Dalje, potrebno je primati događaje sa API servera. Realizovaćemo to na sledeći način:

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

    # Dobijamo metodu za praćenje objekata
    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)

Nakon prijema događaja prelazimo na osnovnu logiku njegovog obrade:

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

Osnovna logika je spremna! Sada sve ovo treba spakovati u jedan Python paket. Uređujemo fajl setup.py, pišemo tamo metainformacije 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='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: Kubernetes klijent za Python ima svoje verzionisanje. Detaljnije informacije o kompatibilnosti verzija klijenta i verzija Kubernetes-a možete pronaći u matricama kompatibilnosti.

Sada naš projekat izgleda ovako:

copyrator
├── copyrator
│   ├── cli.py # Logika rada sa komandnom linijom
│   ├── constant.py # Konstantna vrednosti koje smo naveli ranije
│   ├── load_crd.py # Logika učitavanja CRD
│   └── operator.py # Osnovna logika rada operatera
└── setup.py # Uređivanje paketa

Docker i Helm

Dockerfile će biti krajnje jednostavan: uzimamo osnovnu sliku python-alpine i instaliramo naš paket. Njegovu optimizaciju odlažemo za bolja vremena:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Implementacija za operaterja je tudi zelo preprosta:

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

Nazadnje, potrebno je ustvariti ustrezno vlogo za operaterja z zahtevano pravicami:

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

Zaključek

Tako, brez strahu, očitkov in učenja Go jezika, smo uspeli zbrati svojega lastnega operaterja za Kubernetes v Pythonu. Seveda se lahko še razvije: v prihodnosti bi lahko obdeloval več pravil, deloval v več niti, samostojno spremljal spremembe svojih CRD …

Da bi bolje spoznali kodo, smo jo zbrali v javnem repozitoriju. Če iščete primere bolj resnih operaterjev, izvedenih s pomočjo Pythona, lahko obrnete svojo pozornost na dva operaterja za namestitev mongodb (prvi in drugi).

P.S. Če se vam ne da ukvarjati s Kubernetes dogodki ali pa vam je bolj prijetno uporabljati Bash — naši kolegi so pripravili rešitev v obliki shell-operatorja (smo napovedali to aprila).

P.P.S.

Preberite tudi na našem blogu:

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster