この記事は Elastic stack Advent Calendarの12日目の記事となります。
最近、実務で Docker + Go言語 + Elasticsearch でAPIを作っているのですが、Elasticsearchの便利さはもちろん、BI環境の構築をする際にそのスムーズさに感動したので、今回はGo言語での開発を例にBI環境の構築をハンズオン形式で紹介します。Dockerで立ち上げや、ElasticsearchのCluster構成や、LogstashによるBI環境の立て方、Slackへの通知等を学びます。
リポジトリも一応乗っけときます。
https://github.com/po3rin/go-elastic-starter
今回作るもの & Stack紹介
Go言語 + Docker + Elastic Stack で簡単なBI環境を作ります。APIで出るログは Logstash で収集し、Slackへの通知 + Elasticsearch へのログ保存を行います。Elasticsearchに入ったデータはKibanaで可視化できるようにします。
Elasticsearch
Elasticsearch は Elastic 社が開発しているオープンソースの分散型RESTful検索/分析エンジンです。検索速度や分析柔軟性に優れています。
Kibana
Kibana も Elastic社が開発しているビジュアライゼーションのためのツール。Elasticsearchというデータベースに入っているデータを様々な形式で描画することができます。
Logstash
そしてこちらも Elastic社が提供するオープンソースのデータ収集エンジン。
リアルタイムのパイプライン処理で異なる種類のデータを統一的なフォーマットへ変換して任意の転送先へ送ることができる。
Log収集ツールとして使われているのをよくみる。
Elasticsearch + Kibana 環境を Dockerで立てる。
まずは上の形を目指しましょう。ここでKibanaも立てます。
このセクションでは下記の形のファイル構成になります。
go-elastic-starter
├── docker-compose.yml
├── elasticsearch
│ ├── Dockerfile
│ └── config
│ ├── elasticsearch.yml
│ └── log4j2.properties
└── kibana
└── Dockerfile
では早速、ElasticsearchとKibanaを立ち上げる準備をしましょう。
Elasticsearch クラスターの立ち上げ
Elasticsearchからです。今回は3つのクラスター構成を試してみます。Elasticsearchではクラスターというもの中にノードという単位でElasticsearchが立ちます。今回は共通の設定は設定ファイルで渡し、node別の設定は docker-compose.yml の環境変数で渡します。
ではまず、elasticsearchディレクトリを作って、その中に Dockerfile を作ります。
FROM docker.elastic.co/elasticsearch/elasticsearch:6.5.1
# # kuromojiをインストールする場合
# RUN elasticsearch-plugin install analysis-kuromoji
config ディレクトリを作ってそこに Elasticsearch 設定用ファイル elasticsearch.yml を作りましょう。
network.host: 0.0.0.0
# bootstrap.memory_lock: true
### Cluster settings
discovery.zen.minimum_master_nodes: 2
elasticsearch.yml では クラスタや各ノード等の設定が可能です。今回はDockerでElasticsearchのクラスターを組むので、各ノードの個別の設定はdocker-compose.ymlで渡すことができるので、ここでは一旦共通の設定のみ行います。
今回は 3つの master-eligible
と呼ばれるノードを立てます。ノードにはいろんな種類があるのですが、このノードは簡単に言うと Mastr になる候補になれるノードです。
discovery.zen.minimum_master_nodes
の値は (ノードの数 / 2) + 1
が推奨されています。例えば3つのmaster-eligibleノードでクラスタを組んだ際に、クラスタ内でネットワーク分断が起きた際、分断後のノード間で別々にクラスタを組み直してしまいます。ここの値は何個のノードが集まったらclusterを構成するかの値です。これで分断された1つのノードがmasterに昇格しなくなります。詳しい解説は下記が参考になります。
そしてログ出力に関する設定を行います。Elasticseardh では log4j2.properties というファイルを使います。
status = error
appender.console.type = Console
appender.console.name = console
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] %marker%m%n
rootLogger.level = info
rootLogger.appenderRef.console.ref = console
これでログの設定ができました。そして docker-compose.ymlに下記を追加します。
version: '3.6'
services:
# ...
elasticsearch:
build: ./elasticsearch
container_name: elasticsearch
environment:
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- node.name=es01
- cluster.name=go_elastic_starter_cluster
ports:
- '9200:9200'
- '9300:9300'
volumes:
# - esdata01:/usr/share/elasticsearch/data
- ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
- ./elasticsearch/config/log4j2.properties:/usr/share/elasticsearch/config/log4j2.properties
elasticsearch2:
build: ./elasticsearch
container_name: elasticsearch2
environment:
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- node.name=es02
- cluster.name=go_elastic_starter_cluster
- "discovery.zen.ping.unicast.hosts=elasticsearch"
volumes:
# - esdata02:/usr/share/elasticsearch/data
- ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
- ./elasticsearch/config/log4j2.properties:/usr/share/elasticsearch/config/log4j2.properties
elasticsearch3:
build: ./elasticsearch
container_name: elasticsearch3
environment:
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- node.name=es03
- cluster.name=go_elastic_starter_cluster
- "discovery.zen.ping.unicast.hosts=elasticsearch"
volumes:
# - esdata03:/usr/share/elasticsearch/data
- ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
- ./elasticsearch/config/log4j2.properties:/usr/share/elasticsearch/config/log4j2.properties
各ノードの設定は環境変数で渡します。ノードの名前やクラスター名を指定しています。(今回は省略していますが、ここでJVMヒープサイズの設定もできます。ファイルで渡すのであれば jvm.options
というファイルを作ります)
これで準備が整いました!立ち上げでみましょう。
# Elasticsearchクラスタ立ち上げ
$ docker-compose up -d
# 立ち上げ確認
$ curl "localhost:9200/_cat/health?v"
これでノード3台のクラスターが完成しました!node.total が3であることを確認してください。
そしてこの勢いでKibanaを立ち上げます。一旦コンテナを落としておきましょう。
$ docker-compose down
Kibana の立ち上げ
kibana ディレクトリを作って そこに Dockerfile を作りましょう。
FROM docker.elastic.co/kibana/kibana:6.5.1
# kibana にプラグインを入れる際にはここに記載
そして docker-compose.yml に追加!
version: '3.6'
services:
# ...
kibana:
build: ./kibana
links:
- elasticsearch
environment:
- ELASTICSEARCH_URL=http://elasticsearch:9200
ports:
- 5601:5601
今回は設定はデフォルトのままで良いでしょう。これで立ち上げます。
$ docker-compose up -d
http://localhost:5601 で Kibanaの画面が見れます。
これで Elasticsearch + Kibanaが立ち上がりました!
Logstash を立ち上げる
logstash ディレクトリを作り、Dockerfileを作りましょう。今回はSlackへの通知も行うので pluginを追加します。このセクションで下記のような構成ができます。
.
├── Makefile
├── README.md
├── docker-compose.yml
├── elasticsearch
│ ├── Dockerfile
│ ├── README.md
│ └── config
│ ├── elasticsearch.yml
│ └── log4j2.properties
├── kibana
│ └── Dockerfile
└── logstash
├── Dockerfile
└── logstash.conf
FROM docker.elastic.co/logstash/logstash:6.4.3
RUN logstash-plugin install logstash-output-slack
そして大事なlogstash.conf を作ります。先ほど作った Dockerfile
と同じ階層におきます。
ここで output を elasticsearch と slack に設定しています。
input {
tcp {
port => 5959
}
}
output {
elasticsearch {
hosts => ["http://elasticsearch:9200"]
}
slack {
url => ["{{ WEB_HOOK_URL }}"]
channel => ["{{ #CHANNEL_NAME }}"]
username => ["po3rin"]
icon_emoji => [":simple_smile:"]
format => ["%{message}"]
}
}
{{ WEB_HOOK_URL }}
と {{ #CHANNEL_NAME }}
は Slack API でアプリケーション登録して所得します。左のメニューの Incoming Webhooks から両方所得できます。
そして docker-compose.yml に Logstash に関する記載を追加します。
logstash:
build: ./logstash
volumes:
- ./logstash:/logstash_dir
command: logstash -f /logstash_dir/logstash.conf
links:
- elasticsearch
ports:
- "5959:5959"
- "9600:9600"
$ docker-compose up -d
これでLogstashも立ち上がりました。確認してみましょう。
curl localhost:9600
{"host":"03ad2934dd30","version":"6.4.3","http_address":"0.0.0.0:9600","id":"ccf108c4-aaf5-40ae-ac37-41866641e3de","name":"03ad2934dd30","build_date":"2018-10-31T00:19:35Z","build_sha":"17e7a50dfb0beb05f5338ee5a0e8338e68eb130b","build_snapshot":false}
いいですね。ここでも次のセクションのためにコンテナを一度落としましょう
$ docker-compose down
Go言語で作ったAPIサーバーから Logstash にログを送信する。
このセクションで最後の形まで持っていきます。
このセクションで最終的に下記のような構成になります。
.
├── Makefile
├── README.md
├── api_server
│ ├── Dockerfile
│ ├── go.mod
│ ├── go.sum
│ ├── logger
│ │ └── logger.go
│ ├── main.go
│ └── wait-for-logstash.sh
├── docker-compose.yml
├── elasticsearch
│ ├── Dockerfile
│ ├── README.md
│ └── config
│ ├── elasticsearch.yml
│ └── log4j2.properties
├── kibana
│ └── Dockerfile
└── logstash
├── Dockerfile
└── logstash.conf
Go言語の開発環境を整えましょう。Go1.11以上を準備してください。そしてapi_serverディレクトリを作り、go.modを作り、同階層にmain.goを作ります。一旦 logging は設定しません。
package main // import "api_server"
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(hello))
http.ListenAndServe(":8080", mux)
}
func hello(w http.ResponseWriter, r *http.Request) {
msg := "Hello Elastic Stack"
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, msg)
}
簡単ですね。がっつり開発していく前に開発用の Docker + fresh のホットリロード環境を作りましょう。
ここから解説しますが、詳しくは過去記事をご覧ください。
Go v1.11 + Docker + fresh でホットリロード開発環境を作って愉快なGo言語生活
Dockerfileを作ります。
FROM golang:1.11.1
WORKDIR /api_server
COPY . .
ENV GO111MODULE=on
RUN go get github.com/pilu/fresh
これでローカルでファイルを更新したらDockerコンテナ内でホットリロードできる環境が準備できました。起動コマンドの fresh
は docker-compose.yml
で宣言します。それではdocker-compose.yml
に記載を追加します。
# ...
version: '3.6'
services:
go_elastic_starter:
build: ./api_server
ports:
- '8080:8080'
volumes:
- ./api_server:/go/src/go_elastic_starter/api_server
command: fresh
早速立ち上げましょう。
$ docker-compose up -d
これでコンテナにserverが立ってます。
curl localhost:8080/
Hello Elastic Stack
これで開発環境が整いました!では本題の logger を設定しましょう。main.go
と同じ階層に logger/logger.go
を作ります。
package logger
import (
"github.com/bshuster-repo/logrus-logstash-hook"
"github.com/sirupsen/logrus"
)
// Log - log client.
var Log *logrus.Logger
func init() {
logger := logrus.New()
logstashHook, err := logrustash.NewHook("tcp", "logstash:5959", "from_logstash")
if err != nil {
panic(err)
}
logger.Hooks.Add(logstashHook)
Log = logger
}
// Debug - shorthand Log.Debug
func Debug(msg string) {
Log.Debug(msg)
}
// Info - shorthand Log.Info
func Info(msg string) {
Log.Info(msg)
}
// Warn - shorthand Log.Warn
func Warn(msg string) {
Log.Warn(msg)
}
// Error - shorthand Log.Error
func Error(msg string) {
Log.Error(msg)
}
使った logger パッケージは sirupsen/logrus
です。基本的な機能に加え logging 時に使える様々な Hook を提供しています。
https://github.com/sirupsen/logrus
今回は Hook として bshuster-repo/logrus-logstash-hook
を使います。これは Logstash へのログ送信を行ってくれます。他の logrus で使える Hook の一覧はこちらになります。
https://github.com/sirupsen/logrus/wiki/Hooks
それでは main.go に APIへ通知が来たことを伝える logging を追加しましょう。
package main // import "api_server"
import (
"fmt"
"net/http"
"api_server/logger"
)
func main() {
mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(hello))
http.ListenAndServe(":8080", mux)
}
func hello(w http.ResponseWriter, r *http.Request) {
msg := "Hello Elastic Stack"
// logging 追加 !!!!
logger.Info(msg)
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, msg)
}
これでGo言語の実装は終了しました。しかし、まだやることがあります。実は Logstash がリクエストを受けつける前に Go言語で作ったAPIが Logstash に繋ぎにいってしまいエラーになります。これを回避するために Logstash の起動を待つスクリプトを書く必要があります。api_server
ディレクトリにスクリプトを追加しましょう。
#!/bin/bash
## Or whatever command is used for checking logstash availability
until curl 'http://logstash:9600' > /dev/null; do
echo "Waiting for logtash..."
sleep 3;
done
# Start your server
fresh
このスクリプトは Logstash に接続を試み、もし、レスポンス200が返ってこなければ 3秒待って再度接続テストを行います。
そしてこれを docker-compose,yml
のコマンドに渡します。
# ...
go_elastic_starter:
build: ./api_server
ports:
- '8080:8080'
volumes:
- ./api_server:/api_server
links:
- logstash
# コマンドを差し替え
command: bash wait-for-logstash.sh
動作確認
これで全ての準備が整いました。起動してみましょう。
$ docker-compose up -d
これで API にアクセスしてみます。
curl localhost:8080/
Hello Elastic Stack
Slack にも API にアクセスがあった旨の通知が来ます。
当然 Kibana にも log のデータが可視化できます。
これで Go言語で APIサーバーを作る際の BI環境が整いました!
今後の為のオススメの Elasticsearch 勉強方法
これは僕がやっていた勉強方法ですが、Elasticsearch を学ぶ際は Kibana の Dev Tools + 公式Document が便利です。Kibana の 左メニューに Dev Tools があるのでここにドキュメントにある形をそのまま打てばクエリを走らせれます。
GET /_cat/health?v
もちろんドキュメントから curl としてもコピーできるのでローカルでも試せます!
そして、今回は紹介しませんでしたが、このままGo言語から Elasticsearch につないでデータを保存することも可能です。Go言語で Elasticsearch を扱う際には下記のパッケージが便利です。
https://github.com/olivere/elastic
まとめ
Elastic Stack も Docker のおかげで速攻で立ち上げることができて最高です。