Operatori Kubernetes në Python pa korniza dhe SDK

Operatori Kubernetes në Python pa korniza dhe SDK

Go aktualisht ka një monopol në gjuhët e programimit që njerëzit zgjedhin të shkruajnë deklarata për Kubernetes. Ka arsye objektive për këtë, si p.sh.

  1. Ekziston një kornizë e fuqishme për zhvillimin e operatorëve në Go - Operatori SDK.
  2. Aplikacionet që ndryshojnë lojën si Docker dhe Kubernetes janë shkruar në Go. Të shkruash operatorin tënd në Go do të thotë të flasësh të njëjtën gjuhë me ekosistemin.
  3. Performancë e lartë e aplikacioneve Go dhe mjete të thjeshta për të punuar me konkurencë jashtë kutisë.

NB: Nga rruga, si të shkruani deklaratën tuaj në Go, ne tashmë të përshkruara në një nga përkthimet tona nga autorë të huaj.

Por, çka nëse ju pengon të mësoni Go nga mungesa e kohës ose, thënë thjesht, motivimi? Artikulli ofron një shembull se si mund të shkruani një deklaratë të mirë duke përdorur një nga gjuhët më të njohura që di pothuajse çdo inxhinier DevOps - Piton.

Takoni: Kopjues - operator kopjimi!

Si shembull, merrni parasysh zhvillimin e një deklarate të thjeshtë të krijuar për të kopjuar një ConfigMap ose kur shfaqet një hapësirë ​​e re emri ose kur ndryshon një nga dy entitetet: ConfigMap dhe Secret. Nga pikëpamja praktike, operatori mund të jetë i dobishëm për përditësimin masiv të konfigurimeve të aplikacionit (duke përditësuar ConfigMap) ose për përditësimin e të dhënave sekrete - për shembull, çelësat për të punuar me Regjistrin Docker (kur shtoni Secret në hapësirën e emrave).

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

  1. Ndërveprimi me operatorin kryhet duke përdorur Përkufizime të personalizuara të burimeve (më tej referuar si CRD).
  2. Operatori mund të konfigurohet. Për ta bërë këtë, ne do të përdorim flamujt e linjës së komandës dhe variablat e mjedisit.
  3. Ndërtimi i grafikut të kontejnerit Docker dhe Helm është projektuar në mënyrë që përdoruesit të mund të instalojnë lehtësisht (fjalë për fjalë me një komandë) operatorin në grupin e tyre Kubernetes.

CRD

Në mënyrë që operatori të dijë se çfarë burimesh të kërkojë dhe ku të kërkojë, ne duhet të vendosim një rregull për të. Çdo rregull do të përfaqësohet si një objekt i vetëm CRD. Çfarë fushash duhet të ketë kjo CRD?

  1. Lloji i burimit, të cilin do ta kërkojmë (ConfigMap ose Secret).
  2. Lista e hapësirave të emrave, në të cilën duhet të vendosen burimet.
  3. Përzgjedhës, me anë të së cilës do të kërkojmë burime në hapësirën e emrave.

Le 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 ne do ta krijojmë atë menjëherë rregull i thjeshtë — për të kërkuar në hapësirën e emrave me emrin default të gjitha ConfigMap me etiketa si copyrator: "true":

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

Gati! Tani duhet të marrim disi informacione për rregullin tonë. Më lejoni të bëj një rezervim menjëherë se ne nuk do t'i shkruajmë kërkesat vetë serverit API të grupit. Për ta bërë këtë, ne do të përdorim një bibliotekë të gatshme Python kubernetes-klient:

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 ekzekutimit të këtij kodi, marrim sa vijon:

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

E shkëlqyeshme: arritëm të merrnim një rregull për operatorin. Dhe më e rëndësishmja, ne bëmë atë që quhet mënyra Kubernetes.

Variabla mjedisore apo flamuj? Ne marrim gjithçka!

Le të kalojmë te konfigurimi i operatorit kryesor. Ekzistojnë dy qasje themelore për konfigurimin e aplikacioneve:

  1. përdorni opsionet e linjës së komandës;
  2. përdorni variablat e mjedisit.

Opsionet e linjës së komandës ju lejojnë të lexoni cilësimet në mënyrë më fleksibël, me mbështetjen dhe vërtetimin e llojit të të dhënave. Biblioteka standarde e Python ka një modul argparser, të cilin do ta përdorim. Detajet dhe shembujt e aftësive të tij janë në dispozicion në dokumentacion zyrtar.

Për rastin tonë, kështu do të dukej një shembull i konfigurimit të leximit të flamujve të linjës së komandës:

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

Nga ana tjetër, duke përdorur variablat e mjedisit në Kubernetes, mund të transferoni lehtësisht informacionin e shërbimit për podin brenda kontejnerit. Për shembull, ne mund të marrim informacion në lidhje me hapësirën e emrave në të cilën pod po funksionon me ndërtimin e mëposhtëm:

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

Logjika e operatorit

Për të kuptuar se si të veçojmë metodat për të punuar me ConfigMap dhe Secret, ne do të përdorim harta speciale. Atëherë mund të kuptojmë se cilat metoda na duhen për të gjurmuar dhe krijuar objektin:

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

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

Tjetra, ju duhet të merrni ngjarje nga serveri API. Le ta zbatojmë atë si më poshtë:

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)

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 t'i paketojmë të gjitha këto në një paketë Python. Ne përgatisim dosjen setup.py, shkruani meta informacione rreth projektit atje:

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: Klienti kubernetes për Python ka versionin e tij. Më shumë informacion rreth përputhshmërisë midis versioneve të klientit dhe versioneve të Kubernetes mund të gjenden në matricat e përputhshmërisë.

Tani projekti ynë duket si ky:

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

Docker dhe Helm

Dockerfile do të jetë tepër e thjeshtë: merrni imazhin bazë python-alpin dhe instaloni paketën tonë. Le të shtyjmë optimizimin e tij deri në kohë më të mira:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Vendosja për operatorin ë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

Së fundi, ju duhet të krijoni një rol të përshtatshëm 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 }}

Total

Kështu, pa frikë, qortim ose duke mësuar Go, ne mundëm të ndërtonim operatorin tonë për Kubernetes në Python. Natyrisht, ajo ka ende hapësirë ​​për t'u rritur: në të ardhmen do të jetë në gjendje të përpunojë rregulla të shumta, të punojë në fije të shumta, të monitorojë në mënyrë të pavarur ndryshimet në CRD-të e saj...

Për t'ju dhënë një vështrim më të afërt të kodit, ne e kemi vendosur atë depo publike. Nëse dëshironi shembuj të operatorëve më seriozë të zbatuar duke përdorur Python, mund ta ktheni vëmendjen tuaj te dy operatorë për vendosjen e mongodb (первый и i dytë).

PS Dhe nëse jeni shumë dembel për t'u marrë me ngjarjet e Kubernetes ose thjesht jeni mësuar të përdorni Bash, kolegët tanë kanë përgatitur një zgjidhje të gatshme në formën shell-operator (Ne i shpallur atë në prill).

PPS

Lexoni edhe në blogun tonë:

Burimi: www.habr.com

Shto një koment