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.

sábado, 8 de agosto de 2020

OpenLDAP - Replicación entre servidores OpenLDAP

Continuamos trabajando con OpenLDAP y en este post, veremos como podemos establecer una replicación entre servidores OpenLDAP para crear arquitecturas tolerantes a fallos.

En los posts anteriores hemos visto como realizar la configuración dinámica de un servidor OpenLDAP y como integrar un servicio Kerberos con OpenLDAP: Hoy, partiendo de otra máquina en la que hemos realizado las mismas tareas, veremos como crear una relación de replicación entre ambos servicios.

Primero vamos con un poco de rollo teórico sobre como OpenLDAP replica su información y permite crear servicios de nombres tolerantes a fallos.

OpenLDAP proporciona diferentes topologías de replicación y, en función de nuestras necesidades, unas se adaptarán mejor que otras a nuestra arquitectura. Independientemente de la topología seleccionada, todas se basan en el motor LDAP sync replication o syncrepl. Este sistema de replicación es un motor de cliente, es decir, los servidores OpenLDAP designados como consumers establecen una conexión con los servidores OpenLDAP designados como providers para mantener una copia del árbol de directorio.

Simplificando mucho el mecanismo de replicación, un consumer realiza una búsqueda en el provider y aplica todos aquellos cambios que se hayan producido desde que realizó la última búsqueda. Si lo explicamos con un poco más de detalle, el provider mantiene un atributo llamado contextCSN  por cada base de datos como el indicador del estado actual de la replicación. Basándose en la diferencia entre el atributo contextCSN del provider y el almacenado en el consumer, se transfieren todos los cambios necesarios del provider al consumer hasta que ambos atributos contextCSN sean idénticos.

Vamos a comenzar configurando nuestro servidor provider, para lo cual cambiaremos la configuración de forma dinámica. Básicamente es necesario añadir el módulo syncprov a la configuración del servidor y activar el overlay syncprov, en la base de datos que queramos replicar.

Para añadir el módulo al servidor OpenLDAP, basta con que creemos un fichero ldif con la configuración deseada, indicando la ruta donde se encuentran los módulos de OpenLDAP y el nombre del fichero del módulo a cargar y añadiremos dicho módulo usando un comando ldapadd, sobre la rama cn=config:
 
Configuración módulo syncprov en formato LDIF
Configuración módulo syncprov en formato LDIF.
 
Carga de la configuración del módulo
Carga de la configuración del módulo.

Con esta carga realizada, podemos comprobar que la configuración del servidor OpenLDAP ha cambiado correctamente y que ahora, este módulo, está cargado en el servidor:
 
Módulo syncprov disponible en el servidor
Módulo syncprov disponible en el servidor.

Ahora que el módulo que proporciona las características de replicación está cargado en el servidor, necesitamos habilitar el overlay syncprov en aquellas bases de datos que queramos replicar. Para esto, de una manera similar a como hemos cargado el módulo, tendremos que crear la configuración correspondiente para habilitar el overlay syncprov en la base de datos y aplicar el fichero a la rama cn=config. El proceso sería similar al siguiente:

Configuración overlay syncprov en formato LDIF
Configuración overlay syncprov en formato LDIF.

Carga de la configuración del oberrlay para la base de datos
Carga de la configuración del oberrlay para la base de datos.

De nuevo, podemos comprobar que ahora el overlay se ha añadido correctamente a la base de datos que queremos replicar:

Overlay syncprov habilitado para la base de datos hdb
Overlay syncprov habilitado para la base de datos hdb.
 
Con esta configuración realizada, podemos comprobar que el árbol de directorio tiene un atributo contextCSN en la raíz del árbol que podemos obtener con el siguiente comando ldapsearch:

Atributo contextCSN de base de datos
Atributo contextCSN de base de datos.
 
Como ya he comentado, basándose en las diferencias entre el atributo contextCSN del servidor provider y el de los servidores consumer, se realiza la replicación de aquellos cambios producidos en el provider. Este atributo se mantiene en memoria y solo se escribe en la base de datos cuando el servidor OpenLDAP se para correctamente. Para generar checkpoints que modifiquen el valor de este atributo, asegurando así la correcta sincronización de los cambios entre los diferentes servidores de la topología de replicación que establezcamos, especificamos con el atributo olcSpCheckpoint que se produzca un checkpoint, y por tanto un cambio del valor del atributo contextCSN cada cierto número de operaciones o si ha pasado un cierto número de minutos. La definición del atributo es:

olcSpCheckpoint <NumOps> <Minutes>
 
con lo que establecemos cuando queremos que se genere un nuevo checkpoint en la base de datos. Es importante entender que, este checkpoint, se realiza tras una operación de modificación correcta y siempre que se hayan realizado tantas operaciones correctas o hayan pasado tantos minutos como especifiquemos desde el último checkpoint. Una forma simple de verlo es realizar varios cambios manualmente y comprobar el atributo contextCSN antes y después de realizar los cambios:

Cambio de contextCSN
Cambio de contextCSN.

Como podemos ver, entre la primera consulta y la segunda, haciendo cambios manualmente en la base de datos, el servidor OpenLDAP ha forzado un checkpoint provocando el cambio del atributo contextCSN.

¿Como obtiene OpenLDAP este contextCSN? aplicando el valor del atributo entryCSN más alto que haya en la base de datos. Cada vez que modificamos o creamos una entrada nueva, el atributo entryCSN, que básicamente es una marca de tiempo, indica cuando se hizo la última modificación sobre la entrada. Por ejemplo, revisando los atributos del siguiente objeto de clase account, vemos lo siguiente:

Un objeto de tipo account
Un objeto de tipo account.
 
