mattintosh note

どこかのエンジニアモドキの備忘録

Raspberry Pi で Elasticsearch と Kibana

AWS Elasticsearch Service をお試してで使ってみたけど用途に対してコスパが悪いので、余ってる Raspberry Pi 3 Model B で運用することにした。Elasticsearch と Kibana を1台の Raspberry Pi 3 Model B で稼働させるのは重いので現在は ASUS Tinker Board や PINE64 Rock64 4GB を使ったり、Kibana だけ手元の Ubuntu で実行していたりする。

各種バージョン

Elasticsearch や Kibana は依存関係のバージョンが厳しいので(例えば Node.js のバージョンが 8.15.0 だと動かない、など)今回は下記の通りに合わせる。Java は Elasticsearch、Node.js は Kibana で使用する。

  • Elasticsearch: 6.5.2
  • OpenJDK: 1.8.0
  • Kibana: 6.5.2
  • Node.js: 8.14.0
  • curator: 5.6.0

Elasticsearch のセットアップ

Debian 系で Elasticsearch を使う場合は DEB パッケージを使うか、MACOS/LINUX 用の TAR を解凍して使う方法がある。今回は保存用のストレージ等も分けるので TAR ファイルを使う方法にする。

Java Runtime Environment 8 をインストールする。特に Java で開発する予定もないのでヘッドレス版。

※そのうち直るかもしれないが、openjdk-8-jre-headless のインストール後処理でエラーが発生する。もう一度 openjdk-8-jre-headless をインストールすれば正常に終了する。

sudo apt-get update
sudo apt-get install openjdk-8-jre-headless

Elasticsearch をダウンロードする。

wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.5.2.tar.gz
tar xf elasticsearch-6.5.2.tar.gz

elasticsearch-6.5.2/config/jvm.optionsJVM のメモリ設定を変更する。Raspberry Pi 3 Model B はメモリが 1 GB なので 512 MB くらいにしておく。

#-Xms1g
#-Xmx1g
-Xms512m
-Xmx512m

Elasticsearch の設定を elasticsearch-6.5.2/config/elasticsearch.yml で行う。xpack.ml.enabled: false の設定が無いと起動しない。localhost 以外からのアクセスも許可するので transport.hosttransport.tcp.port を設定する。

xpack.ml.enabled: false
network.host: 0.0.0.0
http.port: 9200
transport.host: localhost
transport.tcp.port: 9300

Elasticsearch を起動する。起動まで数分かかる。

elasticsearch-6.5.2/bin/elasticsearch

localhost:9200 にアクセスして Elasticsearch が起動しているか確認する。Raspberry Pi では avahi-daemon が有効になっているのでクライアントが対応していれば raspberrypi.local:9200 でもアクセスできる

