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.

Ansible - Repetición de tareas mediante bucles.

Hoy vamos a ver, de manera muy simplificada, como podemos usar bucles en playbooks de Ansible para poder repetir la misma tarea varias veces en cada host.

Para generar bucles, Ansible nos proporciona dos comandos diferentes:
  • El comando loop, con lo que podremos crear bucles simples.
  • Los comandos with_*, que se basan en el uso de plugins de búsqueda, con los cuales podremos acceder a datos de fuentes externas. 
Como esto suena un poco raro, partamos de una situación sencilla como la creación de varios ficheros.

Si usamos loop, podemos usar un bucle tan simple como el siguiente:

Bucle simple usando loop.
En este caso está muy claro que podemos crear bucles, de manera realmente sencilla, con solo definir la lista de elementos dentro del bloque loop. Es importante tener en cuenta que no es necesario que usemos los items, como hemos hecho en este caso, la idea es repetir la misma tarea en un host varias veces.

Si nuestros playbooks son grandes y tenemos que usar este tipo de bucles muchas veces, el código puede quedar un poco feo con lo que, una solución que podemos empelar es definir variables que luego usaremos que contengan los elementos de nuestra iteración.  Por ejemplo, podemos definir un fichero de variables para nuestros hosts donde tengamos algo como lo siguiente:

Definimos una variable para las itereaciones.
Y luego podemos usar la variable definida en nuestro playbook del siguiente modo:

Uso de una variable en un bucle.
De este modo podremos iterar sobre listas de números, cadenas de texto, etc... de manera muy simple y, como vemos, incluso usando nuestras propias variables.

Si utilizamos los comandos with_* tendremos más posibilidades al poder usar diccionarios, lo cual nos dará mucha potencia a la hora de realizar tareas. Por ejemplo, siguiendo con el ejemplo anterior, imaginemos que cada uno de los ficheros que necesitamos crear debe estar en una ruta diferente y debe tener unos permisos distintos. Para este caso, podríamos definir una tarea para cada uno de los ficheros que necesitamos crear con cada una de sus características, o bien podemos usar el siguiente bucle:

Definición de un bucle usando diccionarios.
De este modo, cada item que estamos usando en el bucle es un diccionario que tiene dos pares clave-valor cada una de las cuales podemos usar en un parámetro del módulo file. Al ejecutar este playbook el resultado es justo el esperado:

Resultado de la ejecución del playbook con un bucle.
Como es lógico, podemos expandir la lista de items y añadir todas las claves que necesitemos en cada diccionario, así podríamos crear los subdirectorios y luego los ficheros correspondientes con una sola tarea. Para ello, solo tendríamos que definir nuestra lista del siguiente modo:

Expandimos los diccionarios de cada item y creamos los directorios.
Solo hay que tener en cuenta que, en cada iteración del bucle, ansible usa los valores de cada clave y realiza la tarea especificada. En cada caso, los valores de cada elemento son los que nos permiten especificar si se trata de un directorio o de un fichero, así como los permisos del mismo. Esta lista de items, al igual que el caso anterior, podemos definirla en una variable en el fichero group_vars corrspondiente.

Hasta ahora, las listas de items que hemos usado, las hemos creado de manera manual pero, en general puede que se trate de la salida de una tarea que hayamos registrado en una variable o de algún fact de configuración que hemos recogido de nuestro host.

Como ejemplo sencillo de esto último, podemos listar las direcciones IP de las tarjetas de red de un host con un playbook como el siguiente:

Listamos las direcciones IP de todas las tarjetas de un host.
En este playbook construimos una lista para nuestro bucle con el nombre de todas las tarjetas de red del host. Esta lista está contenida en la variable interna de ansible llamada ansible_interfaces, la cual se obtiene con el comando gather_subset que solo contiene la información de red del host.

En este caso, la variable ansible_interfaces es solo una lista con los nombres de los interfaces con lo que, para poder obtener la dirección IP de cada uno de ellos, es necesario que nos construyamos el nombre de la clave del diccionario que contiene toda la información de cada una de las tarjetas de red del host. Esto lo hacemos en la línea:

"{{ hostvars[inventory_hostname]['ansible_'+ item]['ipv4']['address'] }}"

en la cual estamos diciendo, de las variables del host en el cual se está ejecutando la tarea, quiero obtener el valor de la clave ipv4.address de cada interfaz de red, identificados como ansible_"Nombre del interfaz". El nombre del interfaz está contenido en la lista ansible_interfaces, que dependerá del host en el que se ejecute la tarea.

Hay que reconocer que es bastante raro de leer, al menos a mí me lo parece, pero tenemos que pensar que lo que nos devuelve el comando gather_subset es un diccionario con un conjunto de parejas clave-valor para cada interfaz de red. Teniendo esto en cuenta, lo que necesitamos es acceder a la clave que queremos, en este caso ipv4.address, de cada uno de los interfaces de red del host, que están identificados como ansible_"Nombre del interfaz".
Por tanto, hemos visto como podemos hacer bucles para que una tarea se repita varias veces en un host, además de como usar diccionarios para poder especificar las opciones de un módulo en un bucle. También hemos usado las variables e información recogida por Ansible de un host para hacer un bucle simple recorriendo las interface de red de un host.

En el siguiente post sobre Ansible veremos como podemos usar condicionales y lo complicaremos un poco usando bucles.