¿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.

¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)

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).

¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)

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).

¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)

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.

¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)

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).

¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)

El operador Shell se suscribe a los eventos de Kubernetes y ejecuta estos enlaces en respuesta a los eventos que necesitamos.

¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)

¿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:

¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)

function __config__() {
  cat << EOF
    configVersion: v1
    kubernetes:
    - name: src_secret
      apiVersion: v1
      kind: Secret
      nameSelector:
        matchNames:
        - mysecret
      namespace:
        nameSelector:
          matchNames: ["default"]
      group: main
EOF

Como resultado, el enlace se activará cuando cambie el secreto de origen (src_secret) y recibir el siguiente contexto vinculante:

¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)

Como puede ver, contiene el nombre y el objeto completo.

Seguimiento de los espacios de nombres

Ahora necesitas suscribirte a espacios de nombres. Para hacer esto, especificamos la siguiente configuración de enlace:

- name: namespaces
  group: main
  apiVersion: v1
  kind: Namespace
  jqFilter: |
    {
      namespace: .metadata.name,
      hasLabel: (
       .metadata.labels // {} |  
         contains({"secret": "yes"})
      )
    }
  group: main
  keepFullObjectsInMemory: false

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:

¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)

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):

- name: dst_secrets
  apiVersion: v1
  kind: Secret
  labelSelector:
    matchLabels:
      managed-secret: "yes"
  jqFilter: |
    {
      "namespace":
        .metadata.namespace,
      "resourceVersion":
        .metadata.annotations.resourceVersion
    }
  group: main
  keepFullObjectsInMemory: false

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.

¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)

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;
      • si no se detecta el secreto local, no hace nada.

¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)

Implementación del algoritmo en Bash. puedes descargar en nuestro repositorios con ejemplos.

¡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):

¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)

¿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:

¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)

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.

¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)

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.

¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)

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.

configVersion: v1
kubernetes:
- name: nodes
  apiVersion: v1
  kind: Node
  jqFilter: |
    {
      name: .metadata.name,
      ip: (
       .status.addresses[] |  
        select(.type == "InternalIP") |
        .address
      )
    }
  group: main
  keepFullObjectsInMemory: false
  executeHookOnEvent: []
schedule:
- name: every_minute
  group: main
  crontab: "* * * * *"

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.

¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)

Puede crear cualquier cantidad de colas/ganchos y sus diversas combinaciones. Por ejemplo, una cola puede funcionar con dos ganchos o viceversa.

¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)

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.

¿Ir? ¡Intento! Conozca al operador shell (reseña y reportaje en video de KubeCon EU'2020)

Vídeos y diapositivas

Vídeo de la actuación (~23 minutos):


Presentación del informe:

PS

Lea también en nuestro blog:

Fuente: habr.com

Añadir un comentario