Operador Kubernetes em Python sem frameworks e SDK

Operador Kubernetes em Python sem frameworks e SDK

Go atualmente detém o monopólio das linguagens de programação que as pessoas escolhem para escrever declarações para o Kubernetes. Existem razões objetivas para isso, como:

  1. Existe uma estrutura poderosa para o desenvolvimento de operadores em Go - SDK do Operador.
  2. Aplicativos revolucionários, como Docker e Kubernetes, são escritos em Go. Escrever sua operadora em Go significa falar a mesma língua do ecossistema.
  3. Alto desempenho de aplicativos Go e ferramentas simples para trabalhar com simultaneidade prontas para uso.

NB: A propósito, como escrever sua própria declaração em Go, nós já descrito em uma de nossas traduções de autores estrangeiros.

Mas e se você for impedido de aprender Go por falta de tempo ou, simplesmente, de motivação? O artigo fornece um exemplo de como você pode escrever uma boa declaração usando uma das linguagens mais populares que quase todo engenheiro DevOps conhece - Python.

Conheça: Copiadora - copiadora!

Como exemplo, considere desenvolver uma instrução simples projetada para copiar um ConfigMap quando um novo namespace aparecer ou quando uma das duas entidades for alterada: ConfigMap e Secret. Do ponto de vista prático, o operador pode ser útil para atualização em massa de configurações de aplicativos (atualizando o ConfigMap) ou para atualização de dados secretos - por exemplo, chaves para trabalhar com o Docker Registry (ao adicionar Secret ao namespace).

Assim, o que um bom operador deve ter:

  1. A interação com o operador é realizada por meio de Definições de recursos personalizados (doravante denominado CRD).
  2. O operador pode ser configurado. Para fazer isso, usaremos sinalizadores de linha de comando e variáveis ​​de ambiente.
  3. A construção do contêiner Docker e do gráfico Helm foi projetada para que os usuários possam facilmente (literalmente com um comando) instalar o operador em seu cluster Kubernetes.

CRD

Para que o operador saiba quais recursos procurar e onde procurar, precisamos estabelecer uma regra para ele. Cada regra será representada como um único objeto CRD. Quais campos este CRD deve ter?

  1. Tipo de recurso, que procuraremos (ConfigMap ou Secret).
  2. Lista de namespaces, onde os recursos devem estar localizados.
  3. Seletor, pelo qual procuraremos recursos no namespace.

Vamos descrever 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 vamos criá-lo imediatamente regra simples — para pesquisar no namespace com o nome default todos os ConfigMap com rótulos como copyrator: "true":

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

Preparar! Agora precisamos de alguma forma obter informações sobre nossa regra. Deixe-me fazer uma reserva imediatamente: não escreveremos solicitações para o servidor API do cluster. Para fazer isso, usaremos uma biblioteca Python pronta 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 da execução deste código, obtemos o seguinte:

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

Ótimo: conseguimos uma regra para a operadora. E o mais importante, fizemos o que é chamado de método Kubernetes.

Variáveis ​​ou sinalizadores de ambiente? Levamos tudo!

Vamos passar para a configuração principal do operador. Existem duas abordagens básicas para configurar aplicativos:

  1. use opções de linha de comando;
  2. use variáveis ​​de ambiente.

As opções de linha de comando permitem que você leia as configurações com mais flexibilidade, com suporte e validação de tipos de dados. A biblioteca padrão do Python possui um módulo argparser, que usaremos. Detalhes e exemplos de seus recursos estão disponíveis em documentação oficial.

Para o nosso caso, seria um exemplo de configuração de leitura de sinalizadores de linha 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 outro lado, usando variáveis ​​de ambiente no Kubernetes, você pode transferir facilmente informações de serviço sobre o pod dentro do contêiner. Por exemplo, podemos obter informações sobre o namespace no qual o pod está sendo executado com a seguinte construção:

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

Lógica do operador

Para entender como separar métodos para trabalhar com ConfigMap e Secret, usaremos mapas especiais. Então podemos entender quais métodos precisamos para rastrear e criar o 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',
}

Em seguida, você precisa receber eventos do servidor API. Vamos implementá-lo da seguinte forma:

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)

Após receber o evento, passamos para a lógica principal de seu processamento:

# Типы событий, на которые будем реагировать
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ógica principal está pronta! Agora precisamos empacotar tudo isso em um pacote Python. Preparamos o arquivo setup.py, escreva meta informações sobre o projeto lá:

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 Kubernetes para Python tem seu próprio controle de versão. Mais informações sobre compatibilidade entre versões do cliente e versões do Kubernetes podem ser encontradas em matrizes de compatibilidade.

Agora nosso projeto está assim:

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

Docker e Helm

O Dockerfile será incrivelmente simples: pegue a imagem base python-alpine e instale nosso pacote. Vamos adiar sua otimização para tempos melhores:

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

A implantação para o operador também é muito simples:

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, você precisa criar uma função apropriada para o operador com os direitos necessários:

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

Foi assim que, sem medo, censura ou aprendizado de Go, conseguimos construir nosso próprio operador para Kubernetes em Python. Claro, ainda tem espaço para crescer: no futuro será capaz de processar múltiplas regras, trabalhar em múltiplas threads, monitorar de forma independente alterações em seus CRDs...

Para lhe dar uma visão mais detalhada do código, nós o colocamos em repositório público. Se você quiser exemplos de operadores mais sérios implementados usando Python, você pode voltar sua atenção para dois operadores para implantação do mongodb (primeiro и segundo).

PS E se você tem preguiça de lidar com eventos do Kubernetes ou simplesmente está mais acostumado a usar o Bash, nossos colegas prepararam uma solução pronta no formato operador shell (Nós anunciado isso em abril).

PPS

Leia também em nosso blog:

Fonte: habr.com

Adicionar um comentário