viernes, 25 de diciembre de 2020

Kubernetes - Notas adicionales sobre ReplicaSets

Continuando con los objetos de tipo ReplicaSets, hoy vamos a ver unas notas adicionales que pueden ser de interés y que debemos tener en cuenta.

Como ya vimos en la anterior entrada sobre los objetos ReplicaSet, este tipo de objeto permite controlar de forma automática un conjunto de PODs. Esto implica que el KCP monitorizará en todo momento el número de PODs del ReplicaSet, asegurando que el estado del cluster coincida con la configuración que hemos aplicado y manteniendo el número de PODs que hayamos fijado en la configuración del objeto ReplicaSet.

En resumen, partiendo de una situación como la siguiente:

Objeto ReplicaSet en el cluster.

Vemos que hemos definido un ReplicaSet en el que hemos fijado que siempre debe haber un total de 2 PODs corriendo en todo momento. Evidentemente este caso es muy sencillo y vemos rápidamente la relación entre los PODs y el ReplicaSet correspondiente, pero en entornos reales donde podemos tener cientos de PODs corriendo esto puede ser bastante más confuso, con lo que podemos comprobar como está gestionado un POD haciendo un describe del mismo:

Describe de un POD controlado por un ReplicaSet.

Como vemos en la líinea Controlled By, este POD está controlado por el objeto superior ReplicaSet/webapp.

Sabemos que los PODs controlados por un objeto de tipo ReplicaSet están dados por el selector del ReplicaSet y la etiqueta de los PODs, es decir que todos los PODs que contengan la etiqueta que coincida con el selector definido serán controlados por un ReplicaSet determinado.

Esto nos puede llevar a situaciones en las que, si por alguna razón hay PODs con etiquetas coincidentes con las del campo selector de un ReplicaSet, dichos PODs pasen a estar bajo el control de dicho ReplicaSet. Por ejemplo, partiendo de la situación anterior, si arranco manualmente un POD basado en la siguiente descripción:

Definición de un POD simple.

Al aplicar esta configuración debería existir un nuevo POD, con el nombre webserver-v1, corriendo en el cluster:

Estado del cluster.

Pero, como podemos ver, el nuevo POD no aparece en la salida del comando get all así que ¿donde está el nuevo POD? Es importante que nos fijemos en que el campo selector del ReplicaSet coincide con el campo label asignado en el POD que hemos descrito, por tanto el KCP lo ha puesto directamente bajo el control del ReplicaSet webapp. Como el número de PODs establecido del ReplicaSet ya coincide con el número de PODs corriendo en el sistema, el nuevo POD con nombre webserver-v1 se ha borrado nada más arrancar. Podemos confirmar este hecho consultando la sección de Events del ReplicaSet con el subcomando describe de kubectl:

Descripción del ReplicaSet webapp.

Podemos observar que la última línea muestra que se ha borrado el POD webserver-v1, lo que nos indica que nada más aplicar el fichero de descripción del mismo, el KCP ha puesto dicho POD bajo control del ReplicaSet webapp debido a que su etiqueta coincide con el campo selector del mismo.

Este punto debe tenerse en cuenta ya que implica que, si no identificamos correctamente las templates de PODs de diferentes objetos, podemos provocar conflictos en los cuales objetos de bajo nivel como PODs estén controlados por otros de alto nivel a los que no corresponden.

Hasta aquí lo más básico relacionado con los objetos de tipo ReplicaSet, en próximas entradas veremos como controlar este tipo de objetos con Deployments.

Kubernetes - Conceptos básicos III

Siguiendo con los conceptos básicos de Kubernetes, vamos a estudiar de forma simple la arquitectura de un cluster de Kubernetes.

Vimos en la primera entrada de esta serie de posts que podíamos distinguir entre nodos master y nodos worker, llamados anteriormente minions. En general, dentro de la terminología de Kubernetes, un nodo es directamente un worker, el cual se caracteriza porque dispone del motor de ejecución de contenedores, los procesos kubelet y kube-proxy y es gestionado por los componentes del master.
 
En general, la arquitectura de un cluster de Kubernetes estará formado por uno o más nodos master y varios nodos worker. Los administradores usarán el comando kubectl para comunicarse con un balanceador de carga, que repartirá las conexiones entre todos los nodos master, controlando así el cluster y estableciendo el estado deseado del cluster. La forma más sencilla de ver esta arquitectura es la siguiente:
 
Arquitectura básica de un cluster de Kubernetes.

Esta gestión de los nodos worker por parte del master, se realiza mediante la comunicación entre el apiserver del nodo master y el proceso kubelet de los nodos worker. Esta comunicación permite al master obtener los logs del nodo worker, la ejecución de contenedores y proporcionar la característica de reenvío de puertos de kubelet.

Esta comunicación entre el apiserver y kubelet se realiza mediante el protocolo HTTPS, pero el apiserver por defecto no comprueba el certificado ofrecido por kubelet. Para forzar la comprobación del certificado ofrecido por kubelet, es necesario usar la opción --kubelet-certificate-authority del apiserver especificando un conjunto de certificados raíz que permitan la comprobación del certificado de kubelet.