curl localhost:9200
{
  "name" : "Z0Eokyo",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "NV6kGw3iQBiF7voqb6tIcA",
  "version" : {
    "number" : "6.5.2",
    "build_flavor" : "default",
    "build_type" : "tar",
    "build_hash" : "9434bed",
    "build_date" : "2018-11-29T23:58:20.891072Z",
    "build_snapshot" : false,
    "lucene_version" : "7.5.0",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}

Kibana のセットアップ

Kibana には ARM 版が無いので Linux x86_64 版をダウンロードする。

wget https://artifacts.elastic.co/downloads/kibana/kibana-6.5.2-linux-x86_64.tar.gz
tar xf kibana-6.5.2-linux-x86_64.tar.gz

こちらもアクセス制限は設けないので kibana-6.5.2-linux-x86_64/config/kibana.ymlserver.host: 0.0.0.0 を設定する。

server.host: 0.0.0.0

Raspberry Pi などの ARM デバイスで Kibana を使う場合、同梱されている Node.js が x86_64 用だったりするので Node.js を別途用意する必要がある。ネットで一通り見た感じ「node/bin/node をバックアップして…」とか書かれているが、あまり好きな方法ではないかな。

kibana-6.5.2-linux-x86_64/bin/kibana を見てみると、自動で node ファイルを探すようになっているが、実行できなければ which で他の場所にある node ファイルを探しに行くようになっている。

#!/bin/sh
SCRIPT=$0

# SCRIPT may be an arbitrarily deep series of symlinks. Loop until we have the concrete path.
while [ -h "$SCRIPT" ] ; do
  ls=$(ls -ld "$SCRIPT")
  # Drop everything prior to ->
  link=$(expr "$ls" : '.*-> \(.*\)$')
  if expr "$link" : '/.*' > /dev/null; then
    SCRIPT="$link"
  else
    SCRIPT=$(dirname "$SCRIPT")/"$link"
  fi
done

DIR="$(dirname "${SCRIPT}")/.."
NODE="${DIR}/node/bin/node"
test -x "$NODE" || NODE=$(which node)
if [ ! -x "$NODE" ]; then
  echo "unable to find usable node.js executable."
  exit 1
fi

NODE_ENV=production exec "${NODE}" $NODE_OPTIONS --no-warnings "${DIR}/src/cli" ${@}

で、同梱されている node 実行ファイルは ARM 上では実行できない。

$ node/bin/node
-bash: node/bin/node: cannot execute binary file: Exec format error

同梱されている node は使わないので実行権限を削除する。

chmod -x kibana-6.5.2-x86_64/node/bin/node

https://nodejs.org/dist/v8.14.0/ からデバイスに合ったバイナリをダウンロードする。

wget https://nodejs.org/dist/v8.14.0/node-v8.14.0-linux-armv7l.tar.xz
tar xf node-v8.14.0-linux-armv7l.tar.xz

Node.js のインストールは下記のようなインストールスクリプトを使う方法もあるが、Kibana が Node.js のバージョンをチェックしているため、APT でアップデートが発生すると動かなくなってしまう可能性があり、Kibana との組み合わせではこの方法は向いていないと思われる。

curl -sL https://deb.nodesource.com/setup_8.x | bash -
apt-get install -y nodejs

node にパスを通して Kibana を起動する。

PATH=$HOME/node-v8.14.0-linux-armv7l/bin:$PATH kibana-6.5.2-x86_64/bin/kibana

Python でプログラムを書く

Python 3 用の PIP をインストールする。

sudo apt-get install python3-pip

今回はユーザ環境でしか使わないので ~/.config/pip/pip.conf を下記のように設定しておく。「GPIO にアクセスするのに root 権限必要なんじゃ!」っていうデバイスの倍は sudo でどうぞ。

[global]
user = true

PIP をアップグレードする。

pip3 install -U pip

PATH を通しておく。

PATH=$HOME/.local/bin:$PATH

Python 3 用の elasticsearch モジュールをインストールする。

pip3 install elasticsearch

まずは簡単に CPU の温度を投げるだけのプログラムを書いてみる。Pythonタイムゾーンを扱う場合、datetime.now(timezone(timedelta(hours=+9))) と書くのが最初は少々面倒に感じるが、慣れれば定型文のように感じる。

とりあえずデータを投げる場合

#!/usr/bin/env python3

from elasticsearch import Elasticsearch
from datetime import datetime, timedelta, timezone

es = Elasticsearch('localhost:9200')
with open('/sys/class/thermal/thermal_zone0/temp') as f:
    timestamp, cpu_temp = datetime.now(timezone(timedelta(hours=+9))), int(f.read())
body = {
    '@timestamp': timestamp.isoformat(),
    'temperature': cpu_temp,
}
response = es.index(index='foo', doc_type='_doc', body=body)
print(response)
{'_version': 1, '_type': '_doc', 'result': 'created', '_seq_no': 0, '_index': 'foo', '_shards': {'failed': 0, 'total': 2, 'successful': 1}, '_id': 'RFfzK2gBykMjt7Ru8Nx9', '_primary_term': 1}

インデックスのセッティングとマッピングを追加する場合はこんな感じ。実際には毎回データを投げるわけではなく elasticsearch.helpers を使って1秒間隔で取得したデータを60秒間隔でまとめて Elasticsearch に投げるようにしている。

セッティングとマッピングを行う場合

#!/usr/bin/env python3

from elasticsearch import Elasticsearch
from datetime import datetime, timedelta, timezone
import socket

hostname = socket.gethostname()
es = Elasticsearch('localhost:9200')

# CPU の温度を取得
with open('/sys/class/thermal/thermal_zone0/temp') as f:
    timestamp, cpu_temp = datetime.now(timezone(timedelta(hours=+9))), int(f.read())

# インデックス名の生成(logstash-%Y.%m.%d と同書式)
es_index = '-'.join([hostname, timestamp.strftime('%Y.%m.%d')])

# インデックス作成
if not es.indices.exists(index=es_index):
    es_setting = {
        'settings': {
            'number_of_shards'  : 1,
            'number_of_replicas': 0,
        }
    }
    es_mapping = {
        '_doc': {
            '_all': {
                'enabled': False,
            }
        }
    }
    es.indices.create(index=es_index, body=es_setting)
    es.indices.put_mapping(index=es_index, doc_type='_doc', body=es_mapping)

# ポストデータ作成
es_body = {
    '@timestamp': timestamp.isoformat(),
    'hostname'  : hostname,
    'name'      : 'temperature',
    'value'     : cpu_temp,
}

# ポスト
response = es.index(index=es_index, doc_type='_doc', body=es_body)

print(response)

elasticsearch.helpers で Bulk API

適当に書いたので真似しない方がいいかもしれない。

#!/usr/bin/env python3

from datetime import datetime, timedelta, timezone
from elasticsearch import Elasticsearch, helpers
from time import sleep
import socket

hostname = socket.gethostname()
es = Elasticsearch()

def getCpuTemp():
    with open('/sys/class/thermal/thermal_zone0/temp') as f:
        timestamp, temp = datetime.now(timezone(timedelta(hours=+9))), int(f.read())
    return timestamp, temp

data = []
while True:
    timestamp, cpu_temp = getCpuTemp()
    es_index = '-'.join([hostname, timestamp.strftime('%Y.%m.%d')])
    if not es.indices.exists(index=es_index):
        es.indices.create(index=es_index, body={'settings':{'number_of_shards': 1, 'number_of_replicas': 0}})
        es.indices.put_mapping(index=es_index, doc_type='_doc', body={'_doc':{'_all':{'enabled': False}}})
    source = {
        '@timestamp' : timestamp.isoformat(),
        'hostname'   : hostname,
        'temperature': cpu_temp,
    }
    data.append({
        '_index' : es_index,
        '_type'  : '_doc',
        '_source': source,
    })
    if len(data) >= 60:
        try:
            helpers.bulk(es, data)
            data = []
        except:
            pass
    sleep(1)

X-Pack Monitoring を無効にする

f:id:mattintosh4:20190108163654p:plain
Elasticsearch - X-Pack Monitoring

Monitoring を使うと細かくヘルスチェックデータを保存してくれるけど Raspberry Pi 3 Model B ではそれ自体が重い(CPU クロックが常に上がりっぱなし)。しかもインデックスのサイズが毎日 900 MB くらいになる。

クラスタを組んでいるわけでもないし、個人で使う分には必要ないので X-Pack Monitoring を無効にする。

elasticsearch-6.5.2/config/elasticsearch.yml

xpack.monitoring.enabled: false

家の外からのアクセス

家の中であればプライベート IP アドレスで問題ないんだけど、外でちょっと人に見せたりとかする場合に。めんどくさくて家には VPN 入れてないので SSH でやっている。SSH のローカルフォワードで Elasticsearch と Kibana を動かしている Raspberry Pi にそれぞれ接続している。

例えば Elasticsearch が入った Raspberry Pi192.168.1.1、Kibana が入った Raspberry Pi192.168.1.2 であれば下記のように ssh_config を設定すればよい。

Host home-bastion
    Hostname XXX.XXX.XXX.XXX
    Port XXXXX
    LocalForward 9200 192.168.1.1:9200
    LocalForward 5601 192.168.1.2:5601

これで手元の Ubuntu からは curllocalhost:9200 にアクセスすれば Elasticsearch に繋がるし、ブラウザで localhost:5601 にアクセスすれば Kibana に繋がる。