Atualizar automaticamente o Elastic Stack com Playbooks Ansible
Publicados: 2022-03-11A análise de log para uma rede composta por milhares de dispositivos costumava ser uma tarefa complexa, demorada e chata antes de eu decidir usar o Elastic Stack como a solução de log centralizada. Provou ser uma decisão muito sábia. Não apenas tenho um único local para pesquisar todos os meus logs, mas também obtenho resultados quase instantâneos em minhas pesquisas, visualizações poderosas que são incrivelmente úteis para análise e solução de problemas e belos painéis que me fornecem uma visão geral útil da rede.
O Elastic Stack está constantemente lançando novos e incríveis recursos, mantendo um ritmo de desenvolvimento muito ativo, muitas vezes entregando dois novos lançamentos todos os meses. Gosto de manter meu ambiente sempre atualizado para ter certeza de que posso aproveitar os novos recursos e melhorias. Também para mantê-lo livre de bugs e problemas de segurança. Mas isso exige que eu esteja constantemente atualizando o ambiente.
Embora o site da Elastic mantenha uma documentação clara e detalhada, inclusive sobre o processo de atualização de seus produtos, a atualização manual é uma tarefa complexa, principalmente do cluster Elasticsearch. Há muitas etapas envolvidas e uma ordem muito específica precisa ser seguida. É por isso que decidi automatizar todo o processo, há muito tempo, usando o Ansible Playbooks.
Neste tutorial do Ansible, mostrarei uma série de Playbooks do Ansible que foram desenvolvidos para atualizar automaticamente minha instalação do Elastic Stack.
O que é a pilha elástica
O Elastic Stack, anteriormente conhecido como pilha ELK, é composto por Elasticsearch, Logstash e Kibana, da empresa de código aberto Elastic, que juntos fornecem uma plataforma poderosa para indexação, pesquisa e análise de seus dados. Ele pode ser usado para uma ampla gama de aplicações. Desde registro e análise de segurança até gerenciamento de desempenho de aplicativos e pesquisa de sites.
O Elasticsearch é o núcleo da pilha. É um mecanismo de pesquisa e análise distribuído capaz de fornecer resultados de pesquisa quase em tempo real, mesmo em um grande volume de dados armazenados.
O Logstash é um pipeline de processamento que obtém ou recebe dados de muitas fontes diferentes (50 plugins de entrada oficiais enquanto escrevo), analisa, filtra e transforma e envia para uma ou mais saídas possíveis. No nosso caso, estamos interessados no plug-in de saída do Elasticsearch.
O Kibana é seu frontend de usuário e operação. Ele permite que você visualize, pesquise, navegue em seus dados e crie painéis que fornecem informações incríveis sobre eles.
O que é Ansible
Ansible é uma plataforma de automação de TI que pode ser usada para configurar sistemas, implantar ou atualizar software e orquestrar tarefas complexas de TI. Seus principais objetivos são a simplicidade e a facilidade de uso. Meu recurso favorito do Ansible é que ele é sem agente, o que significa que não preciso instalar e gerenciar nenhum software extra nos hosts e dispositivos que desejo gerenciar. Usaremos o poder da automação do Ansible para atualizar automaticamente nosso Elastic Stack.
Isenção de responsabilidade e uma palavra de cautela
Os playbooks que compartilharei aqui são baseados nas etapas descritas na documentação oficial do produto. Destina-se a ser usado apenas para atualizações da mesma versão principal. Por exemplo: 5.x
→ 5.y
ou 6.x
→ 6.y
onde x
> y
. As atualizações entre as versões principais geralmente exigem etapas extras e esses manuais não funcionarão para esses casos.
Independentemente disso, sempre leia as notas de lançamento, especialmente a seção de alterações importantes antes de usar os manuais para atualizar. Certifique-se de entender as tarefas executadas nos manuais e sempre verifique as instruções de atualização para garantir que nada importante seja alterado.
Dito isso, tenho usado esses playbooks (ou versões anteriores) desde a versão 2.2 do Elasticsearch sem problemas. Na época, eu tinha playbooks completamente separados para cada produto, pois eles não compartilhavam o mesmo número de versão que eles conhecem.
Dito isto, não sou responsável de forma alguma pelo uso das informações contidas neste artigo.
Nosso ambiente fictício
O ambiente no qual nossos playbooks serão executados consistirá em 6 servidores CentOS 7:
- 1 x servidor Logstash
- 1 x servidor Kibana
- 4 x nós Elasticsearch
Não importa se o seu ambiente tem um número diferente de servidores. Você pode simplesmente refleti-lo adequadamente no arquivo de inventário e os manuais devem ser executados sem problemas. Se, no entanto, você não estiver usando uma distribuição baseada em RHEL, deixarei como exercício para você alterar as poucas tarefas que são específicas da distribuição (principalmente as coisas do gerenciador de pacotes)
O inventário
O Ansible precisa de um inventário para saber em quais hosts ele deve executar os manuais. Quatro nosso cenário imaginário vamos usar o seguinte arquivo de inventário:
[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
Em um arquivo de inventário do Ansible, qualquer [section]
representa um grupo de hosts. Nosso inventário possui 3 grupos de hosts: logstash
, kibana
e elasticsearch
. Você notará que eu só uso os nomes dos grupos nos manuais. Isso significa que não importa o número de hosts no inventário, desde que os grupos estejam corretos, o playbook será executado.
O processo de atualização
O processo de atualização consistirá nas seguintes etapas:
1) Faça o pré-download dos pacotes
2) Atualização do Logstash
3) Rolling Upgrade do cluster Elasticsearch
4) Atualização do Kibana
O principal objetivo é minimizar o tempo de inatividade. Na maioria das vezes o usuário nem vai perceber. Às vezes, o Kibana pode ficar indisponível por alguns segundos. Isso é aceitável para mim.
Manual principal do Ansible
O processo de atualização consiste em um conjunto de diferentes manuais. Estarei usando o recurso import_playbook do Ansible para organizar todos os playbooks em um arquivo de playbook principal que pode ser chamado para cuidar de todo o processo.
- 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
Relativamente simples. É apenas uma maneira de organizar a execução dos playbooks em uma ordem específica.
Agora, vamos considerar como usaríamos o exemplo do playbook do Ansible acima. Explicarei como implementamos mais tarde, mas este é o comando que eu executaria para atualizar para a versão 6.5.4:
$ ansible-playbook -i inventory -e elk_version=6.5.4 main.yml
Pré-Baixe os Pacotes
Esse primeiro passo é realmente opcional. O motivo pelo qual uso isso é que considero uma prática geralmente boa interromper um serviço em execução antes de atualizá-lo. Agora, se você tiver uma conexão rápida com a Internet, o tempo para o gerenciador de pacotes baixar o pacote pode ser insignificante. Mas nem sempre é esse o caso e quero minimizar a quantidade de tempo em que qualquer serviço está inativo. É assim que meu primeiro playbook usará o yum para pré-baixar todos os pacotes. Dessa forma, quando os tempos de atualização chegarem, a etapa de download já foi atendida.
- 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, '<')
A primeira linha indica que esta reprodução só se aplicará ao grupo logstash
. A segunda linha diz ao Ansible para não se incomodar em coletar fatos sobre os hosts. Isso acelerará o jogo, mas certifique-se de que nenhuma das tarefas do jogo precise de nenhum fato sobre o host.
A primeira tarefa no jogo validará a variável elk_version
. Essa variável representa a versão do Elastic Stack para a qual estamos atualizando. Isso é passado quando você invoca o comando ansible-playbook. Se a variável não for passada ou não for um formato válido, a jogada será interrompida imediatamente. Essa tarefa será realmente a primeira tarefa em todas as peças. A razão para isso é permitir que as jogadas sejam executadas isoladamente se desejado ou necessário.
A segunda tarefa usará o comando rpm
para obter a versão atual do Logstash e registrar na variável version_found
. Essas informações serão usadas na próxima tarefa. As linhas args:
, warning: warn: no
e changed_when: False
existem para deixar o ansible-lint feliz, mas não são estritamente necessárias.
A tarefa final executará o comando que realmente pré-baixa o pacote. Mas somente se a versão instalada do Logstash for mais antiga que a versão de destino. Não apontar download e versão mais antiga ou igual se não for usada.
As outras duas jogadas são essencialmente as mesmas, exceto que, em vez do Logstash, elas farão o pré-download do Elasticsearch e do 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, '<')
Atualização do Logstash
O Logstash deve ser o primeiro componente a ser atualizado. Isso porque o Logstash tem a garantia de funcionar com uma versão mais antiga do Elasticsearch.
As primeiras tarefas do jogo são idênticas à contraparte pré-download:
- 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
As duas tarefas finais estão contidas em um bloco:

