Operador Kubernetes en Python sense marcs ni SDK

Operador Kubernetes en Python sense marcs ni SDK

Actualment, Go té el monopoli dels llenguatges de programació que la gent tria per escriure declaracions per a Kubernetes. Hi ha raons objectives per a això, com ara:

  1. Hi ha un marc potent per desenvolupar operadors a Go - SDK de l'operador.
  2. Les aplicacions que canvien el joc com Docker i Kubernetes estan escrites a Go. Escriure el teu operador a Go significa parlar el mateix idioma amb l'ecosistema.
  3. Alt rendiment de les aplicacions Go i eines senzilles per treballar amb concurrència de manera immediata.

NB: Per cert, com escriure la vostra pròpia declaració a Go, nosaltres ja descrit en una de les nostres traduccions d'autors estrangers.

Però, què passa si t'impedeix aprendre Go per falta de temps o, simplement, per motivació? L'article proporciona un exemple de com podeu escriure una bona declaració utilitzant un dels idiomes més populars que gairebé tots els enginyers de DevOps coneixen: Pitó.

Meet: copiadora - operador de còpia!

Com a exemple, considereu desenvolupar una instrucció senzilla dissenyada per copiar un ConfigMap quan apareix un espai de noms nou o quan canvia una de les dues entitats: ConfigMap i Secret. Des d'un punt de vista pràctic, l'operador pot ser útil per a l'actualització massiva de les configuracions de l'aplicació (actualitzant el ConfigMap) o per actualitzar dades secretes, per exemple, claus per treballar amb el registre Docker (quan s'afegeix Secret a l'espai de noms).

Per tant, què ha de tenir un bon operador:

  1. La interacció amb l'operador es realitza mitjançant Definicions de recursos personalitzades (d'ara endavant, CRD).
  2. L'operador es pot configurar. Per fer-ho, utilitzarem senyals de línia d'ordres i variables d'entorn.
  3. La creació del contenidor Docker i el gràfic Helm està dissenyada perquè els usuaris puguin instal·lar fàcilment (literalment amb una ordre) l'operador al seu clúster Kubernetes.

CRD

Perquè l'operador sàpiga quins recursos ha de buscar i on buscar, hem de posar-li una regla. Cada regla es representarà com un únic objecte CRD. Quins camps ha de tenir aquest CRD?

  1. Tipus de recurs, que buscarem (ConfigMap o Secret).
  2. Llista d'espais de noms, on s'han d'ubicar els recursos.
  3. Selector, mitjançant el qual buscarem recursos a l'espai de noms.

Descrivim el 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

I el crearem de seguida regla senzilla — per cercar a l'espai de noms amb el nom default tot ConfigMap amb etiquetes com copyrator: "true":

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

A punt! Ara hem d'obtenir informació sobre la nostra regla d'alguna manera. Permeteu-me fer una reserva immediatament perquè no escriurem sol·licituds al servidor d'API del clúster nosaltres mateixos. Per fer-ho, utilitzarem una biblioteca de Python ja feta 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')}

Com a resultat d'executar aquest codi, obtenim el següent:

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

Genial: vam aconseguir una regla per a l'operador. I el més important, vam fer el que s'anomena la manera Kubernetes.

Variables d'entorn o banderes? Ho prenem tot!

Passem a la configuració de l'operador principal. Hi ha dos enfocaments bàsics per configurar aplicacions:

  1. utilitzar opcions de línia d'ordres;
  2. utilitzar variables d'entorn.

Les opcions de línia d'ordres us permeten llegir la configuració de manera més flexible, amb suport i validació de tipus de dades. La biblioteca estàndard de Python té un mòdul argparser, que farem servir. Els detalls i exemples de les seves capacitats estan disponibles a documentació oficial.

Per al nostre cas, aquest és el que semblaria un exemple de configuració de la lectura de senyals de línia d'ordres:

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

D'altra banda, utilitzant variables d'entorn a Kubernetes, podeu transferir fàcilment la informació del servei sobre el pod dins del contenidor. Per exemple, podem obtenir informació sobre l'espai de noms en què s'executa el pod amb la construcció següent:

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

Lògica de l'operador

Per entendre com separar els mètodes per treballar amb ConfigMap i Secret, utilitzarem mapes especials. Aleshores podem entendre quins mètodes necessitem per fer un seguiment i crear l'objecte:

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

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

A continuació, heu de rebre esdeveniments del servidor de l'API. Implementem-ho de la següent manera:

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)

Després de rebre l'esdeveniment, passem a la lògica principal de processar-lo:

# Типы событий, на которые будем реагировать
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 lògica principal està a punt! Ara hem d'empaquetar tot això en un sol paquet Python. Preparem l'arxiu setup.py, escriviu aquí informació meta sobre el projecte:

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: El client kubernetes per a Python té el seu propi control de versions. Podeu trobar més informació sobre la compatibilitat entre les versions del client i les versions de Kubernetes a matrius de compatibilitat.

Ara el nostre projecte té aquest aspecte:

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

Docker i Helm

El Dockerfile serà increïblement senzill: agafeu la imatge base de python-alpine i instal·leu el nostre paquet. Posposam la seva optimització fins a temps millors:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

El desplegament per a l'operador també és molt senzill:

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

Finalment, heu de crear un rol adequat per a l'operador amb els drets necessaris:

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

Així és com, sense por, retrets ni aprenentatge de Go, vam poder construir el nostre propi operador per a Kubernetes a Python. Per descomptat, encara té espai per créixer: en el futur podrà processar diverses regles, treballar en diversos fils, supervisar de manera independent els canvis en els seus CRD...

Per donar-vos una ullada més de prop al codi, l'hem posat repositori públic. Si voleu exemples d'operadors més seriosos implementats amb Python, podeu centrar la vostra atenció en dos operadors per desplegar mongodb (первый и 2).

PD I si ets massa mandrós per fer front als esdeveniments de Kubernetes o simplement estàs més acostumat a utilitzar Bash, els nostres companys han preparat una solució ja feta en forma operador de shell (Nosaltres va anunciar a l'abril).

PPS

Llegeix també al nostre blog:

Font: www.habr.com

Afegeix comentari