Elasticsearchのマシン・ラーニング異常検知の動きを理解する(2) [アラート編]

シリーズ記事
[Elasticsearchのマシン・ラーニング異常検知の動きを理解しよう(1)](https://qiita.com/nobuhikosekiya/items/e0fbb7792a9b70284c57)
[Elasticsearchのマシン・ラーニング異常検知の動きを理解する(2) [アラート編]](https://qiita.com/nobuhikosekiya/items/59cb1705dac98860c5dc)
[Elasticsearchのマシン・ラーニング異常検知の動きを理解する(3) [変更設定編]](https://qiita.com/nobuhikosekiya/items/ee8be363fe6e8faa75f1)

# はじめに
[前回](https://qiita.com/nobuhikosekiya/items/e0fbb7792a9b70284c57)の記事ではElasticsearchに含まれるサンプルデータを使い異常検知ジョブを動かしてみました。
２回目の記事としてジョブの設定についての解説も考えましたが、設定については本番で運用しながら行う方が効率的であるため、今回は早速に異常検知を本番で使っていくためのやり方について書きたいと思います。

今回はアラートについてです。以下のことを実施します。
* 異常検知を使ってアラートを出す
* アラートをメール通知 (or Slackなどに通知）
* Webhookを使い、アラートからKibanaのケースを自動作成

本記事はElastic Stackバージョン8.8を使っています。Elatic Cloud環境を使っていますが、自分でインストールするセルフマネージドでも同じように動作できるはずです。

# 実施ステップ
### アラートの作成
では早速、前回の記事で作成したジョブに対してのアラートルールを作成してきましょう。
Anomaly DetectionジョブのActionsメニューからCreate alert ruleでアラートルールを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/cf4917c3-483a-59f8-2b1d-675bea9cf990.png)

次のアラートルールの設定画面では以下のポイントを設定しました。
* Name ...　任意のアラート名
* Result type ... これはAnomaly（異常）の判定対象の選択です。これはアラート設定の中で重要な設定であって設定する際は違いを把握した方が良いのですが、一旦ここではデフォルトのBucketを選択します。これについて詳しくは、以下のWebに書いてあります。
https://www.elastic.co/guide/en/machine-learning/current/ml-ad-view-results.html
https://www.elastic.co/blog/machine-learning-anomaly-scoring-elasticsearch-how-it-works
* Severity ... アラートが出やすくなるように中間の50に下げます。
* Check every ... アラート条件に合致するかのチェック頻度。デフォルトの1分にする必要はないので、10mくらいに変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/3863d3c6-c380-ebc8-696e-9b6e14c825c4.png)

### 通知アクションを選択
そのまま設定の下の方でアラートに対してアクションを設定します。色々なものがありますが、今回はメール通知、Slack通知とWebhookを使います。アクションは何個でも追加できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/0af3f105-b721-7998-3e3a-b7a21fc53b37.png)

### メール通知アクション
Elastic Cloudを使っている場合は、準備されたSMTPコネクターですぐにメールを送ることができます。（ただし、受信できるメールアドレスを一度Elastic Cloud管理画面のContactsで設定する必要があります。[こちら](https://www.elastic.co/guide/en/cloud/current/ec-organizations-notifications-domain-allowlist.html)を参照ください）
Emailコネクターを選択します。自分のメールアドレスと件名を設定し、メール本文はデフォルトのメッセージがありますので、それをそのまま使います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/79a03aea-270e-c104-95fe-3df7c60acd3b.png)

### Slack通知アクション
Slackのアクションを追加したら、一度Add connectorからコネクタを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/a6935635-a648-2698-def8-9966d8e83b60.png)

続いて、メッセージ本文と通知タイミングを設定します。Slack用のはアラート条件に合致する間は毎回アラートを受けるように設定してみました。(Action frequency=On check intervals)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/3402580d-87ee-77db-29c5-002903558bc2.png)


### WebhookアクションでKibanaケースを作成
将来的にはMLのアラートから自動的なKibanaケース作成もできるようになるかもしれませんが、現在は実装されていないので、Webhookを使って実装してます。
Kibanaケースを生成するためのWebhookコネクタを設定します。こちらの[Kibana API](https://www.elastic.co/guide/en/kibana/current/cases-api-create.html)にリクエストを送るようにします。
WebhookをActionsから選択し、Add connectorでコネクタを一度作成します。
* URL ... Elastic Cloudであればポート9243で終わるKibana Endpointを設定し、/api/casesをパスとして追記ます。ローカルでKibanaを動かしているような場合は例えばhttp://localhost:5601/api/casesとなります。
* Authentication ... 権限が十分あるElasticsearchのユーザーを使います。今回はデモなのでスーパーユーザーであるelasticユーザーを使います。
* Headerには以下の２点が必須となります。
 kbn-xsrf: true
 Content-Type: application/json

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/a3c6742f-6857-4942-b531-81aa80983a6a.png)