- 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, '<')
A condicional when
garante que as tarefas do bloco só serão executadas se a versão alvo for mais recente que a versão atual. A primeira tarefa dentro do bloco executa a atualização do Logstash e a segunda tarefa reinicia o serviço.
Atualização sem interrupção do cluster do Elasticsearch
Para garantir que não haverá tempo de inatividade no cluster do Elasticsearch, devemos realizar uma atualização sem interrupção. Isso significa que atualizaremos um nó por vez, apenas iniciando a atualização de qualquer nó depois de verificarmos que o cluster está em um estado verde (totalmente íntegro).
Desde o início da peça, você notará algo diferente:
- name: Elasticsearch rolling upgrade hosts: elasticsearch gather_facts: no serial: 1
Aqui temos a linha serial: 1
. O comportamento padrão do Ansible é executar a reprodução em vários hosts em paralelo, o número de hosts simultâneos definidos na configuração. Esta linha garante que a jogada será executada contra apenas um host por vez.
A seguir, definimos algumas variáveis a serem utilizadas ao longo da peça:
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
O significado de cada variável ficará claro à medida que aparecem na peça.
Como sempre, a primeira tarefa é validar a versão 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+")
Muitas das tarefas a seguir consistirão na execução de chamadas REST no cluster do Elasticsearch. A chamada pode ser executada em qualquer um dos nós. Você pode simplesmente executá-lo no host atual no jogo, mas alguns dos comandos serão executados enquanto o serviço Elasticsearch estiver inativo para o host atual. Portanto, nas próximas tarefas, selecionamos um host diferente para executar as chamadas REST. Para isso, usaremos o módulo set_fact e a variável groups do inventário 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]"
Em seguida, garantimos que o serviço esteja ativo no nó atual 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
Como nas jogadas anteriores, verificaremos a versão atual. Exceto por este tempo, usaremos a API REST do Elasticsearch em vez de executar o rpm. Também poderíamos ter usado o comando rpm, mas quero mostrar essa alternativa.
- name: Check current version uri: url: http://localhost:{{ es_http_port }} method: GET register: version_found retries: 10 delay: 10
As demais tarefas estão dentro de um bloco que só será executado se a versão atual for mais antiga que a versão 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 }}"
Agora, se você seguiu meu conselho e leu a documentação, deve ter notado que este passo deve ser o oposto: desabilitar a alocação de shards. Eu gosto de colocar essa tarefa aqui primeiro caso os shards tenham sido desativados antes por algum motivo. Isso é importante porque a próxima tarefa aguardará que o cluster fique verde. Se a alocação de estilhaços estiver desabilitada, o cluster permanecerá amarelo e as tarefas serão suspensas até atingir o tempo limite.
Então, depois de nos certificarmos de que a alocação de estilhaços está habilitada, garantimos que o cluster esteja em um 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
Após a reinicialização de um serviço de nó, o cluster pode levar muito tempo para retornar ao verde. Essa é a razão das retries: 500
e delay: 15
. Isso significa que vamos esperar 125 minutos (500 x 15 segundos) para que o cluster volte a ficar verde. Você pode precisar ajustar isso se seus nós armazenarem uma quantidade realmente grande de dados. Para a maioria dos casos, é muito mais do que suficiente.
Agora podemos desabilitar a alocação de estilhaços:
- 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 }}
E antes de encerrar o serviço, executamos a limpeza de sincronização opcional, mas recomendada. Não é incomum obter um erro 409 para alguns dos índices quando realizamos um flush de sincronização. Como isso é seguro ignorar, adicionei 409 à lista de códigos de status de sucesso.
- name: Perform a synced flush uri: url: http://localhost:{{ es_http_port }}/_flush/synced method: POST status_code: "200, 409"
Agora, este nó está pronto para ser atualizado:
- name: Shutdown elasticsearch node systemd: name: elasticsearch state: stopped - name: Update elasticsearch yum: name: elasticsearch-{{ elk_version }} state: present
Com o serviço parado esperamos que todos os shards sejam alocados antes de iniciar o node novamente:
- 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
Após a realocação dos shards, reiniciamos o serviço Elasticsearch e esperamos que ele esteja completamente pronto:
- 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
Agora, verificamos se o cluster está amarelo ou verde antes de reativar a alocação 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
E esperamos que o nó se recupere totalmente antes de processar o próximo:
- 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
Claro que, como eu disse antes, este bloco só deve ser executado se realmente estivermos atualizando a versão:
when: version_found.json.version.number is version_compare(elk_version, '<')
Atualização do Kibana
O último componente a ser atualizado é o Kibana.
Como você pode esperar, as primeiras tarefas não são diferentes da atualização do Logstash ou das reproduções de pré-download. Exceto para a definição de uma variável:
- 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
Explicarei a variável set_default_index
quando chegarmos à tarefa que a utiliza.
O restante das tarefas estará dentro de um bloco que só será executado se a versão instalada do Kibana for mais antiga que a versão de destino. As duas primeiras tarefas atualizarão e reiniciarão o Kibana:
- name: Update kibana yum: name: kibana-{{ elk_version }} state: present - name: Restart kibana systemd: name: kibana state: restarted enabled: yes daemon_reload: yes
E para Kibana isso deveria ter sido suficiente. Infelizmente, por algum motivo, após a atualização, o Kibana perde sua referência ao padrão de índice padrão. Isso faz com que ele solicite ao primeiro usuário que acessar após a atualização para definir o padrão de índice padrão, o que pode causar confusão. Para evitar isso, certifique-se de incluir uma tarefa para redefinir o padrão de índice padrão. No exemplo abaixo, é syslog
, mas você deve alterá-lo para o que você usa. Antes de definir o índice, porém, temos que ter certeza de que o Kibana está ativo e pronto para atender às solicitações:
- 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 }}"
Conclusão
O Elastic Stack é uma ferramenta valiosa e eu definitivamente recomendo que você dê uma olhada se ainda não o usa. É ótimo como é e está melhorando constantemente, tanto que pode ser difícil acompanhar a atualização constante. Espero que esses Playbooks do Ansible possam ser tão úteis para você quanto são para mim.
Eu os disponibilizei no GitHub em https://github.com/orgito/elk-upgrade. Eu recomendo que você teste em um ambiente de não produção.
Se você é um desenvolvedor Ruby on Rails que deseja incorporar o Elasticsearch em seu aplicativo, confira Elasticsearch for Ruby on Rails: A Tutorial to the Chewy Gem by Core Toptal Software Engineer Arkadiy Zabazhanov.