Open Distro for Elasticsearchの異常検出(Anomaly Detection)をDockerを使ってローカルで試してみた

Amazon Elasticsearch Serviceで使用できる異常検出機能をローカルでDockerを使って試してみました。
2020.11.17

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

CX事業本部のうらわです。

Amazon Elasticsearch Serviceではバージョン7.4から異常検出(Anomaly Detection)の機能を使うことができるようになりました。

面白そうな機能なので実際にAWSでAmazon Elasticsearch Serviceを起動してデータを入れて検証…とやっているとドメイン構築やアクセスポリシーの設定等が必要で少し手間がかかるため、Dockerを使ってローカルマシンで検証してみました。

Open Distro for Elasticsearchについて

Amazon Elasticsearch Serviceの異常検出機能はAWSが公開・管理しているOpen Distro for Elasticsearchによって動作しています。

そのため、今回はElastic社が提供しているElasticsearchのDockerイメージではなく、Amazon Elasticsearch Serviceと同じ異常検出のプラグインがインストールされているOpen Distro for Elasticsearchのイメージを利用します。

Elastic社が開発しているElasticsearchとOpen Distro Elasticsearchの違いについては以下記事をご参照ください。

異常検出機能自体は以下の記事に解説がありますので本記事では割愛します。

環境構築

検証は以下の環境で進めます。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.6
BuildVersion:   19G2021

$ node -v
v12.18.3

$ npm -v
6.14.6

$ docker --version
Docker version 19.03.13, build 4484c46d9d

$ docker-compose --version
docker-compose version 1.27.4, build 40524192

本記事で紹介しているコードは以下のGitHubリポジトリに格納してあります。

https://github.com/urawa72/odfe-sample

Elasticsearch/KibanaのDockerによる環境構築はOpen Distro Elasticsearch公式ドキュメントのdocker-compose.ymlのサンプルをそのまま使用すると簡単にできます。

https://opendistro.github.io/for-elasticsearch-docs/docs/install/docker/#sample-docker-compose-file

ただし、今回はより手軽に検証するため、デフォルトで有効になっているSSLやログイン等のセキュリティ系のプラグインを無効にします。

この方法についても公式ドキュメントに手順が記載されています。

https://opendistro.github.io/for-elasticsearch-docs/docs/security/configuration/disable/

まずはセキュリティ系プラグインを削除したDockerイメージを作成します。この際、以下のkibana.ymlを使用します。

kibana.yml

---
server.name: kibana
server.host: "0"
elasticsearch.hosts: http://localhost:9200

Dockerfile

FROM amazon/opendistro-for-elasticsearch-kibana:1.11.0
RUN /usr/share/kibana/bin/kibana-plugin remove opendistro_security
COPY --chown=kibana:kibana kibana.yml /usr/share/kibana/config/

ビルドしておきます。

docker build -t kibana-no-security .

イメージがビルドできたら、サンプルのdocker-compose.ymlを修正します。以下のハイライト箇所が公式ドキュメントからの変更点です。

docker-compose.yml

version: '3'

services:
  odfe-node1:
    image: amazon/opendistro-for-elasticsearch:1.11.0
    container_name: odfe-node1
    environment:
      - cluster.name=odfe-cluster
      - node.name=odfe-node1
      - discovery.seed_hosts=odfe-node1,odfe-node2
      - cluster.initial_master_nodes=odfe-node1,odfe-node2
      - bootstrap.memory_lock=true # along with the memlock settings below, disables swapping
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM
      - opendistro_security.disabled=true # セキュリティプラグインの無効化
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536 # maximum number of open files for the Elasticsearch user, set to at least 65536 on modern systems
        hard: 65536
    volumes:
      - odfe-data1:/usr/share/elasticsearch/data
    ports:
      - 9200:9200
      - 9600:9600 # required for Performance Analyzer
    networks:
      - odfe-net

  odfe-node2:
    image: amazon/opendistro-for-elasticsearch:1.11.0
    container_name: odfe-node2
    environment:
      - cluster.name=odfe-cluster
      - node.name=odfe-node2
      - discovery.seed_hosts=odfe-node1,odfe-node2
      - cluster.initial_master_nodes=odfe-node1,odfe-node2
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - opendistro_security.disabled=true # セキュリティプラグインの無効化
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - odfe-data2:/usr/share/elasticsearch/data
    networks:
      - odfe-net

  kibana:
    image: kibana-no-security # セキュリティプラグインを削除したイメージを指定
    container_name: odfe-kibana
    ports:
      - 5601:5601
    expose:
      - "5601"
    environment:
      ELASTICSEARCH_URL: http://odfe-node1:9200 # https を http に
      ELASTICSEARCH_HOSTS: http://odfe-node1:9200 # https を http に
    networks:
      - odfe-net

volumes:
  odfe-data1:
  odfe-data2:

networks:
  odfe-net:

起動確認

docker-composeでコンテナを起動します。

docker-compose up -d

Elasticsearchが起動しているかどうかはAPIで確認することができます。

