Автоматическое обновление эластичного стека с помощью Ansible Playbooks
Опубликовано: 2022-03-11Анализ журнала для сети, состоящей из тысяч устройств, был сложной, трудоемкой и скучной задачей, прежде чем я решил использовать Elastic Stack в качестве решения для централизованного ведения журнала. Это оказалось очень мудрым решением. У меня есть не только единое место для поиска всех моих журналов, но и почти мгновенные результаты поиска, мощные визуализации, которые невероятно полезны для анализа и устранения неполадок, и красивые информационные панели, которые дают мне полезный обзор сети.
Elastic Stack постоянно выпускает новые и удивительные функции, поддерживая очень активный темп развития, он часто выпускает два новых выпуска каждый месяц. Мне нравится всегда обновлять свою среду, чтобы быть уверенным, что я могу воспользоваться преимуществами новых функций и улучшений. Также, чтобы избежать ошибок и проблем с безопасностью. Но это требует от меня постоянного обновления среды.
Несмотря на то, что веб-сайт Elastic содержит четкую и подробную документацию, в том числе по процессу обновления их продуктов, обновление вручную — сложная задача, особенно для кластера Elasticsearch. Здесь много шагов, и нужно следовать очень определенному порядку. Вот почему я давно решил автоматизировать весь процесс с помощью Ansible Playbooks.
В этом руководстве по Ansible я расскажу о серии Ansible Playbooks, которые были разработаны для автоматического обновления моей установки Elastic Stack.
Что такое эластичный стек
Стек Elastic, ранее известный как стек ELK, состоит из Elasticsearch, Logstash и Kibana от компании Elastic с открытым исходным кодом, которые вместе представляют собой мощную платформу для индексации, поиска и анализа ваших данных. Его можно использовать для широкого спектра применений. От регистрации и анализа безопасности до управления производительностью приложений и поиска по сайту.
Elasticsearch — это ядро стека. Это распределенная система поиска и аналитики, способная выдавать результаты поиска практически в реальном времени даже в отношении огромного объема хранимых данных.
Logstash — это конвейер обработки, который получает или получает данные из множества различных источников (на момент написания статьи — 50 официальных входных плагинов), анализирует их, фильтрует, преобразует и отправляет на один или несколько возможных выходов. В нашем случае нас интересует плагин вывода Elasticsearch.
Kibana — это ваш пользовательский и рабочий интерфейс. Он позволяет вам визуализировать, искать, перемещаться по вашим данным и создавать информационные панели, которые дают вам потрясающую информацию об этом.
Что такое Анзибл
Ansible — это платформа автоматизации ИТ, которую можно использовать для настройки систем, развертывания или обновления программного обеспечения и организации сложных ИТ-задач. Его основные цели — простота и удобство использования. Моя любимая особенность Ansible заключается в том, что он не содержит агентов, а это означает, что мне не нужно устанавливать и управлять каким-либо дополнительным программным обеспечением на хостах и устройствах, которыми я хочу управлять. Мы будем использовать возможности автоматизации Ansible для автоматического обновления нашего эластичного стека.
Отказ от ответственности и слово предостережения
Схемы, которыми я поделюсь здесь, основаны на шагах, описанных в официальной документации продукта. Он предназначен для использования только для обновлений одной и той же основной версии. Например: 5.x
→ 5.y
или 6.x
→ 6.y
где x
> y
. Обновления между основными версиями часто требуют дополнительных шагов, и эти сценарии не подходят для таких случаев.
Несмотря на это, всегда читайте примечания к выпуску, особенно раздел критических изменений, прежде чем использовать сборники сценариев для обновления. Убедитесь, что вы понимаете задачи, выполняемые в плейбуках, и всегда проверяйте инструкции по обновлению, чтобы убедиться, что ничего важного не изменилось.
Сказав это, я использую эти сборники игр (или более ранние версии) с версии 2.2 Elasticsearch без каких-либо проблем. В то время у меня были совершенно отдельные playbooks для каждого продукта, так как у них не было одного и того же номера версии, который они знают.
При этом я не несу никакой ответственности за использование вами информации, содержащейся в этой статье.
Наша вымышленная среда
Среда, в которой будут работать наши плейбуки, будет состоять из 6 серверов CentOS 7:
- 1 сервер Logstash
- 1 сервер Кибана
- 4 узла Elasticsearch
Не имеет значения, если в вашей среде есть другое количество серверов. Вы можете просто отразить это соответствующим образом в файле инвентаризации, и плейбуки должны работать без проблем. Однако, если вы не используете дистрибутив на основе RHEL, я оставлю вам в качестве упражнения изменение нескольких задач, специфичных для дистрибутива (в основном, менеджер пакетов).
Инвентарь
Ansible нуждается в инвентаризации, чтобы знать, на каких хостах он должен запускать плейбуки. В нашем воображаемом сценарии мы будем использовать следующий файл инвентаризации:
[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
В файле инвентаризации Ansible любой [section]
представляет группу хостов. В нашем инвентаре есть 3 группы хостов: logstash
, kibana
и elasticsearch
. Вы заметите, что я использую только имена групп в playbooks. Это означает, что не имеет значения количество хостов в инвентаре, пока группы правильны, playbook будет работать.
Процесс обновления
Процесс обновления будет состоять из следующих шагов:
1) Предварительно скачать пакеты
2) Обновление Логсташа
3) Последовательное обновление кластера Elasticsearch.
4) Обновление Кибаны
Основная цель — минимизировать время простоя. Большую часть времени пользователь даже не заметит. Иногда Kibana может быть недоступен в течение нескольких секунд. Это приемлемо для меня.
Основная книга Ansible Playbook
Процесс обновления состоит из набора различных плейбуков. Я буду использовать функцию import_playbook Ansible для организации всех книг воспроизведения в одном основном файле книги воспроизведения, который можно вызывать для управления всем процессом.
- 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
Довольно просто. Это просто способ организовать выполнение плейбуков в определенном порядке.
Теперь давайте рассмотрим, как мы будем использовать приведенный выше пример плейбука Ansible. Я объясню, как мы это реализуем позже, но это команда, которую я бы выполнил для обновления до версии 6.5.4:
$ ansible-playbook -i inventory -e elk_version=6.5.4 main.yml
Предварительно загрузите пакеты
Этот первый шаг на самом деле является необязательным. Причина, по которой я использую это, заключается в том, что я считаю хорошей практикой останавливать работающую службу перед ее обновлением. Теперь, если у вас есть быстрое подключение к Интернету, время загрузки пакета вашим менеджером пакетов может быть незначительным. Но это не всегда так, и я хочу свести к минимуму время, в течение которого какая-либо служба не работает. Таким образом, мой первый плейбук будет использовать yum для предварительной загрузки всех пакетов. Таким образом, когда придет время обновления, шаг загрузки уже будет выполнен.
- 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, '<')
Первая строка указывает, что это воспроизведение будет применяться только к группе logstash
. Вторая строка говорит Ansible не утруждать себя сбором фактов о хостах. Это ускорит игру, но следите за тем, чтобы ни одно из заданий в игре не нуждалось в каких-либо фактах о ведущем.
Первая задача в игре проверит переменную elk_version
. Эта переменная представляет версию Elastic Stack, до которой мы обновляемся. Это передается, когда вы вызываете команду ansible-playbook. Если переменная не передана или имеет недопустимый формат, воспроизведение будет немедленно остановлено. Это задание фактически будет первым заданием во всех пьесах. Причина этого в том, чтобы при желании или необходимости пьесы можно было исполнять изолированно.
Вторая задача будет использовать команду rpm
для получения текущей версии Logstash и регистрации в переменной version_found
. Эта информация будет использована в следующем задании. Строки args:
, warn: no
и changed_when: False
нужны для того, чтобы сделать ansible-lint счастливым, но они не являются строго необходимыми.
Последняя задача выполнит команду, которая фактически предварительно загрузит пакет. Но только в том случае, если установленная версия Logstash старше целевой версии. Не указывайте загрузку и более старую или ту же версию, если она не будет использоваться.
Две другие игры по сути одинаковы, за исключением того, что вместо Logstash они предварительно загружают Elasticsearch и 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, '<')
Обновление Логсташа
Logstash должен быть первым компонентом, который нужно обновить. Это связано с тем, что Logstash гарантированно работает со старой версией Elasticsearch.
Первые задания игры идентичны предзагрузочному аналогу:
- 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
Две последние задачи содержатся в блоке:

