Operator Kubernetes në Python pa framework dhe SDK

Operator Kubernetes në Python pa framework dhe SDK

Go është aktualisht një monopol në mesin e gjuhëve të programimit që njerëzit zgjedhin për të shkruar operatorët për Kubernetes. Kësaj i japin arsyet objektive si:

  1. Ekziston një kornizë e fuqishme për zhvillimin e operatorëve në Go — Operator SDK.
  2. Në Go janë shkruar aplikacione që kanë 'transformuar lojën' si Docker dhe Kubernetes. Të shkruash operatorin tënd në Go do të thotë të flasësh me ekosistemën në një gjuhë.
  3. Performanca e lartë e aplikacioneve në Go dhe mjetet e thjeshta për punë me concurrency 'nga kutia'.

NB: Sidoqoftë, si të shkruash operatorin tënd në Go, ne kemi përshkruar tashmë në një nga përkthimet tona nga autorë të huaj.

Por çfarë nëse mungesa e kohës apo, thjesht, motivimi, po të pengon që të mësosh Go? Artikulli ilustron një shembull se si mund të shkruash një operator të mirë duke përdorur një nga gjuhët më të njohura, të cilën gati çdo inxhinier DevOps e njeh — Python.

Njoftoni: Koptirator — operatori i kopjimit!

Si shembull, le të shqyrtojmë zhvillimin e një operatori të thjeshtë, i destinuar për të kopjuar ConfigMap ose në momentin e shfaqjes së një namespace të ri, ose kur ndryshon një nga dy njësi: ConfigMap dhe Secret. Nga një këndvështrim praktik, operatori mund të jetë i dobishëm për azhurnimin masiv të konfigurimeve të aplikacionit (nëpërmjet azhurnimit të ConfigMap) ose për azhurnimin e të dhënave sekrete — për shembull, çelësave për përdorimin me Docker Registry (kur shtohet një Secret në namespace).

Pra, çfarë duhet të ketë një operator i mirë:

  1. Interaksioni me operatorin bëhet përmes Custom Resource Definitions (më pas — CRD).
  2. Operatori mund të konfigurohet. Për këtë do të përdorim flamujt e komandave dhe variablat e mjedisit.
  3. Ndërtimi i kontejnerit Docker dhe Helm-chartet janë përpunuar në atë mënyrë që përdoruesit të mund të instalojnë lehtësisht (praktikisht me një komandë) operatorin në klasterin e tyre Kubernetes.

CRD

Për të qenë në dijeni të burimeve dhe vendndodhjes së tyre, ne duhet t'i japim një rregull operatorit. Çdo rregull do të paraqitet si një objekt CRD. Çfarë fushash duhet të ketë ky CRD?

  1. Lloji i burimit, të cilin do të kërkojmë (ConfigMap ose Secret).
  2. Lista e namespaceve, në të cilat duhet të ndodhen burimet.
  3. Selector, sipas të cilit do të kërkojmë burimet në namespace.

Të përshkruajmë 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

Dhe menjëherë do të krijojmë një rregull të thjeshtë — për kërkimin në hapësirën emërtuese me emrin default të gjithë ConfigMap me etiketat e tipit copyrator: "true":

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

Kemi mbaruar! Tani duhet në një mënyrë të marrim informacion rreth rregullit tonë. Menjëherë theksoj se ne nuk do të shkruajmë vetë kërkesa për API Server-in e klustrit. Për këtë do të përdorim një bibliotekë të gatshme në Python 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')}

Si rezultat i këtij kodi do të marrim këtë:

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

Shkëlqyeshëm: arritëm të marrim rregullin për operatorin. Dhe më e rëndësishmja — e bëmë këtë, siç thuhet, në stilin e Kubernetes.

Këto janë variablat e ambientit apo flamujt? Marrim gjithçka!

Tani kalojmë në konfigurimin kryesor të operatorit. Ka dy qasje bazë për konfigurimin e aplikacioneve:

  1. përdorimi i parametrave të linjës së komandës;
  2. përdorimi i variablave të ambientit.

Parametrat e linjës së komandës lejojnë leximin më të fleksibël të konfigurimeve, me mbështetje dhe validim të llojeve të të dhënave. Në bibliotekën standarde të Python-it ka një modul argparser, të cilin do ta shfrytëzojmë. Detajet dhe shembujt e mundësive të tij janë të disponueshme në dokumentacionin zyrtar.