El apiserver también se conecta con los nodos y contenedores usando el protocolo HTTP y aunque, puede cambiarse a HTTPS no se realiza ningún tipo de validación de credenciales ni de comprobación de certificados en estas conexiones.

Adicionalmente, los nodos worker y servicios del propio master se comunican con el apiserver, que por defecto acepta peticiones HTTPS en el puerto 443. Para añadir más seguridad a estas comunicaciones es importante utilizar autenticación de clientes, por ejemplo mediante certificados de cliente, así como autorizaciones para dichas conexiones.

Podemos obtener el estado de un worker, en base a una serie de características del mismo, utilizando el comando siguiente:

Descripción de estado de un nodo.
 
La salida de este comando es bastante extensa y entre toda la información que proporciona, nos devolverá el rol del nodo, su hostname y dirección interna y externa, la capacidad del nodo en terminos de CPU y memoria disponible, la versión del motor de ejecución de contenedores, una lista de los últimos eventos y el estado de condiciones como falta de memoria o disco.

Dentro de los tipos de condiciones de un nodo worker, es muy importante la condición Ready. Esta condición indica al master si el nodo worker es capaz de ejecutar contenedores y puede tener tres valores diferentes, True, False o Unknown. Si la condición Ready de un nodo worker permanece en estado False o Unknown durante más de un tiempo determinado, que por defecto son 5 minutos y se denomina pod-eviction-timeout, el kube-controller-manager del master lanza un borrado de los contenedores que se estén ejecutando en el nodo worker. Si el master no puede comunicarse con el proceso kubelet del nodo worker, es posible que los contenedores sigan ejecutándose en el nodo fallido hasta que la comunicación con el kube-apiserver vuelva a establecerse.

En caso de querer realizar algún tipo de operación de mantenimiento con un nodo worker, que implique que el nodo no puede aceptar contenedores, podemos hacer el nodo worker no disponible con el comando:

Marcando un nodo como no disponible.

El proceso kube-controller-manager del master ejecuta controladores que operan sobre recursos y objetos del cluster de Kubernetes, siendo uno de estos el controlador de nodos. Este controlador de nodos es responsable, entre otras cosas, de monitorizar la salud de todos los nodos del cluster y en caso de ser necesario, de mover los contenedores de un nodo que no responde a otro cuyo estado Ready sea True.

domingo, 20 de diciembre de 2020

Kubernetes y el soporte de Docker

Tras el anuncio por parte del equipo de Kubernetes de no continuar soportando Docker como motor de ejecución de contenedores, muchos hemos pensado ¿y ahora que es lo que debemos hacer? Pues para empezar, lo mejor es leer el anuncio oficial de Kubernetes, el cual podéis encontrar en el siguiente enlace.

Otro artículo interesante, publicado por Red Hat, explica un poco más las razones detrás de este cambio y en el cual podemos ver que está muy relacionado con la complejidad del desarrollo que implica integrar Docker como motor de contenedores.

Lo importante que sacamos en claro de ambos artículos, es que podemos seguir usando todas las imágenes que hemos desarrollado hasta hora utilizando Docker con lo que, en principio, el impacto debería ser mínimo.

Sin embargo, para ir adelantándonos un poco, vamos a ver de forma rápida como podemos adaptar una instalación que tengamos de minikube para que el motor de ejecución de contenedores sea otro diferente a Docker.

En un post anterior vimos como instalar minikube usando un host con Docker. Aquella instalación utilizaba Docker como motor para la ejecución de contenedores. Ahora, lo que vamos a hacer es sencillamente volver a crear un cluster de Kubernetes mediante minikube, pero en este caso vamos a especificar que el runtime de contenedores será otro diferente, en mi caso he escogido containerd ya que se instala por defecto con Docker. El comando en cuestión es el siguiente:

Creación del "cluster" con minikube.

Es recomendable actualizar a las últimas versiones disponibles de minikube y containerd, además de especificar en el comando que la versión de Kubernetes a usar es la 1.20.0. Para el correcto funcionamiento de la red, en mi caso ha sido necesario copiar el contenido de la ruta /usr/libexec/cni a /opt/cni/bin. Esto se debe a que, por defecto, los plugins CNI se buscan en /opt/cni/bin pero el paquete containernetworking-plugins los instala en /usr/libexec/cni. Como referencia, podemos ver que el fichero de configuración de containerd (/etc/containerd/config.toml) especifica que la ruta para la búsuqeda de plugins de red es /opt/cni/bin así que podemos cambiarlo igualmente a la ruta de instalación del paquete containernetworking-plugins:

Configuración de los plugins CNI.

Una vez creado correctamente nuestro cluster, podemos comprobar que el motor de contenedores ya no es Docker con solo comprobar si hay contenedores corriendo bajo su control:

Contenedores controlados por Docker.
 
Podemos ver, sin embargo, que los PODs de los servicios de infraestructura de Kubernetes se encuentran corriendo correctamente:
 
Servicios de infraestructura de Kubernetes.

Como hemos cambiado el runtime de contenedores a containerd, podemos comprobar que estos se están ejecutando correctamente usando el comando siguiente:

Contenedores bajo el control de containerd.

Un punto importante del comando anterior, es que he tenido que especificar el namespace del cual quiero obtener información, siendo por defecto k8s.io para containerd. En este namespace veremos tanto los contenedores de infraestructura de Kubernetes, así como aquellos correspondientes a nuestros servicios.

Por tanto hemos cambiado el runtime de nuestro cluster a containerd correctamente. Ahora solo tenemos que comprobar que, cualquiera de nuestras imágenes usadas hasta ahora con Docker, siguen funcionando correctamente. Por ejemplo, creando un replicaset con la imagen de un servidor Apache simple almacenada en Docker Hub, tenemos lo siguiente:

Despliegue de un replicaset usandoo una imagen en Docker Hub.

Por tanto, nuestras imágenes pueden seguir usándose sin problemas y el impacto que podemos tener es menor de lo que podíamos pensar. De todos modos, es importante revisar el impacto que implica en clusters reales el cambio del runtime de contenedores de Docker a containerd o cri-o. Además será importante investigar un poco más como funcionan y como podemos interactuar coon estos motores de contenedores.

lunes, 7 de diciembre de 2020

ELK - Creando visualizaciones simples

Hace demasiado que no publicaba una nueva entrada sobre la pila ELK, la última entrada tiene más de un año, así que hoy vamos con un nuevo post en el que vamos a utilizar las capacidades de ingesta de Elasticsearch y, con los datos que tengamos, realizaremos una gráfica simple. 
 
En las entradas anteriores enviabamos datos a Logstash, para procesarlos y crear documentos con los campos necesarios para posteriormente almacenarlos en un índice de Elasticsearch. Además, también vimos como crear un índice y definíamos el tipo de dato específico de los campos que nos interesan.

Desde entonces ha llovido mucho y aunque continuamos con una configuración similar, vamos a introducir un pequeño cambio y usaremos un nodo de elasticsearch como nodo de ingesta, en el cual podremos definir un pipeline para procesar los datos que recibimos sin necesidad de usar Logstash. Básicamente, lo que estamos haciendo es pasar la funcionalidad de Logstash directamente al cluster de Elastic.
 
Para esto, vamos a empezar de forma sencilla analizando los mensajes que vamos a recibir desde un nodo que nos envía información mediante filebeat, la cual vamos a generar usando syslog_generator:
 
Mensajes recibidos desde filebeat.
 
Como vemos, filebeat envía una gran cantidad de campos que no nos interesan, ya que solo queremos quedarnos con el campo message que contiene el mensaje real creado por syslog_generator.
 
Vamos a empezar de forma simple creando un pipeline en Elastic que elimine todos esos campos que nos sobran. Para este tipo de tareas, como siempre, lo mejor es utilizar la consola de desarrollo de Kibana. Veamos directamente el pipeline para analizarlo posteriormente:
 
Pipeline básico de eliminación de campos.

Con este pipeline definimos que queremos eliminar una serie de campos de los mensajes recibidos, en concreto los que aparecen en el campo field dentro del procesador remove, que podemos ver en la imagen superior.

Una vez definido un pipeline, este queda almacenado en el cluster, pero para usarlo necesitamos asociarlo a un índice. Para seguir con nuestras pruebas, lo que hacemos es editar el índice que se ha creado automáticamente en el cluster en cuanto ha empezado a recibir datos de los nodos con filebeat y modificamos su atributo index.default_pipeline. Este atributo establece que pipeline hay que aplicar a los documentos recibidos antes de almacenarlos en el índice. Podemos editarlo desde la opción Edit Settings de la sección Index Management:

Modificación de las propiedades del índice.

Al especificar que debe aplicarse el pipeline a los documentos antes de almacenarlos, todos los campos que hemos indicado dentro del procesador remove del pipeline son eliminados, con lo que ahora podemos ver lo siguiente desde la sección Discover para el patrón del índice generado por filebeat:

Documentos en el índice filebeat tras pasar por el pipeline.

Como vemos, ahora cada documento se ha almacenado en el índice sin los campos que hemos eliminado anteriormente.

Para llegar a este punto hemos tenido que modificar manualmente el índice que se ha creado de forma automática pero, como esto no es muy operativo que se diga, empecemos por establecer en la configuración de filebeat que pipeline debe utilizarse estableciendo la opción pipeline en el fichero filebeat.yml de nuestro nodo filebeat origen:

Configuracion de filebeat aplicando el pipeline.

Por tanto, como vemos, podemos aplicar el pipeline a la salida configurada de filebeat en origen directamente. Al hacerlo comprobamos que todos los campos que hemos configurado en el procesador remove del pipeline remove_extra_fields, no se han almacenado en los documentos del índice filebeat.

Por tanto, ahora que tenemos claro que podemos pasar cierta funcionalidad de Logstash directamente a Elasticsearch, pasemos a hacer un pipeline que procese nuestro campo message adecuadamente, nos devuelva los campos que necesitamos, elimine el resto y para finalizar, escriba los documentos en un índice diferente, donde estableceremos los mapeos necesarios para cada campo, asegurándonos que el tipo de dato de cada uno se almacena de forma correcta.

