Elasticsearchを活用したサービス監視ですこしだけ幸せになる

[エイチームライフスタイルアドベントカレンダー2017](https://qiita.com/advent-calendar/2017/ateam-lifestyle)、2日目です。
本日は [株式会社エイチームライフスタイル](http://life.a-tm.co.jp/) のインフラエンジニア @ihsiek が担当します。

突然ですが皆さん、アクセスログを可視化していますか？
**Elasticsearch+Kibana**でやってるよーというところもそこそこありますよね？

そこで今回は、**Elasticsearch上のデータを解析してアラートを投げる [ElastAlert](https://github.com/Yelp/elastalert)**というツールの導入手順と社内での運用方法について紹介していきます。

ちなみにこの仕組みを導入したことで、**アラート本文を見るだけでサーバで起きている事象を瞬時に判断し、障害を切り分けられるようになりました。** :tada:
サーバのアクセスログどころか、Kibanaすら見なくてよくなった＆経験の浅いメンバーがアラート対応に入れるようになったのが地味に幸せです。 :relaxed:

# ElastAlertとは？

Elasticsearch上のIndexに対して、運用者が指定したある条件(クエリや監視ルールタイプ)に応じた通知を可能にするYelp社が開発したOSSツールです。

弊社では、このツールを使い、Elasticsearchに取り込んだアクセスログから、**レスポンス遅延**と**HTTPステータス500番台のエラー**を検知する仕組みを導入しました。

当初は、**指定した期間内**にクエリとマッチするイベントが**N件**あったらアラート送信する**Frequency**という監視ルールタイプを使用していました。
これではサービスごとのリクエスト数に応じて閾値を変えたりと運用が大変なので、現在は、**指定した期間内**にクエリとマッチするイベントが**N%**あったらアラートを送信する**Percentage Match**を採用しています。

# 導入
## ElastAlert
### インストール
早速、ElastAlertをインストールします。
Elasticsearchは、運用しているバージョンにあわせてインストールしてください。
弊社ではElasticsearch 5系を運用しているため、以下のコマンドでインストールします。

```shell-session
$ sudo pip install "elasticsearch>=5.0.0" elastalert
$ elastalert -h
usage: elastalert [-h] [--config CONFIG] [--debug] [--rule RULE]
...
```

インストールはこれで完了です。

### 共通設定
次にグローバルコンフィグファイルを作成します。
--configオプションに定義するだけなので、どこに作成してもいいのですが、ほかの運用メンバーが迷わないように/etc/elastalert/config.ymlに作成します。

##### 設定ファイル

```yaml:/etc/elastalert/config.yml
# The Elasticsearch hostname for metadata writeback
# Note that every rule can have its own Elasticsearch host
es_host: elasticsearch.example.com

# The Elasticsearch port
es_port: 443

# Connect with TLS to Elasticsearch
use_ssl: True

# This is the folder that contains the rule yaml files
# Any .yaml file will be loaded as a rule
rules_folder: /var/lib/elastalert/rules

# How often ElastAlert will query Elasticsearch
# The unit can be anything from weeks to seconds
run_every:
  minutes: 2

# ElastAlert will buffer results from the most recent
# period of time, in case some log sources are not in real time
buffer_time:
  minutes: 5

# The index on es_host which is used for metadata storage
# This can be a unmapped index, but it is recommended that you run
# elastalert-create-index to set a mapping
writeback_index: elastalert_status

# If an alert fails for some reason, ElastAlert will retry
# sending the alert until this time period has elapsed
alert_time_limit:
  days: 1
```

##### 設定ファイル(解説)

| キー | 内容 |
|:--|:--|
| es_host | 接続するElasticsearchのエンドポイントを指定します。 |
| es_port | 接続するElasticsearchのポート番号を指定します。  |
| use_ssl | SSL接続を選択する場合はTrueを指定します。 |
| rules_folder | 監視ルールを格納するパスを指定します。<br>ElastAlert実行時、このパスに格納されたルールが再帰的に評価されます。 |
| run_every | クエリをElasticsearchに投げる間隔(監視の間隔)を指定します。 |
| buffer_time | ログをバッファリングする期間を指定します。<br>**1分間隔**の監視で**過去5分間**に異常値が**10回**出現したらアラート送信するというケースでの利用が想定されます。 |
| writeback_index | ElastAlertのシステム用Index名を指定します。 |
| alert_time_limit | アラート送信に失敗した場合に再送をリトライする期間を指定します。 |

### ElastAlertのシステム用Indexの作成
このままではElasticsearch上にElastAlertの監視ログなどを書き出せないため、以下のコマンドを実行してElasticsearchにIndexを作成します。

```shell-session
$ elastalert-create-index --config /etc/elastalert/config.yml
```

### 監視ルールファイル
先ほどグローバルコンフィグファイルで指定したパスに監視ルールファイルを格納していきます。
今回は監視対象Indexの更新状況・レスポンス遅延・HTTPステータスコードのエラー(以降、HTTP5XXエラー)を検知する3つのルールを設置します。

監視ルールのファイル自体は同一階層にまとめてもいいのですが、弊社では監視ルールごとに階層を分けてその下にサービス名ごとのファイルを置くことで管理しています。

```shell-session
$ sudo mkdir -p /var/lib/elastalert/rules/{logforward,response,status-5XX}
```

以下に監視ルールのサンプルを掲載しますが、詳細は[公式のドキュメント](http://elastalert.readthedocs.io/en/latest/ruletypes.html)を参考にしてください。

#### 監視ルール - 監視対象Indexの更新状況

ElasticsearchのIndexが更新されなくなると根本が揺らぐため、Indexが更新されているかをチェックします。

##### 設定ファイル

```yaml:/var/lib/elastalert/rules/logforward/sample.yml
es_host: elasticsearch1.example.com
es_port: 443
use_ssl: True

index: access-log-*

name: No access log on hogehoge

type: flatline
threshold: 1
timeframe:
  hours: 1

timestamp_field: "time"

use_count_query: True
doc_type: access-log

alert:
- "email"
email:
- "hoge@example.com"
```

##### 設定ファイル(解説)

| キー | 必須 | 説明 |
|:--|:-:|:--|
| es_host | | グローバルコンフィグを上書きする場合に指定します。 |
| es_port | | 〃 |
| use_ssl | | 〃 |
| index | ○ | 監視対象のElasticsearchのIndexを指定します。 |
| name | ○ | 全監視ルールに対して一意な名前を指定します。 |
| type | | 監視ルールタイプを指定します。<br>flatlineは**イベント数**が**threshold**を下回った場合にアラートを送信します。 |
| threshold | ※ | アラートのトリガとなる閾値(イベント数の下限)を指定します。<br>※flatlineを指定した場合、必須。 |
| timeframe | ※ | 閾値監視の期間を指定します。<br>1時間を指定した場合、1時間以内にイベント数がthresholdを下回った場合にアラートを送信します。 |
| timestamp_field | | timestampを保持するフィールドを指定します。 |
| use_count_query | | Count APIを使用するかどうかを指定します。<br>Trueを指定するとElasticsearchのCount APIを使用します。 |
| doc_type | ※ | Indexのdoc_typeを指定します。<br>※use_count_queryを指定した場合、必須。 |
| alert | ○ | アラートの通知方法を指定します。 |
| email | ※ | アラートの通知先メールアドレスを指定します。<br>※alertにemailを指定した場合、必須。 |

#### 監視ルール - レスポンス遅延

##### 設定ファイル

```yaml:/var/lib/elastalert/rules/response/sample.yml
es_host: elasticsearch1.example.com
es_port: 443
use_ssl: True

index: access-log-*
doc_type: access-log

name: Response was delayed on hogehoge

type: percentage_match

match_bucket_filter:
- query_string:
    query: "response: >3000000 AND -path:(csv AND sitemap)"

max_percentage: 10

top_count_keys:
  - "host"
  - "agent"
  - "path"

timeframe:
  minutes: 5

timestamp_field: "time"

use_kibana4_dashboard: "https://kibana.example.com/#/discover/alert-response-hoge"

alert:
- "email"
email:
- "hoge@example.com"
```

##### 設定ファイル(解説)

| キー | 必須 | 説明 |
|:--|:-:|:--|
| match_bucket_filter | ※ | アラートとして引っ掛けたい条件を定義します。<br>query_stringにはKibanaのクエリと同じものが使えるため、そちらであらかじめ想定通り動作するか確認することをお勧めします。<br>※percentage_matchの場合、必須。 |
| max_percentage | ※ | **match_bucket_filter**にマッチするイベントの出現比率の上限を指定します。<br>総イベント数に対する比率がこの閾値を上回った場合にアラートを送信します。<br>※percentage_matchの場合、min_percentage, max_percentagのいずれかが必須。 |
| top_count_keys | | Indexの任意のキーを指定します。<br>**match_bucket_filter**にマッチするイベントのうち、**top_count_keys**に指定したキーをそれぞれ出現率の多い順に表示します。 |
| use_kibana4_dashboard | | アラートの本文にKibanaへのリンクを挿入します。 |

**flatlineと重複する説明は省略しています。**

##### アラート本文

以下のようなアラートが送られるようになります。

```text
Response was delayed on hoge.

Percentage violation, value: 10.5504587156 (min: None max : 10)

host.keyword:
xxx.yyy.zzz.aaa: 31
xxx.yyy.zzz.bbb: 13
xxx.yyy.zzz.ccc: 1
xxx.yyy.zzz.ddd: 1

agent.keyword:
Mozilla/5.0 (iPhone; CPU iPhone OS 10_0 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) Version/10.0 Mobile/14A346 Safari/602.1: 31
Mozilla/5.0 (iPhone; CPU iPhone OS 11_1_2 like Mac OS X) AppleWebKit/604.3.5 (KHTML, like Gecko) Mobile/15B202 YJApp-IOS jp.co.yahoo.ipn.appli/4.8.9: 13
Googlebot-Image/1.0: 1
Mozilla/5.0 (iPhone; CPU iPhone OS 11_1_2 like Mac OS X) AppleWebKit/604.3.5 (KHTML, like Gecko) Version/11.0 Mobile/15B202 Safari/604.1: 1

path.keyword:
/sample1.jpg: 2
/sample2.jpg: 2
/sample3.jpg: 2
/sample4.jpg: 2
/sample5.jpg: 2

kibana_link: https://kibana.example.com/#/discover/alert-response-hoge?_g=%28refreshInterval%3A%28display%3AOff%2Csection%3A0%2Cvalue%3A0%29%2Ctime%3A%28from%3A%272017-11-24T16%3A27%3A28.717549Z%27%2Cmode%3Aabsolute%2Cto%3A%272017-11-24T16%3A37%3A28.717549Z%27%29%29
num_hits: 436
num_matches: 1
percentage: 10.5504587156
time: 2017-11-24T16:32:28.717549Z
```

#### 監視ルール - HTTP5XXエラー

```yaml:/var/lib/elastalert/rules/status-5XX/sample.yml
es_host: elasticsearch1.example.com
es_port: 443
use_ssl: True

index: access-hoge-*
doc_type: access-log

name: HTTP status 5XX on hoge

type: percentage_match

match_bucket_filter:
- query_string:
    query: "code: [500 TO 599]"

max_percentage: 1

top_count_keys:
  - "host"
  - "agent"
  - "path"

timeframe:
  minutes: 5

timestamp_field: "time"

use_kibana4_dashboard: "https://kibana.example.com/#/discover/alert-5xx-hoge"
alert:
- "email"
email:
- "hoge@example.com"
```
---
クエリ以外は、レスポンスの監視とほぼ同じなので説明は割愛します。

### 検証
監視ルールの準備が完了したので検証してみます。
検証にはelastalert-test-ruleを使用します。

```shell-session
$ elastalert-test-rule --config /etc/elastalert/config.yml
```

実際にアラートを飛ばしてみたいときは、`--alert`をつけて先ほどのコマンドを実行してください。
検証でエラーが出なければ、下準備は完了です。


# 運用
ここからは実際に本番環境で運用する際に行ったことと、本番運用を開始してトラブったことを少しだけ紹介していきます。
## デーモン化
ElastAlert自体にデーモン化する機能がないため、[Supervisor](http://supervisord.org/)を使ってDaemon化してしまいます。

### Supervisor
#### インストール
まずはSupervisorをインストールします。

```shell-session
$ sudo pip install supervisor
```

#### 設定ファイルの作成
次に設定ファイルを用意します。
設定ファイルは、ElastAlertプロジェクトに用意されている[サンプル](https://github.com/Yelp/elastalert/blob/master/supervisord.conf.example)も参考にしてみてください。

```text:/etc/supervisord.conf
[unix_http_server]
file=/var/run/supervisor.sock

[supervisord]
logfile=/var/log/supervisord.log
logfile_maxbytes=1MB
logfile_backups=2
loglevel=warn
nodaemon=false
directory=%(here)s

[inet_http_server]
port = 127.0.0.1:9001

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///var/run/supervisor.sock

[program:elastalert]
command=elastalert --config /etc/elastalert/config.yml --verbose
process_name=elastalert
autorestart=true
startsecs=15
stopsignal=TERM
stopasgroup=true
killasgroup=true
redirect_stderr=false
stderr_logfile=/var/log/supervisord/elastalert_stderr.log
stderr_logfile_maxbytes=1MB
stderr_logfile_backups=5
```

#### Supervisorの起動

```shell-session
Supervisorの本体を起動します。
$ supervisord -c /etc/supervisord.conf
SupervisorからElastAlertをデーモンとして起動します。
$ supervisorctl start elastalert
```

#### supervisorのinitスクリプト設置

サーバーが再起動されても大丈夫なように、[こちらの記事](https://qiita.com/yushin/items/15f4f90c5663710dbd56)を参考にinitスクリプトを設置します。

```shell-session
$ sudo curl -o /etc/rc.d/init.d/supervisord https://raw.githubusercontent.com/Supervisor/initscripts/master/redhat-init-equeffelec
$ sudo chmod 755 /etc/rc.d/init.d/supervisord
$ sudo chkconfig --add supervisord
```

これでElastAlertのデーモン化が完了しました。
とりあえずの運用はここまででも大丈夫かと思います。

# ElastAlert導入の注意点
## ElasticsearchのCPUリソースに気を付けましょう
当初のElasticsearchの運用では、障害分析などのタイミングでログを見るために使うくらいしか想定していなかったため、Amazon Elasticsearch Service(ES)のt2.microを使っていました。
ElasticAlertを導入し毎分クエリが投げられるようになったことで、CPUリソースを食い潰す事象が発生したため、ESのスケールアップとクエリの実行間隔(run_every)の見直しをしています。
どの程度リソースを消費するのか見えにくいと思うので、導入初期はCPUリソースの監視をしっかり行ったほうがいいと思います。

## top_count_keysのサマリが実は・・・。
監視期間の全数からサマリが作られてしまうため、「監視期間中、HTTPステータスコード5XXになった」イベントの上位5件を見ているつもりが、監視期間の全数に対する上位5件が表示されてしまい、まったく見当はずれな情報になっています。
なので、「監視期間中、HTTPステータスコード5XXになった」イベントのURIを確認したいという場合、監視ルールをカスタマイズする必要があります。
この監視ルールをカスタマイズしたスクリプトも用意していますが、今回の記事が相当なボリュームになってしまったので、次の機会に紹介します。
==2017/12/23 追記==
ElastAlertの監視ルールをカスタマイズする方法を別記事にまとめました。
サンプルコードも記載しているので、参考にしてみてください。
[ElastAlertの監視ルールを拡張してすこしだけ幸せになる
](https://qiita.com/ihsiek/items/e8e89b274e99e48067a2)

# 参考
- [ElastAlert GitHub](https://github.com/Yelp/elastalert)
- [ElastAlert Document](http://elastalert.readthedocs.io)
- [supervisor Document](http://supervisord.org/)
- [Supervisor/initscripts GitHub](https://github.com/Supervisor/initscripts)
- [Supervisorで簡単にデーモン化](https://qiita.com/yushin/items/15f4f90c5663710dbd56)

# 最後に
[エイチームライフスタイルアドベントカレンダー2017](https://qiita.com/advent-calendar/2017/ateam-lifestyle)の2日目、いかがでしたでしょうか。
明日は[株式会社エイチームライフスタイル](http://life.a-tm.co.jp/)でフロントエンドからインフラまで幅広く担当している @maa0984 さんが ReactNative に関する記事を書いてくれるらしいので、お楽しみに。

---

[株式会社エイチームライフスタイル](http://life.a-tm.co.jp/)では、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。興味を持たれた方はぜひエイチームグループ採用サイトを御覧ください。
http://www.a-tm.co.jp/recruit/