ElasticsearchのIndex TemplateをRubyで管理する「Secateurs」の紹介

## Index Template

みなさんはElasticsearchのIndex Templateをどのように管理していますか。Index TemplateはJSONで定義することになりますが、込み入った設定を行おうとすると複雑になりがちで個人的には大分辛いなと思っています。

```json
PUT /_template/template_1
{
  "template": "te*",
  "settings": {
    "number_of_shards": 1
  },
  "mappings": {
    "_default_": {
      "dynamic_templates": [
         {
           "string_template": {
             "mapping": {
               "index": "not_analyzed",
               "type": "string"
             },
             "match_mapping_type": "string",
             "match": "*"
           }
        }
      ]
    },
    "type1": {
      "_source": {
        "enabled": false
      },
      "properties": {
        "@timestamp": {
          "type": "date"
        },
        "created_at": {
          "type": "date",
          "format": "EEE MMM dd HH:mm:ss Z YYYY"
        },
      }
    }
  }
}
```

あ、またカンマが一つ多いですね。 :innocent: 

JSONにありがちな話でもありますが、現状の個人的な不満として

* コメントを書きたい
* Index Templateの適用前にdry-runがしたい  
* 共通の項目をまとめて使いまわしたい
* カンマの有無が面倒臭い

というのがあり、コマンドラインツールにしてDSLで管理すれば解決出来そうなので作りました。

追記： 共通の項目に関しては`template "*"`と`order`の調整でも対応できるとの事でした。 jothaniさん有難うございます！

## :scissors: Secateurs

https://github.com/daichirata/secateurs

### Examples

Index Templateの定義には Ruby・JSON・YAML の3フォーマットに対応しています。Ruby DSLの構文に関しては[rails/jbuilder](https://github.com/rails/jbuilder)を使って定義します。

#### Ruby DSL

```rb
define_template "template_1" do
  template "te*"

  settings do
    index do
      number_of_shards "1"
    end
  end

  mappings do
    type1 do
      _source do
        enabled false
      end

      properties do
        # メソッド名に使えない文字列が含まれる場合はset!を使う
        set! "@timestamp" do
          type "date"
        end

        created_at do
          format "EEE MMM dd HH:mm:ss Z YYYY"
          type "date"
        end

        host_name do
          index "not_analyzed"
          type "string"
        end
      end
    end
  end
end
```

Ruby DSLのみ部分テンプレートと外部テンプレートの読み込みに対応しています。

* 部分テンプレート

```rb:partial!
def common_props(key)
  set! key do
    test true
  end
end
# or 
common_props2 = -> {
  test2 true
}
# or 
common_pros3 = ->(json) {
  json.test3 true
}

define_template "template_1" do
  template "te*"

  mappings do
    partial! method(:common_props), "test-key"

    type1 do
      partial! common_props2
    end

    type2 do
      common_props3.(self)
    end
  end
end
```

* 外部テンプレート読込

```rb:include_template
include_template "/path/to/template_file"

# You can include a template by relative path.
include_template "templae_1"
```

#### JSON
 
```json
{
  "template_1": {
    "template": "te*",
    "settings": {
      "index": {
        "number_of_shards": "1"
      }
    },
    "mappings": {
      "type1": {
        "_source": {
          "enabled": false
        },
        "properties": {
          "@timestamp": {
            "type": "date"
          },
          "created_at": {
            "format": "EEE MMM dd HH:mm:ss Z YYYY",
            "type": "date"
          },
          "host_name": {
            "index": "not_analyzed",
            "type": "string"
          }
        }
      }
    }
  }
}
```

#### YAML

```yaml
---
template_1:
  template: te*
  settings:
    index:
      number_of_shards: '1'
  mappings:
    type1:
      _source:
        enabled: false
      properties:
        "@timestamp":
          type: date
        created_at:
          format: EEE MMM dd HH:mm:ss Z YYYY
          type: date
        host_name:
          index: not_analyzed
          type: string
```

### Commands

Index Templateのapplyとexportに対応しています。

```
$ secateurs help
Commands:
  secateurs apply FILE      # apply index template file.
  secateurs export          # export index template to local file.
  secateurs help [COMMAND]  # Describe available commands or one specific command
```

#### Export

```
$ secateurs help export
Usage:
  secateurs export

Options:
  -h, [--host=HOST]
                                   # Default: localhost
  -p, [--port=N]
                                   # Default: 9200
  -f, [--format=FORMAT]
                                   # Default: ruby
                                   # Possible values: ruby, json, yaml
  -o, [--output=OUTPUT]

      [--split], [--no-split]
      [--verbose], [--no-verbose]

export index template to local.
```

* 実行例

```sh
$ secateurs export > Templatefile

$ secateurs export -o Templatefile

$ secateurs export --format json > Templatefile.json

# Ruby DSLに限り、テンプレートごとに分割してエクスポートすることが出来ます。
$ secateurs export --split -o templates/Templatefile

$ ls templates/*
Templatefile
template_1.rb
template_2.rb

$ cat templates/Templatefile
include_template "template_1"
include_template "template_2"
```

#### Apply

```
$ secateurs help apply
Usage:
  secateurs apply FILE

Options:
  -h, [--host=HOST]
                                   # Default: localhost
  -p, [--port=N]
                                   # Default: 9200
  -f, [--format=FORMAT]
                                   # Default: ruby
                                   # Possible values: ruby, json, yaml
      [--color], [--no-color]
                                   # Default: true
      [--dry-run], [--no-dry-run]
      [--verbose], [--no-verbose]

apply index template file.
```

* 実行例

```sh
$ secateurs apply Templatefile

$ secateurs apply --dry-run Templatefile.json

$ secateurs apply --format json Templatefile.json
```

* dry-run

反映予定のテンプレートファイルとElasticsearchに登録されているIndex Templateの差分を表示します。

```
$ secateurs apply --dry-run Templatefile
Apply `Templatefile` to index template (dry-run)
Include template from /ops/elasticsearch/Templatefile

Comparing changes `apache_access`
   "mappings": {
     "_default_": {
       "properties": {
-        "agent": {
-          "index": "not_analyzed",
-          "type": "string"
-        },
         "agent_category": {
           "index": "not_analyzed",
           "type": "string"
```

## TODO

* Index TemplateのValidation
    * jothaniさんの https://github.com/elastic/elasticsearch/pull/8802 非常に良さそう

## おわり

以上、Index Templateの管理ツールの紹介でした。