El pìpeline que realiza toda la manipulación de campos que necesitamos es el siguiente:

Pipeline de eliminación de campos y procesado del campo message.

Como en la imagen anterior no queda muy claro, podéis encontrar el pipeline completo, junto con la creación del índice definitivo, anexado con este post.

Ya hemos comprobado que este pipeline modifica el documento, eliminando aquellos campos que no necesitamos y crea los nuevos campos a partir del procesado del campo message original. A continuación crearemos un índice especificando los campos que necesitamos en cada documento y, lo más importante, con el tipo de dato correcto. Ya sabemos que, para crear un índice, lo mejor es usar la consola de desarrollo de Kibana. Podemos crear el índice de una manera similar a la siguiente:

Creación del índice.

Una vez creado nuestro nuevo índice, creamos el index pattern necesario para que Kibana pueda obtener datos de Elasticsearch. Para esto, desde el menú Management, en la sección Index Patterns de Kibana, creamos el nuevo patrón especificando que el campo @timestamp es el que contiene la información de fecha y hora para poder hacer el filtrado por tiempo:

 

Creación del index pattern correspondiente al nuevo índice.

Seleccion del campo de fecha del index pattern.

Es importante que nos demos cuenta que el campo @timestamp de cada documento contiene la fecha y hora de recepción de la información enviada por filebeat, la cual no es la misma que aparece en el campo message. Para corregir esto, el pipeline hace una conversión del campo EVENT_TIMESTAMP que hemos construido con el procesador dissect y copia dicha información en el campo @timestamp. Esta operación de conversión la realiza el siguiente procesador:

Modificación del campo @timestamp.

A continuación cambiamos la configuración de filebeat en el nodo origen para especificar el nombre del pipeline que deseamos usar antes de almacenar los documentos en el índice:

Configuración definitiva de filebeat.

Con toda la configuración ya realizada, podemos comprobar como tenemos datos en el índice en los campos deseados y con el tipo de dato correcto:

Datos del indice temperature_sensors.

Ya con nuestros datos, podemos pasar a hacer una representación gráfica simple de los valores de temperatura recibidos desde la sección Visualize. Podemos crear una gráfica de tipo lineal para ver los valores medios de temperatura:

Valores medios de temperatura.

Esta representación nos muestra los valores medios de temperatura para cada intervalo temporal que seleccionemos. Para esto establecemos que en el eje Y queremos el valor medio del campo TEMPERATURE_VALUE de cada documento y, en el eje X establecemos un histograma basado en el campo de fecha y hora @timestamp.

El problema de esta visualización, es que estamos obteniendo el valor medio de los valores de temperatura enviados por tres sensores diferentes. Para mostrar en la misma gráfica el valor medio de temperatura, por cada uno de los sensores que estamos simulando con syslog_generator, tenemos que añadir filtros en el eje X para los identificadores de cada uno de los sensores. Esto podemos hacerlo más o menos del siguiente modo:

Filtro por cada sensor.

Al realizar esta configuración, pasamos a tener una gráfica como la siguiente:

Valor medio de temperatura por cada sensor.

Es importante tener en cuenta que cada uno de los valores que vemos en cada gráfica, se corresponden con el valor medio de todos los valores recibidos en ese intervalo de tiempo. Si aumentamos la resolución, disminuyendo el tiempo de representación de la gráfica, podemos ver algo como lo siguiente:

Valores de temperatura por segundo de cada sensor.

En esta gráfica ya tenemos una resolución de un segundo, correspondiente a la tasa de envío de información que he configurado en syslog_generator para simular la información de temperatura de cada sensor simulado.

Por tanto y en resumen, hemos visto como crear un pipeline que nos permite procesar los documentos que llegan a Elasticsearch, sin necesidad de utilizar Logstash, hacer las modificaciones necesarias para obtener los campos que nos interesan y almacenar dichos documentos en un índice diferente.

Además, basándonos en dichos datos, hemos creado una gráfica simple en la que hemos podido aplicar filtros para diferenciar entre diferentes fuentes del mismo índice.

En el siguiente enlace podéis encontrar el fichero que contiene la definición del pipeline así como la del índice utilizados a lo largo del post.

domingo, 27 de septiembre de 2020

Linux namespaces y cgroups

Hasta ahora hemos hablado mucho de contenedores, como crearlos y administrarlos, como usar orquestadores de contenedores como Kubernetes, etc. Pero ¿en que se basan soluciones como Docker para la creación de contenedores?

Basicamente aprovechan características del kernel de Linux que proporcionan capacidades para limitar los recursos disponibles para un proceso o conjunto de procesos. Esta característica del kernel se denomina cgroups o control groups. Por tanto, mediante la definición de cgroups, podemos particionar los recursos del sistema y asignarlos a procesos, asegurando así que ninguno consume más recursos de los necesarios.

El interfaz con esta característica del kernel es el pseudo sistema de archivos cgroupfs, el cual nos permite el control de un cgroup mediante la creación, borrado o renombrado de subdirectorios dentro del mismo.

