Operatore Kubernetes in Python senza frameworks è SDK

Operatore Kubernetes in Python senza frameworks è SDK

Attualmente Go hà un monopoliu nantu à e lingue di prugrammazione chì a ghjente sceglie di scrive dichjarazioni per Kubernetes. Ci sò ragiuni oggettivi per questu, cum'è:

  1. Ci hè un quadru putente per u sviluppu di l'operatori in Go - Operatore SDK.
  2. L'applicazioni chì cambianu u ghjocu cum'è Docker è Kubernetes sò scritte in Go. Scrivite u vostru operatore in Go significa parlà a stessa lingua cù l'ecosistema.
  3. Alte prestazioni di l'applicazioni Go è arnesi simplici per travaglià cù a simultaneità fora di a scatula.

NB: Per via, cumu scrive a vostra propria dichjarazione in Go, noi digià descrittu in una di e nostre traduzzioni da autori stranieri.

Ma chì s'ellu ùn hè impeditu di amparà Andà per mancanza di tempu o, simpliciamente, motivazione ? L'articulu furnisce un esempiu di cumu pudete scrive una bona dichjarazione utilizendu una di e lingue più populari chì quasi tutti l'ingegneri DevOps cunnosci - pitone.

Meet: Copiatore - operatore di copia!

Per esempiu, cunzidira à sviluppà una dichjarazione simplice pensata per copià un ConfigMap sia quandu un novu spaziu di nome appare o quandu una di duie entità cambia: ConfigMap è Secret. Da un puntu di vista praticu, l'operatore pò esse utile per l'aghjurnamentu di massa di e cunfigurazioni di l'applicazione (aghjurnendu u ConfigMap) o per l'aghjurnà di dati secreti - per esempiu, chjave per travaglià cù u Docker Registry (quandu aghjunghje Secret à u namespace).

Cusì, ciò chì un bon operatore deve avè:

  1. L'interazzione cù l'operatore hè realizatu cù l'usu Definizioni di risorse persunalizate (ci-après dénommé CRD).
  2. L'operatore pò esse cunfiguratu. Per fà questu, useremu bandieri di linea di cumanda è variabili di l'ambiente.
  3. A custruzione di u containeru Docker è u graficu Helm hè pensatu per chì l'utilizatori ponu facilmente (literalmente cun un cumandamentu) installà l'operatore in u so cluster Kubernetes.

CRD

Per chì l'operatore sappia quale risorse per circà è induve circà, avemu bisognu di stabilisce una regula per ellu. Ogni regula serà rapprisintata cum'è un oggettu CRD unicu. Chì campi deve avè stu CRD?

  1. Tipu di risorsa, chì avemu da circà (ConfigMap o Secret).
  2. Lista di spazii di nomi, in quale i risorse deve esse situatu.
  3. Selettore, da quale avemu da circà risorse in u namespace.

Descrivimu u 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

È avemu da creà subitu regula simplice - per circà in u namespace cù u nome default tutti ConfigMap cù etichette cum'è copyrator: "true":

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

Pronti ! Avà avemu bisognu di qualchì infurmazione nantu à a nostra regula. Lasciami fà una riservazione subitu chì ùn scriveremu micca richieste à u cluster API Server. Per fà questu, useremu una biblioteca Python pronta 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')}

In u risultatu di eseguisce stu codice, avemu i seguenti:

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

Grande: avemu riesciutu à ottene una regula per l'operatore. È più impurtante, avemu fattu ciò chì hè chjamatu u modu Kubernetes.

Variabili d'ambiente o bandiere? Pigliemu tuttu !

Passemu à a cunfigurazione principale di l'operatore. Ci hè dui approcci basi per cunfigurà l'applicazioni:

  1. aduprà l'opzioni di linea di cumanda;
  2. aduprà variabili di ambiente.

L'opzioni di linea di cumanda permettenu di leghje i paràmetri in modu più flexible, cù supportu è validazione di tipu di dati. A biblioteca standard di Python hà un modulu argparser, chì avemu aduprà. I dettagli è esempi di e so capacità sò dispunibili in documentazione ufficiale.

Per u nostru casu, questu hè ciò chì un esempiu di stallà i bandieri di a linea di cummanda di leghje:

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

Per d 'altra banda, utilizendu variabili di l'ambiente in Kubernetes, pudete facilmente trasfiriri l'infurmazioni di serviziu nantu à u pod in u containeru. Per esempiu, pudemu avè infurmazione nantu à u spaziu di nomi in quale u pod hè in esecuzione cù a seguente custruzzione:

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

Lògica di l'operatore

Per capisce cumu si separà i metudi per travaglià cù ConfigMap è Secret, avemu aduprà carte speciali. Allora pudemu capisce chì metudi avemu bisognu di seguità è creà l'ughjettu:

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

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

Dopu, avete bisognu di riceve avvenimenti da u servitore API. Implementemu cusì cusì:

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)

Dopu avè ricivutu l'avvenimentu, andemu à a logica principale di trasfurmà:

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

A logica principale hè pronta! Avà avemu bisognu di imballà tuttu questu in un pacchettu Python. Preparamu u schedariu setup.py, scrivite meta infurmazione nantu à u prugettu quì:

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: U cliente kubernetes per Python hà a so propria versione. Più infurmazione nantu à a cumpatibilità trà e versioni di u cliente è e versioni di Kubernetes pò esse truvata in matrici di cumpatibilità.

Avà u nostru prughjettu s'assumiglia cusì:

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

Docker è Helm

U Dockerfile serà incredibbilmente simplice: pigliate l'imaghjini basati di python-alpine è installate u nostru pacchettu. Postponemu a so ottimisazione finu à tempi megliu:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

A implementazione per l'operatore hè ancu assai simplice:

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

Infine, avete bisognu di creà un rolu adattatu per l'operatore cù i diritti necessarii:

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

U risultatu

Hè cusì chì, senza paura, rimproveru, o amparà Go, pudemu custruisce u nostru propiu operatore per Kubernetes in Python. Di sicuru, hà sempre spaziu per cultivà: in u futuru puderà processà parechje regule, travaglià in parechje fili, monitorizà indipindente i cambiamenti in i so CRD ...

Per dà un ochju più vicinu à u codice, l'avemu messu in repository publicu. Se vulete esempi di operatori più seri implementati cù Python, pudete turnà a vostra attenzione à dui operatori per implementà mongodb (первый и u sicondu).

PS È s'è vo site troppu pigro per trattà cun l'avvenimenti di Kubernetes o sì solu più abituatu à aduprà Bash, i nostri culleghi anu preparatu una suluzione pronta in a forma shell-operatore (Noi annunziatu in aprile).

PPS

Leghjite puru nant'à u nostru blog:

Source: www.habr.com

Add a comment