Kubernetes Operator u Pythonu bez okvira i SDK-a

Kubernetes Operator u Pythonu bez okvira i SDK-a

Go trenutno ima monopol nad programskim jezicima koje ljudi biraju za pisanje izjava za Kubernetes. Za to postoje objektivni razlozi kao što su:

  1. Postoji snažan okvir za razvoj operatora u Gou - Operator SDK.
  2. Aplikacije koje mijenjaju igru ​​poput Dockera i Kubernetesa napisane su u Gou. Pisanje vašeg operatera u Go znači govoriti istim jezikom s ekosustavom.
  3. Visoke performanse Go aplikacija i jednostavni alati za rad s paralelnošću odmah po otvaranju.

NB: Usput, kako napisati vlastitu izjavu u Go, we već opisano u jednom od naših prijevoda stranih autora.

Ali što ako vas u učenju Goa sprječava nedostatak vremena ili, jednostavno rečeno, motivacije? Članak daje primjer kako možete napisati dobru izjavu koristeći jedan od najpopularnijih jezika koji gotovo svaki DevOps inženjer zna - Piton.

Upoznajte: Copier - kopir operater!

Kao primjer, razmislite o razvoju jednostavne izjave dizajnirane za kopiranje ConfigMap-a bilo kada se pojavi novi imenski prostor ili kada se promijeni jedan od dva entiteta: ConfigMap i Secret. S praktične točke gledišta, operator može biti koristan za skupno ažuriranje konfiguracija aplikacije (ažuriranjem ConfigMapa) ili za ažuriranje tajnih podataka - na primjer, ključeva za rad s Docker registrom (prilikom dodavanja Secreta u imenski prostor).

Dakle, što dobar operater treba imati:

  1. Interakcija s operaterom provodi se pomoću Prilagođene definicije resursa (u daljnjem tekstu CRD).
  2. Operator se može konfigurirati. Da bismo to učinili, koristit ćemo oznake naredbenog retka i varijable okoline.
  3. Izrada Docker spremnika i Helm grafikona dizajnirana je tako da korisnici mogu jednostavno (doslovno jednom naredbom) instalirati operatora u svoj Kubernetes klaster.

CRD

Kako bi operater znao koje resurse treba tražiti i gdje tražiti, moramo mu postaviti pravilo. Svako pravilo bit će predstavljeno kao jedan CRD objekt. Koja polja treba sadržavati ovaj CRD?

  1. Vrsta izvora, koje ćemo tražiti (ConfigMap ili Secret).
  2. Popis imenskih prostora, u kojem bi se resursi trebali nalaziti.
  3. Selektor, pomoću kojeg ćemo tražiti resurse u imenskom prostoru.

Opišimo 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 stvorit ćemo ga odmah jednostavno pravilo — za pretraživanje u imenskom prostoru s imenom default sav ConfigMap s oznakama poput copyrator: "true":

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

Spreman! Sada moramo nekako doći do informacija o našem pravilu. Dopustite mi da odmah napomenem da sami nećemo pisati zahtjeve API poslužitelju klastera. Da bismo to učinili, koristit ćemo gotovu Python biblioteku kubernetes-klijent:

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 pokretanja ovog koda dobivamo sljedeće:

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

Super: uspjeli smo dobiti pravilo za operatera. I što je najvažnije, napravili smo ono što se zove Kubernetes način.

Varijable okruženja ili zastavice? Uzimamo sve!

Prijeđimo na glavnu konfiguraciju operatera. Postoje dva osnovna pristupa konfiguriranju aplikacija:

  1. koristiti opcije naredbenog retka;
  2. koristiti varijable okoline.

Opcije naredbenog retka omogućuju vam fleksibilnije čitanje postavki, uz podršku i provjeru vrste podataka. Pythonova standardna biblioteka ima modul argparser, koji ćemo koristiti. Pojedinosti i primjeri njegovih mogućnosti dostupni su u službena dokumentacija.

Za naš slučaj, ovako bi izgledao primjer postavljanja zastavica čitanja naredbenog retka:

   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štenjem varijabli okruženja u Kubernetesu možete lako prenijeti servisne informacije o podu unutar spremnika. Na primjer, možemo dobiti informacije o prostoru imena u kojem se pod izvodi sa sljedećom konstrukcijom:

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

Operatorska logika

Da bismo razumjeli kako odvojiti metode za rad s ConfigMap i Secret, koristit ćemo posebne karte. Tada možemo razumjeti 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',
}

Zatim trebate primati događaje s API poslužitelja. Implementirajmo to na sljedeći način:

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)

Nakon primitka događaja, prelazimo 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 sve ovo trebamo upakirati u jedan Python paket. Pripremamo datoteku setup.py, tamo napišite 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='[email protected]',
    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 vlastitu verziju. Više informacija o kompatibilnosti između verzija klijenta i verzija Kubernetesa možete pronaći u matrice kompatibilnosti.

Sada naš projekt izgleda ovako:

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

Docker i Helm

Dockerfile će biti nevjerojatno jednostavan: uzmite osnovnu python-alpine sliku i instalirajte naš paket. Odgodimo njegovu optimizaciju do boljih vremena:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Implementacija za operatera također je vrlo jednostavna:

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, trebate stvoriti 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 }}

Ukupan

Tako smo, bez straha, prijekora ili učenja Goa, uspjeli izgraditi vlastiti operator za Kubernetes u Pythonu. Naravno, ima još prostora za rast: u budućnosti će moći obrađivati ​​više pravila, raditi u više niti, samostalno pratiti promjene u svojim CRD-ovima...

Da bismo vam pobliže pogledali kôd, stavili smo ga javno spremište. Ako želite primjere ozbiljnijih operatora implementiranih pomoću Pythona, možete obratiti pažnju na dva operatora za implementaciju mongodb-a (первый и drugi).

PS A ako ste previše lijeni baviti se Kubernetes događajima ili ste jednostavno više navikli koristiti Bash, naši kolege su pripremili gotovo rješenje u obliku ljuska-operator (Mi najavio to u travnju).

PPS

Pročitajte i na našem blogu:

Izvor: www.habr.com

Dodajte komentar