Kubernetes Operator u Python-u bez okvira i SDK-a

Kubernetes Operator u Python-u bez okvira i SDK-a

Go trenutno ima monopol na programske jezike koje ljudi biraju da pišu izjave za Kubernetes. Za to postoje objektivni razlozi, kao što su:

  1. Postoji moćan okvir za razvoj operatera u Go - Operator SDK.
  2. Aplikacije koje mijenjaju igre kao što su Docker i Kubernetes napisane su u Go. Pisati svog operatera u Go znači govoriti istim jezikom sa ekosistemom.
  3. Visoke performanse Go aplikacija i jednostavni alati za rad sa konkurentnošću iz kutije.

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

Ali šta ako vas u učenju Go spriječi nedostatak vremena ili, jednostavno rečeno, motivacija? Članak daje primjer kako možete napisati dobru izjavu koristeći jedan od najpopularnijih jezika koje gotovo svaki DevOps inženjer zna - piton.

Upoznajte: Kopir aparat - fotokopir operater!

Kao primjer, razmislite o razvoju jednostavne izjave dizajnirane da kopira ConfigMap bilo kada se pojavi novi prostor imena ili kada se promijeni jedan od dva entiteta: ConfigMap i Secret. Sa praktične tačke gledišta, operator može biti koristan za masovno ažuriranje konfiguracija aplikacije (ažuriranjem ConfigMap-a) ili za ažuriranje tajnih podataka - na primjer, ključeva za rad sa Docker Registry-om (prilikom dodavanja Secret u imenski prostor).

Tako šta dobar operater treba da ima:

  1. Interakcija sa operaterom se vrši pomoću Prilagođene definicije resursa (u daljem tekstu CRD).
  2. Operater se može konfigurisati. Da bismo to uradili, koristićemo zastavice komandne linije i varijable okruženja.
  3. Izrada Docker kontejnera i Helm grafikona je dizajnirana tako da korisnici mogu lako (bukvalno jednom komandom) da instaliraju operatera u svoj Kubernetes klaster.

CRD

Da bi operater znao koje resurse treba tražiti i gdje tražiti, moramo mu postaviti pravilo. Svako pravilo će biti predstavljeno kao jedan CRD objekat. Koja polja treba da ima ovaj CRD?

  1. Vrsta resursa, koju ćemo tražiti (ConfigMap ili Secret).
  2. Lista imenskih prostora, u kojoj bi se resursi trebali nalaziti.
  3. Selektor, pomoću koje ćemo tražiti resurse u imenskom prostoru.

Hajde da opiš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 kreiraćemo ga odmah jednostavno pravilo — za pretragu u imenskom prostoru sa imenom default sav ConfigMap sa oznakama poput copyrator: "true":

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

Spremni! Sada moramo nekako doći do informacija o našem pravilu. Odmah da rezervišem da nećemo sami pisati zahteve API serveru 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, dobijamo sljedeće:

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

Odlično: uspjeli smo dobiti pravilo za operatera. I što je najvažnije, uradili 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 konfigurisanju aplikacija:

  1. koristiti opcije komandne linije;
  2. koristiti varijable okruženja.

Opcije komandne linije omogućavaju vam da fleksibilnije čitate postavke, uz podršku za tip podataka i validaciju. Pythonova standardna biblioteka ima modul argparser, koji ćemo koristiti. Detalji i primjeri njegovih mogućnosti dostupni su u službena dokumentacija.

U našem slučaju, ovako bi izgledao primjer postavljanja zastavica komandne linije za čitanje:

   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, koristeći varijable okruženja u Kubernetesu, možete lako prenijeti servisne informacije o podu unutar kontejnera. Na primjer, možemo dobiti informacije o imenskom prostoru u kojem se pod izvodi sa sljedećom konstrukcijom:

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

Operatorska logika

Da bismo razumjeli kako razdvojiti metode za rad sa ConfigMap i Secret, koristit ćemo posebne mape. Tada možemo razumjeti koje metode su nam 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',
}

Zatim morate primati događaje sa API servera. Hajde da ga implementiramo na sledeć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 što primimo događaj, 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 spakovati u jedan Python paket. Pripremamo fajl 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 svoje verzije. Više informacija o kompatibilnosti između verzija klijenta i verzija Kubernetesa možete pronaći u matrice kompatibilnosti.

Sada naš projekat izgleda ovako:

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

Docker i Helm

Dockerfile će biti nevjerovatno jednostavan: uzmite osnovnu python-alpine sliku i instalirajte naš paket. Odložimo njegovu optimizaciju do boljih vremena:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Postavljanje za operatera je također vrlo jednostavno:

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

Konačno, morate kreirati odgovarajuću ulogu za operatera sa 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 }}

Rezultat

Tako smo, bez straha, prijekora ili učenja Go-a, uspjeli da napravimo vlastiti operator za Kubernetes u Pythonu. Naravno, još uvijek ima prostora za rast: u budućnosti će moći obraditi više pravila, raditi u više niti, nezavisno pratiti promjene u svojim CRD-ovima...

Da bismo vam bolje pogledali kod, stavili smo ga javno spremište. Ako želite primjere ozbiljnijih operatera implementiranih pomoću Pythona, možete skrenuti pažnju na dva operatora za implementaciju mongodb (первый и drugi).

PS A ako ste previše lijeni da se bavite Kubernetes događajima ili ste jednostavno više navikli koristiti Bash, naše kolege su pripremile gotovo rješenje u obliku shell-operator (Mi najavljeno u aprilu).

PPS

Pročitajte i na našem blogu:

izvor: www.habr.com

Dodajte komentar