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.


sábado, 19 de octubre de 2019

Cluster ONTAP - Replicación de datos con SnapMirror - Parte II

Hoy continuamos estudiando las funcionalidades que nos ofrece SnapMirror y cómo configurarlo para proteger los datos de nuestros sistemas ONTAP.

En el anterior post establecimos una relación de protección entre dos clusters, para lo cual realizamos la configuración de la relación de peering entre ambos y la transferencia inicial o baseline del volumen que queremos proteger.

Recordemos que el funcionamiento de SnapMirror consiste en que el sistema destino, en función del schedule que establezcamos, creará un snapshot del volumen en el sistema origen y lo transferirá junto con otros snapshots que pudiesen existir, lo cual estará en función de la política de replicación que establezcamos para la relación de SnapMirror.

Por tanto, una de las configuraciones es la política de replicación la cual, de forma simple, establece que snapshots queremos transferir al sistema destino. Las políticas de replicación predefinidas en ONTAP son las siguientes:
  • MirrorAllSnapshots, esta política establece que en cada replicación deben transferirse todos aquellos snapshots que se hayan realizado desde la última replicación, así como el snapshot realizado por el propio SnapMirror.
  • MirrorLatest, esta política establece que solo se transferirá el snapshot creado por SnapMirror en cada replicación.
Si listamos las políticas predefinidas para el tipo async-mirror correspondiente a SnapMirror, veremos lo siguiente:

Políticas predefinidas para SnapMirror.
Como podemos ver hay tres políticas diferentes, siendo la política DPDefault la política correspondiente al modo de replicación Data Protection que, como recordaremos, era el modo por defecto empleado al crear una relación SnapMirror hasta ONTAP 9.3. Si comparamos esta polítca con la política MirrorAllSnapshots podemos comprobar que son idénticas, ya que esta es la nueva política por defecto empleada desde ONTAP 9.3.

Como podemos ver en la salida del comando anterior, las políticas contienen una serie de reglas con lo que vamos a analizarlas más detenidamente:

Política MirrorAllSnapshots.
Política MirrorLatest.
Como podemos ver, la principal diferencia entre ambas políticas es que MirrorAllSnapshots contiene una regla que indica que hay que transferir todos los snapshots del origen, indicada por la regla número 2 y la etiqueta all_source_snapshots. La pregunta es ¿que son estas reglas y cual es su relación con la transferencia de snapshots?

Para esto primero empecemos por analizar un snapshot en nuestro sistema origen:

Características de un snapshot.
De todas las características que podemos ver en la salida, vemos que hay un campo Label for SnapMirror Operations, cuyo valor es standard. Debemos recordar, que esta etiqueta de snapshots la fijamos nosotros en el momento de crear las políticas de snapshots para nuestros volúmenes.

Si buscamos este mismo snapshot en el sistema destino veremos que tiene las mismas características y que, salvo los valores que cambian debido a que se ha transferido al cluster destino, veremos que el campo de etiqueta contiene el mismo valor identificado como standard.

Adicionalmente, si nos fijamos en los snapshots creados por las operaciones de snapmirror, identificados con el nombre snapmirror tanto en origen como en destino, veremos que también tienen un campo label, pero en este caso está vacia.

Lo que establecen las reglas de las políticas de SnapMirror es el nombre de la etiqueta de los snapshots que se transferiran y, en el caso de las políticas predefinidas las reglas establecen:
  • sm_created - Esta etiqueta fuerza la transferencia del snapshot creado por SnapMirror en cada transferencia programada. Este snapshot siempre estará identificado con una etiqueta SnapMirror vacía. Como indica la documentación, esta regla no puede modificarse ni puede eliminarse de la política.
  • all_source_snapshots - Esta etiqueta indica que deben transferirse todos los snapshots del volumen origen, realizados desde la útlima replicación.

Veamos un ejemplo modificando la etiqueta de un snapshot del volumen origen. Para esto, primero vamos a crear un snapshot del volumen origen al cual vamos a fijar una etiqueta de snapmirror diferente, lo cual podemos hacer con un comando como el siguiente:

Creación de un snapshot con una etiqueta de pruebas.
Ahora podemos ver los snapshots del volumen y veremos que la etiqueta es difernte del resto de snpashots:

Lista de snapshots del volumen origen.
Con esta nueva etiqueta y la programación que establecimos para la relación de SnapMirror, vemos que la regla all_source_snapshots transfiere todos los snapshots existentes en el volumen origen como ya hemos visto independientemente de su etiqueta, y por tanto ahora tenemos nuestro snapshot snap_test en el volumen destino:

