Kubernetes Operator na Pythonu bez frameworka i SDK-a

Kubernetes Operator na Pythonu bez frameworka i SDK-a

Go trenutno djeluje kao monopolist među programskim jezicima koje ljudi biraju za pisanje operatora za Kubernetes. Postoje objektivni razlozi za to, kao što su:

  1. Postoji moćan okvir za razvoj operatora na Go — Operator SDK.
  2. Na Go su napisane aplikacije koje su 'izmijenile igru', kao što su Docker i Kubernetes. Pisanje vlastitog operatora na Go znači razgovarati s ekosustavom na istom jeziku.
  3. Visoka izvedba aplikacija na Go i jednostavni alati za rad s konkurentnošću "iz kutije".

NB: Usput, kako napisati vlastiti operator na Go, mi već smo opisali u jednom od naših prijevoda stranih autora.

Ali šta ako vam nedostatak vremena ili, jednostavno, motivacije ometa učenje Go? U članku je prikazan primjer kako se može napisati dobar operator koristeći jedan od najpopularnijih jezika, koji gotovo svaki DevOps inženjer poznaje — Python.

Upoznajte: Kopirator — kopirni operator!

Kao primjer, razmotrit ćemo razvoj jednostavnog operatora koji je namijenjen kopiranju ConfigMap-a ili pri pojavljivanju novog namespace-a, ili pri promjeni jedne od dviju entiteta: ConfigMap i Secret. Iz praktične primjene, operator može biti koristan za masovno ažuriranje konfiguracija aplikacije (putem ažuriranja ConfigMap-a) ili za ažuriranje tajnih podataka — na primjer, ključeva za rad s Docker Registry (prilikom dodavanja tajne u namespace).

Dakle, što bi trebalo biti kod dobrog operatora:

  1. Interakcija s operatorom odvija se putem Custom Resource Definitions (dalje — CRD).
  2. Operator se može konfigurirati. Za to ćemo koristiti opcije naredbenog retka i varijable okruženja.
  3. Izrada Docker kontejnera i Helm charta osmišljena je tako da korisnici mogu lako (doslovno jednim naredbom) instalirati operator u svoj Kubernetes klaster.

CRD

Kako bi operator znao koje resurse i gdje tražiti, trebamo mu postaviti pravilo. Svako pravilo bit će predstavljeno kao jedan objekt CRD. Koja polja trebaju biti u ovom CRD-u?

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

Opisat ć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

I odmah ćemo stvoriti jednostavno pravilo — za pretraživanje u namespace-u imenom default svi ConfigMap s oznakama oblika copyrator: "true":

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

Gotovo! Sada moramo na neki način dobiti informacije o našem pravilu. Odmah da kažem, da sami nećemo pisati upite ka API Serveru klastera. Za to ćemo iskoristiti gotovu Python biblioteku 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')}

Kao rezultat rada ovog koda dobit ćemo sljedeće:

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

Odlično: uspjeli smo dobiti pravilo za operatora. I što je najvažnije — učinili smo to na Kubernetes način.

Okolišne varijable ili zastavice? Uzimamo sve!

Prelazimo na glavnu konfiguraciju operatora. Postoje dva osnovna pristupa u konfiguriranju aplikacija:

  1. koristiti opcije komandne linije;
  2. koristiti okolišne varijable.

Opcije komandne linije omogućuju fleksibilnije čitanje postavki, uz podršku i validaciju tipova podataka. U standardnoj Python biblioteci postoji modul argparser, koji ćemo koristiti. Detalji i primjeri njegovih mogućnosti dostupni su u službene dokumentacije.

Evo kako će za naš slučaj izgledati primjer konfiguracije za čitanje zastavica komandne linije:

   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, uz pomoć varijabli okruženja u Kubernetesu, lako možemo prenijeti informacije o pod-u unutar kontejnera. Na primjer, informacije o imenskom prostranstvu u kojem je pod pokrenut, možemo dobiti sljedećom konstrukcijom:

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

Logika rada operatora

Da bismo razumjeli kako podijeliti metode za rad s ConfigMap i Secret, iskoristit ćemo posebne mape. Tako ćemo moći shvatiti koje su nam metode potrebne za praćenje i stvaranje 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',
}

Nakon toga potrebno je primati događaje s API servera. Realiziramo to na sljedeći način:

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

    # Dobivamo 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 primitka događaja pređemo na glavnu logiku njegove 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)

Glavna logika je spremna! Sada trebamo sve to upakirati u jedan Python paket. Pripremamo datoteku setup.py, pišemo tamo meta-informacije 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 vlastito verzioniranje. Više o kompatibilnosti verzija klijenta i verzija Kubernetes možete saznati iz matrice kompatibilnosti.

Trenutno naš projekt izgleda ovako:

copyrator
├── copyrator
│   ├── cli.py # Logika rada s komandnom linijom
│   ├── constant.py # Konstantne vrijednosti koje smo naveli ranije
│   ├── load_crd.py # Logika učitavanja CRD
│   └── operator.py # Glavna logika rada operatora
└── setup.py # Priprema paketa

Docker i Helm

Dockerfile će biti neizmjerno jednostavan: uzet ćemo osnovnu sliku python-alpine i instalirati naš paket. Njegovu optimizaciju odgađamo za bolja vremena:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Razvoj za operatera je također vrlo jednostavan:

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

Na kraju, potrebno je kreirati odgovarajuću ulogu za operatera s potrebnim pravima:

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čak

Tako, bez straha, prijekora i proučavanja Go, uspjeli smo sastaviti vlastitog operatera za Kubernetes na Pythonu. Naravno, ima još prostora za rast: u budućnosti će moći obrađivati više pravila, raditi u više tokova, samostalno pratiti promjene svojih CRD…

Kako bismo se bolje upoznali s kodom, skupili smo ga u javnom repozitoriju. Ako tražite primjere ozbiljnijih operatera razvijenih uz pomoć Pythona, možete obratiti pažnju na dva operatera za implementaciju mongodb (prvi i drugi).

P.S. Ako vam se ne da istraživati događaje Kubernetes ili vam je jednostavnije koristiti Bash — naši kolege su pripremili gotovo rješenje u obliku shell-operator (mi najavili ga u travnju).

P.S.

Pročitajte i na našem blogu:

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster