ElasticStack環境をDocker上で構築して、「コロナ」に関するツイートをタグクラウド化した話

# 構築環境

```bash
$ cat /etc/redhat-release
CentOS Linux release 8.1.1911 (Core)
```

# 完成したタグクラウド

![TagCloud.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266508/a619d128-c629-27e4-0992-6e42bb7830ef.png)


# ElasticStack環境構築

## Dockerインストール

今回はDocker Composeを利用して、ElasitcStack（ElasticSearch,Kibana,Logstash）環境を構築するため、まずはDockerをインストールした。Dockerのインストール手順に関しては、別途下記の記事で紹介しているため、こちらを参照ください。

[Dockerインストール手順](https://qiita.com/kenchan1193/items/78b58b5e293b7b5d5b05)

## Docker Composeインストール

Dockerのインストール完了後、続けてDocker Composeをインストールした。Docker Composeのインストール手順に関しても、別途下記の記事で紹介しているため、こちらを参照ください。

[Docker Composeインストール手順](https://qiita.com/kenchan1193/items/b28ff451c08d981460ec)

## 各種設定ファイルの作成

Docker ComposeでElasticStackを起動するために各種設定ファイルを作成した。docker-compose.ymlのみでさくっと起動してもよかったが、今後の拡張性を考えてDockerfileでビルドするように設計することで、プラグインのインストール等がしやすいように設定した。正直、Docker Compose使ってる意味・・・ってぐらいには、面倒なことしてるよね。笑

なお、各種設定ファイルの作成では、下記公式のドキュメントを参考にした。

* [ElasticSearch公式の設定手順](https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html)

* [Kibana公式の設定手順](https://www.elastic.co/guide/en/kibana/current/docker.html)

* [Logstash公式の設定手順](https://www.elastic.co/guide/en/logstash/current/docker.html)

### ディレクトリの構成

```
$ tree --charset=C
.
|-- docker-compose.yml
|-- elasticsearch
|   |-- config
|   |   `-- elasticsearch.yml
|   `-- Dockerfile
|-- kibana
|   |-- config
|   |   `-- kibana.yml
|   `-- Dockerfile
`-- logstash
    |-- config
    |   |-- jvm.options
    |   |-- log4j2.properties
    |   |-- logstash.yml
    |   |-- pipelines.yml
    |   `-- startup.options
    |-- Dockerfile
    `-- pipeline
        `-- twitter_coronavirus.conf
```

### 各種設定ファイルの作成

#### Docker Compose 設定ファイルの作成

```YML:./docker-compose.yml
version: '3'

services:
  elasticsearch:
    build: ./elasticsearch/
    container_name: elasticsearch
    environment:
      - node.name=elasticsearch
      - discovery.type=single-node
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms256m -Xmx256m"
    volumes:
      - elasticstack:/usr/share/elasticsearch/data
    ports:
      - 9200:9200
      - 9300:9300
    networks:
      - elasticstack
    restart: always

  kibana:
    build: ./kibana/
    container_name: kibana
    volumes:
      - ./kibana/config/kibana.yml:/usr/share/kibana/config/kibana.yml
    ports:
      - 5601:5601
    networks:
      - elasticstack
    restart: always

  logstash:
    build: ./logstash/
    container_name: logstash
    volumes:
      - ./logstash/pipeline/:/usr/share/logstash/pipeline/
      - ./logstash/config/:/usr/share/logstash/config/
    networks:
      - elasticstack
    restart: always

volumes:
  elasticstack:
    external: true

networks:
  elasticstack:
    external: true
```

##### メモリ不足でES_JAVA_OPTSを設定した

```
- "ES_JAVA_OPTS=-Xms256m -Xmx256m"
```

メモリ1GB、CPU2コアの貧弱サーバで環境構築したため、ElasticSearchを起動した直後、メモリ不足でエラーになってしまった。

ElasticSearchのデフォルトJVMヒープは1GBに設定されており、物理RAMの50%以下に設定することが推奨されている。また、最小ヒープと最大ヒープはそれぞれ同一の値で設定することが推奨されている。

一旦、JVMのヒープメモリを128mで設定してみたが、起動できなかった。チューニングの結果、256mで今回は設定した。

メモリチューニングに関しては、以下の公式ドキュメントを参考にした。

公式ドキュメント：[ヒープサイズの設定](https://www.elastic.co/guide/en/elasticsearch/reference/current/heap-size.html)

また、ElasticSearchメモリがスワップされないように設定することを公式が推奨しているため、合わせて以下の設定を追加した。

```
- bootstrap.memory_lock=true
```

スワップ無効化の設定に関しては、以下の公式ドキュメントを参考にした。

公式ドキュメント：[スワップの無効化](https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-configuration-memory.html)

上記でメモリ不足の対策を実施したとはいえ、サーバリソースが限られており、コンテナダウンの恐れがあったため、コンテナが落ちても再起動するように、以下の設定を追加しておいた。

```
restart: always
```

#### Docker Volumeの作成

```
// Docker Volumeを作成
$ docker volume create elasticstack
elasticstack

// Docker Volumeが作成されていることを確認
$ docker volume ls
DRIVER              VOLUME NAME
local               elasticstack
```

#### Docker Networkの作成

```
// Docker Networkを作成
$ docker network create elasticstack
0150989c56b3d681bdf03a3377443a04af4e1e0c5e74d1e22154f9a486267fd6

// Docker Networkが作成されていることを確認
$ docker network ls elasticstack
NETWORK ID          NAME                DRIVER              SCOPE
26ff84c0b3aa        bridge              bridge              local
1d9f8f8c280a        elasticstack        bridge              local
a83727599da4        host                host                local
0b0822de74c4        none                null                local
```

#### ElasticSearch 設定ファイルの作成

```Dockerfile:./elasticsearch/Dockerfile
FROM docker.elastic.co/elasticsearch/elasticsearch:7.6.2

COPY ./config/elasticsearch.yml /usr/share/elasticsearch/config/elasticsearch.yml

# Install kuromoji plugin
RUN elasticsearch-plugin  install analysis-kuromoji
```

日本語をより高度に解析するために、kuromojiプラグインをインストールする設定とした。これをインストールすることで、日本語の文字列であっても、しっかりと要素分解してくれるようになるため、このプラグインは適用することをおすすめする。
※ 公式ドキュメント：[kuromojiプラグイン](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html)

```YML:./elasticsearch/config/elasticsearch.yml
network.host: 0.0.0.0

## x-pack settings
xpack.security.enabled: false
# Monitoring function is free for one cluster only.
xpack.monitoring.enabled: true
xpack.graph.enabled: false
xpack.watcher.enabled: false
```

ElasticSearchのデフォルト設定では、ループバックアドレス（127.0.0.1）のみにバインドしている。単一ノードで構成する場合は、この設定で問題ないのだが・・・今回はDockerのコンテナ上で動作させているため、ループバックアドレスはElasticSearchのコンテナ自身を示してしまう。つまり、KibanaコンテナやLogstashコンテナとの通信が行えなくなってしまう。そこで、全ての通信を受け入れるために、「network.host: 0.0.0.0」を設定した。
※ 公式ドキュメント：[network.host](https://www.elastic.co/guide/en/elasticsearch/reference/current/network.host.html)

X-PackはElastic社が提供する有償のプラグインであるが、Monitoringだけは無償で提供しているとのことで、設定を有効にしてみた。MonitoringはKibanaのGUIからElasticStackの監視を行うことができる機能である。X-Packの機能の中で唯一無償で利用することができる機能である。ただし、無償利用は1クラスタまでに限るので注意が必要だ。X-PACKについては、ここでは深く触れないので詳しくは調べてみてください。...というか金無くて、触れれない。笑

#### Kibana 設定ファイルの作成

```Dockerfile:./kibana/Dockerfile
FROM docker.elastic.co/kibana/kibana:7.6.2
```

```YML:./kibana/config/kibana.yml
server.name: kibana
server.host: "0"
elasticsearch.hosts: http://elasticsearch:9200
```

Kibanaのデフォルト設定では、バックエンドサーバの設定がlocalhostになっている。単一ノードで構成する場合は、この設定で問題ないのだが・・・今回はDockerのコンテナ上で動作させているため、バックエンドサーバの指定がコンテナ自信を示してしまう。そこで、外部からの通信を受け付けるために、「server.host: "0"」を設定した。
※ 公式ドキュメント：[server.host](https://www.elastic.co/guide/en/kibana/current/settings.html)

#### Logstash 設定ファイルの作成

```Dockerfile:./logstash/Dockerfile
FROM docker.elastic.co/logstash/logstash:7.6.2
```

```YML:./logstash/config/jvm.options
## JVM configuration

# Xms represents the initial size of total heap space
# Xmx represents the maximum size of total heap space

-Xms1g
-Xmx1g

################################################################
## Expert settings
################################################################
##
## All settings below this section are considered
## expert settings. Don't tamper with them unless
## you understand what you are doing
##
################################################################

## GC configuration
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly

## Locale
# Set the locale language
#-Duser.language=en

# Set the locale country
#-Duser.country=US

# Set the locale variant, if any
#-Duser.variant=

## basic

# set the I/O temp directory
#-Djava.io.tmpdir=$HOME

# set to headless, just in case
-Djava.awt.headless=true

# ensure UTF-8 encoding by default (e.g. filenames)
-Dfile.encoding=UTF-8

# use our provided JNA always versus the system one
#-Djna.nosys=true

# Turn on JRuby invokedynamic
-Djruby.compile.invokedynamic=true
# Force Compilation
-Djruby.jit.threshold=0
# Make sure joni regexp interruptability is enabled
-Djruby.regexp.interruptible=true

## heap dumps

# generate a heap dump when an allocation from the Java heap fails
# heap dumps are created in the working directory of the JVM
-XX:+HeapDumpOnOutOfMemoryError

# specify an alternative path for heap dumps
# ensure the directory exists and has sufficient space
#-XX:HeapDumpPath=${LOGSTASH_HOME}/heapdump.hprof

## GC logging
#-XX:+PrintGCDetails
#-XX:+PrintGCTimeStamps
#-XX:+PrintGCDateStamps
#-XX:+PrintClassHistogram
#-XX:+PrintTenuringDistribution
#-XX:+PrintGCApplicationStoppedTime

# log GC status to a file with time stamps
# ensure the directory exists
#-Xloggc:${LS_GC_LOG_FILE}

# Entropy source for randomness
-Djava.security.egd=file:/dev/urandom

# Copy the logging context from parent threads to children
-Dlog4j2.isThreadContextMapInheritable=true
```

jvm.optionsの設定は何も変更する必要はなかったが、docker-compose.ymlでjvm.optionsを配置するパスをコンテナにマウントする設定にしてしまったため、デフォルトの設定ファイルを配置している。正直かっこいい設定方法じゃなかったが、一旦許容した。

```properties:./logstash/config/log4j2.properties
status = error
name = LogstashPropertiesConfig

appender.console.type = Console
appender.console.name = plain_console
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c]%notEmpty{[%X{pipeline.id}]} %m%n

appender.json_console.type = Console
appender.json_console.name = json_console
appender.json_console.layout.type = JSONLayout
appender.json_console.layout.compact = true
appender.json_console.layout.eventEol = true

rootLogger.level = ${sys:ls.log.level}
rootLogger.appenderRef.console.ref = ${sys:ls.log.format}_console
```

log4j2.propertiesの設定は何も変更する必要はなかったが、docker-compose.ymlでlog4j2.propertiesを配置するパスをコンテナにマウントする設定にしてしまったため、デフォルトの設定ファイルを配置している。正直かっこいい設定方法じゃなかったが、一旦許容した。

```YML:./logstash/config/logstash.yml
http.host: "0.0.0.0"
xpack.monitoring.elasticsearch.hosts: [ "http://elasticsearch:9200" ]
```

```YML:./logstash/config/pipelines.yml
- pipeline.id: coronavirus
  path.config: "/usr/share/logstash/pipeline/twitter_coronavirus.conf"
  queue.type: persisted
```

現時点では、同一プロセスで複数のパイプラインを実行することはないが、今後の拡張性を考えてマルチパイプライン設定をしておいた。
※ 公式ドキュメント[Multiple Pipelines](https://www.elastic.co/guide/en/logstash/current/multiple-pipelines.html)

```options:./logstash/config/startup.options
################################################################################
# These settings are ONLY used by $LS_HOME/bin/system-install to create a custom
# startup script for Logstash and is not used by Logstash itself. It should
# automagically use the init system (systemd, upstart, sysv, etc.) that your
# Linux distribution uses.
#
# After changing anything here, you need to re-run $LS_HOME/bin/system-install
# as root to push the changes to the init script.
################################################################################

# Override Java location
#JAVACMD=/usr/bin/java

# Set a home directory
LS_HOME=/usr/share/logstash

# logstash settings directory, the path which contains logstash.yml
LS_SETTINGS_DIR=/etc/logstash

# Arguments to pass to logstash
LS_OPTS="--path.settings ${LS_SETTINGS_DIR}"

# Arguments to pass to java
LS_JAVA_OPTS=""

# pidfiles aren't used the same way for upstart and systemd; this is for sysv users.
LS_PIDFILE=/var/run/logstash.pid

# user and group id to be invoked as
LS_USER=logstash
LS_GROUP=logstash

# Enable GC logging by uncommenting the appropriate lines in the GC logging
# section in jvm.options
LS_GC_LOG_FILE=/var/log/logstash/gc.log

# Open file limit
LS_OPEN_FILES=16384

# Nice level
LS_NICE=19

# Change these to have the init script named and described differently
# This is useful when running multiple instances of Logstash on the same
# physical box or vm
SERVICE_NAME="logstash"
SERVICE_DESCRIPTION="logstash"

# If you need to run a command or script before launching Logstash, put it
# between the lines beginning with `read` and `EOM`, and uncomment those lines.
###
## read -r -d '' PRESTART << EOM
## EOM
```

startup.optionsの設定は何も変更する必要はなかったが、docker-compose.ymlでstartup.optionsを配置するパスをコンテナにマウントする設定にしてしまったため、デフォルトの設定ファイルを配置している。正直かっこいい設定方法じゃなかったが、一旦許容した。

```conf:./logstash/pipeline/twitter_coronavirus.conf
input {
  twitter {
    consumer_key => "Your Twitter App’s consumer key."
    consumer_secret => "Your Twitter App’s consumer secret."
    oauth_token => "Your oauth token."
    oauth_token_secret => "Your oauth token secret."
    keywords => ["コロナ","自粛","緊急事態宣言"]
    ignore_retweets => true
    full_tweet => true
  }
}

output {
  elasticsearch {
    hosts => ["http://elasticsearch:9200/"]
    index => "twitter_coronavirus"
  }
}
```
TwitterAPIを使用して、「コロナ」「自粛」「緊急事態宣言」のワードで検索されたツイートを取得し、最終的にElasticSearchへ出力されるように設定した。

上記３ワードがホット過ぎて、データ量が膨大になり過ぎたため、ignore_retweetsを有効にすることで、リツイートを無視するように設定した。

## ElasticStackの起動

Docker Composeによって、ElasticSearch,Kibana,Logstashのコンテナを起動する。

```
$ docker-compose up -d
Starting elasticsearch ... done
Starting logstash      ... done
Starting kibana        ... done
```

コンテナの起動に成功したことを念のため確認してみる。

```
$ docker ps
CONTAINER ID        IMAGE                        COMMAND                  CREATED              STATUS              PORTS                                            NAMES
7f58541a00bb        elasticstack_elasticsearch   "/usr/local/bin/dock…"   About a minute ago   Up 59 seconds       0.0.0.0:9200->9200/tcp, 0.0.0.0:9300->9300/tcp   elasticsearch
c4a64c7c5bee        elasticstack_logstash        "/usr/local/bin/dock…"   About a minute ago   Up 59 seconds       5044/tcp, 9600/tcp                               logstash
1df52608efec        elasticstack_kibana          "/usr/local/bin/dumb…"   About a minute ago   Up 59 seconds       0.0.0.0:5601->5601/tcp                           kibana
```

コンテナの起動に成功しているため、今度は各種サービスの動作を確認してみる。

### ElasticSearchの動作確認

HTTP レスポンスステータスコードが200で返却されているし、構成詳細もしっかりと出力されているから問題なさそうだ。

```
$ curl -v localhost:9200
* Rebuilt URL to: localhost:9200/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9200 (#0)
> GET / HTTP/1.1
> Host: localhost:9200
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: application/json; charset=UTF-8
< content-length: 549
<
{
  "name" : "elasticsearch",
  "cluster_name" : "elasticsearch-cluster",
  "cluster_uuid" : "0p2uJ5q3TQay0HkcI6I9kw",
  "version" : {
    "number" : "7.6.2",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "ef48eb35cf30adf4db14086e8aabd07ef6fb113f",
    "build_date" : "2020-03-26T06:34:37.794943Z",
    "build_snapshot" : false,
    "lucene_version" : "8.4.0",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}
* Connection #0 to host localhost left intact
```

### Kibanaの動作確認

http://localhost:5601/ にアクセスして、しっかりとKibanaのホーム画面が表示されたから問題なさそうだ。

![kibanaTop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266508/90d1a4b4-a92d-55b8-cbd0-28621aa14e7e.png)


### Logstashの動作確認

以下の手順でHello Worldすることができるが・・・今回はLogstashからElasticSearchにインデックスが取り込まれていることが確認できてしまったので、動作確認をスキップしてしまった。しっかりと動作確認したい方はこちらを参照ください。

公式手順：[Logstash Hello World](https://www.elastic.co/guide/en/logstash/current/first-event.html)

今回の場合、しっかりとElasticStackの起動に成功していれば、KibanaのGUI上のIndex Management画面から下記のようにインデックスが作成されていることが確認できる。

![IndexManagement.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266508/56e751e8-6d9b-22c1-a82a-8b361777c1d8.png)


## Visualizationsの作成

あとは直感的に画面を操作してVisualizationsを作成して、完成！！

![TagCloud.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266508/172b3e36-22b9-01f1-24de-514c5da17f79.png)