Snapshots del volumen destino incluyendo el snapshot de test.
Teniendo en cuenta que SnapMirror es una solución de disaster recovery y que, como es lógico, deberíamos tener nuestros datos de origen totalmente replicados en destino, el comportamiento de la política MirrorAllSnapshots es el esperado en ese caso.

Al comparar esta política con la política MirrorLatest, vemos que la diferencia fundamental es que únicamente contiene la regla con la etiqueta sm_created, es decir, todas aquellas relaciones de protección que utilicen esta política solo transferiran el snapshot creado por SnapMirror en cada actualización.

Y nos podemos preguntar ¿y si quiero crear mi propia política de SnapMirror y especificar reglas diferentes? Pues en ese caso, como podemos ver a continuación, veremos que no es posible cuando usamos una política de tipo async-mirror.

Emepcemos creando una nueva política de Snapmirror en nuestro cluster destino, en la cual vamos a especificar que es de tipo async-mirror por tratarse de una relación SnapMirror y que habrá una regla para los snapshots con una etiqueta DEVELOP, forzando así que no se transfieran todos los snapshots que puedan existir en el volumen origen, sino solo aquellos que contengan dicha etiqueta. Esta política podemos crearla con el siguiente comando:
Creación de una política de SnapMirror.
Como ya hemos visto, esta política recien creada tiene por defecto la regla sm_created, para asegurar que se transferirá en todas las actualziaciones de la relación de protección, el snapshot creado por SnapMirror:

Nuestra política recien creada. La regla sm_created la incluye ONTAP.
Si ahora intentamos añadir la regla indicando la etiqueta de los snapshots de origen que queremos que se transfieran, que en este caso es DEVELOP recibiremos un bonito mensaje de error como el siguiente:

Error al intentar modificar una política de tipo async-mirror.
De nuevo pensemos que SnapMirror es una solución de disaster recovery, está diseñado para replicar todos los datos de nuestros volúmenes de origen y establecer reglas para determinadas etiquetas implicaría no replicar toda la información. Para poder establecer relaciones de protección, en las que controlar los snapshots de origen que deben transferirse a destino, tendremos que establecer relaciones de SnapVault lo cual veremos en futuras entradas.

Por tanto, y en resumen, las políticas de SnapMirror contienen reglas que definen su comportamiento. En el caso de las relaciones SnapMirror de tipo async-mirror, o lo que es lo mismo relaciones de protección para disaster recovery, estas reglas se asegurarán de transferir siempre la información actual del volumen origen y, si así lo deseamos, de todos los snapshots que se hayan generado desde la última transferencia de datos.

sábado, 31 de agosto de 2019

ANSIBLE - Ejecución condicional de tareas.

En el último post sobre Ansible vimos como usar bucles para repetir una tarea varias veces en el mismo host.

En ocasiones nos encontraremos con tareas que, o bien no queremos que se ejecuten en un host o bien deben ejecutarse de forma diferente en función de alguna característica del host. Por tanto, en este post, veremos cómo controlar la ejecución de tareas mediante el uso de condicionales.

En general, podemos imaginar muchas situaciones en las que puede ser necesario usar condicionales, como por ejemplo configurar una regla de firewall si un determinado servicio está corriendo, lanzar un comando si un interfaz de red tiene una dirección IP de un rango determinado o instalar un paquete de software en un conjunto de hosts basándonos en la versión del sistema operativo.

Como siempre la forma más sencilla de entenderlo es viendo un ejemplo, así que empecemos con uno simple como crear un fichero en función del sistema operativo del host. Lo importante de este ejemplo es que es necesario obtener los facts de nuestros hosts para poder realizar las comparaciones de manera correcta. Veamos con un ejemplo a lo que me refiero:

Uso de una condición simple.
Al ejecutar este playbook recibimos el siguuiente mensaje de error:

Error en la ejecución de la tarea.
Como vemos en el mensaje de error, ansible indica que no hay un atributo ansible_os_family con el que realizar la comparación y, debido a esto, se produce el fallo de ejecución. Esto es debido a que he especificado que no quiero que se recojan los facts de los hosts sobre los que se ejecuta la tarea, con la línea gather_facts: no, y por tanto ansible no dispondrá de la información contenida en dichas variables. Si cambiamos el playbook para que se recojan los facts de todos los hosts y lo volvemos a ejecutar, ahora el resultado será correcto:

