sábado, 30 de mayo de 2020

OpenLDAP - Autenticación passthrough usando SASL

Hoy vamos con una entrada rápida sobre OpenLDAP en la cual vamos a revisar como podemos configurar la autenticación passthrough de OpenLDAP.

Y ¿que es exactamente la autenticación passthrough? De forma muy resumida es la capacidad de la que dispone OpenLDAP para, utilizando la biblioteca SASL, delegar el proceso de autenticación a un servicio de autenticación externo.

Este tipo de configuración puede ser útil en determinados casos, como por ejemplo, si la organización de nuestro servidor de directorio divide las entradas de usuarios por secciones o unidades organizativas y cada una de ellas debe usar diferentes tipos de servicios de autenticación o para validar usuarios desde aplicaciones web.

Esta característica de delegación del proceso de autenticación se basa en el uso del servicio saslauthd. Este servicio puede configurarse para recibir peticiones de autenticación de otros procesos que soporten SASL, realizando la autenticación al servicio externo que hayamos configurado y devolviendo el resultado del proceso de autenticación.

Partiendo de un sistema donde ya se encuentra instalado un servidor OpenLDAP, el cual debe tener compilado el soporte de la biblioteca SASL, usaremos un servicio Kerberos 5 para validar a los usuarios mediante saslauthd. En el caso de CentOS 7.8.2003, se incluye OpenLDAP versión 2.4.44 con soporte SASL.

Por tanto, a partir del sistema anterior y de forma muy resumida, el proceso se basa los siguientes pasos:
  1. Primero necesitamos instalar el servicio saslauthd con las bibliotecas necesarias. Es importante tener en cuenta que vamos a instalar el paquete básico de la biblioteca SASL sin ningún mecanismo de autenticación adicional. Por tanto solo tenemos que buscar el paquete que proporciona el servicio saslauthd:
  2. Servicio saslauthd.

  3. Como queremos usar Kerberos 5 como servicio de autenticación, tenemos que instalar los paquetes que proporcionan los servidores de Kerberos. Es recomendable instalar también los paquetes que incluyan las herramientas como kinit para poder hacer pruebas y administrar correctamente el servicio:
  4. Instalación de Kerberos 5.
  5. Una vez instalado Kerberos 5 es necesario crear lo que, en terminología de Kerberos, se denomina un reino de autenticación. De forma muy resumida podemos entender un reino de autenticación de Kerberos como el dominio sobre el cual un servidor de Kerberos tiene la autoridad para autenticar a un usuario, host o servicio. De momento, y para resumir, la forma más rápida de crear un reino Kerberos es mediante la configuración del fichero /var/kerberos/krb5kdc/kdc.conf que contiene la configuración del servidor kdc que implementa y proprociona el servicio Kerberos. Una configuración básica es más o menos la siguiente:
  6. Configuración básica servidor kdc.
  7. Una vez realizada la configuración del servidor kdc, ya podemos inicializar el reino Kerberos. Este proceso crea la base de datos de Kerberos y el denominado fichero stash. El fichero stash contiene la contraseña maestra usada para encriptar la base de datos de Kerberos. Sin este fichero, el proceso krb5kdc del servidor no será capaz de arrancar de forma automática solicitando la password maestra en el arranque, con lo que es muy importante que creemos dicho fichero. Para inicializar el reino y crear el fichero solo necesitamos ejecutar un comando como el siguiente:
  8. Inicialización del reino Kerberos LAB.INT.
  9. En este punto ya está inicializado el reino Kerberos pero no hay ningún usuario o cuenta creada en la base de datos. Cada una de las entradas de una base de datos de Kerberos se denomina principal, independientemente de si corresponde a un usuario, host o servicio. Por tanto el primer paso consiste en crear el principal del administrador, que tendrá permisos sobre todos los principales del reino Kerberos. Para esto solo necesitamos hacer algo como lo siguiente:
  10. Creación del principal administrador.
  11. Para restringir el acceso a los ficheros de base de datos de Kerberos al principal del administrador, es necesario que establezcamos una ACL que incluya únicamente a dicho principal. Es importante señalar que un servidor Kerberos está constituido por dos servicios diferentes, el krb5kdc que implementa el servicio Kerberos y el servicio kadmind para las tareas de administración de la base de datos. Por tanto necesitamos establecer una ACL para el servidor kadmind que restrinja las operaciones sobre la base de datos del reino que hemos creado para el principal que hemos denominado como administrador. Para esto solo es necesario que editemos el fichero /var/kerberos/krb5kdc/kadm5.acl del siguiente modo:  
  12. ACL para el administrador.
Ya tenemos la configuración básica necesaria para crear un reino Kerberos. Ahora, para poder realizar pruebas, creamos unos cuantos principales que mapearemos a un objeto de usuario que luego crearemos en el servidor OpenLDAP. Para crear principales solo es necesario que ejecutemos un comando como el que utilizamos para crear el principal con el rol adminsitrador:
Creación de principales de usuarios.
Llegado este punto tenemos un servidor OpenLDAP recién instalado y un reino Kerberos creado, cuyos servicios podemos arrancar si no lo hemos hecho antes. Suponiendo que nuestra idea es crear un servidor de nombres para sistemas Unix/Linux basado en LDAP, usando como servicio de autenticación Kerberos 5, es necesario que extendamos el schema del servidor OpenLDAP. Esta extensión del schema es necesaria para incluir los atributos y clases de objeto necesarios para el uso de un LDAP como servicio de nombres según la RFC 2307. Esta RFC establece como debe ser la estructura, atributos y clases de objeto para poder usar un servidor LDAP como servicio de nombres.

