Kubernetesの監視#2 Elastiecsearch & Kibana on ROOK Ceph

ROOK Ceph の永続ストレージを利用したアプリケーション利用としてログ分析基盤を導入してみたい。これまで [ROOK Ceph のブロックストレージを検証したメモ](https://qiita.com/MahoTakara/items/a1ed9811c790cd2a0d5f)のQiita記事でROOK Cephのストレージを構築して、永続ストレージを利用したアプリケーションとして[Kubernetesの監視#1 Promethus & Grafana on ROOK Ceph](https://qiita.com/MahoTakara/items/5c980f00250a89757020)でメトリックス監視を構築した。この記事では、ログ分析 エラスティックサーチ(Elastiecsearch) とブラウザUI キバナ (Kibana)を導入してみたい。


## Elastiecsearchとサンプルアプリの名前空間作成

Elastiecsearch や　Guestbook など複数のアプリケーションを Kubernetesクラスタへデプロイして運用する場合、必ず専用の名前空間を作成して、その中で動作させるのが良い。名前空間にリソース使用の上限を設定することができるためである。

ここでは名前空間を作成して、コンテキストと対応させて固定的に名前空間をアクセスできるようにする。

~~~
kubectl create ns elasticsearch
kubectl create ns guestbook
kubectl config set-context es --namespace=elasticsearch --cluster=kubernetes --user=kubernetes-admin
kubectl config set-context gs --namespace=guestbook --cluster=kubernetes --user=kubernetes-admin
kubectl config get-contexts
~~~

コンテキストを es に切り替えて、--namespace オプションが省略された場合でも、elasticsearchにターゲットが設定されるようにする。

~~~
kubectl config use-context es
kubectl config get-contexts
CURRENT   NAME                          CLUSTER      AUTHINFO           NAMESPACE
*         es                            kubernetes   kubernetes-admin   elasticsearch
          gs                            kubernetes   kubernetes-admin   guestbook
          kubernetes-admin@kubernetes   kubernetes   kubernetes-admin   
~~~


## Elastiecsearchのインストール

Elasticsearchのドキュメントには最新版バージョン8が表示されているが、まだGAとなっていない様なので インターネットで入手できる 7.5.2 を使用する。最近までクイックインストールガイドでは バージョン 6.8 が導入されていたが、これでは最近主流の `containerd` エンジン環境で動作させることができない。

HelmでStableを指定した場合のサーチ結果、6.8.2 では動作しないので、サーチのパラメータを変更する。

~~~
helm search repo stable/elasticsearch
NAME                         	CHART VERSION	APP VERSION	DESCRIPTION                                       
stable/elasticsearch         	1.32.2       	6.8.2      	Flexible and powerful open source, distributed ...
stable/elasticsearch-curator 	2.1.3        	5.7.6      	A Helm chart for Elasticsearch Curator            
stable/elasticsearch-exporter	2.2.0        	1.1.0      	Elasticsearch stats exporter for Prometheus       
~~~

リポジトリを追加して、以下の elastic/elasticsearch を利用してセットアップを進める。

~~~
helm repo add elastic https://helm.elastic.co
~~~

これにより、一番上に表示された 7.5.2 を利用できるようになった。

~~~
helm search repo elasticsearch
NAME                         	CHART VERSION	APP VERSION	DESCRIPTION                                       
elastic/elasticsearch        	7.5.2        	7.5.2      	Official Elastic helm chart for Elasticsearch     
stable/elasticsearch         	1.32.2       	6.8.2      	Flexible and powerful open source, distributed ...
stable/elasticsearch-curator 	2.1.3        	5.7.6      	A Helm chart for Elasticsearch Curator       
~~~

パラメータをファイルに抜き出して編集する。

~~~
helm inspect values  elastic/elasticsearch > elasticsearch-values.yaml
~~~

パラメータの修正箇所は、以下の diff の結果通り一箇所とした。

~~~
tkr@luigi:~/sandbox-3/elasticsearch$ diff -c elasticsearch-values.yaml elasticsearch-values.yaml.org 
*** elasticsearch-values.yaml	2020-01-26 21:25:07.434287399 +0900
--- elasticsearch-values.yaml.org	2020-02-10 16:45:43.829620681 +0900
***************
*** 82,88 ****
  
  volumeClaimTemplate:
    accessModes: [ "ReadWriteOnce" ]
-   storageClassName: rook-ceph-block  
    resources:
      requests:
        storage: 30Gi
~~~

パラメータファイルを指定してデプロイを実行する。ここでも前述のコンテキストが効いているので 名前空間を指定する必要はない。

~~~
helm install elasticsearch -f elasticsearch-values.yaml elastic/elasticsearch
NAME: elasticsearch
LAST DEPLOYED: Sun Jan 26 12:37:26 2020
NAMESPACE: elasticsearch
STATUS: deployed
REVISION: 1
NOTES:
1. Watch all cluster members come up.
  $ kubectl get pods --namespace=elasticsearch -l app=elasticsearch-master -w
2. Test cluster health using Helm test.
  $ helm test elasticsearch
~~~

上記のHelmの応答メッセージ通り、ポッドがすべて起動し終わったら、テストを実行しておくと良い。
これで、Elasticsearch本体のインストールは終了となる。ここまでの状態で PVC の確保量は、次の通りになった。

~~~
kubectl get pvc
NAME                                          STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
elasticsearch-master-elasticsearch-master-0   Bound    pvc-40c22ed0-183a-439e-a139-2806260dc4f9   30Gi       RWO            rook-ceph-block   3m37s
elasticsearch-master-elasticsearch-master-1   Bound    pvc-c1382774-007c-402a-bbd9-98de0cfe9256   30Gi       RWO            rook-ceph-block   3m37s
elasticsearch-master-elasticsearch-master-2   Bound    pvc-bf45fd8b-74c6-4a06-9727-1485df232134   30Gi       RWO            rook-ceph-block   3m37s
~~~


## Kibanaのインストール

次にUIのKibanaをインストールする。これまでと同様に Helmチャートからパラメータファイルを生成する。

~~~
helm inspect values  elastic/kibana > kibana-values.yaml
~~~

編集する箇所は、以下の差分のように、ClusterIP ー> NodePortに変更、nodePort番号をアサインする。

~~~
diff -c kibana-values.yaml kibana-values.yaml.org
*** kibana-values.yaml	2020-01-26 21:53:56.023471404 +0900
--- kibana-values.yaml.org	2020-02-10 16:55:22.029673918 +0900
***************
*** 3,9 ****
  elasticsearchURL: "" # "http://elasticsearch-master:9200"
  elasticsearchHosts: "http://elasticsearch-master:9200"
  
- 
  replicas: 1
  
  # Extra environment variables to append to this nodeGroup
--- 3,8 ----
***************
*** 78,86 ****
    type: "Recreate"
  
  service:
!   type: NodePort
    port: 5601
!   nodePort: 31920
    labels: {}
    annotations: {}
      # cloud.google.com/load-balancer-type: "Internal"
--- 77,85 ----
    type: "Recreate"
  
  service:
!   type: ClusterIP
    port: 5601
!   nodePort: ""
    labels: {}
    annotations: {}
      # cloud.google.com/load-balancer-type: "Internal"
~~~


パラメータを編集したファイル名をインプットにして、デプロイする。

~~~
helm install kibana --namespace elasticsearch -f kibana-values.yaml elastic/kibana
~~~

変更した通りNodePortでアクセス可能となっていることを確認する。

~~~
kubectl get svc 
NAME                            TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)             AGE
kibana-kibana                   NodePort    10.32.0.84    <none>        5601:31920/TCP      4m5s
~~~

