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.