Webhookコネクタを作ったら、このWebhookに送信する情報と、通知タイミングを設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/7596dfb1-c01f-bb02-73ce-2b6f42a589ec.png)

以下のJSONをそのままコピペしてください。重要なのはdescriptionにアラートの詳細を入れていることです。詳細はメールアクションの本文で自動設定されていたものをそのままコピーしています。”””のトリプルクオートで囲むようにしてください。
```:Body
{
  "owner" : "cases",
  "settings" : {
    "syncAlerts" : true
  },
  "title" : "Anomaly alert {{rule.name}}",
  "description" : """[{{rule.name}}] Elastic Stack Machine Learning Alert:
- Job IDs: {{context.jobIds}}
- Time: {{context.timestampIso8601}}
- Anomaly score: {{context.score}}

{{context.message}}

{{#context.topInfluencers.length}}
 Top influencers:
 {{#context.topInfluencers}}
 {{influencer_field_name}} = {{influencer_field_value}} [{{score}}]
 {{/context.topInfluencers}}
{{/context.topInfluencers.length}}

{{#context.topRecords.length}}
 Top records:
 {{#context.topRecords}}
 {{function}}({{field_name}}) {{by_field_value}}{{over_field_value}}{{partition_field_value}} [{{score}}]. Typical: {{typical}}, Actual: {{actual}}
 {{/context.topRecords}}
{{/context.topRecords.length}}

{{! Replace kibanaBaseUrl if not configured in Kibana }}
[Open in Anomaly Explorer]({{{kibanaBaseUrl}}}{{{context.anomalyExplorerUrl}}})
""",
  "tags" : [ "test" ],
  "connector": {
    "id": "none",
    "name": "none",
    "type": ".none",
    "fields": null
  }
}

```
上記の設定が完了したらSaveにて、ジョブlow_request_rateに対するアラート設定を終了します。

2つ目の異常検知ジョブ response_code_ratesに対してもほぼ同じようにアラートルールを作成しましょう。詳細は省きます。変更する箇所は名前の設定だけで、他は同じですが、Result TypeだけはInfluencerをここでは選択しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/719abf2c-1137-19b8-5172-50786dcabb38.png)

# アラートが出るようにサンプルデータと異常検知ジョブを編集する
１回目の記事で使用したKibana Sample Data Logsを今回も使います。データをクリーンにするために、Homeのサンプルデータの追加画面からRemove -> Addとやってください。
現在の筆者の環境ではこのように見えています。毎日15:00~19：００にかけてログの件数が増えていく状況です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/635012d5-2c82-eb5d-e260-726cda9dbd90.png)

ここからアラートをテストするためのデータに対する変更を行っていきたいと思います。
***この操作は対象のデータが多いと負荷がかかるので、実際に自分のデータで行う場合は開発環境で実施した方がいいです。***

#### 1つ目の仕掛け ... HTTPステータスコードの変更
KibanaのDev Toolsを開き、以下のupdate by queryのコードを使ってある程度適当に決めて17:00-19:00の2時間のデータのResponseコードを200 から 503に変更しました。日時は自分の環境に合わせて変更ください。
```
POST /kibana_sample_data_logs/_update_by_query?refresh
{
  "query": {
    "bool": {
      "must": [],
      "filter": [
        {
          "range": {
            "timestamp": {
              "format": "strict_date_optional_time",
              "gte": "2023-06-14T17:00:00+0900",
              "lte": "2023-06-14T19:00:00+0900"
            }
          }
        }
      ],
      "should": [],
      "must_not": []
    }
  },
  "script": {
    "source": "ctx._source.response = 503"
  }
}
```

#### 2つ目の仕掛け ... ログの削除
Delete by Queryを使い、その後の2時間の19:00-21:00はログをDeleteします。（サーバからログが生成されなくなったというストーリーです）
```
POST /kibana_sample_data_logs/_delete_by_query?refresh
{
  "query": {
    "bool": {
      "must": [],
      "filter": [
        {
          "range": {
            "timestamp": {
              "format": "strict_date_optional_time",
              "gte": "2023-06-14T19:00:00+0900",
              "lte": "2023-06-14T21:00:00+0900"
            }
          }
        }
      ],
      "should": [],
      "must_not": []
    }
  }
}
```

