使用 Ansible Playbook 自动更新 Elastic Stack
已发表: 2022-03-11在我决定使用 Elastic Stack 作为集中式日志记录解决方案之前,对由数千台设备组成的网络进行日志分析曾经是一项复杂、耗时且无聊的任务。 事实证明这是一个非常明智的决定。 我不仅可以在一个地方搜索我的所有日志,而且我的搜索结果几乎是即时的,强大的可视化对分析和故障排除非常有帮助,漂亮的仪表板让我对网络有一个有用的概述。
Elastic Stack 不断发布令人惊叹的新功能,保持非常积极的开发步伐,通常每月发布两个新版本。 我喜欢始终让我的环境保持最新,以确保我可以利用新功能和改进。 还可以使其免受错误和安全问题的影响。 但这需要我不断更新环境。
尽管 Elastic 网站维护了清晰详细的文档,包括其产品的升级过程,但手动升级是一项复杂的任务,尤其是 Elasticsearch 集群。 涉及的步骤很多,需要遵循一个非常具体的顺序。 这就是为什么我决定在很久以前使用 Ansible Playbooks 自动化整个过程。
在本 Ansible 教程中,我将向我们介绍一系列 Ansible Playbook,这些手册是为自动升级我的 Elastic Stack 安装而开发的。
什么是弹性堆栈
Elastic Stack,以前称为 ELK 堆栈,由开源公司 Elastic 的 Elasticsearch、Logstash 和 Kibana 组成,它们共同为索引、搜索和分析数据提供了一个强大的平台。 它可用于广泛的应用。 从日志记录和安全分析到应用程序性能管理和站点搜索。
Elasticsearch是堆栈的核心。 它是一个分布式搜索和分析引擎,即使针对大量存储数据也能提供近乎实时的搜索结果。
Logstash是一个处理管道,它从许多不同的来源(我正在编写的 50 个官方输入插件)获取或接收数据,对其进行解析、过滤和转换,并将其发送到一个或多个可能的输出。 在我们的例子中,我们对 Elasticsearch 输出插件感兴趣。
Kibana是您的用户和操作前端。 它使您可以可视化、搜索、导航您的数据并创建仪表板,从而为您提供有关数据的惊人见解。
什么是 Ansible
Ansible 是一个 IT 自动化平台,可用于配置系统、部署或升级软件以及编排复杂的 IT 任务。 它的主要目标是简单和易用。 我最喜欢 Ansible 的特性是它是无代理的,这意味着我不需要在我想要管理的主机和设备上安装和管理任何额外的软件。 我们将使用 Ansible 自动化的强大功能来自动升级我们的 Elastic Stack。
免责声明和注意事项
我将在这里分享的剧本基于官方产品文档中描述的步骤。 它仅用于同一主要版本的升级。 例如: 5.x
→ 5.y
或6.x
→ 6.y
其中x
> y
。 主要版本之间的升级通常需要额外的步骤,而这些剧本不适用于这些情况。
无论如何,在使用 playbook 进行升级之前,请务必阅读发行说明,尤其是重大更改部分。 确保您了解剧本中执行的任务,并始终检查升级说明以确保没有任何重要更改。
话虽如此,自 Elasticsearch 2.2 版以来,我一直在使用这些剧本(或更早版本),没有任何问题。 当时我对每个产品都有完全独立的剧本,因为它们并不像他们所知道的那样共享相同的版本号。
话虽如此,我对您使用本文中包含的信息不承担任何责任。
我们的虚拟环境
我们的 playbook 将运行的环境将包含 6 个 CentOS 7 服务器:
- 1 个 Logstash 服务器
- 1 个 Kibana 服务器
- 4 个 Elasticsearch 节点
如果您的环境具有不同数量的服务器,则无关紧要。 您可以简单地在清单文件中相应地反映它,并且剧本应该可以毫无问题地运行。 但是,如果您没有使用基于 RHEL 的发行版,我将把它作为练习留给您更改特定于发行版的少数任务(主要是包管理器的东西)
库存
Ansible 需要一个清单来了解它应该针对哪些主机运行 playbook。 四个我们的想象场景我们将使用以下库存文件:
[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
。 你会注意到我只使用剧本中的组名。 这意味着清单中的主机数量无关紧要,只要组正确,剧本就会运行。
升级过程
升级过程将包括以下步骤:
1) 预下载包
2) Logstash 升级
3)Elasticsearch集群滚动升级
4) Kibana 升级
主要目标是最大限度地减少停机时间。 大多数时候用户甚至不会注意到。 有时 Kibana 可能会在几秒钟内不可用。 这是我可以接受的。
主要的 Ansible 剧本
升级过程由一组不同的剧本组成。 我将使用 Ansible 的 import_playbook 功能将所有 playbook 组织到一个主 playbook 文件中,可以调用该文件来处理整个过程。
- 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 playbook 示例。 稍后我将解释我们如何实现它,但这是升级到版本 6.5.4 时要执行的命令:
$ ansible-playbook -i inventory -e elk_version=6.5.4 main.yml
预下载软件包
第一步实际上是可选的。 我使用它的原因是我认为在升级之前停止正在运行的服务是一种普遍的好习惯。 现在,如果您有一个快速的 Internet 连接,那么您的包管理器下载包的时间可能可以忽略不计。 但情况并非总是如此,我想尽量减少任何服务停机的时间。 这就是我的第一本剧本将使用 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 版本比目标版本旧。 不点下载和旧版本或相同版本如果不会使用。
其他两个玩法基本相同,只是它们将预下载 Elasticsearch 和 Kibana,而不是 Logstash:
- 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 应该是第一个要升级的组件。 这是因为保证 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+")
以下许多任务将包括对 Elasticsearch 集群执行 REST 调用。 可以对任何节点执行调用。 您可以简单地对游戏中的当前主机执行它,但是某些命令将在当前主机的 Elasticsearch 服务关闭时执行。 因此,在接下来的任务中,我们确保选择不同的主机来运行 REST 调用。 为此,我们将使用来自 Ansible 库存的 set_fact 模块和 groups 变量。
- 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 升级
最后一个要升级的组件是 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 来说,这应该已经足够了。 不幸的是,由于某种原因,在升级之后,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 for Ruby on Rails:Core Toptal 软件工程师 Arkadiy Zabazhanov 的 Chewy Gem 教程。