Obteniendo los facts de los hosts la condición se evalúa correctamente.
Por tanto, la primera conclusión que sacamos es que, para usar condiciones basadas en características de los hosts será necesario obtener los facts de los mismos durante la ejecución del playbook o bien tenerlos cacheados, si queremos evitar tener que obtenerlos cada vez que ejecutemos nuestros playbooks.

Un punto interesante a tener en cuenta es que podemos hacer referencia a estas variables de host de dos formas, usando hostvars o bien usando ansible_facts:
  • Mediante el uso de hostvars podemos hacer referencia a una variable de cualquier host, no solamente del host sobre el que se está ejecutando la tarea. Para esto es necesario que, o bien ya se hayan recogido los facts del host referenciado o realicemos caching de facts.
  • Mediante ansible_facts solo haremos referencia a las variables del host sobre el cual se está ejecutando la tarea en ese momento.
En resumen, cualquiera de las dos opciones siguientes son equivalentes para nuestro ejemplo:

Opciones para usar variables de host en condicionales.
Por comodidad y si no vamos a hacer referencia a variables de otros hosts, lo mejor es usar la segunda forma en la que solo usaremos las variables del host en el que estamos ejecutando la tarea pero, en ambos casos, necesitaremos especificar la opción gather_facts a yes.

Este ejemplo tan sencillo nos ha permitido ver cómo podemos usar una condición muy simple, si ahora nos complicamos un poco más veremos que podemos usar operadores lógicos para hacer condiciones más complejas. Siguiendo con el ejemplo anterior, vamos a crear el fichero solo en aquellos hosts CentOS cuya versión sea superior a la 6, con lo cual la condición pasará a ser:

Uso de operadores lógicos en condiciones.
Y al lanzar el playbook, de nuevo creará el fichero correctamente en los hosts donde se cumpla dicha condición.

¿Cúal es la salida si el host no cumple la condición?¿que salida genera Ansible en ese caso? veremos que la tarea para el host se marca como skipping, indicando así que no se ha ejecutado:

Tarea no ejecutada en un host.
En el caso anterior, he fijado en la condición que la versión del sistema operativo debe ser igual a 6 y, al no cumplirse, Ansible no la ejecuta y la marca como skipping.

Para terminar, también podemos crear condiciones basadas en los resultados de la ejecución de otras tareas de nuestros playbooks. Por ejemplo, si necesitamos crear un directorio cuando el número de ficheros contenidos en otro directorio supere un determinado número, podemos hacer algo como lo siguiente:
Condición basada en una variable registrada en otra tarea.


En este caso registramos la salida del comando en la variable num_of_files la cual, al ser un diccionario, contiene en la clave stdout el resultado del comando ejecutado. Realizando la comparación que necesitamos con el valor de esa clave, podemos controlar cuando se ejecuta la tarea de creación del directorio. Así, cuando en el directorio hay más de 8 archivos, la ejecución será:

Resultado de la ejecucióon del playbook anterior.
En resumen, podemos controlar la ejecución de tareas mediante el uso de condiciones, ya sean con variables de los hosts o bien con aquellas que generemos durante la ejecución de tareas. Estas condiciones pueden agruparse mediante operadores lógicos, permitiendo la creación de condiciones más complejas que nos permitirán controlar nuestros playbooks con detalle.

sábado, 6 de julio de 2019

ELK - Introducción a Logstash IV

Tras las entradas anteriores, vamos a crear un pipeline en el que procesaremos dos entradas diferentes procedentes de dos hosts distintos, generadas ambas con syslog_generator, y que enviaremos a Elasticsearch.

La definición de pipelines en Logstash se establece en el fichero pipelines.yml, el cual por defecto tiene la siguiente configuración:

Fichero de configuración pipelines.yml.
Como podemos ver, por defecto hay definido un pipeline denominado main y cuya configuración estará en la ruta indicada en el fichero dado por la opción path.config.

En este fichero podremos indicar opciones de configuración para cada pipeline, así como la ruta en la que se encontrarán los ficheros de configuración de cada uno de ellos. Hay múltiples opciones de configuración disponibles que podremos establecer para cada pipeline de forma individual, siendo algunas de las más interesantes:
  • pipeline.workers. La configuración por defecto de Logstash está optimizada para un único pipeline, con lo que habrá competición por los recursos del sistema si configuramos múltiples pipelines. Cada una de ellas usará un worker por cada un de los cores de CPU disponibles, siendo por tanto recomendable reducir el número de workers de cada pipeline y ajustar el número en función del rendimiento del sistema y CPUs disponibles.
  • queue.type. Logstash utiliza colas basadas en memoria para almacenar los eventos entre las diferentes fases de un pipeline. Como es lógico, en caso de fallo, los datos que se encuentren en memoria se perderán. Para evitar esto, se puede configurar el tipo de cola como persisted para almacenar los eventos en disco, aunque es importante resaltar que determinados plugins de entrada no pueden ser protegidos contra la pérdida de datos, independientemente del tipo de cola usado.