Adicionalmente, podemos limitar la visibilidad que un grupo de procesos tiene del resto del sistema mediante la definición de namespaces. Un namespace es un conjunto de características del sistema, como los interfaces de red disponibles, los puntos de montaje o la lista de procesos, que aparecerán para los procesos ejecutándose en dicho namespace como los únicos disponibles. Estos recursos solo serán visibles para los procesos dentro del namespace y estarán aislados del resto de posibles namespaces existentes. Los namespaces existentes en el kernel de Linux son los siguientes:

  • Mount (mnt). Este namespace controla los puntos de montaje, proporcionando aislamiento a la lista de puntos de montaje que están disponibles para los procesos de un namespace.
  • Process ID (pid). Este namepsace controla y aisla el espacio de números de procesos, lo cual permite que diferentes procesos, en diferentes namespaces, tengan el mismo PID.
  • Network (net). Mediante este namespace, un conjunto de procesos tendrá sus propios recursos de red, incluyendo dispositivos de red, tabla de rutas, protocolos IPv4 e IPv6, firewall, etc.
  • Hostname y nombre de dominio NIS (UTS). Este namespace permite controlar el nombre de host así como el nombre de dominio NIS que verán los procesos ejecutándose dentro del namespace.
  • User ID (user). Este namespace controla y aisla el espacio de identificadores de usuario y grupos, permitiendo realizar mapeos entre usuarios y grupos dentro y fuera de un namespace. Es importante tener en cuenta que este namespace incluye las capacidades que tendrán los procesos.
  • Interprocess communications (ipc). Este namespace permite el aislamiento de objetos IPC, como colas de mensjaes, entre procesos que pertenezcan a diferentes namespaces.
  • Control groups (cgroup). Este namespace permite aislar grupos de control de tal manera que un proceso tenga una jerarquía de grupo de procesos aislada del resto.
  • Time. Este namespace proporciona vistas de los relojes del sistema CLOCK_MONOTONIC y CLOCK_BOOTTIME, lo cual permite que los procesos de un namespace tengan diferente fecha y hora que los de otros namespaces.
Pues muy bien todo esto, porque visto así no queda excesivamente claro ¿verdad? Veamos como podemos controlar un proceso de sistema operativo, creando un nuevo cgroup y asignándolo al mismo.

Como hemos dicho, los cgroups se controlan mediante subdirectorios dentro del pseudo sistema de ficheros cgroupfs. Este se encuentra accesible en la ruta /sys/fs/cgroup:

Pseudo sistema de archivos cgroupfs.

Cada uno de los puntos de montaje que vemos se corresponde con lo que se denomina un controlador. De momento no vamos a complicarnos con todos los controladores y vamos a crear un nuevo cgroup, dentro del controlador memory, para controlar la cantidad de memoria de los procesos que pasemos a dicho cgroup.
 
Para crear el cgroup solo tenemos que crear un nuevo directorio y especificar el nombre que queramos:
 
Creación de un nuevo cgroup.

Observamos que, al crear el nuevo cgroup, el contenido del directorio hereda el contenido del directorio padre, con lo que en principio este nuevo cgroup es hijo del cgroup principal que se crea cuando arranca el sistema. Por tanto, de momento, podemos decir que este cgroup tiene los mismos límites de memoria que tiene el cgroup principal. Como podemos ver, uno de los ficheros existentes se llama cgroup.procs el cual ahora mismo está vacio ya que, todos los procesos del sistema operativo, están en el cgroup por defecto. ¿Como muevo un proceso a este nuevo cgroup? pues haciendo algo tan sencillo como escribir el PID del proceso que quiero mover en el fichero cgroup.procs. Por ejemplo, para mover el proceso postfix a mi nuevo cgroup:

Moviendo un proceso a un cgroup.
 
Lo cierto es que esto está muy bien, hemos movido un proceso a un cgroup nuevo pero, ¿como puedo comprobar esto? Afortunadamente tenemos una serie de herramientas en el sistema operativo que nos permiten controlar los cgroups activos en el sistema. Una de estas herramientas es systemd-cgtop que nos mostrará el uso de recursos de cada cgroup existente en el sistema, de una manera similar a como lo hace el comando top:

Comando systemd-cgtop.

Como vemos en la salida del comando, el cgroup mail tiene una tarea y de momento no tiene ningún tipo de consumo.

Para tener más información sobre los diferentes cgroups, y no hacerlo todo tan artesanalmente, podemos instalar el paquete libcgroup-tools el cual incluye una serie de herramientas de línea de comandos que nos permitirán manejar los cgroups de una manera más cómoda.

Por ejemplo, si quiero fijar un límite de memoria en el cgroup mail que he creado, puedo hacer lo siguiente:

Modificación del límite de memoria del cgroup mail.

Los mensajes que recibimos relacionados con el resto de controladores se debe a que, cuando hemos creado el cgroup, este solo lo hemos creado bajo el controlador memory ya que lo que queremos es limitar su uso de memoria.

Ahora probemos a realizar una conexión con el servidor SMTP con un simple telnet y veamos que sucede:

El cgroup mail.