Como podemos ver, la parte inicial del atributo entryCSN coincide con el atributo createTimestamp, indicando así la fecha de creación del objeto. La diferencia entre ambos atributos es que entryCSN registra el timestamp incluyendo microsegundos y ciertos valores adicionales, como un contador del número de operación y un identificador de réplica, para dar más granularidad a dicho atributo y poder controlar mejor las replicaciones. Si comparamos el valor del atributo entryCSN de este objeto, con el valor actual del atributo contextCSN vemos que ambos coinciden:

Valor de atributo contextCSN
Valor de atributo contextCSN.
 
Al modificar esta entrada, por ejemplo al cambiar el atributo description, vemos como cambia el valor del atributo entryCSN y, el valor del atributo contextCSN pasa a ser el nuevo valor del atributo entryCSN de esta entrada:
 
Nuevo valor del atributo entryCSN
Nuevo valor del atributo entryCSN.
 
Nuevo contextCSN tras checkpoint.
 
Es importante recordar que el checkpoint se forzará cada cierto número de operaciones correctas, o si han pasado tantos minutos como especifquemos, desde el último checkpoint. En este ejemplo solo he realizado un cambio, pero el checkpoint en este caso se ha realizado por el número de minutos pasados desde el anterior checkpoint.
 
Una vez que hemos establecido la configuración necesaria en el servidor provider, continuamos con la configuración de aquellos servidores que designemos como consumers o réplicas. Ya que nuestros consumers también serás servidores Kerberos, debemos realizar todas las configuraciones necesarias para que los servicios kdc y kadmin utilicen como base de datos el servidor OpenLDAP como ya vimos esta entrada.

Al igual que hemos hecho con el servidor provider, debemos cargar el módulo syncprov, para habilitar las características de replicación y añadir un atributo específico a la base de datos que queremos usar como destino de la replica. Para cargar el módulo podemos utilizar el mismo LDIF que empleamos para el provider y el atributo a añadir a la base de datos, en una de sus formas más simples, es más o menos el siguiente:
 
Atributo olcSyncRepl para establecer la réplica
Atributo olcSyncRepl para establecer la réplica.
 
Este atributo contiene las siguientes opciones de configuración que merece la pena explicar:
  • La linea rid, establece la identidad del consumer. Debe ser un número de tres dígitos.
  • La línea provider establece el nombre o dirección IP del servidor consumer.
  • La linea type establece el tipo de replicación que se utilizará. Existen dos tipos diferentes, refreshOnly y refreshAndPersist, cuyas diferencias veremos un poco más abajo.
  • La línea retry indica la estrategia de reintentos en casoo de fallo de conexión. Establece parejas de intervalo de reintento y número de reintentos. En este ejemplo, el consumer reintentará la conexión cada 5 segundos las 5 primeras veces y, si no es capaz de conectar, cada 300 segundos indefinidamente.
  • La linea searchbase establece la base donde se realizará la búsqueda de cambios en el provider.
  • La línea attrs establece que atributos van a replicarse. La especificación *,+ establece que se replicarán todos los atributos de usuario y operacionales.
  • Las lineas bindmethod, binddn y credentials, establecen que tipo de bind y el usuario que utilizará el servidor consumer para conectarse al provider y realizar la búsqueda necesaria para realizar la replicación. Como es lógico, dicho DN debe existir en el servidor provider.
Es muy importante crear una ACL dedicada para el DN que usaremos como replicador. Ese DN debe tener permisos de lectura en todo el árbol de directorio, salvo que haya OUs u otros objetos específicos que no queramos replicar. Esto es especialmente importante para el contenedor del reino Kerberos, que creamos y está controlado por un DN específico, como vimos en el post sobre la inegración de Kerberos con un backend OpenLDAP.

Uno de los puntos más importantes de esta configuración es el tipo de replicación a emplear. Como ya he indicado existen dos tipos, cuyas principales características son:
  • Tipo refreshOnly. En este caso, el servidor consumer se conecta al servidor provider, realiza la búsqueda de todas las modificaciones, realiza la sincronización y cierra la conexión. Cada cierto tiepo, establecido en la configuración del consumer, repite la misma operación. Por tanto podemos ver este tipo de replicación como una replicación de tipo pull, en la que el consumer siempre inicia la operación de replicación de información.
  • Tipo refreshAndPersist. En este caso, el servidor consumer se conecta al servidor provider, realiza la búsqueda de todas las modificaciones, realiza la sincronización y la conexión se mantiene establecida. En este caso, cualquier modificación realizada al provider es inmediatamente propagada al consumer. Este tipo de replicación podemos verla como una replicación de tipo push, en la que tras la conexión inicial, el provider envía los cambios realizados.

Una vez que tenemos el atributo listo podemos cargarlo directamente usando un comando ldapadd del siguiente modo:

Modificación de la base de datos de réplica
Modificación de la base de datos de réplica.

Una vez establecida la configuración en el servidor réplica, si no se nos ha escapado la correcta configuración de ningún posbile cortafuegos y la base de datos a replicar no es excesivamente grande, podremos ver como el servidor réplica sincroniza el contenido completo de la base de datos del provider sin problemas:

Base de datos del provider
Base de datos del provider.

 

Base de datos del consumer
Base de datos del consumer.

Como podemos ver, el contenido de ambas bases de datos es idéntico y, si nos fijamos en el atributo contextCSN del raíz de ambas bases de datos, vemos que coinciden perfectamente.

Hasta aquí como realizar la configuración necesaria para establecer una replicación entre servidores OpenLDAP, en entradas posteriores mejoraremos la seguridad de esta conexión y trabajaremos con las ACLs para controlar el acceso a los objetos y atributos del servidor LDAP.


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.