Con la distribucion de OpenLDAP se incluyen varios schemas adicionales que se pueden cargar si es necesario con solo usar el comando ldapadd. En el caso del paquete OpenLDAP incluido con CentOS 7, estos ficheros de schema están en la ruta /etc/openldap/schema:

Schemas incluidos con OpenLDAP.
De esta manera podremos extender el schema básico del servidor OpenLDAP en caso de ser necesario con alguno de los incluidos en caso de ser necesario.

Un punto importante a tener en cuenta a la hora de extender el schema de cualquier servidor de directorio es que, en general, pueden existir dependencias entre diferentes schemas. Esto se debe a que un schema puede requerir un atributo que está definido por otro schema y, por tanto, será necesario cargar ambos schemas siguiendo el orden de dependencia entre ellos.

Teniendo en cuenta lo anterior, los siguientes pasos serían:
  1. Extendemos el schema de OpenLDAP añadiendo el schema nis, que es el que cumple con la RFC 2307. Este schema depende de los schemas core y cosine, con lo que ambos deben ser añadidos previamente. Para extender el schema solo es necesario usar un comando como el siguiente:
  2. Extensión del schema de OpenLDAP.
  3. A continuación creamos cuentas de usuario y de grupo en el servidor OpenLDAP para poder usarlo como servicio de nombres. Para esto podemos usar cualquier editor LDAP o mediante el comando ldapadd y ficheros ldif. En definitiva, lo que es necesario hacer es crear objetos de los tipos posixAccount para las cuentas de usuario y posixGroup para las de grupo. Una vez creados, tendríamos algo como lo siguiente:
  4. Cuentas de usuario y de grupo en el servidor OpenLDAP.
  5.  A continuación configuramos los sistemas Unix/Linux de la infraestructura para que utilicen el servidor OpenLDAP como servicio de nombres y el servidor Kerberos 5 como servicio de autenticación. Este paso dependerá de la distribución Linux que estemos utilizando. En el caso de distribuciones basadas en Red Hat, como CentOS, es muy recomendable utilizar authconfig-tui. En resumen será necesario configurar el cliente LDAP así como el cliente Kerberos 5 del sistema operativo cliente. Una configuración muy básica, y muy mejorable, para ambos servicios es la siguiente:
Configuración básica cliente Kerberos 5.

Configuración básica cliente LDAP.
Con esta configuración podemos comprobar que el sistema está correctamente configurado con solo realizar búquedas de información de usuarios y grupos así como validaciones mediante el comando kinit:

Pruebas de validación de principales Kerberos.
Comprobación del servicio de nombres.
Por tanto, en este punto, el servicio de nombres y autenticación funciona correctamente y los sistemas desplegados pueden utilizarlo como servicio centralizado de nombres y autenticación.

A continuación nos falta configurar saslauthd y OpenLDAP para poder delegar la validación de usuarios al servicio Kerberos 5 mediante la característica de passthrough de la que he hablado al principio del artículo.

Para este caso, la configuración de saslauthd utilizará como mecanismo de validación de contraseñas el sistema PAM del propio servidor. Para establecer esta configuración solo tenemos que modificar el fichero de configuración /etc/sysconfig/saslauthd como se muestra en la siguiente imagen:

Configuración saslauthd.
Ahora es necesario establecer la configuración de OpenLDAP necesaria para que emplee SASL y más concretamente saslauthd, para realizar el passthrough del proceso de autenticación. Este proceso de configuración de OpenLDAP se resume en los siguientes pasos:
  1. Creamos el fichero /etc/sasl2/slapd.conf que le indicará al servicio slapd de OpenLDAP que debe usar saslauthd para validar contraseñas:
  2. Configuración fichero /etc/sasl2/slapd.conf.
  3. Ahora es necesario modificar la configuración del propio servidor OpenLDAP para añadir una serie de atributos de configuración relacionados con SASL. Estos atributos establecen que host va a realizar el procesado SASL siendo en este caso el propio servidor, así como cual es el reino SASL por defecto y las características de seguridad. Utilizando la configuracióon dinámica de OpenLDAP, quedaría del siguiente modo:
  4. Configuración SASL de OpenLDAP.
  5. El último paso de configuración necesario en OpenLDAP debe realizarse en todas aquellas entradas que representen las cuentas de usuario cuya autenticación queramos delegar a saslauthd. Este punto se basa en establecer el atributo userPassword de cada entrada de usuario usando una cadena como {SASL}principal@REINO lo cual indica a OpenLDAP que, al recibir una petición de validación que incluya el DN correspondiente a dicha entrada de usuario, esta debe delegarse al host SASL configurado en el paso anterior. Esta validación se realizará mediante el uso de saslauthd el cual, al haberlo configurado para usar el mecanismo PAM del servidor, realizará la autenticación contra el servicio Kerberos 5. Una entrada de usuario quedaría, por tanto, del siguiente modo:
