Mostrando entradas con la etiqueta Kubernetes. Mostrar todas las entradas
Mostrando entradas con la etiqueta Kubernetes. Mostrar todas las entradas

sábado, 21 de septiembre de 2024

Kubernetes - Redes y PODs II

En la anterior entrada de esta serie, repasamos como funciona la red entre contenedores que están encapsulados en el mismo POD o que están en PODs diferentes, pero todos se encuentran en el mismo nodo del cluster.

Como también vimos, en un sistema real tendremos un cluster de Kubernetes formado por diferentes nodos y por tanto, es importante que comprendamos perfectamente como se comunican los PODs entre si cuando estos se encuentran en hosts diferentes. Teniendo esto en cuenta, de forma muy simple, un cluster de Kubernetes de N nodos será algo como lo siguiente:

Comunicaciones entre nodos de un clsuter.

Con este diagrama de red podemos decir que, como es lógico, cada host o nodo miembro del cluster puede comunicarse con el resto usando el interfaz eth0 ya que todos se encuentran en la misma red. Pero, si recordamos lo que vimos en el post anterior de esta serie, lo que realmente tenemos es algo como lo siguiente:

Comunicaciones entre todos los elementos de un cluster.

Esta imagen nos permite darnos cuenta de la complejidad que tienen las comunicaciones entre todos los miembros de un cluster de Kubernetes ya que, por defecto, puede suceder que las direcciones IP internas de todos los componentes, como el bridge virtual cni0 o las direcciones IP asignadas a los interfaces veth, coincidan en dos o más de los nodos que forman parte del cluster. Aunque es el peor caso que se puede dar, incluso aunque todas las direcciones IP fuesen diferentes, un nodo que forma parte del cluster no tendría forma de saber, a priori, que direcciones IP tienen el resto de bridges o interfaces veth del resto de nodos del cluster.

Esto podemos comprobarlo con un cluster muy simple, formado por un nodo master y dos nodos worker. Partiendo de este cluster, en el cual empleamos cri-o como motor de contenedores, podemos ver lo siguiente si lanzamos un deployment simple para crear una réplica de un contenedor en cada uno de los nodos worker:

Deployment en un cluster de kubernetes recién creado.

Como se puede comprobar en la imagen anterior, ambos contenedores tienen la misma dirección IP, la cual se corresponde al rango de direcciones IP proporcionadas por el motor de contenedores que está instalado en cada nodo del cluster, en este caso cri-o.

Además, como es lógico, las direcciones IP de los interfaces de red bridge a los que se conectan los contenedores es la misma en todos los nodos:

Direccionamiento IP de interfaz bridge cni0 en los nodos del cluster.

Es importante que recordemos que, en todos los nodos, tenemos instalado el mismo motor de contenedores. Este motor se encarga de asignar direcciones IP a los PODs, en función de la configuración por defecto del mismo y Kubernetes es la capa de orquestación que controla el motor, pero no controla la asignación de direcciones IP. En resumen, este comportamiento es el esperado en un cluster de Kubernetes recien instalado. 

Evidentemente, las comunicaciones entres los PODs que se encuentran en diferentes nodos, es totalmente imposible ya que las direcciones IP son iguales y no hay manera de enrutar los paquetes correctamente entre ellos.

Para poder solventar este problema y asegurar la correcta comunicación entre todos los PODs del cluster, es necesario que instalemos un plugin de red CNI. Este plugin de red, basándose en la configuración que apliquemos, hará dos cosas, primero asignará un rango de direcciones IP global a los bridges en cada nodo y dentro de ese rango de direcciones, asignará una dirección IP al interfaz bridge en función del nodo donde se construya dicho interfaz. Adicionalmente añadirá reglas de enrutamiento en el nodo, indicando que interfaz físico es el primer salto o gateway para alcanzar las direcciones de red de cada uno de los bridges correspondientes al resto de nodos. De esta manera, aseguramos la correcta comunicación entre todos los PODs que puedan ejecutarse en el cluster.

Por tanto el plugin o add-on de red, el cual es totalmente necesario para permitir las correctas comunicaciones entre todos los elementos del cluster, nos permite establecer la comunicación entre todos los PODs del cluster.

Podemos simular esto si modificamos la configuración del rango de red proporcionado por el motor de contenedores y añadimos rutas de red en los nodos worker. Por ejemplo, podemos hacer lo siguiente en los nodos worker:

1.- Editamos el fichero /etc/cni/net.d/11-crio-ipv4-bridge.conflist y modificamos el rango de la dirección de subred de los nodos worker. En uno de los nodos worker fijamos como subred para PODs la dirección de red 10.85.1.0/24, mientras que en el otro usaremos la red 10.85.2.0/24. A continuación reiniciamos el servicio crio de ambos workers y arrancamos un par de PODs. Para esta prueba usaré netshoot, una imagen que contiene herramientas de red, pensada para analizar problemas de comunicaciones en entornos de contenedores.
 
Al comprobar el estado del cluster podemos ver que, ahora, las direcciones IP de cada POD son de redes diferentes:

PODs arrancados con direcciones IP de subredes diferentes.

2.- A continuación añadimos rutas de red en cada nodo worker, de tal modo que indicamos como acceder a la subred asignada a los PODs remotos.
 
3.-  Desde la consola de cada contenedor comprobamos que hay comunicación entre los PODs, aunque se encuentren en diferentes nodos worker:

Comunicación entre PODs.

Esto que hemos realizado a mano, es lo que básicamente implementa el plugin de red CNI que tenemos que instalar en nuestros clusters de Kubernetes para permitir las comunicaciones entre los PODs que se encuentren en diferentes nodos. En general, un plugin de red, se encargará de la asignación de direcciones IP a los interfaces bridge de los nodos, asignación de direcciones IP a los diferentes PODs que corren en cada uno de ellos y creación de todas las rutas necesarias para permitir estas comunicaciones. Adicionalmente, algunos plugins ofrecen además características como el control de red mediante políticas, permitiendo controlar las comunicaciones entre los diferentes PODs, así como entre estos y el exterior del cluster.

En resumen, para permitir las comunicaciones entre todos los PODs de un cluster de Kubernetes, es necesario que instalemos un plugin de red CNI, el cual controlará todos los aspectos de red necesarios para implementar el modelo de red de Kubernetes, así como funcionalidades adicionales dependiendo del plugin que utilicemos.

lunes, 1 de julio de 2024

Kubernetes - Redes y PODs I

Uno de los aspectos que, al menos a mi, me resulta más confuso cuando hablamos de Kubernetes es como funciona la red. Hay diferentes conceptos y abstracciones involucradas que se relacionan entre si, para permitir que los PODs se comuniquen entre ellos y con el exterior del cluster.

Empezando por lo básico, recordemos que un POD es una abstracción de uno o más contenedores que se ejecutan en el mismo host, compartiendo recursos como volúmenes y, para el caso que nos ocupa, la pila de red. Este punto es muy importante porque, si todos los contenedores que forman parte de un POD comparten la pila de red, pueden comunicarse entre ellos accediendo a localhost.

Comencemos por lo más simple, que consiste en que un POD solo encapsule un contenedor. En este caso, vamos a intentar explicarlo a partir de la imagen siguiente:

Un contenedor encapsulado en un POD.

El host donde se ejecuta el motor de contenedores tiene un interfaz de red físico eth0, al cual se conecta el bridge que crea dicho motor de contenedores. Este bridge, dependiendo de nuestra configuración, tendrá una dirección IP la cual, y es esto es muy importante, será el gateway de cualquier contenedor que creemos. A este bridge, se conectará el interfaz de red virtual veth del contenedor en el momento de su creación, siendo la dirección IP asignada a dicho interfaz veth, del mismo rango de red que el del bridge.

Podemos ver lo anterior con solo ejecutar el comando ip addr show en nuestro host físico:

Red en el host.

Como vemos en la imagen anterior, además del interfaz físico y de loopback, aparece el bridge cni0 creado por el motor de contenedores.

Ahora, podemos crear manualmente un POD y asignar un contenedor a dicho POD, para lo cual podemos hacer algo como lo siguiente:

Creación de un contenedor dentro de un POD.

En el momento de crear el POD, un interfaz veth aparece en el sistema y al inspeccionar el POD creado con los comandos anteriores, podemos ver la dirección IP asignada al mismo:

Dirección IP asignada al POD.