#### 3つ目の仕掛け ... データのタイムスタンプをずらす
テストデータでアラートのテストをするときは、過去のデータだったりすると、アラートのテストに使えません。データ投入時にタイムスタンプを未来日付に編集するのもアリですが、もっと簡単なのはデータ投入後にタイムスタンプを好きに調整することです。
ここではデータ全体のタイムスタンプを後ろにずらし、これから障害が送るかのようにデータを調整します。
過去の時間帯のデータを使いたい場合、timestampに時間足す(plusHours）ことで可能です。将来の時間帯のデータを使いたい場合、timestampの時間を減らす(minusHours）ことで可能でアラート発生タイミングを近くに持ってくることができます。以下のリクエストをDev Toolsで実行してください。
```
POST /kibana_sample_data_logs/_update_by_query?refresh
{
  "script": {
    "source": "ctx._source['@timestamp'] = ZonedDateTime.parse(ctx._source['@timestamp']).plusHours(12)"
  }
}
```


#### 異常検知ジョブの初期化
前回の記事で使った異常検知ジョブは一度リセットで初期化しましょう。
その後にまたDatafeedで過去データの学習と、これからの継続的なデータの読み込みをするようにします。

まずlow_request_rateジョブに対して設定します。

ジョブがまだopenの場合は一度Datafeedを停止し、ジョブもcloseしてください。その後Resetをします。このようにProcessed recordsが0の初期状態にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/142babd2-fbe3-0f7a-d7ed-59cc8ac07f1c.png)

次に、ActionsメニューからStart datafeedをクリックし、Start timeはbegginning of data、End timeはNo end time (Real-time search)を指定してStartしてください。Real-time searchにしないとこの後アラートは上がりませんのでご注意ください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/9028d524-8812-c33c-18c5-d9ed3cc5b837.png)

ジョブの右隅のチャートアイコンをクリックしてSingle Metric Viewerを開くと、先ほど時間をずらしたデータに対して学習されたのがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/b11d12ff-a4c0-669e-9285-33f684e16375.png)

response_code_rateジョブも同様にDatafeedを設定してください。

あとは事象の発生タイミングが来るまで待ちましょう。

# アラートの確認
無事にアラートメールが飛んできました。このような感じです。詳しく読むと、ステータス503のカウントが通常より多く発生し、アノマリースコア95を記録したとあります。また、ステータス200は通常よりも減っていることも書いてあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/94604187-3b1a-89be-1158-c38a4a53f817.png)
上のメールは10:02AM(JST)に受信しました。メール本文にあるアラートのタイムスタンプは00:00:00UTC(=9:00JST)となっていますが、これは実際は9:00~10:00 JSTのデータに対するアラートなので、実質最新のログデータに対して2分のラグでアラートが飛んできています。アラートのチェック間隔は今回10分にしていたので、2分のラグはこの10分の範囲内ということで納得できます。

メールに埋め込まれたリンクを開くと、KibanaのAnomaly Explorerの該当箇所に飛ぶことができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/aab77860-3d06-410c-f501-2219f0fc870c.png)

２つ目のジョブのアラートも仕掛け通り２時間後に来ました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/8f3b7936-e2d1-3cf2-183e-2644f9d8039b.png)

CaseもWebhookで作成されました。CaseはKibanaのStack Management配下のメニューから確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/b9b58171-ba06-ebe7-1676-126794afb777.png)

KibanaのCases機能は簡易的なケースマネジメント機能ですが、発生したアラートを誰がチェックしたかを漏れなく把握するには使えるのではないでしょうか。確認した人がコメントを残し、ケースをIn ProgressやCloseにする感じです。本格的な運用では、サードパーティのチケット管理機能に連携すると良いです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/8de28e16-65b8-7456-73c8-021341b81109.png)

最後、Slackの通知はこんな感じになります。（別日に撮り直したアラートなので今回のと内容が少し違います）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/c04fd916-c4d5-be05-8135-f26b1c44e972.png)

ちなみにスマホでSlackのAnomaly Explorerリンクを開くというこういう風に確認が可能でした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/cc6b73ea-5750-cf54-f581-f4e38bd08fa1.png)

# 終わり
異常検知を運用に乗せるために必要なアラートの設定の基本について見ていきました。
最初はどうしても異常検知の精度も荒いので、しばらくはSlackなどで多めにアラートを受信し、様子を観察して調整していくのが良いのではないでしょうか。
便利なことにElasticでは監視対象のデータを自由にいじることができるので、今回のようにテストデータをうまく使ってアラートの動作を詳しく見ていくことが可能です。（これができないと、アラートのテストも一苦労ですよね。特にMLを使うようなアラートは。）
アラートについての細かい設定や動作についてはまた別の機会にブログにしたいと思います。