Operador de Kubernetes en Python sen marcos e SDK

Operador de Kubernetes en Python sen marcos e SDK

Go actualmente ten o monopolio das linguaxes de programación que a xente escolle para escribir declaracións para Kubernetes. Hai razóns obxectivas para iso, como:

  1. Hai un marco poderoso para desenvolver operadores en Go - SDK do operador.
  2. As aplicacións que cambian o xogo como Docker e Kubernetes están escritas en Go. Escribir o teu operador en Go significa falar o mesmo idioma co ecosistema.
  3. Alto rendemento das aplicacións Go e ferramentas sinxelas para traballar con simultaneidade fóra da caixa.

NB: Por certo, como escribir a túa propia declaración en Go, nós xa descrito nunha das nosas traducións de autores estranxeiros.

Pero e se che impide aprender Ir por falta de tempo ou, simplemente, de motivación? O artigo ofrece un exemplo de como podes escribir unha boa declaración usando un dos idiomas máis populares que case todos os enxeñeiros de DevOps coñecen: Pitão.

Coñeza: copiadora - operador de copia!

Como exemplo, considere desenvolver unha instrución sinxela deseñada para copiar un ConfigMap cando apareza un novo espazo de nomes ou cando cambie unha das dúas entidades: ConfigMap e Secret. Desde un punto de vista práctico, o operador pode ser útil para a actualización masiva das configuracións das aplicacións (actualizando o ConfigMap) ou para actualizar datos secretos, por exemplo, claves para traballar co Rexistro Docker (ao engadir Secret ao espazo de nomes).

Así, o que debería ter un bo operador:

  1. A interacción co operador realízase mediante Definicións de recursos personalizados (en diante CRD).
  2. O operador pódese configurar. Para iso, utilizaremos marcas de liña de comandos e variables de ambiente.
  3. A compilación do contedor Docker e do gráfico Helm está deseñada para que os usuarios poidan instalar facilmente (literalmente cun só comando) o operador no seu clúster de Kubernetes.

CRD

Para que o operador saiba que recursos buscar e onde buscar, hai que establecerlle unha regra. Cada regra representarase como un único obxecto CRD. Que campos debe ter este CRD?

  1. Tipo de recurso, que buscaremos (ConfigMap ou Secret).
  2. Lista de espazos de nomes, no que se deben situar os recursos.
  3. Selector, mediante o que buscaremos recursos no espazo de nomes.

Imos describir o 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 crearémolo de inmediato regra simple — para buscar no espazo de nomes co nome 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! Agora necesitamos dalgunha maneira obter información sobre a nosa regra. Permíteme facer unha reserva de inmediato para que non escribiremos solicitudes ao servidor da API do clúster. Para iso, utilizaremos unha biblioteca Python preparada kubernetes-cliente:

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 executar este código, obtemos o seguinte:

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

Xenial: conseguimos conseguir unha regra para o operador. E o máis importante, fixemos o que se chama Kubernetes.

Variables de ambiente ou bandeiras? Levamos todo!

Pasemos á configuración do operador principal. Hai dous enfoques básicos para configurar aplicacións:

  1. usar opcións de liña de comandos;
  2. utilizar variables de ambiente.

As opcións de liña de comandos permítenche ler a configuración de forma máis flexible, con compatibilidade e validación de tipos de datos. A biblioteca estándar de Python ten un módulo argparser, que utilizaremos. Os detalles e exemplos das súas capacidades están dispoñibles en documentación oficial.

Para o noso caso, así sería un exemplo de configuración de bandeiras de liña de comandos de lectura:

   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 outra banda, usando variables de ambiente en Kubernetes, pode transferir facilmente a información do servizo sobre o pod dentro do contedor. Por exemplo, podemos obter información sobre o espazo de nomes no que se está a executar o pod coa seguinte construción:

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

Lóxica do operador

Para entender como separar os métodos para traballar con ConfigMap e Secret, utilizaremos mapas especiais. Entón podemos entender que métodos necesitamos para rastrexar e crear o obxecto:

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, cómpre recibir eventos do servidor da API. Implementémolo do seguinte xeito:

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)

Despois de recibir o evento, pasamos á lóxica principal de procesalo:

# Типы событий, на которые будем реагировать
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 lóxica principal está lista! Agora necesitamos empaquetar todo isto nun paquete de Python. Preparamos o arquivo setup.py, escribe alí información meta sobre o proxecto:

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: O cliente de kubernetes para Python ten o seu propio control de versións. Pódese atopar máis información sobre a compatibilidade entre as versións do cliente e as versións de Kubernetes en matrices de compatibilidade.

Agora o noso proxecto ten o seguinte aspecto:

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

Docker e Helm

O Dockerfile será incriblemente sinxelo: toma a imaxe base de python-alpine e instala o noso paquete. Posamos a súa optimización ata tempos mellores:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

A implantación para o operador tamén é moi sinxela:

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, cómpre crear un rol axeitado para o operador cos dereitos 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í foi como, sen medo, reproche nin aprender Go, puidemos construír o noso propio operador para Kubernetes en Python. Por suposto, aínda ten marxe para crecer: no futuro poderá procesar varias regras, traballar en varios fíos, supervisar de forma independente os cambios nos seus CRD...

Para que vexas máis de cerca o código, puxémolo repositorio público. Se queres exemplos de operadores máis serios implementados usando Python, podes centrar a túa atención en dous operadores para implementar mongodb (primeiro и segundo).

PD E se tes demasiado preguiceiro para xestionar eventos de Kubernetes ou simplemente estás máis afeito a usar Bash, os nosos colegas prepararon unha solución preparada no formulario operador de shell (Nós anunciou en abril).

PPS

Lea tamén no noso blog:

Fonte: www.habr.com

Engadir un comentario