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