Con los comandos anteriores, hemos creado un POD y a continuación, hemos creado y arrancado un contenedor empleando una imagen simple de busybox. Al ejecutar el comando /bin/sh en dicho contenedor, podemos comprobar la dirección IP asignada al mismo:

Interfaz de red en el contendor.

Como vemos, la dirección IP es la misma, es decir, la dirección IP asignada al POD es la que se ha asignado al contenedor encapsulado por dicho POD.

En el caso de crear un segundo contenedor, la situación que tendríamos sería muy similar a la siguiente:

Dos contenedores en PODs separados.

De forma similar a como ocurría en el primer caso, el nuevo contenedor tiene un interfaz de red virtual al que se asigna una dirección IP del mismo rango y, si los contenedores tienen alguna manera de descubrimiento, podrán comunicarse entre ellos a través del bridge.

En este segundo caso, en el momento de crear el nuevo POD, podemos comprobar que un nuevo interfaz virtual veth aparece en nuestro host físico:

 

Creación de un segundo POD.

Repitiendo el proceso anterior y creando otro contenedor, encapsulado por este nuevo POD, podemos comprobar que, de nuevo, la dirreción IP del segundo contenedor es la misma que la del POD que lo encapsula:

Configuración de red del segundo contenedor.

Si ambos contenedores necesitan comunicarse entre ellos, podemos ver que esta comunicación funciona sin problema y se realiza a través del bridge cni0.

Los interfaces virtuales veth correspondientes a cada POD, desaparecerán del host en el momento en el que destruyamos los PODs.

Por tanto, hasta aquí tenemos el caso más sencillo usando contenedores pero, como hemos comentado, en Kubernetes un POD puede contener más de un contenedor y es en ese caso cuando todos comparten la pila de red. Por tanto, la imagen anterior, quedaría del siguiente modo:

Dos contenedores encapsulados por el mismo POD.

Para crear un POD y arrancar dos contenedores dentro del mismo, haríamos algo como lo siguiente:

Creación de dos contenedores encapsulados en el mismo POD.

Podemos observar en la salida anterior que ambos contenedores forman parte del mismo POD, ya que la columna POD ID muestra el mismo identificador para ambos. Además, como esperábamos, ahora solo hay un interfaz veth presente en el host:

Interfaces de red en el host.

Al comprobar la red en cada uno de los contenedores, podemos observar que ambos contenedores comparten la misma dirección IP:

Dirección IP en un contendor.....

.... y en el otro.

Es importante recordar que, como indica la documentación de Kubernetes, lo normal es que un POD solo encapsule un contenedor, siendo el caso de varios contenedores "dentro" de un mismo POD un caso de uso avanzado y solo recomendado cuando estos tienen una fuerte dependencia entre ellos.

Pensemos que, al fin y al cabo, lo que está haciendo Kubernetes es controlar al motor de contenedores para indicarle que, al crear el nuevo contenedor dentro del POD, no se cree un nuevo interfaz virtual, sino que utilice el ya existente. Por tanto, en este caso, ambos contenedores tienen un único interfaz de red común compartiendo la pila de red. Como es lógico, esto tiene las siguientes implicaciones:

  • Con la configuración adecuada, ambos contenedores son accesibles desde el exterior.
  • Al compartir la dirección IP, no pueden abrir el mismo puerto a la vez.

Ahora, podemos comprobar que efectivamente ambos contenedores pueden comunicarse entre si usando localhost como dirección haciendo algo como lo siguiente:

Arrancamos el servidor telnet en uno de los contenedores.

Ahora, podemos conectarnos al otro contenedor, usando crictl exec y ejecutar el comando telnet 127.0.0.1 para establecer una conexión contra el otro contenedor:

Conexión telnet con localhost.

Como podemos ver, nos podemos conectar de un contenedor a otro usando directamente localhost ya que, al compartir la pila de red, las redes de ambos contenedores son la misma.

¿Como implementa esto Kubernetes? mediante la ejecución de un contenedor adicional especial cuyo único objetivo es proporcionar la red al resto de contenedores del POD. Este contenedor, que es una instancia de la imagen pause, es el corazón del POD permitiendo la comunicación por red del contenedor o contenedores que estén encapsulados por el POD, ya sea entre ellos o con el exterior del cluster.

Hasta aquí tenemos un resumen breve de como funciona la red entre PODs o contenedores que se encuentran en el mismo host, pero como sabemos, un cluster de Kubernetes estará compuesto por varios nodos y los diferentes PODs deben comunicarse entre ellos estén o no en el mismo nodo del cluster.

