Actualizar automáticamente Elastic Stack con Ansible Playbooks

Publicado: 2022-03-11

El análisis de registros para una red compuesta por miles de dispositivos solía ser una tarea compleja, lenta y aburrida antes de que decidiera usar Elastic Stack como la solución de registro centralizada. Resultó ser una decisión muy sabia. No solo tengo un solo lugar para buscar todos mis registros, sino que obtengo resultados casi instantáneos en mis búsquedas, visualizaciones poderosas que son increíblemente útiles para el análisis y la resolución de problemas, y hermosos tableros que me brindan una útil descripción general de la red.

El Elastic Stack lanza constantemente funciones nuevas y sorprendentes, manteniendo un ritmo de desarrollo muy activo, a menudo ofrece dos nuevos lanzamientos cada mes. Me gusta mantener mi entorno siempre actualizado para asegurarme de poder aprovechar las nuevas funciones y mejoras. También para mantenerlo libre de errores y problemas de seguridad. Pero esto me obliga a estar constantemente actualizando el entorno.

Aunque el sitio web de Elastic mantiene una documentación clara y detallada, incluido el proceso de actualización de sus productos, la actualización manual es una tarea compleja, especialmente el clúster de Elasticsearch. Hay muchos pasos involucrados y se debe seguir un orden muy específico. Es por eso que decidí automatizar todo el proceso, hace mucho tiempo, usando Ansible Playbooks.

En este tutorial de Ansible, nos guiará a través de una serie de Playbooks de Ansible que se desarrollaron para actualizar automáticamente mi instalación de Elastic Stack.

¿Qué es el paquete elástico?

El Elastic Stack, anteriormente conocido como ELK stack, está compuesto por Elasticsearch, Logstash y Kibana, de la empresa de código abierto Elastic, que en conjunto brindan una plataforma poderosa para indexar, buscar y analizar sus datos. Se puede utilizar para una amplia gama de aplicaciones. Desde el registro y el análisis de seguridad hasta la gestión del rendimiento de las aplicaciones y la búsqueda de sitios.

  • Elasticsearch es el núcleo de la pila. Es un motor de búsqueda y análisis distribuido capaz de ofrecer resultados de búsqueda casi en tiempo real, incluso frente a un gran volumen de datos almacenados.

  • Logstash es una tubería de procesamiento que obtiene o recibe datos de muchas fuentes diferentes (50 complementos de entrada oficiales mientras escribo), los analiza, filtra y transforma y los envía a una o más de las posibles salidas. En nuestro caso, estamos interesados ​​en el complemento de salida de Elasticsearch.

  • Kibana es su interfaz de usuario y operación. Le permite visualizar, buscar, navegar por sus datos y crear paneles que le brindan información sorprendente al respecto.

¿Qué es Ansible?

Ansible es una plataforma de automatización de TI que se puede utilizar para configurar sistemas, implementar o actualizar software y orquestar tareas de TI complejas. Sus objetivos principales son la simplicidad y la facilidad de uso. Mi característica favorita de Ansible es que no tiene agente, lo que significa que no necesito instalar ni administrar ningún software adicional en los hosts y dispositivos que quiero administrar. Usaremos el poder de la automatización de Ansible para actualizar automáticamente nuestro Elastic Stack.

Descargo de responsabilidad y una palabra de precaución

Los libros de jugadas que compartiré aquí se basan en los pasos descritos en la documentación oficial del producto. Está destinado a ser utilizado solo para actualizaciones de la misma versión principal. Por ejemplo: 5.x5.y 6.x6.y donde x > y . Las actualizaciones entre versiones principales a menudo requieren pasos adicionales y esos libros de jugadas no funcionarán en esos casos.

Independientemente, siempre lea las notas de la versión, especialmente la sección de cambios importantes antes de usar los libros de jugadas para actualizar. Asegúrese de comprender las tareas ejecutadas en los libros de jugadas y consulte siempre las instrucciones de actualización para asegurarse de que no cambie nada importante.

Habiendo dicho eso, he estado usando esos playbooks (o versiones anteriores) desde la versión 2.2 de Elasticsearch sin ningún problema. En ese momento tenía libros de jugadas completamente separados para cada producto, ya que no compartían el mismo número de versión que conocen.

Habiendo dicho eso, no soy responsable de ninguna manera por el uso que usted haga de la información contenida en este artículo.

Nuestro entorno ficticio