Cuenta de usuario.
Hagamos una prueba de validación del usuario operator1 mediante la ejecución de un comando ldapsearch:

Búsqueda en OpenLDAP.
Como vemos la autenticación ha fallado y, si revisamos los logs del sistema encontraremos que se debe a que, al intentar validar al usuario mediante PAM, saslauthd intenta usar el módulo PAM para el servicio ldap el cual no existe por defecto:

Error de validación saslauthd.
Una forma rápida de solucionarlo, pero no la mejor ni la más adecuada, es hacer un enlace simbólico en el servidor para crear el fichero del servicio ldap usando la configuración de autenticación general del sistema. En caso de un sistema CentOS, algo tan sencillo como lo siguiente:

Creación entrada servicio ldap para PAM.
Al hacer esto, si volvemos a realizar la búsqueda ahora el resultado será satisfactorio y podremos comprobar en el log del servidor kdc como llega la petición de autenticación del usuario:

Búsqueda realziada correctamente.
Petición de validación del usuario.
En resumen, podemos configurar un servidor OpenLDAP para delegar la autenticación de todas o de ciertas entradas que representen usuarios, utilizando así el mecanismo de autenticación del propio servidor OpenLDAP para determinados usuarios y mecanismos externos, como Kerberos 5, en el caso de otros.

Como puntos importantes a revisar para futuras entradas, es el funcionamiento de PAM y como crear una entrada correcta para el servicio LDAP que utiliza saslauthd en una configuración como esta así como establecer una configuración más segura del cliente LDAP del sistema operativo cliente.


jueves, 23 de abril de 2020

OpenLDAP - Configuración dinámica.


Hoy una entrada rápida sobre como configurar el acceso a la rama cn=config para poder realizar configuraciones dinámicas de versiones modernas de OpenLDAP.

Desde OpenLDAP versión 2.3 se puede establecer la configuración de nuestro servicio slapd de forma dinámica, a diferencia de versiones anteriores donde era neceasario que editaramos el fichero slapd.conf y reiniciaramos el servidor. Este cambio es muy importante, ya que nos permitirá modificar la configuración de nuestro servidor OpenLDAP sin afectar a nuestros servicios.

¿En que consiste la configuración dinámica de OpenLDAP? pues básicamente y de forma predeterminada, el demonio slapd dispone de un árbol que contiene la configuración del servicio, siendo la raíz de este árbol cn=config, con lo que podremos cambiar la configuración dinámicamente con solo acceder a dicho árbol.

Por defecto, la forma de acceder al árbol de configuración está restringida a la consola del servidor, con el usuario root y usando las herramientas estándar de openldap, como ldapsearch y ldapmodify, es decir, no podemos conectarnos con herramientas como ldapbrowser o Apache Directory Studio. ¿Como podemos comprobar si es así? Nada más instalar nuestro servidor Openldap, solo tenemos que ejecutar un comando como el siguiente y buscar la sección donde se define la base de datos de configuración:

Mostramos la rama cn=config.
Sección de configuración de acceso a la rama cn=config.
Como se ve en la imagen anterior, solo el usuario con uidnumber y gidnumber igual a cero, en principio solamente el usuario root, será capaz de acceder a dicho árbol.

Para simplificarnos un poco la vida, lo que tenemos que hacer es añadir un usuario a dicha rama, identificado como olcRootDN con su password correspondiente. Basta con que hagamos un fichero ldif tan sencillo como el siguiente y lo carguemos con el comando ldapadd:

Fichero ldif para añadir un usuario administrador a la rama cn=config.
El atributo olcRootPW contiene la password encriptada de nuestro usuario, la cual habremos generado con el comando slappassd:

Generando una password enciptada con slappasswd.
Ahora solo nos falta cargar el ldif anterior con el comando ldapadd:

Aplicamos el fichero ldif a la rama cn=config.
Tras esto ya podemos conectarnos con nuestra herramienta preferida y configurar el servidor de forma dinámica:
Conexión a la rama cn=config del servidor OpenLDAP.
Por ejemplo, podemos cambiar la configuración del nivel de logging de OpenLDAP de froma dinámica con solo modificar el raíz de la rama de configuración, ya que en ese caso estamos modificando la configuración del propio servidor OpenLDAP. Si queremos aumentar el nivel de log solo tenemos que añadir el atributo olcLogLevel y especificar el nivel de log que queremos:

Modificación del nivel de log del servidor.
Es importante recordar que en versiones anteriores y con la configuración estática, era necesario modificar el fichero de configuración slapd.conf del servidor OpenLDAP y reiniciar el servicio. Con esta configuración dinámica, al aplicar el cambio no será necesario reiniciar el servidor. Por cierto, OpenLDAP siempre usa la facility LOCAL4 de syslog, con lo que probablemente será necesario cambiar la configuración de syslog/rsyslog/syslog-ng para que se registren los mensajes de dicha fuente.

Una vez hecho esto, podemos pasar a configurar nuestro nuevo árbol de directorio o utilizar el que se crea por defecto con la instalación de OpenLDAP. En concreto, la configuración de nuevas bases de datos se realiza directamente desde la rama de configuración añadiendo objetos de la clase olcDatabaseConfig como podemos ver en la base de datos por defecto:

