Kubernetes Operatoro en Python sen kadroj kaj SDK

Kubernetes Operatoro en Python sen kadroj kaj SDK

Go nuntempe havas monopolon pri la programlingvoj, kiujn homoj elektas por skribi deklarojn por Kubernetes. Estas objektivaj kialoj por tio, kiel ekzemple:

  1. Estas potenca kadro por disvolvi funkciigistojn en Go - Funkciigisto SDK.
  2. Ludŝanĝaj aplikoj kiel Docker kaj Kubernetes estas skribitaj en Go. Skribi vian funkciigiston en Go signifas paroli la saman lingvon kun la ekosistemo.
  3. Alta rendimento de Go-aplikoj kaj simplaj iloj por labori kun samtempeco ekstere de la skatolo.

NB: Cetere, kiel skribi vian propran deklaron en Go, ni jam priskribita en unu el niaj tradukoj de eksterlandaj aŭtoroj.

Sed kio se vi malhelpas lerni Iri pro manko de tempo aŭ, simple dirite, motivado? La artikolo donas ekzemplon pri kiel vi povas skribi bonan deklaron uzante unu el la plej popularaj lingvoj, kiujn preskaŭ ĉiu DevOps-inĝeniero konas - python.

Renkontu: Kopiilo - kopiisto!

Ekzemple, konsideru evoluigi simplan deklaron desegnitan por kopii ConfigMap aŭ kiam nova nomspaco aperas aŭ kiam unu el du estaĵoj ŝanĝiĝas: ConfigMap kaj Secret. El praktika vidpunkto, la funkciigisto povas esti utila por amasa ĝisdatigo de aplikaĵagordoj (per ĝisdatigo de la ConfigMap) aŭ por ĝisdatigi sekretajn datumojn - ekzemple ŝlosilojn por labori kun la Docker Registry (dum aldonado de Sekreto al la nomspaco).

Kaj tiel, kion bona operatoro devus havi:

  1. Interago kun la operatoro estas efektivigita uzante Propraj Rimedaj Difinoj (ĉi-poste nomata CRD).
  2. La funkciigisto povas esti agordita. Por fari tion, ni uzos komandliniajn flagojn kaj mediajn variablojn.
  3. La konstruo de la Docker-ujo kaj Helm-diagramo estas dizajnita por ke uzantoj povu facile (laŭvorte per unu komando) instali la funkciigiston en sia Kubernetes-areo.

CRD

Por ke la funkciigisto sciu, kiajn rimedojn serĉi kaj kie serĉi, ni devas starigi regulon por li. Ĉiu regulo estos reprezentita kiel ununura CRD-objekto. Kiujn kampojn devus havi ĉi tiu CRD?

  1. Rimeda tipo, kiun ni serĉos (ConfigMap aŭ Sekreto).
  2. Listo de nomspacoj, en kiu la rimedoj devus troviĝi.
  3. Selektilo, per kiu ni serĉos rimedojn en la nomspaco.

Ni priskribu la 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

Kaj ni tuj kreos ĝin simpla regulo — serĉi en la nomspaco kun la nomo default ĉiuj ConfigMap kun etikedoj kiel copyrator: "true":

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

Preta! Nun ni devas iel akiri informojn pri nia regulo. Mi tuj rezervu, ke ni mem ne skribos petojn al la cluster API-Servilo. Por fari tion, ni uzos pretan Python-bibliotekon kubernetes-kliento:

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

Kiel rezulto de rulado de ĉi tiu kodo, ni ricevas la jenon:

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

Bonege: ni sukcesis ricevi regulon por la funkciigisto. Kaj plej grave, ni faris tion, kion oni nomas la maniero Kubernetes.

Medivariabloj aŭ flagoj? Ni prenas ĉion!

Ni transiru al la ĉefa operatora agordo. Estas du bazaj aliroj al agordo de aplikoj:

  1. uzu komandliniajn opciojn;
  2. uzi mediajn variablojn.

Komandliniaj opcioj permesas vin legi agordojn pli flekseble, kun subteno kaj validigo de datumtipoj. La norma biblioteko de Python havas modulon argparser, kiun ni uzos. Detaloj kaj ekzemploj de ĝiaj kapabloj estas haveblaj en oficiala dokumentaro.

Por nia kazo, jen kiel aspektus ekzemplo de agordo de legado de komandliniaj flagoj:

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

Aliflanke, uzante mediajn variablojn en Kubernetes, vi povas facile translokigi servajn informojn pri la pod en la ujo. Ekzemple, ni povas ricevi informojn pri la nomspaco en kiu la pod funkcias kun la sekva konstruo:

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

Operatora logiko

Por kompreni kiel apartigi metodojn por labori kun ConfigMap kaj Secret, ni uzos specialajn mapojn. Tiam ni povas kompreni kiajn metodojn ni bezonas spuri kaj krei la objekton:

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

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

Poste, vi devas ricevi eventojn de la API-servilo. Ni efektivigu ĝin jene:

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)

Post ricevi la eventon, ni transiras al la ĉefa logiko pri prilaborado de ĝi:

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

La ĉefa logiko estas preta! Nun ni devas paki ĉion ĉi en unu Python-pakaĵon. Ni preparas la dosieron setup.py, skribu metainformojn pri la projekto tie:

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: La kubernetes-kliento por Python havas sian propran versionadon. Pliaj informoj pri kongruo inter klientversioj kaj Kubernetes-versioj troveblas en kongruaj matricoj.

Nun nia projekto aspektas jene:

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

Docker kaj Helm

La Dockerfile estos nekredeble simpla: prenu la bazan bildon python-alpine kaj instalu nian pakaĵon. Ni prokrastu ĝian optimumigon ĝis pli bonaj tempoj:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

Deplojo por la funkciigisto ankaŭ estas tre simpla:

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

Fine, vi devas krei taŭgan rolon por la funkciigisto kun la necesaj rajtoj:

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

La rezulto

Tiel, sen timo, riproĉo aŭ lernado de Go, ni povis konstrui nian propran funkciigiston por Kubernetes en Python. Kompreneble, ĝi ankoraŭ havas lokon por kreski: estonte ĝi povos prilabori plurajn regulojn, labori en pluraj fadenoj, sendepende kontroli ŝanĝojn en siaj CRD-oj...

Por pli detale rigardi la kodon, ni enmetis ĝin publika deponejo. Se vi volas ekzemplojn de pli seriozaj funkciigistoj efektivigitaj uzante Python, vi povas turni vian atenton al du funkciigistoj por deploji mongodb (первый и la dua).

PS Kaj se vi estas tro maldiligenta por trakti Kubernetes-eventojn aŭ vi simple pli kutimas uzi Bash, niaj kolegoj preparis pretan solvon en la formo ŝelo-funkciigisto (Ni anoncita ĝin en aprilo).

PPS

Legu ankaŭ en nia blogo:

fonto: www.habr.com

Aldoni komenton