Operador de Kubernetes en Python sin frameworks ni SDK

Operador de Kubernetes en Python sin frameworks ni SDK

Go es actualmente el monopolista entre los lenguajes de programación que las personas eligen para escribir operadores para Kubernetes. Hay razones objetivas para esto, tales como:

  1. Hay un potente marco para desarrollar operadores en Go — Operator SDK.
  2. Se han desarrollado aplicaciones "revolucionarias" en Go, como Docker y Kubernetes. Escribir su operador en Go significa hablar el mismo idioma que el ecosistema.
  3. El alto rendimiento de las aplicaciones en Go y las herramientas simples para trabajar con concurrencia "fuera de la caja".

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

Pero, ¿qué pasa si la falta de tiempo o, simplemente, la falta de motivación impide que estudies Go? El artículo presenta un ejemplo de cómo se puede escribir un buen operador utilizando uno de los lenguajes más populares, que prácticamente cada ingeniero DevOps conoce — Python.

Con ustedes: Copiador — ¡el operador de copia!

Para el ejemplo, consideremos el desarrollo de un operador simple, destinado a copiar un ConfigMap ya sea al aparecer un nuevo namespace, o al cambiar una de las dos entidades: ConfigMap y Secret. Desde el punto de vista práctico, el operador puede ser útil para actualizar masivamente las configuraciones de la aplicación (actualizando el ConfigMap) o para actualizar datos secretos — por ejemplo, claves para trabajar con Docker Registry (al añadir un Secret en el namespace).

Así que, qué debe tener un buen operador:

  1. La interacción con el operador se realiza a través de Custom Resource Definitions (en adelante — CRD).
  2. El operador puede ser configurado. Para ello, utilizaremos banderas de línea de comandos y variables de entorno.
  3. La construcción del contenedor Docker y del Helm chart se trabaja de tal forma que los usuarios puedan instalar fácilmente (literalmente con un solo comando) el operador en su clúster de Kubernetes.

CRD

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

  1. El tipo de recurso, que vamos a buscar (ConfigMap o Secret).
  2. La lista de namespaces, en los que deben estar los recursos.
  3. Selector, por el cual buscaremos recursos en el namespace.

Describamos 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

Y inmediatamente crearemos una regla simple — para buscar en el namespace llamado default todos los ConfigMap con etiquetas del tipo copyrator: "true":

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

¡Listo! Ahora necesitamos obtener información sobre nuestra regla. Debo aclarar que no escribiremos consultas al API Server del clúster por nuestra cuenta. Para esto utilizaremos una biblioteca de 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 la ejecución de este código, obtendremos lo siguiente:

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

Excelente: hemos logrado obtener la regla para el operador. Y lo más importante — lo hicimos, por así decirlo, al estilo 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 parámetros de línea de comandos;
  2. usar variables de entorno.

Los parámetros de línea de comandos permiten leer configuraciones de manera más flexible, con soporte y validación de tipos de datos. En la biblioteca estándar de Python hay un módulo argparser, que es el que utilizaremos. Los detalles y ejemplos de sus capacidades están disponibles en documentación oficial.

Así es como se verá un ejemplo de configuración para la lectura de parámetros de línea de comandos en nuestro caso:

   parser = ArgumentParser(
        description='Copyrator - operador de copia.',
        prog='copyrator'
    )
    parser.add_argument(
        '--namespace',
        type=str,
        default=getenv('NAMESPACE', 'default'),
        help='Namespace del operador'
    )
    parser.add_argument(
        '--rule-name',
        type=str,
        default=getenv('RULE_NAME', 'main-rule'),
        help='Nombre de CRD'
    )
    args = parser.parse_args()

Por otro lado, a través de variables de entorno en Kubernetes, se puede trasladar fácilmente la información de servicio del pod dentro del contenedor. Por ejemplo, la información sobre el namespace en el que se ejecuta el pod se puede obtener con la siguiente construcción:

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

La lógica de funcionamiento del operador

Para comprender cómo dividir los métodos para trabajar con ConfigMap y Secret, utilizaremos mapas especiales. Así podremos entender qué métodos necesitamos para supervisar 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, es necesario recibir eventos del servidor API. Lo implementaremos de la siguiente manera:

def handle(specs):
    kubernetes.config.load_incluster_config()
    v1 = kubernetes.client.CoreV1Api()

    # Obtenemos el método para supervisar objetos
    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, pasamos 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 principal está lista! Ahora necesitamos empaquetar todo esto en un solo paquete de Python. Creamos el archivo setup.py, escribimos allí la metainformación sobre el proyecto:

from sys import version_info

from setuptools import find_packages, setup

if version_info[:2] < (3, 5):
    raise RuntimeError(
        'Versión de python no soportada %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 de kubernetes para Python tiene su propio versionado. Más detalles sobre la compatibilidad entre las versiones del cliente y las versiones de Kubernetes se pueden encontrar en la matriz de compatibilidad.

Ahora nuestro proyecto se ve así:

copyrator
├── copyrator
│   ├── cli.py # Lógica para el trabajo con la línea de comandos
│   ├── constant.py # Constantes que proporcionamos anteriormente
│   ├── load_crd.py # Lógica de carga de CRD
│   └── operator.py # Lógica principal del operador
└── setup.py # Formato del paquete

Docker y Helm

El Dockerfile será extremadamente simple: tomaremos la imagen base python-alpine e instalaremos nuestro paquete. Su optimización la dejaremos para otro momento:

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, es necesario crear un rol correspondiente para el operador con los permisos 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 }}

Summary

Así, sin miedo, reproches y sin necesidad de aprender Go, pudimos construir nuestro propio operador para Kubernetes en Python. Por supuesto, aún tiene mucho por mejorar: en el futuro podrá manejar múltiples reglas, trabajar en múltiples hilos, y monitorear los cambios de sus CRD de manera autónoma...

Para que se puedan conocer mejor el código, lo hemos colocado en un repositorio público. Si estás interesado en ejemplos de operadores más serios, implementados con Python, puedes fijarte en dos operadores para desplegar mongodb (el primero y el segundo).

P.D. Y si no quieres lidiar con los eventos de Kubernetes o prefieres utilizar Bash, nuestros colegas han preparado una solución lista en forma de shell-operator (lo anunció hicimos en abril).

P.P.D.

También puedes leer en nuestro blog:

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster