ProHoster > Blog > administración > ¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)
¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)
Este año, la principal conferencia europea de Kubernetes, KubeCon + CloudNativeCon Europe 2020, fue virtual. Sin embargo, tal cambio de formato no nos impidió entregar nuestro informe largamente planeado “¿Ir? ¡Intento! Conozca al operador Shell” dedicado a nuestro proyecto Open Source operador de shell.
Este artículo, inspirado en la charla, presenta un enfoque para simplificar el proceso de creación de operadores para Kubernetes y muestra cómo puede crear el suyo propio con un mínimo esfuerzo utilizando un operador de shell.
Presentando vídeo del informe (~23 minutos en inglés, notablemente más informativo que el artículo) y el extracto principal en forma de texto. ¡Ir!
En Flant optimizamos y automatizamos todo constantemente. Hoy hablaremos de otro concepto apasionante. Encontrarse: scripts de shell nativos de la nube!
Sin embargo, comencemos por el contexto en el que sucede todo esto: Kubernetes.
API y controladores de Kubernetes
La API en Kubernetes se puede representar como una especie de servidor de archivos con directorios para cada tipo de objeto. Los objetos (recursos) en este servidor están representados por archivos YAML. Además, el servidor cuenta con una API básica que te permite hacer tres cosas:
recibir recurso por su tipo y nombre;
cambiar recurso (en este caso, el servidor almacena solo objetos "correctos"; se descartan todos los que están formados incorrectamente o destinados a otros directorios);
seguir para el recurso (en este caso, el usuario recibe inmediatamente su versión actual/actualizada).
Así, Kubernetes actúa como una especie de servidor de archivos (para manifiestos YAML) con tres métodos básicos (sí, en realidad hay otros, pero los omitiremos por ahora).
El problema es que el servidor sólo puede almacenar información. Para que funcione necesitas controlador - el segundo concepto más importante y fundamental en el mundo de Kubernetes.
Hay dos tipos principales de controladores. El primero toma información de Kubernetes, la procesa según la lógica anidada y la devuelve a los K8. El segundo toma información de Kubernetes pero, a diferencia del primer tipo, cambia el estado de algunos recursos externos.
Echemos un vistazo más de cerca al proceso de creación de una implementación en Kubernetes:
Controlador de implementación (incluido en kube-controller-manager) recibe información sobre la implementación y crea un ReplicaSet.
ReplicaSet crea dos réplicas (dos pods) basándose en esta información, pero estos pods aún no están programados.
El programador programa pods y agrega información de nodos a sus YAML.
Kubelets realiza cambios en un recurso externo (por ejemplo, Docker).
Luego, toda esta secuencia se repite en orden inverso: el kubelet verifica los contenedores, calcula el estado del pod y lo devuelve. El controlador ReplicaSet recibe el estado y actualiza el estado del conjunto de réplicas. Lo mismo sucede con el Controlador de implementación y el usuario finalmente obtiene el estado actualizado (actual).
operador de shell
Resulta que Kubernetes se basa en el trabajo conjunto de varios controladores (los operadores de Kubernetes también son controladores). Surge la pregunta, ¿cómo crear tu propio operador con el mínimo esfuerzo? Y aquí viene al rescate el que desarrollamos. operador de shell. Permite a los administradores del sistema crear sus propias declaraciones utilizando métodos familiares.
Ejemplo sencillo: copiar secretos
Veamos un ejemplo sencillo.
Digamos que tenemos un clúster de Kubernetes. Tiene un espacio de nombres default con algun secreto mysecret. Además, existen otros espacios de nombres en el clúster. Algunos de ellos tienen adherida una etiqueta específica. Nuestro objetivo es copiar Secret en espacios de nombres con una etiqueta.
La tarea se complica por el hecho de que pueden aparecer nuevos espacios de nombres en el clúster y algunos de ellos pueden tener esta etiqueta. Por otro lado, cuando se elimina la etiqueta, también se debe eliminar Secret. Además de esto, el secreto en sí también puede cambiar: en este caso, el nuevo secreto debe copiarse en todos los espacios de nombres con etiquetas. Si Secret se elimina accidentalmente en cualquier espacio de nombres, nuestro operador debe restaurarlo inmediatamente.
Ahora que se ha formulado la tarea, es hora de comenzar a implementarla utilizando el operador shell. Pero primero vale la pena decir algunas palabras sobre el propio operador shell.
Cómo funciona el operador shell
Al igual que otras cargas de trabajo en Kubernetes, el operador de shell se ejecuta en su propio módulo. En este pod en el directorio /hooks Se almacenan los archivos ejecutables. Pueden ser scripts en Bash, Python, Ruby, etc. A estos archivos ejecutables los llamamos ganchos (manos).
El operador Shell se suscribe a los eventos de Kubernetes y ejecuta estos enlaces en respuesta a los eventos que necesitamos.
¿Cómo sabe el operador de shell qué gancho ejecutar y cuándo? La cuestión es que cada anzuelo tiene dos etapas. Durante el inicio, el operador de shell ejecuta todos los enlaces con un argumento --config Esta es la etapa de configuración. Y después, los ganchos se lanzan de la forma habitual, en respuesta a los eventos a los que están unidos. En el último caso, el gancho recibe el contexto vinculante (contexto vinculante) - datos en formato JSON, de los que hablaremos con más detalle a continuación.
Hacer un operador en Bash
Ahora estamos listos para la implementación. Para hacer esto, necesitamos escribir dos funciones (por cierto, recomendamos biblioteca shell_lib, lo que simplifica enormemente la escritura de ganchos en Bash):
el primero es necesario para la etapa de configuración: muestra el contexto vinculante;
el segundo contiene la lógica principal del gancho.
#!/bin/bash
source /shell_lib.sh
function __config__() {
cat << EOF
configVersion: v1
# BINDING CONFIGURATION
EOF
}
function __main__() {
# THE LOGIC
}
hook::run "$@"
El siguiente paso es decidir qué objetos necesitamos. En nuestro caso, necesitamos rastrear:
secreto de origen para cambios;
todos los espacios de nombres del clúster, para que sepa cuáles tienen una etiqueta adjunta;
secretos de destino para garantizar que todos estén sincronizados con el secreto de origen.
Suscríbete a la fuente secreta.
La configuración vinculante para ello es bastante simple. Indicamos que nos interesa Secret con el nombre mysecret en el espacio de nombres default:
Como puedes ver, ha aparecido un nuevo campo en la configuración con el nombre jqFilter. Como sugiere su nombre, jqFilter filtra toda la información innecesaria y crea un nuevo objeto JSON con los campos que nos interesan. Un gancho con una configuración similar recibirá el siguiente contexto vinculante:
Contiene una matriz filterResults para cada espacio de nombres en el clúster. variable booleana hasLabel Indica si una etiqueta está adjunta a un espacio de nombres determinado. Selector keepFullObjectsInMemory: false indica que no es necesario mantener objetos completos en la memoria.
Seguimiento de secretos de objetivos
Nos suscribimos a todos los Secretos que tengan una anotación especificada. managed-secret: "yes" (estos son nuestro objetivo dst_secrets):
En este caso jqFilter filtra toda la información excepto el espacio de nombres y el parámetro resourceVersion. El último parámetro se pasó a la anotación al crear el secreto: le permite comparar versiones de secretos y mantenerlas actualizadas.
Un gancho configurado de esta manera, cuando se ejecute, recibirá los tres contextos de enlace descritos anteriormente. Se pueden considerar como una especie de instantánea (instantánea) grupo.
A partir de toda esta información se puede desarrollar un algoritmo básico. Itera sobre todos los espacios de nombres y:
si hasLabel asuntos true para el espacio de nombres actual:
compara el secreto global con el local:
si son iguales no hace nada;
si difieren - ejecuta kubectl replace o create;
si hasLabel asuntos false para el espacio de nombres actual:
se asegura de que Secret no esté en el espacio de nombres dado:
si el secreto local está presente, elimínelo usando kubectl delete;
¡Así es como pudimos crear un controlador de Kubernetes simple usando 35 líneas de configuración YAML y aproximadamente la misma cantidad de código Bash! El trabajo del operador shell es vincularlos.
Sin embargo, copiar secretos no es el único campo de aplicación de la utilidad. Aquí hay algunos ejemplos más para mostrar de lo que es capaz.
Ejemplo 1: realizar cambios en ConfigMap
Veamos una implementación que consta de tres pods. Los pods usan ConfigMap para almacenar alguna configuración. Cuando se iniciaron los pods, ConfigMap estaba en un estado determinado (llamémoslo v.1). En consecuencia, todos los pods utilizan esta versión particular de ConfigMap.
Ahora supongamos que ConfigMap ha cambiado (v.2). Sin embargo, los pods utilizarán la versión anterior de ConfigMap (v.1):
¿Cómo puedo hacer que cambien al nuevo ConfigMap (v.2)? La respuesta es sencilla: utiliza una plantilla. Agreguemos una anotación de suma de verificación a la sección. template Configuraciones de implementación:
Como resultado, esta suma de verificación se registrará en todos los pods y será la misma que la de Implementación. Ahora solo necesita actualizar la anotación cuando cambie ConfigMap. Y el operador shell resulta útil en este caso. Todo lo que necesitas hacer es programar un gancho que se suscribirá al ConfigMap y actualizará la suma de comprobación.
Si el usuario realiza cambios en ConfigMap, el operador de shell los notará y recalculará la suma de comprobación. Después de lo cual entrará en juego la magia de Kubernetes: el orquestador matará el pod, creará uno nuevo y esperará a que se convierta en Ready, y pasa al siguiente. Como resultado, Deployment se sincronizará y cambiará a la nueva versión de ConfigMap.
Ejemplo 2: trabajar con definiciones de recursos personalizadas
Como sabes, Kubernetes te permite crear tipos de objetos personalizados. Por ejemplo, puedes crear tipos MysqlDatabase. Digamos que este tipo tiene dos parámetros de metadatos: name и namespace.
apiVersion: example.com/v1alpha1
kind: MysqlDatabase
metadata:
name: foo
namespace: bar
Disponemos de un cluster de Kubernetes con diferentes espacios de nombres en el que podemos crear bases de datos MySQL. En este caso, el operador shell se puede utilizar para rastrear recursos. MysqlDatabase, conectándolos al servidor MySQL y sincronizando los estados deseados y observados del cluster.
Ejemplo 3: Monitoreo de la red del clúster
Como sabes, utilizar ping es la forma más sencilla de monitorear una red. En este ejemplo mostraremos cómo implementar dicho monitoreo usando el operador de shell.
En primer lugar, deberá suscribirse a los nodos. El operador del shell necesita el nombre y la dirección IP de cada nodo. Con su ayuda, hará ping a estos nodos.
Parámetro executeHookOnEvent: [] evita que el gancho se ejecute en respuesta a cualquier evento (es decir, en respuesta a cambiar, agregar o eliminar nodos). Sin embargo, él correrá (y actualizar la lista de nodos) programado - cada minuto, según lo prescrito por el campo schedule.
Ahora surge la pregunta: ¿cómo sabemos exactamente acerca de problemas como la pérdida de paquetes? Echemos un vistazo al código:
function __main__() {
for i in $(seq 0 "$(context::jq -r '(.snapshots.nodes | length) - 1')"); do
node_name="$(context::jq -r '.snapshots.nodes['"$i"'].filterResult.name')"
node_ip="$(context::jq -r '.snapshots.nodes['"$i"'].filterResult.ip')"
packets_lost=0
if ! ping -c 1 "$node_ip" -t 1 ; then
packets_lost=1
fi
cat >> "$METRICS_PATH" <<END
{
"name": "node_packets_lost",
"add": $packets_lost,
"labels": {
"node": "$node_name"
}
}
END
done
}
Recorremos la lista de nodos, obtenemos sus nombres y direcciones IP, les hacemos ping y enviamos los resultados a Prometheus. El operador Shell puede exportar métricas a Prometheus, guardándolos en un archivo ubicado según la ruta especificada en la variable de entorno $METRICS_PATH.
Como este puede crear un operador para un monitoreo simple de la red en un clúster.
Mecanismo de cola
Este artículo estaría incompleto sin describir otro mecanismo importante integrado en el operador de shell. Imagine que ejecuta algún tipo de gancho en respuesta a un evento en el clúster.
¿Qué pasa si, al mismo tiempo, sucede algo en el cluster? una cosa mas ¿evento?
¿El operador de shell ejecutará otra instancia del gancho?
¿Qué pasa si, digamos, ocurren cinco eventos a la vez en el clúster?
¿El operador de shell los procesará en paralelo?
¿Qué pasa con los recursos consumidos como la memoria y la CPU?
Afortunadamente, el operador shell tiene un mecanismo de cola incorporado. Todos los eventos se ponen en cola y se procesan secuencialmente.
Ilustremos esto con ejemplos. Digamos que tenemos dos ganchos. El primer evento va al primer gancho. Una vez que se completa su procesamiento, la cola avanza. Los siguientes tres eventos se redirigen al segundo gancho: se eliminan de la cola y se ingresan en ella en un "paquete". Eso es El gancho recibe una serie de eventos. – o, más precisamente, una serie de contextos vinculantes.
También estos Los eventos se pueden combinar en uno grande.. El parámetro es responsable de esto. group en la configuración de enlace.
Puede crear cualquier cantidad de colas/ganchos y sus diversas combinaciones. Por ejemplo, una cola puede funcionar con dos ganchos o viceversa.
Todo lo que necesitas hacer es configurar el campo en consecuencia. queue en la configuración de enlace. Si no se especifica un nombre de cola, el enlace se ejecuta en la cola predeterminada (default). Este mecanismo de cola le permite resolver completamente todos los problemas de administración de recursos cuando trabaja con ganchos.
Conclusión
Explicamos qué es un operador de shell, mostramos cómo se puede utilizar para crear operadores de Kubernetes de forma rápida y sin esfuerzo y dimos varios ejemplos de su uso.
Información detallada sobre el operador shell, así como un tutorial rápido sobre cómo usarlo, está disponible en el correspondiente repositorios en GitHub. No dude en contactarnos si tiene preguntas: puede discutirlas en un especial grupo de telegramas (en ruso) o en este foro (en inglés).
Y si te gustó, siempre estaremos felices de ver nuevos números/PR/estrellas en GitHub, donde, por cierto, puedes encontrar otros. proyectos interesantes. Entre ellos cabe destacar operador-adicional, que es el hermano mayor del operador shell. Esta utilidad utiliza gráficos de Helm para instalar complementos, puede entregar actualizaciones y monitorear varios parámetros/valores de gráficos, controla el proceso de instalación de gráficos y también puede modificarlos en respuesta a eventos en el clúster.