
Go es actualmente el monopolista entre los lenguajes de programación que las personas eligen para escribir operadores para Kubernetes. Hay razones objetivas para esto, tales como:
- Hay un potente marco para desarrollar operadores en Go — .
- Se han desarrollado aplicaciones "revolucionarias" en Go, como Docker y Kubernetes. Escribir su operador en Go significa hablar el mismo idioma que el ecosistema.
- El alto rendimiento de las aplicaciones en Go y las herramientas simples para trabajar con concurrencia "fuera de la caja".
NB: Por cierto, cómo escribir su propio operador en Go ya en una de nuestras traducciones de autores extranjeros.
Pero, ¿qué pasa si la falta de tiempo o, simplemente, la falta de motivación impide que estudies Go? El artículo presenta un ejemplo de cómo se puede escribir un buen operador utilizando uno de los lenguajes más populares, que prácticamente cada ingeniero DevOps conoce — Python.
Con ustedes: Copiador — ¡el operador de copia!
Para el ejemplo, consideremos el desarrollo de un operador simple, destinado a copiar un ConfigMap ya sea al aparecer un nuevo namespace, o al cambiar una de las dos entidades: ConfigMap y Secret. Desde el punto de vista práctico, el operador puede ser útil para actualizar masivamente las configuraciones de la aplicación (actualizando el ConfigMap) o para actualizar datos secretos — por ejemplo, claves para trabajar con Docker Registry (al añadir un Secret en el namespace).
Así que, qué debe tener un buen operador:
- La interacción con el operador se realiza a través de (en adelante — CRD).
- El operador puede ser configurado. Para ello, utilizaremos banderas de línea de comandos y variables de entorno.
- La construcción del contenedor Docker y del Helm chart se trabaja de tal forma que los usuarios puedan instalar fácilmente (literalmente con un solo comando) el operador en su clúster de Kubernetes.
CRD
Para que el operador sepa qué recursos buscar y dónde, necesitamos definir una regla para él. Cada regla se representará como un objeto CRD. ¿Qué campos debe tener este CRD?
- El tipo de recurso, que vamos a buscar (ConfigMap o Secret).
- La lista de namespaces, en los que deben estar los recursos.
- Selector, por el cual buscaremos recursos en el namespace.
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 inmediatamente crearemos una regla simple — para buscar en el namespace llamado default todos los ConfigMap con etiquetas del tipo 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 obtener información sobre nuestra regla. Debo aclarar que no escribiremos consultas al API Server del clúster por nuestra cuenta. Para esto utilizaremos una biblioteca de Python ya preparada :
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 la ejecución de este código, obtendremos lo siguiente:
{'ruleType': 'configmap', 'selector': {'copyrator': 'true'}, 'namespace': ['default']}Excelente: hemos logrado obtener la regla para el operador. Y lo más importante — lo hicimos, por así decirlo, al estilo de Kubernetes.
¿Variables de entorno o banderas? ¡Tomamos todo!
Pasamos a la configuración principal del operador. Hay dos enfoques básicos para configurar aplicaciones:
- usar parámetros de línea de comandos;
- usar variables de entorno.
Los parámetros de línea de comandos permiten leer configuraciones de manera más flexible, con soporte y validación de tipos de datos. En la biblioteca estándar de Python hay un módulo argparser, que es el que utilizaremos. Los detalles y ejemplos de sus capacidades están disponibles en .
Así es como se verá un ejemplo de configuración para la lectura de parámetros de línea de comandos en nuestro caso:
parser = ArgumentParser(
description='Copyrator - operador de copia.',
prog='copyrator'
)
parser.add_argument(
'--namespace',
type=str,
default=getenv('NAMESPACE', 'default'),
help='Namespace del operador'
)
parser.add_argument(
'--rule-name',
type=str,
default=getenv('RULE_NAME', 'main-rule'),
help='Nombre de CRD'
)
args = parser.parse_args()Por otro lado, a través de variables de entorno en Kubernetes, se puede trasladar fácilmente la información de servicio del pod dentro del contenedor. Por ejemplo, la información sobre el namespace en el que se ejecuta el pod se puede obtener con la siguiente construcción:
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace La lógica de funcionamiento del operador
Para comprender cómo dividir los métodos para trabajar con ConfigMap y Secret, utilizaremos mapas especiales. Así podremos entender qué métodos necesitamos para supervisar 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, es necesario recibir eventos del servidor API. Lo implementaremos de la siguiente manera:
def handle(specs):
kubernetes.config.load_incluster_config()
v1 = kubernetes.client.CoreV1Api()
# Obtenemos el método para supervisar 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)Después 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 solo paquete de Python. Creamos el archivo setup.py, escribimos allí la metainformación sobre el proyecto:
from sys import version_info
from setuptools import find_packages, setup
if version_info[:2] < (3, 5):
raise RuntimeError(
'Versión de python no soportada %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: El cliente de kubernetes para Python tiene su propio versionado. Más detalles sobre la compatibilidad entre las versiones del cliente y las versiones de Kubernetes se pueden encontrar en .
Ahora nuestro proyecto se ve así:
copyrator
├── copyrator
│ ├── cli.py # Lógica para el trabajo con la línea de comandos
│ ├── constant.py # Constantes que proporcionamos anteriormente
│ ├── load_crd.py # Lógica de carga de CRD
│ └── operator.py # Lógica principal del operador
└── setup.py # Formato del paqueteDocker y Helm
El Dockerfile será extremadamente simple: tomaremos la imagen base python-alpine e instalaremos nuestro paquete. Su optimización la dejaremos para otro momento:
FROM python:3.7.3-alpine3.9
ADD . /app
RUN pip3 install /app
ENTRYPOINT ["copyrator"]El despliegue para el operador también es muy simple:
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, es necesario crear un rol correspondiente para el operador con los permisos 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 }}Summary
Así, sin miedo, reproches y sin necesidad de aprender Go, pudimos construir nuestro propio operador para Kubernetes en Python. Por supuesto, aún tiene mucho por mejorar: en el futuro podrá manejar múltiples reglas, trabajar en múltiples hilos, y monitorear los cambios de sus CRD de manera autónoma...
Para que se puedan conocer mejor el código, lo hemos colocado en . Si estás interesado en ejemplos de operadores más serios, implementados con Python, puedes fijarte en dos operadores para desplegar mongodb ( y ).
P.D. Y si no quieres lidiar con los eventos de Kubernetes o prefieres utilizar Bash, nuestros colegas han preparado una solución lista en forma de (lo hicimos en abril).
P.P.D.
También puedes leer en nuestro blog:
- «»;
- «»;
- «»;
- «»;
- «».
Fuente: habr.com