Podemos observar que aumenta el número de tareas dentro del cgroup, ya que la conexión que establecemos es manejada por un proceso hijo del que hemos movido al cgroup mail y que la memoria consumida es de 840K, cerca del límite que hemos fijado. Por tanto, si lanzamos varias conexiones simultaneamente conseguiremos lo siguiente:

Error de límite de memoria en cgroup mail.

Como podemos ver en la salida anterior, el cgroup mail se ha quedado sin memoria, con lo que el kernel ha invocado el OOM killer para liberar memoria. En este caso se ha matado un proceso smtpd, uno de los hijos creado por el proceso master para gestionar una conexión, con lo que el proceso principal continua corriendo.

Revisando la salida del comando systemd-cgtop podemos ver como ahora el cgroup mail tiene un número de tareas y un uso de memoria que está por debajo del límite que hemos marcado:

Estado del cgroup.

En general el número de tareas coincidirá con el contenido del fichero cgroup.procs dentro del cgroup que hayamos definido, que en este caso es /sys/fs/cgroup/memory/mail.

¿Que podemos hacer con todo esto? pues lo cierto es que, sin pensar en contenedores, podemos asegurarnos de limitar el consumo de recursos en nuestros sistemas en caso de ser necesario llegando al nivel de granularidad de aplicarlo a procesos individuales.

Este ejemplo es muy simple, pero sirve para empezar a hacernos una idea de en que se basan tecnologías como Docker. Como ejemplo de esto, podemos ver que, si arrancamos un contenedor, podemos encontrar su cgroup correspondiente en cada uno de los controladores del sistema.

Levantamos un contenedor, en este caso de una imagen de MySQL.

Fijándonos en el container ID, podemos ver que tenemos definido un cgroup para dicho contenedor en los diferentes controladores:

Cgroup correspondiente al coontenedor.

Y confirmamos que, efectivamente, se corresponde a nuestro contenedor no solo por el ID del mismo, sino porque dentro del fichero cgroup.procs contiene el PID del proceso mysqld ejecutado por el contenedor:

El proceso mysqld dentro del cgroup creado para el contenedor.

Por último vemos en la salida anterior que el usuario propietario del proceso mysqld es polkitd, lo cual se debe a que en el contenedor el propietario del proceso es el usuario mysql con un UID igual al del usuario polkitd del sistema. Esto es posible gracias al namespace de user IDs que está usando Docker y que permite el aislamiento y reutilización de UIDs entre el cgroup del contenedor y el cgroup del sistema.

En caso de querer eliminar un cgroup, primero es necesario que no exista ningún proceso dentro del mismo y bastará con borrar la estructura de directorios creada o bien usar el comando cgdelete.

Para terminar es importante tener en cuenta que hay dos versiones diferentes de cgroups disponibles en el Kernel de Linux y en este post hemos comentado los aspectos más básicos de la v1 de dicha implementación.


sábado, 26 de septiembre de 2020

Kubernetes - Objetos básicos II

Continuando con los objetos básicos que podemos definir en un cluster de Kubernetes, hoy vamos a ver los objetos de tipo ReplicaSet.

Como ya comentamos en las entradas sobre PODs y servicios, lo habitual es que no trabajemos directamente con PODs y objetos de tipo service como hemos hecho hasta ahora. La principal razón es que, en ese caso, Kubernetes no monitoriza los PODs y no los reinicia en caso de que se caigan. Por tanto, y por decirlo de una manera simple, cuando definimos PODs directamente, el KCP solo se asegura de arrancar un POD con las características que le hayamos especificado, pero no realizará ninguna acción para mantener ese POD ejecutándose en caso de fallo o parada manual del mismo.

Evidentemente esto no es lo que queremos que suceda en un entorno productivo, ya que no podemos estar monitorizando el estado de los PODs y controlándolos manualmente. Así que, para solucionar este problema, tenemos los objetos de tipo ReplicaSet que permiten definir un estado del cluster en el cual queremos que haya un determinado número de PODs. Una vez definido y aplicado al cluster, el KCP monitorizará de forma continua el estado del cluster y se asegurará que hay tantos PODs ejecutándose como el número especificado en el objeto ReplicaSet.

Volviendo a la estructura de la definición de cualquier objeto en Kubernetes, la cual recordamos que es:

Definición de un objeto en Kubernetes
Definición de un objeto en Kubernetes.

lo único que debemos hacer es añadir una nueva capa de configuración sobre la definición de un POD, encapsulándolo en la definición del objeto ReplicaSet. Esto, dicho así, suena algo confuso, pero si pensamos que un POD es la encapsulación de un contenedor, es lógico pensar que cualquier objeto que haga uso de PODs necesitará que encapsulemos el objeto inferior dentro de su propia configuración.

Veamoslo de forma práctica para que quede más claro, de tal manera que además podamos ver alguna de las características de un objeto ReplicaSet. Una definición simple sería como la siguiente:

Definición de un objeto ReplicaSet
Definición de un objeto ReplicaSet.