El entorno en el que se ejecutarán nuestros libros de jugadas consistirá en 6 servidores CentOS 7:

  • 1 servidor Logstash
  • 1 servidor Kibana.
  • 4 nodos de búsqueda elástica

No importa si su entorno tiene una cantidad diferente de servidores. Simplemente puede reflejarlo en consecuencia en el archivo de inventario y los libros de jugadas deberían ejecutarse sin problemas. Sin embargo, si no está utilizando una distribución basada en RHEL, lo dejaré como ejercicio para que cambie las pocas tareas que son específicas de la distribución (principalmente las cosas del administrador de paquetes)

El ejemplo de Elastic Stack que implementarán nuestros Ansible Playbooks

El inventario

Ansible necesita un inventario para saber en qué hosts debe ejecutar los libros de jugadas. En nuestro escenario imaginario vamos a utilizar el siguiente archivo de inventario:

 [logstash] server01 ansible_host=10.0.0.1 [kibana] server02 ansible_host=10.0.0.2 [elasticsearch] server03 ansible_host=10.0.0.3 server04 ansible_host=10.0.0.4 server05 ansible_host=10.0.0.5 server06 ansible_host=10.0.0.6

En un archivo de inventario de Ansible, cualquier [section] representa un grupo de hosts. Nuestro inventario tiene 3 grupos de hosts: logstash , kibana y elasticsearch . Notarás que solo uso los nombres de grupo en los libros de jugadas. Eso significa que no importa la cantidad de hosts en el inventario, siempre que los grupos sean correctos, se ejecutará el libro de jugadas.

El proceso de actualización

El proceso de actualización constará de los siguientes pasos:

1) Pre-descarga de los paquetes

2) Actualización de Logstash

3) Actualización continua del clúster de Elasticsearch

4) Actualización Kibana

El objetivo principal es minimizar el tiempo de inactividad. La mayoría de las veces el usuario ni siquiera se dará cuenta. A veces, Kibana puede no estar disponible durante unos segundos. Eso es aceptable para mí.

Guía principal de Ansible

El proceso de actualización consiste en un conjunto de diferentes libros de jugadas. Usaré la función import_playbook de Ansible para organizar todos los libros de jugadas en un archivo de libro de jugadas principal al que se puede llamar para encargarse de todo el proceso.

 - name: pre-download import_playbook: pre-download.yml - name: logstash-upgrade import_playbook: logstash-upgrade.yml - name: elasticsearch-rolling-upgrade import_playbook: elasticsearch-rolling-upgrade.yml - name: kibana-upgrade import_playbook: kibana-upgrade.yml

Bastante simple. Es solo una forma de organizar la ejecución de los libros de jugadas en un orden específico.

Ahora, consideremos cómo usaríamos el ejemplo anterior del libro de jugadas de Ansible. Explicaré cómo lo implementamos más adelante, pero este es el comando que ejecutaría para actualizar a la versión 6.5.4:

 $ ansible-playbook -i inventory -e elk_version=6.5.4 main.yml

Pre-descarga de los Paquetes

Ese primer paso es en realidad opcional. La razón por la que uso esto es que considero una buena práctica general para detener un servicio en ejecución antes de actualizarlo. Ahora, si tiene una conexión rápida a Internet, el tiempo que tarda su administrador de paquetes en descargar el paquete puede ser insignificante. Pero ese no es siempre el caso y quiero minimizar la cantidad de tiempo que cualquier servicio está inactivo. Esa es la forma en que mi primer libro de jugadas usará yum para descargar previamente todos los paquetes. De esa manera, cuando lleguen los tiempos de actualización, el paso de descarga ya se ha realizado.

 - hosts: logstash gather_facts: no tasks: - name: Validate logstash Version fail: msg="Invalid ELK Version" when: elk_version is undefined or not elk_version is match("\d+\.\d+\.\d+") - name: Get logstash current version command: rpm -q logstash --qf %{VERSION} args: warn: no changed_when: False register: version_found - name: Pre-download logstash install package yum: name: logstash-{{ elk_version }} download_only: yes when: version_found.stdout is version_compare(elk_version, '<')

La primera línea indica que esta jugada solo se aplicará al grupo logstash . La segunda línea le dice a Ansible que no se moleste en recopilar datos sobre los hosts. Esto acelerará el juego, pero asegúrese de que ninguna de las tareas del juego necesite datos sobre el anfitrión.

