
Go é atualmente o único monopolista entre as linguagens de programação que as pessoas escolhem para escrever operadores para o Kubernetes. Existem razões objetivas para isso, como:
- Há um poderoso framework para desenvolvimento de operadores em Go — .
- Usando Go, foram criados aplicativos que mudaram o jogo, como Docker e Kubernetes. Escrever seu próprio operador em Go é comunicar-se com o ecossistema em uma única linguagem.
- Alto desempenho dos aplicativos em Go e ferramentas simples para trabalhar com concorrência "prontas para uso".
NB: A propósito, como escrever seu próprio operador em Go, já em uma de nossas traduções de autores estrangeiros.
Mas e se a falta de tempo ou, simplesmente, a falta de motivação, estiver atrapalhando seu aprendizado de Go? O artigo apresenta um exemplo de como escrever um operador de qualidade, usando uma das linguagens mais populares, que praticamente todo engenheiro DevOps conhece — Python.
Conheçam: Copiador — o operador de cópia!
Para este exemplo, vamos considerar o desenvolvimento de um operador simples, projetado para copiar um ConfigMap sempre que um novo namespace aparecer ou sempre que uma das duas entidades — ConfigMap e Secret — for alterada. Em termos práticos, o operador pode ser útil para atualização em massa de configurações de aplicativos (através da atualização do ConfigMap) ou para atualização de dados secretos — por exemplo, chaves para trabalhar com o Docker Registry (ao adicionar um Secret ao namespace).
Então, o que um bom operador deve ter:
- A interação com o operador é feita por meio de (a seguir — CRD).
- O operador pode ser configurado. Para isso, usaremos flags de linha de comando e variáveis de ambiente.
- A construção do contêiner Docker e do Helm chart é elaborada de forma que os usuários possam facilmente (literalmente com um único comando) instalar o operador em seu cluster Kubernetes.
CRD
Para que o operador saiba quais recursos e onde procurá-los, precisamos definir uma regra para ele. Cada regra será representada como um único objeto CRD. Quais campos devem existir neste CRD?
- Tipo de recurso, que vamos procurar (ConfigMap ou Secret).
- Lista de namespaces, onde os recursos devem estar.
- Selector, pelo qual vamos buscar recursos no namespace.
Descreveremos 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 logo iremos criar uma regra simples — para busca no namespace com o nome padrão de todos os ConfigMaps com rótulos do tipo copyrator: "true":
apiVersion: flant.com/v1
kind: CopyratorRule
metadata:
name: main-rule
labels:
module: copyrator
ruleType: configmap
selector:
copyrator: "true"
namespace: defaultPronto! Agora precisamos obter informações sobre nossa regra. Devo esclarecer que não vamos escrever consultas ao API Server do cluster por conta própria. Para isso, usaremos uma biblioteca Python já pronta :
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, obteremos o seguinte:
{'ruleType': 'configmap', 'selector': {'copyrator': 'true'}, 'namespace': ['default']}Ótimo: conseguimos recuperar a regra para o operador. E o mais importante — fizemos isso, por assim dizer, da maneira Kubernetes.
Variáveis de ambiente ou flags? Vamos pegar tudo!
Vamos para a configuração principal do operador. Existem duas abordagens básicas para configurar aplicativos:
- usar parâmetros da linha de comando;
- usar variáveis de ambiente.
Os parâmetros da linha de comando permitem ler configurações de forma mais flexível, com suporte e validação de tipos de dados. Na biblioteca padrão do Python, existe o módulo argparser, que é o que vamos usar. Detalhes e exemplos de suas funcionalidades estão disponíveis em .
Aqui está como um exemplo de configuração para leitura de flags da linha de comando ficaria no nosso caso:
parser = ArgumentParser(
description='Copyrator - operador de cópia.',
prog='copyrator'
)
parser.add_argument(
'--namespace',
type=str,
default=getenv('NAMESPACE', 'default'),
help='Namespace do Operador'
)
parser.add_argument(
'--rule-name',
type=str,
default=getenv('RULE_NAME', 'main-rule'),
help='Nome do CRD'
)
args = parser.parse_args()Por outro lado, com o uso de variáveis de ambiente no Kubernetes, é fácil transferir informações de serviço sobre o pod para dentro do contêiner. Por exemplo, as informações sobre o namespace em que o pod está em execução podem ser obtidas com a seguinte construção:
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace A lógica de funcionamento do operador
Para entender como dividir os métodos para trabalhar com ConfigMap e Secret, utilizaremos mapas especiais. Assim, poderemos entender quais métodos precisamos para monitorar 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, precisamos receber eventos do servidor API. Vamos implementar isso da seguinte forma:
def handle(specs):
kubernetes.config.load_incluster_config()
v1 = kubernetes.client.CoreV1Api()
# Obtemos o método para monitorar 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)Após receber o evento, passamos para a lógica principal de processamento dele:
# Типы событий, на которые будем реагировать
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. Vamos editar o arquivo setup.py, escrevendo as metainformações sobre o projeto:
from sys import version_info
from setuptools import find_packages, setup
if version_info[:2] < (3, 5):
raise RuntimeError(
'Versão de python não suportada %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: O cliente kubernetes para Python tem sua própria versão. Mais detalhes sobre a compatibilidade entre versões do cliente e versões do Kubernetes podem ser encontrados na .
Atualmente, nosso projeto se parece com isto:
copyrator
├── copyrator
│ ├── cli.py # Lógica de trabalho com a linha de comando
│ ├── constant.py # Constantes que mencionamos anteriormente
│ ├── load_crd.py # Lógica de carregamento do CRD
│ └── operator.py # Lógica principal de funcionamento do operador
└── setup.py # Formatação do pacoteDocker e Helm
O Dockerfile será extremamente simples: vamos pegar a imagem base python-alpine e instalar nosso pacote. Vamos adiar a sua otimização para um futuro melhor:
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 }}-accFinalmente, é necessário criar a função correspondente 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 }}Resultado
Assim, sem medo, reprovação ou necessidade de aprender Go, conseguimos criar nosso próprio operador para Kubernetes em Python. Claro, ainda há espaço para crescimento: no futuro, ele poderá lidar com várias regras, trabalhar em múltiplas threads e monitorar automaticamente as mudanças em seus CRD…
Para que você possa conhecer melhor o código, o colocamos em . Se você está em busca de exemplos de operadores mais complexos implementados com Python, pode prestar atenção em dois operadores para o deploy do mongodb ( e ).
P.S. E se você está com preguiça de entender os eventos do Kubernetes ou simplesmente prefere usar Bash — nossos colegas prepararam uma solução pronta na forma de (nós o lançamos em abril).
P.P.S.
Confira também em nosso blog:
- «»;
- «»;
- «»;
- «»;
- «».
Fonte: habr.com