Base de datos incluida con OpenLDAP.
Como vemos en la configuración, para nuestro árbol de directorio es necesario que defininamos un objeto con las siguientes clases de objeto:
  • olcDatabaseConfig, que especifica que estamos definiendo la configuración de una base de datos.
  • olcHDBConfig, para especificar que el backend de base de datos a utilizar será el tipo HDB.
Además, debemos especificar la ruta donde se almacenarán los ficheros de la base de datos, el DN del administrador de la misma y parámetros adicionales, como los índices de búsqueda adecuados para las operaciones en función de los objetos que vayamos a almacenar.

Por tanto, podemos crear una nueva entrada de base de datos de una manera similar a la siguiente:

Definición de nueva base de datos.
Con los valores anteriores crearíamos una nueva base de datos para un nuevo árbol de directorio cuya raíz sería dc=lab,dc=int, donde fijamos determinados índices sobre ciertos atributos que vamos a utilizar y además establecemos ciertos parámetros de configuración adicionales. Lo importante es que comprobemos que podemos realizar la configuración de una nueva base de datos completa de forma totalmente dinámica.

Por tanto, ya tenemos una nueva base datos, pero está vacía y necesitamos, al menos, crear el objeto raíz de dicha base de datos. Para esto, basta de nuevo con que hagamos un fichero ldif similar al siguiente:

Fichero ldif definiendo la raíz del árbol de directorio.
Ahora solo necesitamos cargar esta nueva netrada usando el comando ldapadd y ya podremos comenzar a trabajar en la estructura del nuevo árbol de directorio. Porejmplo, ya podremos crear las estructuras organizativas que necesitemos, como por ejemplo algo como lo siguiente:

Una estructura básica para un servicio de directorio.
Por último, vamos a asegurar la base de datos que contiene el árbol de directorio por defecto incluido con la instalación de OpenLDAP. Para esto solo es necesario que borremos el atributo olcRootDN del objeto definido en la rama config para asegurar que no se podrá acceder a dicho árbol de directorio de ninguna manera.

sábado, 29 de febrero de 2020

Kubernetes - Objetos básicos I

Hoy, en esta nueva entrada sobre Kubernetes, vamos a comenzar el estudio de los objetos básicos existentes en la API de Kubernetes. Estos objetos son representaciones del estado que queremos o necesitamos del cluster con el que controlaremos que cargas queremos desplegar y como desplegarlas.

Vamos a empezar con los dos objetos fundamentales sobre los que se construyen otros objetos más complejos. Estos objetos son:
  • Pod. Un pod es la encapsulación de un contenedor en Kubernetes y es la unidad básica de ejecución disponible.
  • Service. Es un recurso dentro de un cluster de Kubernetes que me permite exponer un pod al exterior, permitiendo así el acceso al mismo.
Como este resumen es demasiado simple, veamos cada uno de estos objetos con más detalle.

Kubernetes es un sistema de orquestación de contenedores y uno de los requistios necesarios para su instalación es disponer de un motor de ejecución de contenedores como Docker. Ahora bien, aunque Kubernetes utilizará Docker para ejecutar contenedores, estos debemos configurarlos siguiendo la representación que veremos en otro post de esta serie sobre los conceptos básicos de Kubernetes. De esta manera, Kubernetes será capaz de controlar dichos contenedores y establecer el estado del sistema al estado deseado por nosotros. La representación de un objeto de Kubernetes debe seguir el siguiente formato:

Spec de un objeto de Kubernetes.
En el caso de querer definir un pod, la especificación del mismo sería algo como lo siguiente:

Especificación de un POD básico.
Analizando la definición anterior, podemos ver la encapsulación a la que me refería al definir un pod. En este caso tenemos los campos necesarios en los que establecemos que versión de API queremos utilizar, que el tipo de objeto es Pod, en metadata definimos el nombre de nuestro pod y, en la sección spec, establecemos que queremos un contenedor que debe ejecutar la imgen oficial del servidor http de Apache. En esta especificación vemos como objetos diferentes, en este caso el objeto pod y el objeto containers, pueden tener el mismo nombre.

Cada objeto disponible en la API de Kubernetes dispone de múltiples opciones para su especificación y es importante consultarla para definir correctamente nuestros objetos. En el caso del objeto pod, la referencia en el API de Kubernetes para la versión 1.17 podéis encontrarla aquí.

Lo habitual es que no interactuemos directamente con pods, sino que utilicemos objetos superiores que nos darán otras funcionalidades de las que el objeto pod carece, pero para empezar desde lo más simple creemos nuestro primer pod usando la configuración anterior. Para esto solo tenemos que usar el comando kubectl y aplicar dicho fichero con la especificación del objeto:

Creación de un pod simple.
Si queremos listar todos los pods que están ejecutándose en el cluster solo tenemos que emplear el comando siguiente:

Lista de pods en el cluster.
He indicado que un pod es una encapsulación de un contenedor básico con la información necesaria para que Kubernetes pueda controlarlo. Al utilizar Docker como motor de contenedores, podemos entender mejor esta encapsulación listando los contenedores que se están ejecutando en el sistema:

Lista de contenedores ejecutándose en el sistema.
Como podemos ver, hay dos contenedores relacionados con el pod que acabamos de crear. Uno de ellos ejecuta la imagen httpd que hemos especificado en el fichero de definición de nuestro pod y el otro una imagen propia de Kubernetes.

Podemos comprobar mejor esta relación si creamos un nuevo pod en el cual definimos nombres diferentes para el pod y el contenedor. Por ejemplo, si definimos un nuevo pod con la siguiente especificación y lo aplicamos:

Definición de un pod simple.
Contenedores relacionados con el pod creado.
Por tanto, con esto ejemplo, vemos como definimos un objeto básico y la relación que hay entre Kubernetes y el motor de contenedores que se ejecuta en los nodos que forman nuestro cluster.

Ahora que hemos creado dos pods que ejecutan una imagen del servidor web de Apache, tenemos que publicar dicho servicio al exterior del cluster ya que, por defecto, los pods solo son accesibles por otros pods. Para esto definiremos otro objeto de Kubernetes que se denomina service, la definición básica que podemos usar para este tipo de objeto sería:

Definición básica de un objeto service.
De nuevo, recordando el formato de especificación que debemos seguir con todos los objetos de Kubernetes, observamos los campos requeridos, siendo en este caso el tipo Service y dentro de la especificación del tipo de objeto tenemos los siguientes elementos:
  • El campo selector me permite definir una serie de etiquetas con las que especificar a que pods vamos a aplicar este service.
  • El type Nodeport indica a Kubernetes que debe publicarse el servicio en todos los nodos del cluster en el puerto público indicado por nodePort. El puerto del pod al que deben enrutarse las peticiones de los clientes está dado por port.
Por tanto, lo que estamos fijando es que el puerto 80 de los pods que tengan la etiqueta source: webserver, se publicarán al exterior a través del puerto 30080 de cada nodo del cluster. Al aplicar esta definición de servicio al cluster pasamos a tener lo siguiente:

Aplicamos el servicio y listamos los objectos existentes.
Como podemos ver tenemos los dos pods iniciales y dos servicios, siendo uno de ellos propio de Kubernetes y del cual, por el momento, no vamos a preocuparnos.

Si obtenemos la descripción del servicio que acabamos de crear, veremos la siguiente salida:

Descripción del servicio webservice.
A partir de nuestra especificación podemos ver que la dirección IP interna asignada al servicio es la 10.100.154.243, que el puerto en los nodos es el 30080 y que el puerto target, o puerto en los pods, es el puerto 80 pero, además, vemos que el campo Endpoints está vacio.

Fijándonos en el campo Selector de nuestra definición, hemos establecido que este servicio debe publicar el puerto 80 de aquellos pods que contengan la etiqueta source=webserver. Cómo no hemos creado los pods con dicha etiqueta, ahora mismo este servicio está escuchando en el puerto 30080 de nuestros nodos pero no hay un pod que reciba las peticiones, con lo que no podremos conectarnos.

Para poder conectar este servicio con un pod que ejecute nuestro servidor web, tenemos que modificar la especificación del mismo y añadir una etiqueta al pod que coincida con el selector especificado en el servicio. Ahora nuestro pod quedaría del siguiente modo:

Añadimos la etiqueta a la especificación del pod.
Borramos los pods creados anteriormente y aplicamos la nueva definición de pod, con lo que ahora tenemos lo siguiente:

Creamos el nuevo pod.
Ahora, al obtener de nuevo la descripción del objeto service/webservice vemos una diferencia en el campo Endpoints:

El objeto service con el endpoint del nuevo pod.
Como ya hemos dicho, el KCP comprueba continuamente que el estado del cluster es idéntico al que hayamos establecido por configuración. Esto nos permite, para algunos objetos, hacer cambios de su configuración en caliente. De este modo podemos cambiar la definición de un objeto y volver a aplicarla mientras el objeto está corriendo sin necesidad de eliminar dicho objeto. Esta idea aplica al cambio que acabamos de realizar sobre la definición de los PODs. Podemos cambiar la definición del POD, añadiendo las etiquetas que necesitemos y aplicar la nueva configuración a los PODs que estén ejecutándose, con lo que el resultado sería el mismo, pero evitamos borrar los PODs.

Con lo que ahora, si podremos conectarnos usando un navegador web:

Conexión con nuestro cluster de Kubernetes.
Una de las conclusiones que podemos obtener de este ejemplo es que, una vez creado un servicio, Kubernetes está comprobando de forma continua si hay endpoints disponibles para el mismo. Como hemos visto, una vez que existe un pod con la etiqueta correcta, Kubernetes enlaza el servicio que hemos definido con dicho pod permitiendo el acceso desde el exterior del cluster.

Al igual que con la definición del objeto pod, en este enlace podemos consultar la entrada correspondiente al objeto service del API de  Kubernetes.

En resumen, hemos visto cómo definir los objetos pod y service así como la flexibilidad que tiene el separar ambos objetos. Además hemos comprobado la relación entre un pod y un contenedor y hemos visto cómo Kubernetes aplica el estado que fijamos por configuración, relacionando un servicio con un pod cuando la etiqueta del pod coincide con el campo selector del servicio.

martes, 7 de enero de 2020

Kubernetes - Usando minikube

