使用 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 教程。