これで Kibanaのポッドが起動したら　http://192.168.1.91:31920/app/kibana でアクセスすることができる。

![kibana1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/a2c2f55e-1992-9485-79b9-8f9bac85103b.png)

この時点ではログシッパーの設定がされていないため、ログの参照はできない。


## Elasticsearch Beatsとは

Elasticsearch Beatsは、データシッピング専用のツールで、何百、何千ものマシンからLogstashやElasticsearchにデータを転送できる。用途に合わせた下記のツールがある。

* Filebeat ログファイル転送
* Metricbeat メトリック
* Packetbeat ネットワークパケット
* Winlogbeat Windowsイベントログ
* Auditbeat 監査データ
* Heartbeat 稼働状態の監視
* Functionbeat サーバーレスシッパー

過去は、Elastiecsearchはログ分析基盤と言い換えていたが、Beatsの機能からメトリックスの監視にも利用できるということがわかる。

本記事では、Filebeat, Metricbeat, Auditbeatをセットアップする。


## Beatのインストール

Beatのインストールには、GitHubから7.5.2をクローンして利用する。それ以前のバージョンのBeatでは コンテナエンジン containerd に対応していないため、古いBeatsでは動作しない。 OpenShift4 を利用する場合コンテナエンジンは cri-o となるために注意が必要である。

