Operador Kubernetes en Python sin frameworks y SDK



Go es actualmente un monopolista entre los lenguajes de programación que la gente elige para escribir declaraciones para Kubernetes. Existen razones objetivas como:

  1. Existe un poderoso marco para desarrollar operadores en Go - Operator SDK .
  2. Go ha escrito aplicaciones al revés como Docker y Kubernetes. Para escribir su propio operador en Go, hable el mismo idioma con el ecosistema.
  3. Aplicaciones de alto rendimiento en Go y herramientas simples para trabajar con la concurrencia lista para usar.

NB : Por cierto, ya describimos cómo escribir su propio operador en Go en una de nuestras traducciones de autores extranjeros.

Pero, ¿qué pasa si aprender Go se evita por falta de tiempo o, trivialmente, por motivación? El artículo proporciona un ejemplo de cómo puede escribir un operador sólido utilizando uno de los lenguajes más populares que casi todos los ingenieros de DevOps conocen: Python .

Conoce: redactor - operador de copia!


Por ejemplo, considere el desarrollo de un operador simple diseñado para copiar ConfigMap cuando aparezca un nuevo espacio de nombres o cuando cambie una de las dos entidades: ConfigMap y Secret. Desde el punto de vista de la aplicación práctica, el operador puede ser útil para la actualización masiva de las configuraciones de la aplicación (actualizando ConfigMap) o para actualizar datos secretos, por ejemplo, claves para trabajar con el Registro Docker (al agregar Secreto al espacio de nombres).

Entonces, qué buen operador debería tener :

  1. La interacción con el operador se lleva a cabo utilizando definiciones de recursos personalizados (en adelante, CRD).
  2. El operador puede ser personalizado. Para hacer esto, usaremos banderas de línea de comando y variables de entorno.
  3. El ensamblaje del contenedor Docker y el diagrama Helm se está trabajando para que los usuarios puedan instalar fácilmente (literalmente con un comando) el operador en su clúster Kubernetes.

CRD


Para que el operador sepa qué recursos y dónde buscarlo, necesitamos establecer una regla para él. Cada regla se representará como un único objeto CRD. ¿Qué campos debe tener este CRD?

  1. El tipo de recurso que estamos buscando (ConfigMap o Secret).
  2. Una lista de espacios de nombres donde deben ubicarse los recursos.
  3. Selector , por el cual buscaremos recursos en el espacio de nombres.

Describimos 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 

E inmediatamente cree una regla simple : buscar en el espacio de nombres con el nombre default todos los ConfigMap con etiquetas como copyrator: "true" :

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

Hecho Ahora necesita obtener de alguna manera información sobre nuestra regla. Debo hacer una reserva de inmediato para que no escribamos solicitudes en la API del servidor de clúster. Para hacer esto, usaremos la biblioteca Python ya preparada 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')} 

Como resultado de este código, obtenemos lo siguiente:

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

Excelente: logramos obtener una regla para el operador. Y lo más importante, hicimos lo que se llama la forma de Kubernetes.

Variables de entorno o banderas? Tomamos todo!


Pasamos a la configuración principal del operador. Hay dos enfoques básicos para configurar aplicaciones:

  1. Usar opciones de línea de comando
  2. utilizar variables de entorno

Las opciones de línea de comando le permiten leer la configuración de manera más flexible, con soporte y validación de tipos de datos. La biblioteca estándar de Python tiene un módulo argparser , que usaremos. Los detalles y ejemplos de sus capacidades están disponibles en la documentación oficial .

Así es como se verá el ejemplo de configuración de lectura de banderas de línea de comando para nuestro caso:

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

Por otro lado, utilizando variables de entorno en Kubernetes, puede transferir fácilmente información de servicio sobre el pod al contenedor. Por ejemplo, podemos obtener información sobre el espacio de nombres en el que se ejecuta el pod con la siguiente construcción:

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

Operador Lógico


Para comprender cómo separar los métodos para trabajar con ConfigMap y Secret, utilizaremos tarjetas especiales. Entonces podemos entender qué métodos necesitamos para rastrear y crear el objeto:

 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ón, debe recibir eventos del servidor API. Lo implementamos de la siguiente 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) 

Después de recibir el evento, procedemos a la lógica principal de su procesamiento:

 #  ,     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 básica está lista! Ahora necesita empacar todo esto en un paquete Python. setup.py , escribimos metainformación sobre el proyecto allí:

 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='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 : el cliente kubernetes para Python tiene su propio control de versiones. Puede obtener más información sobre la compatibilidad entre versiones de cliente y versiones de Kubernetes en la matriz de compatibilidad .

Ahora nuestro proyecto se ve así:

 copyrator ├── copyrator │ ├── cli.py #      │ ├── constant.py # ,     │ ├── load_crd.py #   CRD │ └── operator.py #     └── setup.py #   

Docker y timón


El Dockerfile será escandalosamente simple: tome la imagen básica de python-alpine e instale nuestro paquete. Pospondremos su optimización hasta tiempos mejores:

 FROM python:3.7.3-alpine3.9 ADD . /app RUN pip3 install /app ENTRYPOINT ["copyrator"] 

El despliegue para el operador también es muy simple:

 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 

Finalmente, debe crear el rol apropiado para el operador con los derechos necesarios:

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

Resumen


Entonces, sin temor, reproche y aprendizaje de Go, pudimos armar nuestro propio operador para Kubernetes en Python. Por supuesto, todavía tiene espacio para crecer: en el futuro, podrá procesar varias reglas, trabajar en varios hilos, monitorear independientemente los cambios en su CRD ...

Para ver más de cerca el código, lo colocamos en un repositorio público . Si desea ejemplos de operadores más serios implementados usando Python, puede dirigir su atención a dos operadores para implementar mongodb (el primero y el segundo ).

PD: y si eres demasiado vago para lidiar con los eventos de Kubernetes o si te sientes más cómodo usando Bash, nuestros colegas han preparado una solución preparada en forma de operador de shell (lo anunciamos en abril).

PPS


Lea también en nuestro blog:

Source: https://habr.com/ru/post/459320/


All Articles