Antes de continuar explorando Kubernetes, es necesario que veamos de forma rápìda como desplegar minikube. Esta herramienta permite de manera sencilla, simular un cluster de Kubernetes con el que poder realizar pruebas simples y comenzar a usar el comando kubectl.

Con minikube podemos simular un cluster de Kubernetes en un único host, sin tener que desplegar un cluster real, lo cual haremos más adelante.

Para instalar minikube seguiremos las instrucciones dadas en este enlace, teniendo en cuenta los siguientes puntos:
  • Debemos instalar una versión soportada de un motor de contenedores. En mi caso instalaré la versión de Docker 19.03.5 que está soportada en la última versión de minikube. Para ver las versiones soportadas, os recomiendo consultar el changelog de minikube en Github.
  • Es necesario deshabilitar el swap de nuestra máquina para un correcto funcionamiento de minikube.
  • Si el firewall de la máquina está habilitado, hay que asegurar que determinados puertos están abiertos.
  • Son necesarias al menos dos CPUs para la ejecución de minikube.
  • Es recomendable deshabilitar SELinux.
Además de instalar minikube es necesario que instalemos la herramienta que nos permita interactuar con nuestro cluster de Kubernetes y configurarlo, es decir debemos instalar kubectl. Para esto solo es necesario que hagamos lo siguiente:
 
Instalación de kubectl.
Cuando lancemos la inicialización veremos como realizará una serie de comprobaciones y nos mostrará todos los puntos que debamos corregir, indicándo si la configuración de nuestra máquina es o no correcta. Por ejemplo, algunas de las comprobaciones que realiza y como un error puede abortar la inicialización, podemos verlas en la siguiente imagen:

Comprobaciones realizadas por la inicialización minikube.
En caso de encontrar cualquier fallo es necesario corregirlo antes de volver a lanzar de nuevo la inicialización del cluster.

Por tanto, partiendo de una máquina con Docker ya instalado y todas las configuraciones realizadas, pasamos a lanzar la instalación de minikube, para lo cual solo es necesario que hagamos lo siguiente:

Instalación de minikube.
A continuación solo debemos arrancar minikube teniendo en cuenta el driver de virtualización que vamos a utilizar. ¿Que quiere decir esto? básicamente que podemos ejecutar minikube simulando un cluster de Kubernetes dentro de uan máquina virtual, por ejemplo instalándolo en Windows y utilizando VirtualBox o Hyper-V para ejecutar minikube. En mi caso utilizo una máquina virtual sobre la que quiero ejecutar directamente minikube, con lo cual solo necesito inicializar el cluster indicando que el driver de virtualización a utilizar es none:

Inicialización de minikube.

Una vez instalado ya solo tenemos que comprobar el estado de nuestro cluster con el comando minikube y listar los objetos disponibles en el cluster con kubectl:
 
Estado de minikube tras la inicialización.


Objetos existentes en el "cluster" de Kubernetes recién creado.

Por tanto ya tenemos un entorno de pruebas de Kubernetes operativo y, para terminar de comprobar que efectivamente es así y que podemos desplegar cargas correctamente, despleguemos una prueba sencilla:

Creación de un deployment y un service asociado.
Con los comandos anteriores hemos creado un deployment de una aplicación que, al acceder desde un navegador web nos mostrará un mensaje Hello World!. Además lo hemos asociado a un service que publica el puerto 8080 de los pods asociados a dicho deployment con un puerto en el host, en este caso podemos ver que el puerto publicado es el 31973. Por tanto, si accedemos a la IP de esta VM a dicho puerto tenemos lo siguiente:

Conexión al service definido en Kubernetes.
Por tanto, ya tenemos disponible un entorno que podremos usar para realizar todas las pruebas que queramos y poder avanzar con Kubernetes, con lo que en las próximas entradas empezaremos a estudiar los objetos básicos de Kubernetes.
 
 


domingo, 22 de diciembre de 2019

Kubernetes - Conceptos básicos I

Ahora que he sacado algo de tiempo es hora de empezar a estudiar Kubernetes, así que vamos con un poco de rollo teórico empezando con la pregunta básica, ¿que es Kubernetes? De forma rápida y simple podemos decir que es un orquestador de contenedores. Siendo más precisos, es un sistema mediante el cual podremos gestionar, desplegar y escalar los contenedores que forman y proporcionan nuestros servicios.

Por tanto, Kubernetes es una capa adicional para nuestra infraestructura de contenedores que nos proporciona multitud de funcionalidades adicionales, como por ejemplo:
  • Sencillez para realizar despliegues, gracias a la posibilidad de realizar el despliegue de nuevas versiones a un ritmo fijado por nosotros.
  • Control del estado de los contenedores. Kubernetes reiniciará aquellos contenedores que fallen o que no respondan a las comprobaciones que definamos.
  • Orquestación del almacenamiento usado por los contenedores, tanto almacenamiento local como proporcionado por proveedores cloud.
La forma de funcionamiento de Kubernetes se basa en la definición del estado de nuestro sistema, es decir, estableciendo la configuración que debe tener Kubernetes en todo momento. En concreto definimos el estado del sistema como las aplicaciones que queremos ejecutar, las imágenes que ejecutaran los contenedores, el número de réplicas de cada uno de ellos, los recursos de red y disco asignados, etc.