Para este ejemplo vamos a crear dos pipelines diferentes, uno procesará datos de tipo FIXED y otro datos de tipo TEMP, ambos generados por syslog_generator. La estructura de cada uno de estos eventos será:
Tipo FIXED.
Tipo TEMP.
Con todo esto, pasamos a establecer la siguiente configuación de pipelines en Logstash:

Configuración de múltiples pipelines.
Siendo la configuración de cada pipeline la siguiente:

Configuración de pipeline para eventos de tipo TEMP.
Configuración de pipeline para eventos de tipo FIXED.
Como vemos en los ficheros de configuración anteriores, la diferencia principal con lo que habíamos hecho hasta ahora es la sección output de ambos pipelines. En ambos establecemos que el plugin de salida es elasticsearch, que nos permitirá enviar la salida de cada piepline a elasticsearch. Hay varias opciones de configuración disponibles para este plugin, pero de momento solo usaremos la opción que nos permite especificar el host de destino.

También conviene destacar que cada pipeline levanta un listener de entrada en el host Logstah, con lo que es necesario cambiar los puertos en el plugin de entrada si necesitamos usarlo para diferentes orígenes.

Con esta configuración establecida, cuando arrancamos syslog_generator en los nodos fuente, estos comenzarán a enviar datos a Logstash, cada pipeline aplicará el filtro correspondiente y enviará los datos procesados a Elasticsearch.

Al estar usando la configuración por defecto, veremos que se crea un índice identificado por la cadena logstash-YYYY.MM.DD, lo cual podemos comprobar desde Kibana, en la sección Index Management:

Lista de índices disponibles en Elasticsearch.

Para poder trabajar con los documentos de estos índices, solo tenemos que establecer un patrón desde la sección Index Patterns de Kibana, lo que nos permitirá comenzar a trabajar con los datos disponibles. En este caso, la forma más sencilla de crear el patrón sería:

Creación de un index pattern para los datos procesados en Logstash.
Como vemos, al establecer un patrón que coincide con algún índice existente, estos se muestran para que podamos elegir el índice deseado, o bien aplicarlo a todos aquellos que cumplan con el patrón. En el siguiente paso de configuración, Kibana nos preguntará que campo queremos emplear como registro de tiempo para cada evento y seleccionaremos el que nos ofrece por defecto, identificado como @timestamp.
  
Tras la configuración, podremos comprobar cuales son los campos disponibles en la siguiente pantalla:

Lista de todos los campos disponibles en el índice logstash.
Con esta configuración realizada, desde la sección Discover podremos empezar a trabajar con los datos recibidos desde los hosts. Por ejemplo, podemos filtrar la información disponible de todos los documentos y seleccionar los campos tempsensor_temperature y tempsensor_time de entre todos los campos disponibles en los documentos del índice. Para eso solo es necesario que seleccionemos que campos queremos mostrar dentro del apartado Selected fields:

Búsqueda de campos en Kibana.
De este modo, Kibana nos mostrará solamente los campos que hemos seleccionado de todos los documentos del índice en el período de tiempo específicado.

También desde aquí podemos realizar una búsqueda usando el lenguaje de queries de Kibana, para esto solo tenemos que especificar el campo o campos que queremos buscar. Por ejemplo, podemos especificar que busque aquellos documentos en los que existan los campos tempsensor_temperature y tempsensor_time, siendo el resultado el siguiente:

Búsqueda de documentos usando una query.
Al hacer esta búsqueda, ahora solo tendremos aquellos documentos que cumplan la condición de la query que hemos especificado. Del mismo modo, si queremos que nos muestre aquellos documentos donde el campo tempsensor_temperature se encuentre entre dos valores, podemos hacer una query como la siguiente:

Búsqueda de documentos usando una query simple.
Como podemos ver, Kibana solo muestra los documentos que cumplen con la condición establecida en nuestra query.

En resumen, hemos podido ver como configurar Logstash para enviar datos a Elasticsearch y crear un índice en Kibana para poder realizar búsquedas simples en nuestros documentos de prueba.

En las próximas entradas veremos como podemos realizar gráficas, la importancia del mapeo de los campos de nuestros documentos a la hora de crear los índices y como mejorar la configuración de los pipelines.