Operador de Kubernetes en Python sin frameworks ni SDK

Operador de Kubernetes en Python sin frameworks ni SDK

Actualmente, Go tiene el monopolio de los lenguajes de programación que la gente elige para escribir declaraciones para Kubernetes. Hay razones objetivas para esto, tales como:

  1. Existe un marco poderoso para desarrollar operadores en Go: SDK del operador.
  2. Las aplicaciones innovadoras como Docker y Kubernetes están escritas en Go. Escribir tu operador en Go significa hablar el mismo idioma con el ecosistema.
  3. Alto rendimiento de las aplicaciones Go y herramientas sencillas para trabajar con concurrencia listas para usar.

NB: Por cierto, cómo escribir tu propia declaración en Go, nosotros ya descrito en una de nuestras traducciones de autores extranjeros.

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

Conozca: Copiadora - ¡operador de fotocopias!

Como ejemplo, considere desarrollar una declaración simple diseñada para copiar un ConfigMap cuando aparece un nuevo espacio de nombres o cuando cambia una de dos entidades: ConfigMap y Secret. Desde un punto de vista práctico, el operador puede resultar útil para la actualización masiva de configuraciones de aplicaciones (mediante la actualización de ConfigMap) o para actualizar datos secretos, por ejemplo, claves para trabajar con Docker Registry (al agregar Secret al espacio de nombres).

Por lo tanto, lo que debe tener un buen operador:

  1. La interacción con el operador se realiza mediante Definiciones de recursos personalizados (en adelante denominado CRD).
  2. El operador se puede configurar. Para hacer esto, usaremos indicadores de línea de comando y variables de entorno.
  3. La compilación del contenedor Docker y el gráfico Helm está diseñada para que los usuarios puedan instalar fácilmente (literalmente con un comando) el operador en su clúster de Kubernetes.

CRD

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

  1. Tipo de recurso, que buscaremos (ConfigMap o Secret).
  2. Lista de espacios de nombres, en el que deben ubicarse los recursos.
  3. Selector, mediante el cual buscaremos recursos en el espacio de nombres.

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 lo crearemos de inmediato. regla simple — para buscar en el espacio de nombres con el nombre default todo 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

¡Listo! Ahora necesitamos de alguna manera obtener información sobre nuestra regla. Permítanme hacer una reserva de inmediato: nosotros mismos no escribiremos solicitudes en el servidor API del clúster. Para hacer esto, usaremos una biblioteca Python ya preparada. cliente-kubernetes:

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 ejecutar este código, obtenemos lo siguiente:

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

Genial: logramos conseguir una regla para el operador. Y lo más importante, hicimos lo que se llama el estilo Kubernetes.

¿Variables de entorno o banderas? ¡Nosotros nos llevamos todo!

Pasemos 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 documentación oficial.

Para nuestro caso, así es como se vería un ejemplo de configuración de lectura de indicadores de línea de comando:

   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, al utilizar variables de entorno en Kubernetes, puede transferir fácilmente información de servicio sobre el pod dentro del 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 

Lógica del operador

Para comprender cómo separar los métodos para trabajar con ConfigMap y Secret, usaremos mapas especiales. Entonces podremos 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. Implementémoslo 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)

Luego 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 paquete de Python. Preparamos el archivo setup.py, escriba 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='[email protected]',
    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 control de versiones. Puede encontrar más información sobre la compatibilidad entre las versiones del cliente y las versiones de Kubernetes en matrices 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á increíblemente simple: tome la imagen base de python-alpine e instale nuestro paquete. Pospongamos 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 sencillo:

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

Total

Así fue como, sin miedos, reproches ni aprendizaje de Go, pudimos construir nuestro propio operador para Kubernetes en Python. Por supuesto, todavía tiene margen para crecer: en el futuro podrá procesar múltiples reglas, trabajar en múltiples hilos, monitorear de forma independiente los cambios en sus CRD...

Para darle una mirada más cercana al código, lo hemos incluido repositorio público. Si desea ejemplos de operadores más serios implementados usando Python, puede prestar atención a dos operadores para implementar mongodb (primero и segundo).

PD: Y si eres demasiado vago para lidiar con los eventos de Kubernetes o simplemente estás más acostumbrado a usar Bash, nuestros colegas han preparado una solución lista para usar en el formulario operador de shell (Nosotros Anunciado en abril).

PPS

Lea también en nuestro blog:

Fuente: habr.com

Añadir un comentario