Filebeatが更新する複数インデクスをIndex Lifecycle Management（ILM）で自動ロールオーバーしてみる

#はじめに
指定された名前の複数インデクスを[Filebeat](https://www.elastic.co/jp/downloads/beats/filebeat)が更新する際にそのインデクスをElasticsearchの[Index lifecycle management (ILM)](https://www.elastic.co/guide/en/elasticsearch/reference/7.6/index-lifecycle-management.html)を使って自動ロールオーバーするやり方について説明する機会があったのでその備忘録。せっかく（？）なのでKibanaをなるべく使って設定してみました。

※少し前のバージョンのElasticsearchを使います。[データストリーム](https://www.elastic.co/guide/en/elasticsearch/reference/current/data-streams.html)を使うといろいろと捗りそうですがその場合には[こちら](https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-index-lifecycle-management.html)をご参照ください。

#利用するソフトウェア
- OS: Ubuntu 18.04
- Elasticsearch (7.8.1) 
- Kibana (7.8.1)
- Filebeat(7.8.1)

#やりたいこと
![filebeat-log-ilm1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362125/bcb4dc4e-27d5-0cd6-d175-ec481a061f8f.png)

###ホストA/Bのイベントログ
ホストAおよびBは何らかのイベントログをFilebeatが読み込むログファイルへ転送しているが、Elasticsearchのインデクスに格納する際にはホスト毎に別々のインデクスへ格納したい。またインデクス名「eventlog-host-a」（エイリアス）とか「eventlog-host-a-*」で参照する。
Filebeatからの更新はエイリアス経由で[Write index](https://www.elastic.co/guide/en/elasticsearch/reference/7.6/indices-aliases.html#aliases-write-index)へ行う。一方でILMがインデクスを自動的にロールオーバーする（「eventlog-host-a-000002」とか）。

※Filebeatのデフォルトテンプレートの自動アップロードも有効化しておきます（イベントログが独自フォーマットの場合にはElasticsearch側にマッピングテンプレート定義をするかもしれませんが、FilebeatがElasticsearchへ転送する形式には[ECS](https://github.com/elastic/ecs)で定義されているフィールドがいくつか追加されるため）。

####イベントログ例
説明のために以下のような適当なイベントログを生成しておきます。

```json:ホストAのイベントログ例
{
  "event_id": "89b20ab8-09d1-4f82-873c-b1ddc9856718",
  "host_name": "host-a",
  "data": {
    "data_id": "4f2c4521-fedb-4c6d-86a2-1be84722a6c5",
...
  }
}
```

* event_id: イベントログのユニークID
* host_name: ホスト名。host_aまたはhost_bが入ります
* data: 適当なデータ

eventlog-*.jsonには以下のようにインデントなし形式で書き込まれます。この例ではホストAとホストBのログが同一ファイルへ混在しています。

```text
...
{"event_id": "e3ce617d-9220-4e20-b63c-68640089090e", "host_name": "host-a", "data": {"data_id": "c1a83642-e2a0-406a-96ac-29808d4666c4"}}
{"event_id": "71a90178-aa21-40bd-9009-edaf4c0e817d", "host_name": "host-a", "data": {"data_id": "c41fd91a-6509-447c-a38e-df9156491de5"}}
{"event_id": "305675ca-bf06-4366-81a7-859093fa4f7d", "host_name": "host-a", "data": {"data_id": "389e6038-c15d-4419-9309-cb3f7450174c"}}
{"event_id": "6572d7e7-67df-4e5f-8e8b-aebe7c42abc2", "host_name": "host-b", "data": {"data_id": "a8825f3a-8259-4f22-970e-1bb2700ad879"}}
{"event_id": "b3ff56b5-4fc3-4192-95d7-e0f20317c27b", "host_name": "host-b", "data": {"data_id": "d83e944e-06f3-4ca4-ba46-4a93348b1da4"}}
{"event_id": "69871416-a0ae-4b41-8d55-437855090564", "host_name": "host-b", "data": {"data_id": "8938e8b0-a5e3-4931-b427-513f435b634f"}}
...
```

FilebeatによってElasticsearchのインデクスへ書き込まれるドキュメント形式は以下です。

```json
{
  "_index": "eventlog-host-a-000001",
  "_type": "_doc",
  "_id": "89b20ab8-09d1-4f82-873c-b1ddc9856718",
  "_version": 1,
  "_score": null,
  "_source": {
    "@timestamp": "2020-12-05T09:03:15.468Z",
    "log": {
      "file": {
        "path": "/.../eventlog/eventlog-1.json"
      },
      "offset": 0
    },
    "input": {
      "type": "log"
    },
    "ecs": {
      "version": "1.5.0"
    },
    "host": {
      "os": {
        "version": "18.04.3 LTS (Bionic Beaver)",
        "family": "debian",
        "name": "Ubuntu",
        "kernel": "4.15.0-126-generic",
        "codename": "bionic",
        "platform": "ubuntu"
      },
      "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "containerized": false,
      "ip": [
        "192.168.0.100"
      ],
      "name": "beats-test",
      "mac": [
        "00:00:00:00:00:00"
      ],
      "hostname": "beats-test",
      "architecture": "x86_64"
    },
    "agent": {
      "ephemeral_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "name": "beats-test",
      "type": "filebeat",
      "version": "7.8.1",
      "hostname": "beats-test"
    },
    "json": {
      "host_name": "host-a",
      "data": {
        "data_id": "4f2c4521-fedb-4c6d-86a2-1be84722a6c5"
      }
    }
  },
  "fields": {
    "@timestamp": [
      "2020-12-05T09:03:15.468Z"
    ],
    "suricata.eve.timestamp": [
      "2020-12-05T09:03:15.468Z"
    ]
  },
  "sort": [
    1607396595468
  ]
}
```

元データはjsonフィールド配下へ入ります（filebeat.yml:json.keys_under_root: false）。またドキュメントIDには元データのevent_idの値を使っています（filebeat.yml:json.document_id: event_id）。

#インデクステンプレートの設定

Filebeatが書き込むイベントログのマッピングを定義し、インデクステンプレートをkibanaで作成します。ILMはこのテンプレートにマッチするインデクスに対して適用しますのでホスト毎に用意します。
※ECS部分についてはFilebeatが自動作成してくれるものを使うので上図のjsonフィールド配下だけ定義します。

###ホストA用の定義例

![filebeat-log-ilm2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362125/310ef13b-aa97-4208-0201-922b5cf96046.png)
![filebeat-log-ilm3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362125/101e3b6b-4973-85ed-5133-e58528e3a27c.png)
![filebeat-log-ilm4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362125/1d5ad23e-c86e-4f7b-0036-926dd5327c29.png)

```json
  "mappings": {
    "properties": {
      "json": {
          "properties": {
            "data": {
              "properties": {
                "data_id": {
                  "type": "keyword",
                  "ignore_above": 1024
                }
              }
            },
            "host_name": {
              "type": "keyword",
              "ignore_above": 1024
            },
            "event_id": {
              "type": "keyword",
              "ignore_above": 1024
            }
          }
      }
    }
  }
```

![filebeat-log-ilm5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362125/1036bcf2-d256-55f7-c26b-a18be9c744a2.png)
![filebeat-log-ilm6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362125/3b18e7a3-c962-1ca4-0608-f4dd72ec6798.png)

ホストBも同様に定義しておきます。

#Index lifecycle management (ILM)の設定
対象インデクス毎（ホスト毎）にILMポリシーを作成します。＃両ホストで共通のILMポリシーでもOKです。

###ホストA用の定義例
####ILMポリシーを作成
![filebeat-log-ilm7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362125/2611274e-164a-99ab-d84e-41911c89150c.png)

ここでは説明のため5ドキュメントをロールオーバーの閾値に設定しています。

####ILMポリシーをホストA用のインデクステンプレートへ設定
各ILMポリシー行のActionsからインデクステンプレートへ紐づけます。

![filebeat-log-ilm9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362125/feec813d-281d-3e2e-be06-b0281679216f.png)

ホスト毎にILMの対象インデクスをエイリアス名(eventlog-host-a/eventlog-host-b)でラップします。


#ブートストラップのインデクスを作成
ILMが監視を開始するホスト毎の元インデクス（空でOK）をつくります。ポイントは
* Write Index指定をすること("is_write_index": true)：
* インデクス名に番号(-0000001)をつけること(例：eventlog-host-a-000001)
です。

詳細は[Rollover index API](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-rollover-index.html)を参照ください。

![filebeat-log-ilm10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362125/992dd371-3ef0-a76b-74b0-146b620b1f23.png)

ここではKibanaのDev Toolsコンソールから作成しています。
![filebeat-log-ilm11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362125/ba1df286-d432-9cf3-8e95-f8cf60fbdfde.png)

この辺りがわかりづらい・・・

#Filebeatの設定

/etc/filebeat/filebeat.ymlを設定します。

```yml:filebeat.yml
...
# ============================== Filebeat inputs ===============================

filebeat.inputs:

# Each - is an input. Most options can be set at the input level, so
# you can use different inputs for various configurations.
# Below are the input specific configurations.

- type: log

  # Change to true to enable this input configuration.
  enabled: true

  # Paths that should be crawled and fetched. Glob based paths.
  paths:
    #イベントログファイルがおかれるパス(e.g. eventlog-1.json)
    - /.../eventlog/*.json

  # Exclude lines. A list of regular expressions to match. It drops the lines that are
  # matching any regular expression from the list.
  #exclude_lines: ['^DBG']

  # Include lines. A list of regular expressions to match. It exports the lines that are
  # matching any regular expression from the list.
  #include_lines: ['^ERR', '^WARN']

  # Exclude files. A list of regular expressions to match. Filebeat drops the files that
  # are matching any regular expression from the list. By default, no files are dropped.
  #exclude_files: ['.gz$']

  # Optional additional fields. These fields can be freely picked
  # to add additional information to the crawled log files for filtering
  #fields:
  #  level: debug
  #  review: 1

  #jsonフィールドの設定
  json.keys_under_root: false
  #ドキュメントIDとしてユニーク想定のイベントIDを使います（インデクス再実行時の重複回避）
  json.document_id: event_id 

# ======================= Elasticsearch template setting =======================

setup.template.settings:
  index.number_of_shards: 1
  index.number_of_replicas: 0 #シングルノードなのでレプリカ無効化
  #index.codec: best_compression
  #_source.enabled: false

#Filebeatデフォルトのインデクステンプレートは自動設定にしておきます(ECSのため)
setup.template.enabled: true
#Filebeatのマッピングテンプレートを適用します
setup.template.pattern: "eventlog-*" 

#Filebeat側ではILMを無効化
setup.ilm.enabled: false

...

# ================================== Outputs ===================================

# Configure what output to use when sending the data collected by the beat.

# ---------------------------- Elasticsearch Output ----------------------------
output.elasticsearch:
  # Array of hosts to connect to.
  hosts: ["localhost:9200"]

  # Protocol - either `http` (default) or `https`.
  #protocol: "https"

  # Authentication credentials - either API key or username/password.
  #api_key: "id:api_key"
  #username: "elastic"
  #password: "changeme"

  #ホスト毎にインデクスを振り分けます
  #(元イベントデータのhost_nameフィールド値を使います)
  indices:
    #filebeatがエイリアス名に対して更新をかけるように設定
    - index: eventlog-host-a 
      when.equals:
        json.host_name: host-a
    - index: eventlog-host-b
      when.equals:
        json.host_name: host-b

...

# ================================= Processors =================================

# Configure processors to enhance or manipulate events generated by the beat.

processors:
  - add_host_metadata: ~
  - add_cloud_metadata: ~
  - add_docker_metadata: ~
  - add_kubernetes_metadata: ~

  #元イベントデータをjsonパーシングしてからElasticsearchへ転送します
  - decode_json_fields:
      fields: [message]
      process_array: true
      max_depth: 8
      overwrite_keys: true
      add_error_key: true

# ================================== Logging ===================================

# Sets log level. The default log level is info.
# Available log levels are: error, warning, info, debug
#logging.level: debug

# At debug level, you can selectively enable logging only for some components.
# To enable all selectors use ["*"]. Examples of other selectors are "beat",
# "publish", "service".
#logging.selectors: ["*"]
...
```

#イベントログを投入！

ホスト毎にイベント(ドキュメント)を投入してみます。ILMの閾値が5ドキュメントのためロールオーバー対象になります。しばらくすると・・・

![filebeat-log-ilm14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362125/6f19bfef-28f8-fb61-7a27-a3264e9ca7bb.png)

新しいインデクスeventlog-host-a-000002/eventlog-host-b-000002が自動作成されています。ILMがホスト毎インデクスとしてロールオーバー処理してくれてますね。

#Tips
* ElasticsearchのILMのポーリング間隔（デフォルト：10分）をデバッグ時には短めにしておいてもいいかもです。
  * [indices.lifecycle.poll_interval](https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-settings.html) 

#おまけ

適当なサンプルイベントデータを生成するスクリプトです。

```python
import os
import sys
import uuid
import json
import copy

template = {
    "event_id": None,
    "host_name": None,
    "data": {
        "data_id": None
    }
}

for i in range(10):
    event = copy.deepcopy(template)
    event["event_id"] = "{}".format(uuid.uuid4())
    event["host_name"] = "host-a"
    event["data"]["data_id"] = "{}".format(uuid.uuid4())
    print(json.dumps(event))

for i in range(10):
    event = copy.deepcopy(template)
    event["event_id"] = "{}".format(uuid.uuid4())
    event["host_name"] = "host-b"
    event["data"]["data_id"] = "{}".format(uuid.uuid4())
    print(json.dumps(event))
```