La primera tarea en el juego validará la variable elk_version . Esta variable representa la versión del Elastic Stack a la que estamos actualizando. Eso se pasa cuando invoca el comando ansible-playbook. Si la variable no se pasa o no es un formato válido, la jugada se cancelará de inmediato. Esa tarea será en realidad la primera tarea en todas las jugadas. La razón de ello es permitir que las jugadas se ejecuten de forma aislada si se desea o es necesario.

La segunda tarea usará el comando rpm para obtener la versión actual de Logstash y registrarse en la variable version_found . Esa información se utilizará en la siguiente tarea. Las líneas args: , warn: no y changed_when: False están ahí para hacer feliz a ansible-lint, pero no son estrictamente necesarias.

La tarea final ejecutará el comando que realmente descarga previamente el paquete. Pero solo si la versión instalada de Logstash es anterior a la versión de destino. No apuntar la descarga y la versión anterior o la misma si no se utilizará.

Las otras dos jugadas son esencialmente las mismas, excepto que en lugar de Logstash, descargarán previamente Elasticsearch y Kibana:

 - hosts: elasticsearch gather_facts: no tasks: - name: Validate elasticsearch Version fail: msg="Invalid ELK Version" when: elk_version is undefined or not elk_version is match("\d+\.\d+\.\d+") - name: Get elasticsearch current version command: rpm -q elasticsearch --qf %{VERSION} args: warn: no changed_when: False register: version_found - name: Pre-download elasticsearch install package yum: name: elasticsearch-{{ elk_version }} download_only: yes when: version_found.stdout is version_compare(elk_version, '<') - hosts: kibana gather_facts: no tasks: - name: Validate kibana Version fail: msg="Invalid ELK Version" when: elk_version is undefined or not elk_version is match("\d+\.\d+\.\d+") - name: Get kibana current version command: rpm -q kibana --qf %{VERSION} args: warn: no changed_when: False register: version_found - name: Pre-download kibana install package yum: name: kibana-{{ elk_version }} download_only: yes when: version_found.stdout is version_compare(elk_version, '<')

Actualización de Logstash

Logstash debe ser el primer componente en actualizarse. Esto se debe a que se garantiza que Logstash funcionará con una versión anterior de Elasticsearch.

Las primeras tareas del juego son idénticas a la contraparte previa a la descarga:

 - name: Upgrade logstash hosts: logstash gather_facts: no tasks: - name: Validate ELK Version fail: msg="Invalid ELK Version" when: elk_version is undefined or not elk_version is match("\d+\.\d+\.\d+") - name: Get logstash current version command: rpm -q logstash --qf %{VERSION} changed_when: False register: version_found

Las dos tareas finales están contenidas en un bloque:

 - block: - name: Update logstash yum: name: logstash-{{ elk_version }} state: present - name: Restart logstash systemd: name: logstash state: restarted enabled: yes daemon_reload: yes when: version_found.stdout is version_compare(elk_version, '<')

El condicional when garantiza que las tareas en el bloque solo se ejecutarán si la versión de destino es más nueva que la versión actual. La primera tarea dentro del bloque realiza la actualización de Logstash y la segunda tarea reinicia el servicio.

Actualización gradual del clúster de Elasticsearch

Para asegurarnos de que no habrá tiempo de inactividad en el clúster de Elasticsearch, debemos realizar una actualización gradual. Esto significa que actualizaremos un nodo a la vez, solo comenzando la actualización de cualquier nodo después de asegurarnos de que el clúster esté en un estado verde (completamente saludable).

Desde el comienzo de la obra notarás algo diferente:

 - name: Elasticsearch rolling upgrade hosts: elasticsearch gather_facts: no serial: 1

Aquí tenemos la línea serial: 1 . El comportamiento predeterminado de Ansible es ejecutar el juego contra múltiples hosts en paralelo, la cantidad de hosts simultáneos definidos en la configuración. Esta línea asegura que la jugada se ejecutará contra un solo host a la vez.

A continuación, definimos algunas variables que se utilizarán a lo largo de la obra:

 vars: es_disable_allocation: '{"transient":{"cluster.routing.allocation.enable":"none"}}' es_enable_allocation: '{"transient":{"cluster.routing.allocation.enable": "all","cluster.routing.allocation.node_concurrent_recoveries": 5,"indices.recovery.max_bytes_per_sec": "500mb"}}' es_http_port: 9200 es_transport_port: 9300

El significado de cada variable quedará claro a medida que aparezcan en la obra.

Como siempre, la primera tarea es validar la versión de destino:

 tasks: - name: Validate ELK Version fail: msg="Invalid ELK Version" when: elk_version is undefined or not elk_version is match("\d+\.\d+\.\d+")