~~~
git clone -b 7.5 https://github.com/elastic/beats
cd beats/deploy/kubernetes
ls -al
合計 56
drwxrwxr-x 6 tkr tkr 4096  1月 26 17:28 .
drwxrwxr-x 4 tkr tkr 4096  1月 26 16:14 ..
drwxrwxr-x 2 tkr tkr 4096  1月 26 16:14 .travis
-rw-rw-r-- 1 tkr tkr  575  1月 26 16:14 Makefile
-rw-rw-r-- 1 tkr tkr  323  1月 26 16:14 README.md
drwxrwxr-x 2 tkr tkr 4096  1月 26 16:14 auditbeat
-rw-rw-r-- 1 tkr tkr 4286  1月 26 17:27 auditbeat-kubernetes.yaml
drwxrwxr-x 2 tkr tkr 4096  1月 26 16:14 filebeat
-rw-rw-r-- 1 tkr tkr 4003  1月 26 17:27 filebeat-kubernetes.yaml
drwxrwxr-x 2 tkr tkr 4096  1月 26 16:14 metricbeat
-rw-rw-r-- 1 tkr tkr 8744  1月 26 17:28 metricbeat-kubernetes.yaml
~~~

このディレクトリにある３つのYAMLファイルを利用することで、インストールは完了するのだが、一つ課題がある。 これは、インストールしても、Elasticseachと繋がらないため動作しないとう課題である。その原因と対策について次に対応する。


## Beatの課題対応

filebeat-kubernetes.yamlを例に問題点について確認する。この中で spec.hostNetwork=trueが設定されている。これは


~~~file:filebeat-kubernetes.yaml
apiVersion: apps/v1
kind: DaemonSet
<中略>
    spec:
      serviceAccountName: filebeat
      terminationGracePeriodSeconds: 30
      hostNetwork: true　　<--- 注目
      dnsPolicy: ClusterFirstWithHostNet
      containers:
      - name: filebeat
        image: docker.elastic.co/beats/filebeat:7.5.2
        args: [
          "-c", "/etc/filebeat.yml",
          "-e",
        ]
        env:
	- name: ELASTICSEARCH_HOST
          value: elasticsearch-master.elasticsearch.svc.cluster.local
        - name: ELASTICSEARCH_PORT
          value: "9200"
~~~

ログ取得対象にはノードも含まれるため `hostNetwork: true` を設定する必要がある。そして、DNS名からアドレス解決として、K8sクラスタ内DNSとホストネットDNSをサーチする設定 「ClusterFirstWithHostNet」にしなければならない。しかし、この環境では、ノードのアドレスを外部DNSで解決できない。それが直接的な原因かわからないが、サービスが登録したDNS名 `elasticsearch-master.elasticsearch.svc.cluster.local` のIPアドレスを変換できない。以下にAPIドキュメントの翻訳を添付しておく。

* hostNetwork:ブール値
このポッドに要求されたホストネットワーキング。ホストのネットワーク名前空間を使用します。このオプションを設定する場合、使用するポートを指定する必要があります。デフォルトはfalseです。