Ja si do të duket shembulli i konfigurimit për leximin e flamujve të linjës së komandës për rastin tonë:

   parser = ArgumentParser(
        description='Copyrator - operatori i kopjimit.',
        prog='copyrator'
    )
    parser.add_argument(
        '--namespace',
        type=str,
        default=getenv('NAMESPACE', 'default'),
        help='Hapësira emërtuese e operatorit'
    )
    parser.add_argument(
        '--rule-name',
        type=str,
        default=getenv('RULE_NAME', 'main-rule'),
        help='Emri i CRD'
    )
    args = parser.parse_args()

Në anën tjetër, duke përdorur variablat e ambientit në Kubernetes, mund të transferojmë lehtësisht informacionin shërbimor mbi pod-in brenda kontejnerit. Për shembull, informacionin mbi namespace, në të cilin pod-i është nisur, mund ta marrim me ndihmën e kësaj strukture:

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

Logjika e funksionimit të operatorit

Për të kuptuar se si t’i ndajmë metodat për të punuar me ConfigMap dhe Secret, do të përdorim hartat speciale. Kështu do të mund të kuptojmë cilat metoda na duhen për ndjekjen dhe krijimin e objektit:

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

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

Më pas, duhet të marrim ngjarjet nga API server. Këtë do ta realizojmë si më poshtë:

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

    # Marrim metodën për ndjekjen e objekteve
    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)

Pas marrjes së ngjarjes, kalojmë në logjikën kryesore të përpunimit të saj:

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

Logjika kryesore është gati! Tani duhet ta paketojmë gjithçka në një paketë Python. E paraqisim skedarin setup.py, shkruajmë atje metainformacionin mbi projektin:

from sys import version_info

from setuptools import find_packages, setup

if version_info[:2] < (3, 5):
    raise RuntimeError(
        'Versioni i pa mbështetur i python %s.' % '.'.join(version_info)
    )


_NAME = 'copyrator'
setup(
    name=_NAME,
    version='0.0.1',
    packages=find_packages(),
    classifiers=[
        'Statusi i Zhvillimit :: 3 - Alpha',
        'Gjuha e Programimit :: Python',
        'Gjuha e Programimit :: Python :: 3',
        'Gjuha e Programimit :: Python :: 3.5',
        'Gjuha e Programimit :: Python :: 3.6',
        'Gjuha e Programimit :: 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: Klienti kubernetes për Python ka versionimin e tij. Më shumë për komplementaritetin e versioneve të klientit dhe versioneve të Kubernetes mund të mësohet nga matrica e komplementaritetit.

Tani projekti ynë duket kështu:

copyrator
├── copyrator
│   ├── cli.py # Logjika e punës me komandën e linjës
│   ├── constant.py # Konstanta, të cilat i kemi shqyrtuar më parë
│   ├── load_crd.py # Logjika e ngarkimit të CRD
│   └── operator.py # Logjika kryesore e funksionimit të operatorit
└── setup.py # Paraqitja e paketës

Docker dhe Helm

Dockerfile do të jetë tejet i thjeshtë: do të marrim imazhin bazë python-alpine dhe do të instalojmë paketën tonë. Optimizimin e saj do ta shtyjmë për kohë më të mira:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Për operatorin, vendosja është gjithashtu shumë e thjeshtë:

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

Më në fund, është e nevojshme të krijohet një rol përkatës për operatorin me të drejtat e nevojshme:

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

Përfundimi

Ja si, pa frikë, përbuzje dhe pa studiuar Go, arritëm të ndërtojmë operatorin tonë për Kubernetes në Python. Sigurisht, ai ka ende shumë për të zhvilluar: në të ardhmen, mund të përballojë disa rregulla, të punojë në shumë procese, të monitorojë vetë ndryshimet në CRD-të e tij...

Për t'u njohur më afër me kodin, e kemi vendosur në repo publik. Nëse dëshironi shembuj të operatorëve më seriozë të implementuar me Python, mund ta ktheni vëmendjen tuaj ndaj dy operatorëve për implementimin e mongodb (e parë dhe pjesën e dytë).

P.S. Nëse ju duket e lodhshme të kuptoni ngjarjet e Kubernetes ose thjesht preferoni të përdorni Bash — kolegët tanë kanë përgatitur një zgjidhje të gatshme në formën e shell-operator (ne bëmë njoftimin e publikojmë nëpril).

P.P.S.

Lexoni gjithashtu në blogun tonë:

Burimi: habr.com

Blini hosting të besueshëm për faqe interneti me mbrojtje nga DDoS, serverë VPS VDS 🔥 Blini hosting të besueshëm për faqe interneti me mbrojtje nga DDoS, serverë VPS VDS | ProHoster