Muchas de las siguientes tareas consistirán en ejecutar llamadas REST contra el clúster de Elasticsearch. La llamada se puede ejecutar contra cualquiera de los nodos. Simplemente podría ejecutarlo contra el host actual en el juego, pero algunos de los comandos se ejecutarán mientras el servicio Elasticsearch está inactivo para el host actual. Entonces, en las siguientes tareas, nos aseguramos de seleccionar un host diferente para ejecutar las llamadas REST. Para ello utilizaremos el módulo set_fact y la variable groups del inventario de Ansible.

 - name: Set the es_host for the first host set_fact: es_host: "{{ groups.elasticsearch[1] }}" when: "inventory_hostname == groups.elasticsearch[0]" - name: Set the es_host for the remaining hosts set_fact: es_host: "{{ groups.elasticsearch[0] }}" when: "inventory_hostname != groups.elasticsearch[0]"

A continuación, nos aseguramos de que el servicio esté activo en el nodo actual antes de continuar:

 - name: Ensure elasticsearch service is running systemd: name: elasticsearch enabled: yes state: started register: response - name: Wait for elasticsearch node to come back up if it was stopped wait_for: port: "{{ es_transport_port }}" delay: 45 when: response.changed == true

Al igual que en las jugadas anteriores, comprobaremos la versión actual. Excepto por esta vez, usaremos la API REST de Elasticsearch en lugar de ejecutar rpm. También podríamos haber usado el comando rpm, pero quiero mostrar esta alternativa.

 - name: Check current version uri: url: http://localhost:{{ es_http_port }} method: GET register: version_found retries: 10 delay: 10

Las tareas restantes están dentro de un bloque que solo se ejecutará si la versión actual es anterior a la versión de destino:

 - block: - name: Enable shard allocation for the cluster uri: url: http://localhost:{{ es_http_port }}/_cluster/settings method: PUT body_format: json body: "{{ es_enable_allocation }}"

Ahora, si seguiste mi consejo y leíste la documentación, te habrás dado cuenta de que este paso debería ser el opuesto: deshabilitar la asignación de fragmentos. Me gusta poner esta tarea aquí primero en caso de que los fragmentos se hayan deshabilitado antes por alguna razón. Esto es importante porque la siguiente tarea esperará a que el clúster se vuelva verde. Si la asignación de fragmentos está deshabilitada, el clúster permanecerá en amarillo y las tareas se bloquearán hasta que se agote el tiempo de espera.

Entonces, después de asegurarnos de que la asignación de fragmentos esté habilitada, nos aseguramos de que el clúster esté en un estado verde:

 - name: Wait for cluster health to return to green uri: url: http://localhost:{{ es_http_port }}/_cluster/health method: GET register: response until: "response.json.status == 'green'" retries: 500 delay: 15

Después de reiniciar un servicio de nodo, el clúster puede tardar mucho en volver a verde. Esa es la razón de los retries: 500 y delay: 15 . Significa que esperaremos 125 minutos (500 x 15 segundos) para que el clúster vuelva a ponerse verde. Es posible que deba ajustar eso si sus nodos contienen una gran cantidad de datos. Para la mayoría de los casos, es mucho más que suficiente.

Ahora podemos deshabilitar la asignación de fragmentos:

 - name: Disable shard allocation for the cluster uri: url: http://localhost:{{ es_http_port }}/_cluster/settings method: PUT body_format: json body: {{ es_disable_allocation }}

Y antes de cerrar el servicio, ejecutamos el lavado de sincronización opcional, aunque recomendado. No es raro obtener un error 409 para algunos de los índices cuando realizamos un lavado de sincronización. Dado que esto es seguro de ignorar, agregué 409 a la lista de códigos de estado de éxito.

 - name: Perform a synced flush uri: url: http://localhost:{{ es_http_port }}/_flush/synced method: POST status_code: "200, 409"

Ahora, este nodo está listo para ser actualizado:

 - name: Shutdown elasticsearch node systemd: name: elasticsearch state: stopped - name: Update elasticsearch yum: name: elasticsearch-{{ elk_version }} state: present

Con el servicio detenido, esperamos a que se asignen todos los fragmentos antes de volver a iniciar el nodo:

 - name: Wait for all shards to be reallocated uri: url=http://{{ es_host }}:{{ es_http_port }}/_cluster/health method=GET register: response until: "response.json.relocating_shards == 0" retries: 20 delay: 15