$ curl -X GET http://localhost:9200/
{
  "name" : "odfe-node1",
  "cluster_name" : "odfe-cluster",
  "cluster_uuid" : "SrK7wcBZRRW1fMHyS0lDkQ",
  "version" : {
    "number" : "7.9.1",
    "build_flavor" : "oss",
    "build_type" : "tar",
    "build_hash" : "083627f112ba94dffc1232e8b42b73492789ef91",
    "build_date" : "2020-09-01T21:22:21.964974Z",
    "build_snapshot" : false,
    "lucene_version" : "8.6.2",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}

Kibanaはブラウザでhttp://localhost:5601にアクセスして確認します。ちなみに、セキュリティ系のプラグインが有効のままの場合は最初にログイン画面が表示されます。

データ投入

今回はBulk用のファイルの生成とElasticsearchへのデータ投入用のスクリプトをTypeScriptで作成しました。

データのフォーマットは以下のような適当な時系列データとします。

valueは20〜30のランダム値(小数点第三位まで)です。この値の範囲から大きく外れた値が投入されたら異常とする、というのを期待します。

{
  sequence: 1,
  value: 24.245,
  timestamp: "2020-11-17T03:18:32.159+09:00"
}

APIリファレンスのコードを参考にして5時間分(18000秒)のndjsonファイルを作成します。スクリプト実行時の5時間前からのデータを作成します。

https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/client-helpers.html

make-ndjson.ts

import fs from 'fs';
import { DateTime } from 'luxon';

const output = fs.createWriteStream('output.ndjson', 'utf8');

let date = DateTime.local().minus({ hours: 5 });
for (let i = 1; i <= 18000; i++) {
  date = date.plus(1000);
  const param = {
    sequence: i,
    value: Math.round((Math.random() * (35 - 20) + 20) * 10000) / 10000,
    timestamp: date.toISO(),
  };
  output.write(JSON.stringify(param) + '\n');
}

作成したndjsonファイルをインプットとしてElasticsearchに一括インデックスします。

bulk-index.ts

import * as Es from '@elastic/elasticsearch';
import { createReadStream } from 'fs';
import split from 'split2';

const client = new Es.Client({
  node: 'http://localhost:9200',
});

const bulkIndex = async () => {
  const result = await client.helpers.bulk({
    datasource: createReadStream('output.ndjson').pipe(split()),
    onDocument() {
      return {
        index: { _index: 'my-test' },
      };
    },
  });
  console.log(result);
};

bulkIndex().catch(console.log);

上記の二つのスクリプトを実行し、インデックスの状態を確認します。docs.countが18000になっていれば成功です。

yarn ts-node make-ndjson.ts
yarn ts-node bulk-index.ts

curl -X GET "http://localhost:9200/_cat/indices/my-test?v&pretty"

health status index   uuid                   pri rep docs.count docs.deleted store.size pri.store.size
green  open   my-test JDnu70MSTsOglW_EkXj7gQ   1   1      18000            0      1.8mb        899.3kb

Kibanaでdetectorを作成

Kibanaの左サイドバーメニューからAnomaly Detectorを選択し、新しいdetectorを作成します。

今回は以下のような設定としました。

大項目 中項目
Name and description Name my-test-detector
Data Source Index my-test
Data Source Timestamp field timestamp
Detector Operation Setting Detector interval 1
Detector Operation Setting Window delay 1

続いてmodelを作成します。下記画像のように設定しました。

そのままdetectorをスタートさせます。

detectorの初期化処理が始まります。

異常値を投入してみる

1秒ごとにElasticsearchにデータをインデックスするスクリプトを実行します。100回に1回、異常値としてvalueが100のデータを投入してみます。

create-doc.ts

import * as Es from '@elastic/elasticsearch';
import { DateTime } from 'luxon';

const client = new Es.Client({
  node: 'http://localhost:9200',
});

const createDoc = async () => {
  const sleep = () => new Promise((resolve) => setTimeout(resolve, 1000));
  const loop = true;
  let idx = 0;
  while (loop) {
    ++idx;
    let value = Math.round((Math.random() * (35 - 20) + 20) * 10000) / 10000;
    if (idx % 100 === 0) value = 100;
    const param = {
      sequence: idx,
      value: value,
      timestamp: DateTime.local().toISO(),
    };
    console.log('Create Doc:', JSON.stringify(param));
    await client.index({
      index: 'my-test',
      body: param,
    });
    await sleep();
  }
};

createDoc().catch(console.log);

スクリプトを起動して放置します。

yarn ts-node create-doc.ts

少し経つとdetectorの画面に異常検出の結果が表示されます。一応こちらの意図通りに動作しているように見えます。

なお、Anomaly gradeData confidenceについてはOpen Distro for Elasticsearchの公式ドキュメントに記載があります。

Anomaly grade is a number between 0 and 1 that indicates the level of severity of how anomalous a data point is. An anomaly grade of 0 represents “not an anomaly,” and a non-zero value represents the relative severity of the anomaly.The confidence score is an estimate of the probability that the reported anomaly grade matches the expected anomaly grade. Confidence increases as the model observes more data and learns the data behavior and trends. Note that confidence is distinct from model accuracy.

まとめ

設定値の違いなど細かい仕様まで理解しきれていませんが、ひとまずローカルで異常検出機能を試すことはできました。

Amazon Elasticsearch Serviceでドメインを作成してAnomaly Detectorを使う前に、ローカルでDockerを使ってテストしてみるのは有効だと思います。

今回は都合の良い自作の時系列データを使いましたが、次回は生データを使った際にどのように動作するのか試してみたいと思います。また、この異常検出の結果をAlert機能でSlack等に通知もできる(これもOpen Distro for Elasticsearchの機能)ので、こちらも試してみたいと思います。