La estructura que sigue la definición del ReplicaSet cumple con lo que hemos visto hasta ahora y como podemos ver, contiene la definición de un POD dentro de la sección spec del ReplicaSet. Pero intentemos explicar y aclarar esto un poco más:
  • La primera diferencia clara es que, al definir la versión del API es necesario que especifiquemos apps/v1. Esto se debe a que los ReplicaSets forman parte del grupo de aplicaciones y no del grupo core como los PODs.
  • El tipo o kind de objeto es, evidentemente, ReplicaSet.
  • La sección metadata, como ya hemos visto hasta ahora, nos permite especificar el nombre del objeto ReplicaSet que estamos definiendo.
  • La sección spec, que contiene la configuración del objeto ReplicaSet podemos decir que está dividida en dos partes. Como vemos, tenemos un campo selector, el cual nos permite especificar que PODs son los que va a controlar este ReplicaSet. Debemos pensar que este campo selector actua de forma muy similar al campo selector de un objeto service. A continuación tenemos el número de replicas que queremos tener de los PODs controlados por el ReplicaSet.
  • Siguiendo dentro de la sección spec del ReplicaSet, a continuación tenemos una sección denominada template. Esta sección contiene toda la configuración referente al POD que queremos que arranque y controle el objeto ReplicaSet. Como vemos, el template tiene su sección metadata, donde especificamos las etiquetas que deben coincidir con el selector definido anteriormente y la sección spec de la template, que contiene la definición que ya hemos visto de cualquier POD. La sección metadata no contiene un nombre para los PODs ya que se asignará un nombre a cada POD basado en el nombre del ReplicaSet.
Por tanto y resumiendo esto un poco, podemos decir que establecemos la definición de un POD, que encapsulamos en una template, la cual estará controlada por un ReplicaSet. El ReplicaSet especifica el número de PODs, cuyas características definimos en la sección template, que deben ejecutarse en todo momento en el cluster.

Con esta definición, al aplicar este fichero a la configuración del cluster tendremos lo siguiente:

Aplicamos la definición del ReplicaSet
Aplicamos la definición del ReplicaSet.

Como podemos ver, ha aparecido una nueva sección que contiene los objetos de tipo ReplicaSet donde vemos tres columnas que establecen el número de PODs definidos en el ReplicaSet, cuantos hay actualmente y cuantos hay listos para proporcionar el servicio. Además, aparecen dos nuevos PODs cuyos nombres podemos ver que están derivados del nombre del ReplicaSet.

Por tanto tenemos varios PODs, los cuales, están controlados por el KCP por formar parte de un ReplicaSet, asegurando que siempre habrá tantos PODs ejecutándose como el número de réplicas especificado en el ReplicaSet. Pero, del mismo modo que con un POD individual, seguimos necesitando un servicio para publicar dichos PODs al exterior del cluster si es necesario, con lo que podemos cambiar la definición del servicio que utilizamos en el post anterior para que su campo selector coincida con la etiqueta de la template que define los PODs controlados por el objeto ReplicaSet. Una vez hecha esta modificación, al describir el servicio veremos que el número de endpoints disponibles es 2:

Objeto service para el ReplicaSet definido
Objeto service para el ReplicaSet definido.

Una de las ventajas de usar ReplicaSets es que podemos cambiar el número de réplicas, o número de PODs, de forma simple. Veamos como cambia el número de endpoints disponibles para el servicio al modificar el número de réplicas que queremos. Para esto solo es necesario usar el subcomando scale de kubectl del siguiente modo:

Escalado del número de réplicas
Escalado del número de réplicas.

Como podemos ver, con un solo comando hemos aumentado el número de PODs que deben ejecutarse en todo momento y que están controlados por el ReplicaSet webapp. Al comprobar el número de endpoints del servicio asociado vemos que el número de PODs disponibles para proporcionar el servicio ha aumentado igualmente:

Servicio asociado a los PODs del ReplicaSet
Servicio asociado a los PODs del ReplicaSet.

Por último, veamos como existe efectivamente un control sobre el número de PODs que están corriendo en el cluster. Al definir que el ReplicaSet debe asegurar que haya 5 PODs ejecutándose en todo momento, tras haberlo escalado con el subcomando scale, vamos a simular la caida de algunos de los PODs usando el subcomando delete de kubectl para eliminar varios PODs simultaneamente. Sería algo parecido a lo siguiente:

Eliminamos tres PODs del ReplicaSet
Eliminamos tres PODs del ReplicaSet.

Si somos lo suficientemente rápidos, o abrimos otra sesión, podremos ver como se levantan los PODs que sustituyen a estos tres PODs, manteniéndose así el estado que hemos definido en todo momento para el cluster:

Creación de nuevos PODs
Creación de nuevos PODs.

Como podemos ver en la imagen anterior, hay tres PODs en estado Terminating y otros tres en estado ContainerCreating. Del mismo modo, el ReplicaSet indica que solamente 2 PODs están en estado ready mientras se están creando los 3 nuevos PODs que sustituyen a los tres que hemos eliminado manualmente.

Hasta aquí la entrada de hoy, donde hemos visto como Kubernetes usa los objetos básicos para construir objetos más complejos que nos proporcionan características adicionales, como la alta disponibilidad de los PODs, asegurando así que el servicio está proprocionándose en todo momento.

sábado, 5 de septiembre de 2020

Kubernetes - Conceptos básicos II