* dnsPolicy: ストリング	
ポッドのDNSポリシーを設定します。デフォルトは「ClusterFirst」です。有効な値は「ClusterFirstWithHostNet」、「ClusterFirst」、「Default」または「None」です。DNSConfigで指定されたDNSパラメーターは、DNSPolicyで選択されたポリシーとマージされます。DNSオプションをhostNetworkとともに設定するには、DNSポリシーを明示的に「ClusterFirstWithHostNet」に指定する必要があります。

参考URL https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#podspec-v1-core

この問題として、ワーカノードの外部DNSを設定して、ノードのDNS名を解決できるようにするべきだが、少々大変なので、簡易的な解決策として、ElasticsearchのサービスのIPアドレスをマニフェストに記述することで解決する。

デプロイの度に、毎回 IPアドレスを設定するのはやってられないが、まず最終的な目的とする結果を得た後に考えたい。

## Beat接続先IPアドレスの取得とマニフェストの編集

Beatがログを転送するべきサーバーのIPアドレスは、次のように求めることができる。

~~~
kubectl get svc elasticsearch-master 
NAME                   TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)             AGE
elasticsearch-master   ClusterIP   10.32.0.3    <none>        9200/TCP,9300/TCP   61m
~~~

この`10.32.0.3`を前述の３つのファイルに設定する。編集箇所は、73行目 ELASTICSEARCH_HOST の値になる。

~~~file:filebeat-kubernetes.yaml抜粋
     72         env:
     73         - name: ELASTICSEARCH_HOST
     74           value: 10.32.0.3 ##elasticsearch-master.elasticsearch.svc.cluster.local
     75         - name: ELASTICSEARCH_PORT
     76           value: "9200"
     77         - name: ELASTICSEARCH_USERNAME
     78           value: <>
~~~

編集後にデプロイする。

~~~
kubectl apply -f filebeat-kubernetes.yaml
kubectl get pod -n kube-system -l k8s-app=filebeat
NAME             READY   STATUS    RESTARTS   AGE
filebeat-8j998   1/1     Running   0          60s
filebeat-jszwk   1/1     Running   0          60s
filebeat-t422c   1/1     Running   0          60s
~~~

同様にmetricbeat-kubernetes.yamlとauditbeat-kubernetes.yamlも編集してアプライする。


## Beatの問題の詳細

ELASTICSEARCH_HOSTにDNS名でシッピング先をセットした場合、以下のように lookup で no such host　のエラーとなる。