- 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, '<')
Условие when
гарантирует, что задачи в блоке будут выполняться только в том случае, если целевая версия новее текущей версии. Первая задача внутри блока выполняет обновление Logstash, а вторая задача перезапускает службу.
Последовательное обновление кластера Elasticsearch
Чтобы убедиться, что в кластере Elasticsearch не будет простоев, мы должны выполнить непрерывное обновление. Это означает, что мы будем обновлять по одному узлу за раз, начиная обновление любого узла только после того, как убедимся, что кластер находится в зеленом состоянии (полностью работоспособен).
С самого начала пьесы вы заметите нечто иное:
- name: Elasticsearch rolling upgrade hosts: elasticsearch gather_facts: no serial: 1
Здесь у нас есть serial: 1
. Поведение Ansible по умолчанию заключается в параллельном выполнении воспроизведения на нескольких хостах, количество одновременных хостов определено в конфигурации. Эта строка гарантирует, что игра будет выполняться только против одного хоста за раз.
Затем мы определяем несколько переменных, которые будут использоваться во время игры:
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
Значение каждой переменной станет ясным, когда они появятся в пьесе.
Как всегда, первая задача — проверить целевую версию:
tasks: - name: Validate ELK Version fail: msg="Invalid ELK Version" when: elk_version is undefined or not elk_version is match("\d+\.\d+\.\d+")
Многие из следующих задач будут заключаться в выполнении вызовов REST для кластера Elasticsearch. Вызов может быть выполнен против любого из узлов. Вы можете просто выполнить его на текущем хосте в игре, но некоторые команды будут выполняться, пока служба Elasticsearch не работает для текущего хоста. Итак, в следующих задачах мы обязательно выберем другой хост для выполнения вызовов REST. Для этого мы будем использовать модуль set_fact и переменную groups из инвентаря 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]"
Затем мы убеждаемся, что служба запущена в текущем узле, прежде чем продолжить:
- 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
Как и в предыдущих играх, мы будем проверять текущую версию. За исключением этого времени, мы будем использовать Elasticsearch REST API вместо запуска rpm. Мы также могли бы использовать команду rpm, но я хочу показать этот вариант.
- name: Check current version uri: url: http://localhost:{{ es_http_port }} method: GET register: version_found retries: 10 delay: 10
Остальные задачи находятся внутри блока, который будет выполняться только в том случае, если текущая версия старше целевой:
- 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 }}"
Теперь, если вы последовали моему совету и прочитали документацию, вы заметили, что этот шаг должен быть противоположным: отключить выделение осколков. Мне нравится ставить эту задачу здесь первой на тот случай, если осколки по какой-то причине были отключены ранее. Это важно, потому что следующая задача будет ждать, пока кластер станет зеленым. Если выделение осколков отключено, кластер останется желтым, а задачи будут зависать до истечения времени ожидания.
Итак, убедившись, что распределение сегментов включено, мы убеждаемся, что кластер находится в зеленом состоянии:
- 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
После перезапуска службы узла кластеру может потребоваться много времени, чтобы вернуться к зеленому цвету. Это причина retries: 500
и delay: 15
. Это означает, что мы будем ждать 125 минут (500 x 15 секунд), пока кластер не станет зеленым. Возможно, вам придется настроить это, если ваши узлы содержат действительно огромное количество данных. Для большинства случаев этого более чем достаточно.
Теперь мы можем отключить выделение осколков:
- 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 }}
И прежде чем закрыть службу, мы выполняем необязательную, но рекомендуемую синхронизацию. Нередко возникает ошибка 409 для некоторых индексов, когда мы выполняем сброс синхронизации. Поскольку это безопасно игнорировать, я добавил 409 в список кодов состояния успеха.
- name: Perform a synced flush uri: url: http://localhost:{{ es_http_port }}/_flush/synced method: POST status_code: "200, 409"
Теперь этот узел готов к обновлению:
- name: Shutdown elasticsearch node systemd: name: elasticsearch state: stopped - name: Update elasticsearch yum: name: elasticsearch-{{ elk_version }} state: present
Когда служба остановлена, мы ждем, пока все осколки будут выделены, прежде чем снова запустить узел:
- 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
После перераспределения шардов перезапускаем сервис Elasticsearch и ждем его полной готовности:
- 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
Теперь мы убеждаемся, что кластер желтый или зеленый перед повторным включением распределения осколков:
- 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
И мы ждем полного восстановления узла перед обработкой следующего:
- 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
Конечно, как я уже говорил, этот блок должен выполняться только в том случае, если мы действительно обновляем версию:
when: version_found.json.version.number is version_compare(elk_version, '<')
Обновление Кибаны
Последним обновляемым компонентом является Kibana.
Как и следовало ожидать, первые задачи ничем не отличаются от обновления Logstash или воспроизведения перед загрузкой. За исключением определения одной переменной:
- 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
Я объясню переменную set_default_index
, когда мы перейдем к задаче, которая ее использует.
Остальные задачи будут внутри блока, который будет выполняться только в том случае, если установленная версия Kibana старше целевой версии. Первые две задачи обновят и перезапустят Kibana:
- name: Update kibana yum: name: kibana-{{ elk_version }} state: present - name: Restart kibana systemd: name: kibana state: restarted enabled: yes daemon_reload: yes
И для Кибаны этого должно было быть достаточно. К сожалению, по какой-то причине после обновления Kibana теряет ссылку на шаблон индекса по умолчанию. Это приводит к тому, что он запрашивает у первого пользователя, получившего доступ после обновления, определение шаблона индекса по умолчанию, что может вызвать путаницу. Чтобы избежать этого, обязательно включите задачу для сброса шаблона индекса по умолчанию. В приведенном ниже примере это syslog
, но вы должны изменить его на то, что вы используете. Однако перед настройкой индекса мы должны убедиться, что Kibana запущена и готова обслуживать запросы:
- 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 }}"
Заключение
Elastic Stack — ценный инструмент, и я определенно рекомендую вам взглянуть на него, если вы еще не используете его. Он великолепен сам по себе и постоянно совершенствуется, настолько, что может быть трудно идти в ногу с постоянными обновлениями. Я надеюсь, что эти Ansible Playbook могут оказаться для вас такими же полезными, как и для меня.
Я сделал их доступными на GitHub по адресу https://github.com/orgito/elk-upgrade. Я рекомендую вам протестировать его в непроизводственной среде.
Если вы разработчик Ruby on Rails и хотите включить Elasticsearch в свое приложение, ознакомьтесь с Elasticsearch для Ruby on Rails: учебник Chewy Gem от инженера-программиста Core Toptal Аркадия Забажанова.