En la siguiente entrada, veremos como funciona la red entre PODs cuando hablamos de un cluster y, por tanto, de PODs en diferentes nodos del cluster.


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.

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.

martes, 4 de agosto de 2020

Kubernetes - Notas adicionales sobre PODs y Services

En esta nueva entrada sobre Kubernetes profundizaremos un poco más en los detalles de la relación entre los PODs y los objetos de tipo service, que ya vimos en la anterior entrada de esta serie sobre Kubernetes.

Resumiendo lo visto hasta ahora, Kubernetes controla nuestra infraestructura de contenedores asegurándose que el estado del cluster coincide exactamente con el estado que definamos. Esto quiere decir que se asegurará de que existan los objetos en el cluster que hayamos establecido en nuestra configuración, arrancando y parando los contenedores que sean necesarios.

También hemos visto que un POD es la unidad mínima de ejecución en Kubernetes y que es la encapsulación de un contenedor, por ejemplo de Docker, que le permite al cluster de Kubernetes el control del mismo. Los PODs, por defecto, no son accesibles desde el exterior del cluster con lo cual, para publicar sus servicios, tendremos que crear un objeto de tipo service el cual "conectará" los PODs con el exterior, haciendo así accesibles los servicios a clientes externos.

Como vimos, esta conexión entre un objeto de tipo service y un POD se hace en función del campo selector del service y de las etiquetas del POD, campos que se definen dentro de la sección spec del objecto service y en el caso de los objetos POD, en la seción metadata de los mismos.

Sabemos que un objeto de Kubernetes se define con un fichero que sigue la siguiente estructura:

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

Por tanto, para definir un POD básico que ejecute la imagen oficial del servidor web Apache, podemos establecer la siguiente definición:

Definición POD simple
Definición POD simple.

Como sabemos, basta con aplicar este fichero de definición al cluster para arrancar un POD con estas características:

Ejecución de POD simple
Ejecución de POD simple.

Este POD, aunque controla un contenedor ejecutando una imagen de un servidor web que expone el puerto 80, no es accesible desde el exterior del cluster y sabemos que necesitamos un servicio para poder conectarlo o publicarlo al exterior.

La definición de este servicio será tan simple como la siguiente:

Definición de un servicio simple
Definición de un servicio simple.

Como podemos ver, he creado la definición de este servicio sin especificar el campo selector a pesar de lo cual podemos aplicarlo perfectamente al cluster:

Aplicación de la definición de servicio
Aplicación de la definición de servicio.

Comprobamos que efectivamente el servicio se ha creado en el cluster con el comando kubectl get all:

El objeto servicio aplicado al cluster
El objeto servicio aplicado al cluster.

Como es lógico y ya vimos, ahora mismo tenemos un POD que no es accesible y un servicio que no está redirigiendo conexiones a ningún POD de nuestro cluster, ya que no hay ningún selector definido en el mismo.

Podemos realizar esta comprobación usando el subcomando describe, el cual sabemos que nos proprociona mucha información, o bien podemos usar el comando get sobre objectos específicos y además indicar que nos muestre las etiquetas de los objetos. Así, por ejemplo, podemos mostrar solo los PODs existentes en el cluster con sus etiquetas usando el siguiente comando:

Mostrando las etiquetas de los PODs
Mostrando las etiquetas de los PODs.

Como podemos ver, la definición de este POD no contiene ninguna etiqueta. Para solucionar esto podemos modificar el fichero con la definición del POD, añadir las etiquetas necesarias y volver a aplicar la configuración al cluster o bien, podemos usar la opción --overwrite para modificar dicho campo. Esto último lo haríamos más o menos del siguiente modo:

Modificación de la etiqueta de un POD
Modificación de la etiqueta de un POD.

Como vemos, este cambio se realiza en caliente y es aditivo, es decir, podemos añadir tantas etiquetas como necesitemos al POD. Es importante tener en cuenta que, al hacer el cambio del campo label por comando, debemos cambiar el fichero de definición del POD si estas etiquetas son necesarias ya que el cambio no se aplica al fichero de definición del POD.