Cómo es lógico, al despelgar Kubernetes lo que hacemos es crear un cluster de varios nodos en el cual distinguimos dos roles diferentes:
  • Nodos worker. Encargados de ejecutar los contenedores que encapsulan nuestras aplicaciones.
  • Nodos master. Encargados del control de los contenedores y nodos worker, así como frontend del cluster de Kubernetes con el que interactuamos para establecer el estado del mismo.
Cada tipo de nodo se distingue por los componentes que ejecutan, que en cada caso son:
  • Nodos master.
    • kube-apiserver, expone el API de Kubernetes permitiéndonos interactuar y controlar el estado del cluster.
    • etcd, es un almacen de datos distribuido de tipo clave-valor que almacena toda la información y configuración del cluster.
    • kube-scheduler, encargado de controlar los contenedores creados y asignarlos a un worker teniendo en cuenta todas las posibles restricciones establecidas.
    • kube-controller-manager, encargado de ejecutar controladores que interactuan directamente con el cluster. Estos controladores incluyen el controlador de nodos, de replicaciones, endpoints y cuentas y tokens de servicios. Aunque cada controlador es un proceso separado, todos están compilados en el mismo binario.
    • cloud-controller-manager, encargado de ejecutar controladores que interactuan con proveedores de servicios cloud.
  • Nodos worker.
    • kubelet, agente que se ejecuta en todos los worker y que controla que cada contenedor se encapsula correctamente.
    • kube-proxy, es un proxy de red que permite la comunicación con los contenedores desde el interior y exterior del cluster.
    • motor de contenedores, el software encargado de la ejecución de contenedores. Kubernetes soporta diferentes motores de contenedores, como Docker.
Adicionlamente existen una serie de características del cluster que están proporcionadas directamente por recursos de Kubernetes, siendo el más importante el servicio DNS interno del cluster. Ya veremos cómo Kubernetes crea ciertos recursos, siendo uno de ellos el DNS interno del cluster.

Cómo ya he indicado, Kubernetes se encarga de que el estado actual de un cluster coincida con el que hemos fijado al configurar las aplicaciones que queremos ejecutar, las imágenes que ejecutaran los contenedores, el número de réplicas de cada uno de ellos, los recursos de red y disco asignados, etc.

Para esto y una vez establecido el estado deseado del cluster, el denominado como Kubernetes Control Plane, a partir de ahora KCP para abreviar, se encargará de realizar todas las tareas necesarias para que el estado actual del cluster coincida con el que hemos establecido.

El KCP está formado por los procesos o componentes que hemos establecido para cada tipo de nodo:
  • Los procesos kube-apìserver, kube-controller-manager y kube-scheduler, que se ejecutan en los nodos master del cluster.
  • Los procesos kubelet y kube-proxy que se ejecutan en todos los nodos worker del cluster.
Ahora bien, ¿cómo interactuamos con el cluster? o más concretamente ¿cómo establecemos la configuración deseada? Para esto lo normal es que utilicemos el comando kubectl, el cual nos permitirá crear objetos del API de Kubernetes fijando el estado deseado del cluster. Una vez que hayamos establecido el estado deseado del cluster, el KCP realizará todas las tareas necesarias para que el estado real del cluster coincida con el deseado. Todas estas tareas de configurción las realizaremos interactuando directamente con los nodos master, a través del kube-apiserver.

Suficiente teoría por hoy, así que en la siguiente entrada comenzaremos con los objetos del API de Kubernetes.

sábado, 9 de noviembre de 2019

ELK - Creación de índices y mapeo de campos.

Hoy, tras las entradas anteriores, continuamos trabajando con Elastic para revisar un punto muy importante sobre los campos y el tipo de dato de los mismos. Veremos que en algunos casos será necesario fijar el tipo de dato de un campo, y como podemos reindexar los datos ya disponibles en caso de ser necesario.

En todas las entradas sobre Logstash hasta la fecha hemos enviado los registros de log, generados por syslog_generator, directamente a Logstash donde hemos hecho el mapeo automático de los campos de origen.

Fijándonos en los campos que enviamos mediante syslog_generator, vemos un punto muy interesante sobre los campos de fecha que hemos mapeado al analizar el índice. Podemos consultar el mapeo de campos directamente desde la sección Mapping del índice en cuestión, que podemos encontrar en Index Management:

Mapeo por defecto de campos.
Como podemos ver, los dos campos de hora y fecha que hemos definido procedentes de nuestros hosts son de tipo texto, no de tipo fecha como sería lo correcto. Además, como recordaremos de un post anterior, al crear el patrón sobre el índice que contiene los documentos de Logstash, vimos que usábamos por defecto el campo @timestamp que contiene la fecha y hora de recepción del documento y su introducción en el índice, no la fecha y hora real de generación del evento. Por tanto, ¿que tenemos que hacer para asegurarnos que un campo se mapea con el tipo de dato correcto? y más importante aún ¿puedo cambiar el tipo de dato de un documento ya existente en un índice?

Revisando el resto de campos del índice podemos ver que todos los campos se han mapeado como una cadena, por ejemplo tempsensor_temperature que debería mapearse como un tipo integer. 