Después de reasignar los fragmentos, reiniciamos el servicio Elasticsearch y esperamos a que esté completamente listo:

 - name: Start elasticsearch systemd: name: elasticsearch state: restarted enabled: yes daemon_reload: yes - name: Wait for elasticsearch node to come back up wait_for: port: "{{ es_transport_port }}" delay: 35 - name: Wait for elasticsearch http to come back up wait_for: port: "{{ es_http_port }}" delay: 5

Ahora nos aseguramos de que el clúster sea amarillo o verde antes de volver a habilitar la asignación de fragmentos:

 - name: Wait for cluster health to return to yellow or green uri: url: http://localhost:{{ es_http_port }}/_cluster/health method: GET register: response until: "response.json.status == 'yellow' or response.json.status == 'green'" retries: 500 delay: 15 - name: Enable shard allocation for the cluster uri: url: http://localhost:{{ es_http_port }}/_cluster/settings method: PUT body_format: json body: "{{ es_enable_allocation }}" register: response until: "response.json.acknowledged == true" retries: 10 delay: 15

Y esperamos a que el nodo se recupere por completo antes de procesar el siguiente:

 - name: Wait for the node to recover uri: url: http://localhost:{{ es_http_port }}/_cat/health method: GET return_content: yes register: response until: "'green' in response.content" retries: 500 delay: 15

Por supuesto, como dije antes, este bloque solo debe ejecutarse si realmente estamos actualizando la versión:

 when: version_found.json.version.number is version_compare(elk_version, '<')

Actualización de Kibana

El último componente que se actualizará es Kibana.

Como era de esperar, las primeras tareas no son diferentes de la actualización de Logstash o las reproducciones previas a la descarga. Excepto por la definición de una variable:

 - name: Upgrade kibana hosts: kibana gather_facts: no vars: set_default_index: '{"changes":{"defaultIndex":"syslog"}}' tasks: - name: Validate ELK Version fail: msg="Invalid ELK Version" when: elk_version is undefined or not elk_version is match("\d+\.\d+\.\d+") - name: Get kibana current version command: rpm -q kibana --qf %{VERSION} args: warn: no changed_when: False register: version_found

Explicaré la variable set_default_index cuando lleguemos a la tarea que la usa.

El resto de las tareas estarán dentro de un bloque que solo se ejecutará si la versión instalada de Kibana es anterior a la versión de destino. Las dos primeras tareas actualizarán y reiniciarán Kibana:

 - name: Update kibana yum: name: kibana-{{ elk_version }} state: present - name: Restart kibana systemd: name: kibana state: restarted enabled: yes daemon_reload: yes

Y para Kibana eso debería haber sido suficiente. Desafortunadamente, por alguna razón, después de la actualización, Kibana pierde su referencia a su patrón de índice predeterminado. Esto hace que solicite al primer usuario que acceda después de la actualización que defina el patrón de índice predeterminado, lo que puede causar confusión. Para evitarlo, asegúrese de incluir una tarea para restablecer el patrón de índice predeterminado. En el ejemplo a continuación, es syslog , pero debe cambiarlo a lo que sea que use. Sin embargo, antes de configurar el índice, debemos asegurarnos de que Kibana esté activo y listo para atender las solicitudes:

 - name: Wait for kibana to start listening wait_for: port: 5601 delay: 5 - name: Wait for kibana to be ready uri: url: http://localhost:5601/api/kibana/settings method: GET register: response until: "'kbn_name' in response and response.status == 200" retries: 30 delay: 5 - name: Set Default Index uri: url: http://localhost:5601/api/kibana/settings method: POST body_format: json body: "{{ set_default_index }}" headers: "kbn-version": "{{ elk_version }}"

Conclusión

El Elastic Stack es una herramienta valiosa y definitivamente te recomiendo que le eches un vistazo si aún no lo usas. Es genial tal como es y está mejorando constantemente, tanto que puede ser difícil mantenerse al día con las actualizaciones constantes. Espero que estos Playbooks de Ansible sean tan útiles para usted como lo son para mí.

Los puse a disposición en GitHub en https://github.com/orgito/elk-upgrade. Le recomiendo que lo pruebe en un entorno que no sea de producción.

Si es un desarrollador de Ruby on Rails que busca incorporar Elasticsearch en su aplicación, consulte Elasticsearch para Ruby on Rails: un tutorial para Chewy Gem del ingeniero de software Core Toptal Arkadiy Zabazhanov.