Para modificar nuestro servicio tendremos que editar el fichero, añadir el campo selector con la etiqueta necesaria y aplicar de nuevo la configuración al cluster.

Modificamos la definición del servicio y aplicamos al cluster
Modificamos la definición del servicio y aplicamos al cluster.

De este modo, al obtener la descripción del servicio podremos comprobar que en el campo endpoints aparece la IP del POD webserver y que este es accesible desde el exterior:

Servicio configurado y con endpoint disponible
Servicio configurado y con endpoint disponible.

Servicio web accesible
Servicio web accesible.

Hasta aquí hemos revisado y ampliado un poco lo que ya sabíamos y vimos en el post anterior pero, ¿que ventajas nos proporciona desacoplar el servicio del POD?

Supongamos que el equipo de desarrollo ha creado una versión 0 de la aplicación que debemos desplegar. Para esto han modificado la imagen oficial del servidor Apache, han incluido todos los cambios necesarios y han subido la imagen al repositorio interno de imágenes. Partiendo de este caso, podemos hacer un despliegue como el siguiente donde usamos la misma configuración que hemos establecido hasta ahora, es decir, identificamos el POD con la etiqueta application=webapp y establecemos el selector del service para seleccionar los PODs con dicha etiqueta. En este caso hariamos algo parecido a lo siguiente:

Creación del POD con version 0
Creación del POD con version 0.

En el caso de no disponer de un repositorio de imágenes, y estar usando el repositorio de imágenes de Docker local del propio host, debemos añadir la opción imagePullPolicy: Never en la definición del POD ya que por defecto Kubernetes siempre intentará descargar la imagen de Docker Hub.

Con esto podemos comprobar que el objeto service tiene un endpoint y que podemos acceder al servicio web:

Descripción del servicio webservice
Descripción del servicio webservice.

Acceso al servicio web V0
Acceso al servicio web V0.

Si ahora tenemos que desplegar una nueva versión del servicio web, supongamos que nos proporcionan la imagen de la versión 1 del mismo servicio web, podemos definir y aplicar al cluster el siguiente POD:

Definición del nuevo POD v1
Definición del nuevo POD v1.

Aplicamos la definición del POD v1
Aplicamos la definición del POD v1.
 
Con esto vemos que, en un mismo fichero, podemos incluir la definición de diferentes objetos. Al aplicar la definición al cluster, Kubernetes comprueba la configuración de todos los objetos ya existentes y aplica los cambios que puedan existir o indica que los objetos no han cambiado.

Al llegar a este punto, y teniendo en cuenta cual es el selector del servicio webservice, ¿cual es el resultado obtenido? Como ambos PODs tienen la misma etiqueta y el selector del servicio redirige el tráfico a cualquier pod con la etiqueta application=webapp estaríamos sirviendo ambas versiones del servicio web simultaneamente. Esto podemos verlo al hacer la descripción del servicio y comprobar que hay dos endpoints, uno correspondiente a cada POD. Además, al acceder al servicio accederíamos a ambas versiones:

Servicio con endpoints en PODs v0 y v1
Servicio con endpoints en PODs v0 y v1.

Acceso al servicio web v1
Acceso al servicio web v1.

Como es lógico esta situación no es la que queremos, ya que serviríamos ambas versiones simultaneamente a los clientes. Por tanto, para evitar esto, es necesario que definamos correctamente las labels de nuestros PODs y especifiquemos correctamente el selector del objeto service. Para corregirlo haríamos algo como lo siguiente:

Añadimos etiquetas a cada POD con la versión
Añadimos etiquetas a cada POD con la versión.

Modificacióon del selector del objeto service
Modificacióon del selector del objeto service.

Con esta modificación, y como el campo selector de un objeto service hace un and de todas las etiquetas que especifiquemos, ahora solamente hay un endpoint disponible para el servicio y por tanto solo servimos el servicio web correcto.

Servicio web con el endpoint correcto
Servicio web con el endpoint correcto.

En resumen, está claro que desacoplar el servicio del POD nos proporciona mucha flexibilidad a la hora de hacer despliegues de servicios.

Como veremos más adelante, lo habitual no es trabajar directamente con PODs y services de este modo, pero es importante entender como funcionan y se relacionan entre ellos. Esto nos permitirá comprender objetos más complejos, que se basan en estos y que veremos que proporcionan muchas características y funcionalidades adicionales.