Por tanto, salvo que los campos de nuestros documentos sean siempre de tipo texto, está claro que es necesario crear nuestros índices previamente, analizando la información que queremos procesar con Elastic para establecer los mapeos de los campos correctamente y fijando el tipo de dato de los mismos cuando sea necesario.

En un caso como este, en el cual ya tenemos un índice cuyos documentos necesitamos mantener, vamos a tener que realizar una operación de reindexación, en la cual básicamente copiamos el contenido de un índice en otro índice diferente.

Empecemos creando nuestro nuevo índice con los campos mapeados a un valor correcto, para esto lo más sencillo es que usemos directamente la consola interactiva disponible en el menú Dev Tools:

Consola de desarrollo de Elasticsearch.
Comencemos creando un índice nuevo con las mísmas características del índice existente que necesitamos reindexar. Para esto lo más sencillo es que copiemos y peguemos las características del índice ya existente, las cuales podemos obtener utilizando la operación _search con un comando GET NombreDelIndex desde la consola:

Configuración del índice existente.
Copiamos la salida del comando anterior y la pegamos en la consola, cambiando el tipo de dato de los mapeos de los campos que necesitamos así como el nombre del índice. Es importante que definamos el formato de mapeo de los campos de manera correcta o, como veremos, recibiremos un error al realizar la operación de reindexado. Para empezar establecemos el siguiente formato para nuestros campos de fecha y hora:

Especificación de formato para el campo tempsensor_time.

Especificación de formato para el campo tempsensor_timestamp.

El formato de fecha especificado para el campo tempsensor_timestamp es un formato de fecha personalizado, construido a partir de la información dada por la clase DateTimeFormatter de Java, cuya documentación podéis consultar en el siguiente enlace.

Para el campo tempsensor_timestamsp es tan sencillo como definir el campo en el nuevo índice como tipo integer.

Especificación del campo tempsensor_temperature.

Por último cambiamos el tipo de operación a PUT y, si hemos realizado la configuración correctamente, el resultado será más o menos el siguiente:

Resultado de la creación del nuevo índice.
Ahora nuestro nuevo índice aparece en la sección Index Management siendo el número de documentos disponibles cero:

Nuevo índice ya disponible para su uso.
A continuación reindexaremos los documentos existentes en nuestro índice actual, para lo cual usaremos la operación reindex del API de Elasticsearch. Desde la consola de desarrollo lanzamos la operación de reindexado del siguiente modo:

Operación de reindexado.
En caso de utilizar un formato incorrecto, o en nuestro caso al especificar un formato de fecha incorrecto, recibiríamos el siguiente mensaje:

Error de la operación dereindexado debido a un formato incrrecto.
Para utilizar un formato de fecha correcto, os recomiendo consultar la documentación de Elasticsearch en este enlace que nos explica el tipo de dato date, así como este otro enlace que muestra los tipos de formatos de fecha predefinidos que podemos utilizar. Adicionalmente podremos crear un formato de fecha para el tipo date siguiendo la sintaxis dada por la clase DateTimeFormatter de Java.

Tras realizar la operación de reindexado y al crear el index pattern para nuestro nuevo índice, ya vemos la primera diferencia con el índice anterior. Durante la creación del index pattern para Kibana, ya tenemos disponibles nuestros campos de fecha y hora para ser usados como filtros de tiempo para mostrar y analizar los datos:

Selección del filtro de tiempo durante la creación del index pattern.
 
Al terminar la creación del index pattern ya podemos consultar los datos correspondientes a nuestro nuevo índice, obteniendo el siguiente resultado:

Documentos del índice reindexado.
Como podemos ver, Kibana ha completado los nuevos campos con la fecha, en el caso del campo tempsensor_time y con la hora para el campo tempsensor_timestamp.

Empecemos por corregir los campos que contienen la hora y fecha de cada documento. Para evitar este comportamiento solo necesitamos editar cada uno de estos campos desde el menú Index Patterns, dentro de la sección Management, para eliminar la fecha en el caso del campo tempsensor_time y la hora, en el caso del campo tempsensor_timestamp. Podemos verlo en las siguientes imágenes:

Búsqueda de campos en el index pattern de Kibana.
Al pinchar sobre la opción Edit de cada uno de los campos de tipo date, podremos cambiar como se muestran y, por tanto, eliminar la fecha en el caso del campo tempsensor_time:

Corregimos el formato de represenatción del campo tempsensor_time.
Y eliminar la hora en el caso del campo tempsensor_timestamp:

Corregimos el formato de represenatción del campo tempsensor_timestamp.
Con lo que ahora, al volver a la sección Discover ya vemos los campos de fecha y hora correctamente, además de que el campo tempsensor_temperature ya se reconoce de tipo numérico:

Datos del nuevo índice con los campos ya corregidos.
Como hemos visto, Elastic nos proporciona herramientas para manejar los documentos de índices ya existentes. Esto nos permite corregir errores en los mapeos de los campos y cambiarlos de tipo, cuando sea necesario. Evidentemente esto puede ser bastante problemático si tenemos índices con millones de documentos, con lo que es muy importante que estudiemos detenidamente los datos que vamos a enviar antes de crear el índice para evitar tener que realizar este tipo de operaciones.