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:
Existe uma estrutura poderosa para o desenvolvimento de operadores em Go - SDK do Operador.
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.
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).
O operador pode ser configurado. Para fazer isso, usaremos sinalizadores de linha de comando e variáveis de ambiente.
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?
Tipo de recurso, que procuraremos (ConfigMap ou Secret).
Lista de namespaces, onde os recursos devem estar localizados.
Seletor, pelo qual procuraremos recursos no namespace.
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:
Ó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:
use opções de linha de comando;
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:
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:
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:
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:
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).