~~~
2020-01-26T04:08:33.186Z	WARN	transport/tcp.go:53	DNS lookup failure "elasticsearch-master.elasticsearch.svc.cluster.local": lookup elasticsearch-master.elasticsearch.svc.cluster.local: no such host
2020-01-26T04:08:34.335Z	ERROR	pipeline/output.go:100	Failed to connect to backoff(elasticsearch(http://elasticsearch-master.elasticsearch.svc.cluster.local:9200)): Get http://elasticsearch-master.elasticsearch.svc.cluster.local:9200: lookup elasticsearch-master.elasticsearch.svc.cluster.local: no such host
2020-01-26T04:08:34.335Z	INFO	pipeline/output.go:93	Attempting to reconnect to backoff(elasticsearch(http://elasticsearch-master.elasticsearch.svc.cluster.local:9200)) with 1 reconnect attempt(s)
~~~

しかしながら、DNSユーティリティのコンテナを使ってアドレスを確認すると、確かに登録されており、アドレスの解決ができることがわかる。つまり、Beatのポッドの設定に問題があることが確認できる。

~~~
tkr@luigi:~/sandbox-rook/beats/deploy/kubernetes$ kubectl exec -ti -n default dnsutils -- nslookup 10.32.0.34
34.0.32.10.in-addr.arpa	name = elasticsearch-master.elasticsearch.svc.cluster.local.
~~~



## Kibanaの設定と動作確認

左端に並ぶアイコンの一番したギヤーのアイコンをクリックすると、Beatからデータが転送されていることがわかる。このような状態になっていれば、各Beatからデータが転送されている。もし、表示がなければ、シッピング先のIPアドレスを再確認する。

![kibana2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/207f9666-1c2b-66b6-41ba-abe0b9242cc2.png)

次にDiscoverの設定を行っておく。左のアイコン列の上部方位磁針のアイコンをクリックしてFilebeatの頭文字にアスタリスクを設定する。そして、次へ進み、フィールドに `@timestamp` を設定する。

![kibana3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/379f355d-c5b3-c5c5-e5ba-354d21489876.png)

この設定により、ログの受信状態を取得できる。

![kibana4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/6da75b2d-5e57-38e7-4d08-8f2bca890dd3.png)


ログだけでなく、メトリックス情報も表示できる。

![kibana5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/4b51afc3-6cb3-a88d-b44b-597e2e2b288f.png)


## まとめ

ROOK Ceph でダイナミックプロビジョニングできる永続ストレージを利用して、ログ分析基盤 Elasticsearch と Kibana をデプロイすることができた。Beatは便利なツールで、Filebeatはワーカーノードにデプロイされたコンテナのファイルシステムを直接読み取るため、コンテナのログ収集のために、個別に設定することも不要となる。KibanaやElasticsearchは、とても便利なツールであり、このセットアップにフォーカスして記事を書いてみたい。

ここまでやってみて、一番辛いことは、適切に動作するOSSの組み合わせを探ることだ。また、OSSのプロジェクトは、それぞれ、非同期にプロジェクトを推進して、新しいバージョンやパッチを提供するため、常に注意を払っておかなければならない。また、脆弱性対応という点からも、OSのレベルアップを続けないければならない。ROOKは、このOSSのスタックをベースにしているストレージシステムである事から、安価に構築できる利点もあるが、維持するための運用が重要あることも忘れてはいけない。



## 番外 Guestbookのインストール

このゲストブックは、永続ストレージを利用するわけではないが、ログ出力テストのアプリケーションとして利用できる。

~~~
kubectl config use-context gs
kubectl apply -f https://k8s.io/examples/application/guestbook/redis-master-deployment.yaml
kubectl apply -f https://k8s.io/examples/application/guestbook/redis-master-service.yaml
kubectl apply -f https://k8s.io/examples/application/guestbook/redis-slave-deployment.yaml
kubectl apply -f https://k8s.io/examples/application/guestbook/redis-slave-service.yaml
kubectl apply -f https://k8s.io/examples/application/guestbook/frontend-deployment.yaml
kubectl apply -f https://k8s.io/examples/application/guestbook/frontend-service.yaml
kubectl get svc,po
~~~

全てのポッドがRunning状態となれば、準備完了となる。

~~~
NAME                   TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
service/frontend       NodePort    10.32.0.188   <none>        80:32213/TCP   12s
service/redis-master   ClusterIP   10.32.0.74    <none>        6379/TCP       50s
service/redis-slave    ClusterIP   10.32.0.194   <none>        6379/TCP       32s

NAME                                READY   STATUS    RESTARTS   AGE
pod/frontend-6cb7f8bd65-9kjhp       1/1     Running   0          22s
pod/frontend-6cb7f8bd65-wkrt5       1/1     Running   0          22s
pod/frontend-6cb7f8bd65-xt2mt       1/1     Running   0          22s
pod/redis-master-7db7f6579f-7pgjl   1/1     Running   0          60s
pod/redis-slave-7664787fbc-76zn9    1/1     Running   0          40s
pod/redis-slave-7664787fbc-qxg6x    1/1     Running   0          40s
~~~

GuestBookのウェブページは、NodePortで公開するので、上記`service/frontend`の30000番台のポート番号と仮想サーバーのLAN側IPアドレスを利用してアクセスする。以下は、コマンドで試した例であるが、パソコンのブラウザからアクセスをお勧めする。

~~~console:
$ curl http://192.168.1.91:31178
<html ng-app="redis">
  <head>
    <title>Guestbook</title>
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
<以下省略>
~~~