Tras el post sobre PODs y servicios, hoy vamos con más rollo teorico en el que veremos los objetos de Kubernetes y algunos conceptos importantes que usaremos más adelante.

Ya vimos que definiendo objetos del API de Kubernetes, establecemos la configuración deseada del cluster y que el KCP se encarga de realizar todas las tareas necesarias para que el cluster llegue a dicho estado.

Los objetos disponibles en el API de Kubernetes permiten establecer, entre otras cosas:
  • Las aplicaciones que deben ejecutarse en los contenedores.
  • Los recursos disponibles para la ejecución de dichas aplicaciones.
  • Las políticas que se aplican a dichas aplicaciones.
Ya que un objeto de Kubernetes es una definición de un estado deseado, una vez que hemos creado un objeto, Kubernetes trabajará de forma constante para que dicho objeto exista con las características que hayamos definido para él mismo.

Podemos decir que cada objeto de Kubernetes posee dos conjuntos de información que están relacionados entre sí:
  • La especificación del objeto (object spec), que es la definición de las caracterísiticas del objeto. El spec establece la configuración que deseamos del objeto. Por tanto nosotros seremos quien proporcionemos la especificación del objeto.
  • El estado del objeto (object status), que es el estado real del objeto gestionado por Kubernetes y por tanto, proporcionado y actualizado por el sistema.
Como ya vimos en la entrada anterior de esta serie, el KCP realizará todas las tareas necesarias para que el estado sea idéntico a la especificación del objeto.

¿Cómo podemos establecer la configuración de un objeto de Kubernetes? mediante YAML, siendo la estructura básica para definir un objeto la siguiente:

Definición básica de un objeto.
 
En cada fichero YAML que creamos hay una serie de campos requeridos que debemos definir a la hora de establecer la configuración de un objeto, siendo cada uno de ellos:
  • apiVersion, versión del API de Kubernetes utilizada para definir el objeto.
  • kind, tipo de objeto que estamos definiendo.
  • metadata, información usada para identificar claramente el objeto. Lo normal es que contenga el nombre del objeto y, en algunos casos, etiquetas para establecer relaciones entre diferentes objetos.  
  • spec, contendrá la configuración del objeto y por tanto, el estado deseado del mismo. En función del tipo de objeto (kind), el contenido de esta sección contendrá diferentes campos.
Como vemos, el campo metadata contendrá el nombre del objeto que estamos definiendo o sobre el que queremos realizar alguna operación. El nombre de un objeto debe ser único en el cluster para ese tipo de recurso, es decir, puedo tener objetos con el mismo nombre siempre y cuando sean de diferentes tipos. Además del nombre dado por nosotros, el objeto estará identificado por un UID generado de forma automática por Kubernetes.

Para poder añadir información más descriptiva a los objetos, el campo metadata admite el uso de annotations mediante las cuales podremos añadir parejas clave/valor arbitrarias. Por ejemplo:

Uso de annotations.
 
Adicionalmente también podemos usar labels dentro del campo metadata para incluir más información en los objetos. La principal diferencia entre annotations y labels es que estas últimas se utilizan para identifcar objetos dentro del cluster y como hemos visto con los services, nos permiten relacionar unos objetos con otros.

Por último, el campo metadata también puede especificar en que namespace debe realizarse la operación. Un namespace es una forma de particionar o dividir un cluster de Kubernetes en clusters virtuales, permitiéndonos la asignación de diferentes recursos a cada uno.

Dentro del mismo namespace los nombres de los recursos deben ser únicos, no pueden anidarse entre si y un objeto solo puede existir en un namespace. En un cluster recien desplegado existen tres namespaces por defecto:
  • default, namespace por defecto donde crearemos nuestros objetos.
  • kube-system, namespace del sistema donde se crean objetos propios de Kubernetes.
  • kube-public, namespace reservado para uso del cluster y de lectura para todos los usuarios, incluidos los no autenticados.
Es muy importante tener en cuenta la relación entre el servicio DNS interno de Kubernetes, el cual se ejecuta en el namespace kube-system, y el namespace de un objeto. Cada vez que se crea un objeto de tipo Service, Kubernetes crea una entrada DNS para el mismo con el nombre:

Nombre DNS de un objeto de tipo Service.
 
De esta manera podremos acceder desde un POD a servicios de diferentes namespaces con solo especificar el nombre completo DNS.

Para terminar, no todos los recursos están en un namespace, así recursos de bajo nivel como los nodos que forman el cluster no se encuentran en ningún namespace, lo cual podemos consultar con el comando:

Como listar recursos relacionados con o sin namespace.

Y de nuevo, una vez descrito un objeto en un fichero YAML, solo tendremos que aplicar dicha descripción al cluster mediante el comando: 

Comando kubectl.
 
Emplearemos el comando kubectl continuamente para interactuar con el cluster de kubernetes y cambiar el estado deseado del cluster. Este comando interactua directamente con el kube-apiserver que se encuentra en los nodos master del cluster.

Hasta aquí el roolo teórico de hoy. En próximas entradas seguiremos estudiando los objetos básicos de Kubernetes y trabajando con ellos.