GKE上にElasticsearchとcerebro環境を構築する

この記事は [ただの集団 AdventCalendar PtW.2019](https://gw-advent.9wick.com/calendars/21) の5日目の記事です。
前回はhajimeniさんの[プログラミングする上で避けるべき命名パターン - はじめに。](http://hajimeni.hatenablog.jp/entry/2019/04/30/000000)でした。

# TL;DR
ElasticsearchをGKE上に構築する方法と、やってみて気づいた注意点を書きます。
また、cerebroでのノード監視と、ESのノード1台を落とした場合に新ノードが作成されること(Self-healing)も検証します。

## 前提知識
- Elasticsearchを複数ノードで構築したことがある
- GKEのチュートリアルをこなし、Kubernetesでアプリをデプロイしたことがある
- kubectlがローカルで使える

# 全体構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30677/ed474e7c-dd18-8efc-dcee-da9ba063c430.png)



# ES構築手順
## クラスタの作成
デフォルトのn1-standard-1だとESのメモリ不足になるので、n1-standard-2に変更します。

```bash
$ gcloud container clusters create es-cluster --machine-type=n1-standard-2 --num-nodes=2
```

## StorageClassの作成
Elasticsearchのように状態を持つcontainerの場合、containerのデータを永続化する仕組みが必要になります。まずはデータを保存する先としてStorageClassというオブジェクトを作成します。

#### es-ssdという名前のStorageClassを定義:

```yaml:storage.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: es-ssd
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-ssd
  zone: asia-northeast1-b
```

- `provisioner: kubernetes.io/gce-pd`
  - GCEのPersistent Diskを使用する設定。
- `type: pd-ssd`
  - デフォルトはハードディスクですが、高速化のためGCEの提供するSSDを使用します。

#### StorageClassオブジェクトの作成:

```bash
$ kubectl apply -f storage.yaml
```

## Serviceの作成
ESにHTTPで外部からアクセスするために２つのServiceを定義します。

- ESのノード同士を接続するためのネットワークを提供するes-cluster
- 外部からESのエンドポイントにアクセスするためのes-endpoint

```yaml:service.yaml
apiVersion: v1
kind: Service
metadata:
  name: es-cluster
  labels:
    service: elasticsearch
spec:
  clusterIP: None
  ports:
  - port: 9200
    name: serving
  - port: 9300
    name: node-to-node
  selector:
    service: elasticsearch
---
apiVersion: v1
kind: Service
metadata:
  name: es-endpoint
  labels:
    service: elasticsearch
  annotations:
    cloud.google.com/load-balancer-type: Internal
spec:
  type: LoadBalancer
  selector:
    service: elasticsearch
  ports:
  - protocol: TCP
    port: 9200
```

- `clusterIP: None`
  - Noneとすることで、HeadlessServiceとして起動します
- `type: LoadBalancer`
  - LoadBalancerにすると、GCPのロードバランサを作成し、外部IPアドレスを露出します。
- annotation: `cloud.google.com/load-balancer-type: Internal`
  - ロードバランサを作成すると外部からアクセスできますが、こちらのannotationを指定すると同サブネットからのアクセスのみ許可します。今回は内部のcerebroからのみアクセスします。

#### Serviceの作成：

```bash
$ kubectl apply -f service.yaml
```

## ElasticsearchのStatefulSetの作成
状態を持つPodをデプロイする場合、StatefulSetというコントローラを使用します。代わりにDeploymentとしてもESのデプロイは可能ですが、Pod再生成時にデッドロックになる挙動のため使用が推奨されていません。
([参考](https://cloud.google.com/kubernetes-engine/docs/concepts/persistent-volumes#deployments_vs_statefulsets))

```yaml:elasticsearch-stateful-set.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: elasticsearch
  labels:
    service: elasticsearch
spec:
  serviceName: es-cluster
  replicas: 3
  selector:
    matchLabels:
      service: elasticsearch
  template:
    metadata:
      labels:
        service: elasticsearch
    spec:
      terminationGracePeriodSeconds: 300
      # ESを動かすための諸々の初期設定
      initContainers:
      - name: fix-the-volume-permission
        image: busybox
        command:
        - sh
        - -c
        - chown -R 1000:1000 /usr/share/elasticsearch/data
        securityContext:
          privileged: true
        volumeMounts:
        - name: data
          mountPath: /usr/share/elasticsearch/data
      - name: increase-the-vm-max-map-count
        image: busybox
        command:
        - sysctl
        - -w
        - vm.max_map_count=262144
        securityContext:
          privileged: true
      - name: increase-the-ulimit
        image: busybox
        command:
        - sh
        - -c
        - ulimit -n 65536
        securityContext:
          privileged: true
      containers:
      - name: elasticsearch
        image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.2.4
        ports:
        - containerPort: 9200
          name: http
        - containerPort: 9300
          name: tcp
        resources:
          requests:
            memory: 2Gi
          limits:
            memory: 2Gi
        env:
          - name: cluster.name
            value: elasticsearch-cluster
          - name: node.name
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
          - name: discovery.zen.ping.unicast.hosts
            value: es-cluster
        volumeMounts:
        - name: data
          mountPath: /usr/share/elasticsearch/data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes:
        - ReadWriteOnce
      storageClassName: es-ssd
      resources:
        requests:
          storage: 5Gi

```

- `name: discovery.zen.ping.unicast.hosts value: es-cluster`
  - ESのdiscoveryはKubernetesではServiceの名前を入れるだけで探すことができました。

- `volumeClaimTemplates:`
  - 先程のStorageClassオブジェクトが作成されただけでは、まだ実態としての容量が確保されていません。volumeClaimTemplatesを使うことで実態としての容量を確保します。

#### ESのStatefulSetの作成：

```bash
$ kubectl apply -f elasticsearch-stateful-set.yaml
```

## cerebroの作成
cerebroはESのノード状態をリアルタイムに監視したり、RESTAPIの実行、GUIでの設定変更が行える便利ツールです。

以下でcerebroのDeploymentとServiceを定義します。

```yaml:cerebro.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cerebro
  labels:
    service: cerebro
spec:
  replicas: 1
  selector:
    matchLabels:
      service: cerebro
  template:
    metadata:
      labels:
        service: cerebro
    spec:
      terminationGracePeriodSeconds: 300
      containers:
      - name: cerebro
        image: lmenezes/cerebro
        ports:
        - containerPort: 9000
          name: http
---
apiVersion: v1
kind: Service
metadata:
  name: cerebro
  labels:
    service: cerebro
spec:
  type: LoadBalancer
  ports:
  - port: 9000
  selector:
    service: cerebro
```

#### cerebroの作成：

```bash
$ kubectl apply -f cerebro.yaml
```

#### cerebroのロードバランサのIPを確認：

```
$ kubectl get svc -o wide
NAME          TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)             AGE       SELECTOR
cerebro       LoadBalancer   10.51.254.133   34.85.105.164   9000:32654/TCP      7m        service=cerebro
es-cluster    ClusterIP      None            <none>          9200/TCP,9300/TCP   9m        service=elasticsearch
es-endpoint   LoadBalancer   10.51.246.125   10.146.0.21     9200:32653/TCP      9m        service=elasticsearch

```

#### EXTERNAL-IP:9000でcerebroにアクセス:

ブラウザでhttp://{cerebroのEXTERNAL-IP}:9000


#### ノード確認
cerebroのテキストボックスにhttp://{es-endpointのEXTERNAL-IP}:9200を入力し、overviewの画面でESのノードを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30677/0723cf17-c3aa-389d-3e18-b864f9bd4bb8.png)

無事ノードを確認できたら完了です。


# 検証
簡単な検証としてpodを1つ消してもESのアクセスができるか確認します。

まず、適当なindexを作成します。cerebroのメニューの`rest`から行くとESのAPIが手軽に叩けます。

```
PUT twitter
```


以下のように適当なドキュメントを複数個作成します。

```
POST twitter/_doc
{
    "user" : "kimchy",
    "post_date" : "2009-11-15T14:12:12",
    "message" : "trying out Elasticsearch"
}
```

cerebroのoverview画面でシャードができているのが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30677/9dc343f6-02e4-902b-c120-8c84d967cedd.png)


podを1つ落とします。

```bash
$ kubectl delete pod elasticsearch-0
```

cerebroの画面では数秒ヘルスチェックがyellowになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30677/260e8aa4-f18a-89cd-4c48-0e41e4f2f195.png)

その後待つとelasticsearch-0のノードが復活しgreenに戻ることが無事、確認できます。

## まとめ：ESにKubernetesを使用してみて
まだ学習したばかりですが、以下利点があるなと感じました。

- Self-healingやAutoScaleがやりやすい
  - デフォルトでSelf-healingの仕組みがあり、Podの水平垂直オートスケールに加えNodeもオートスケールが可能。
- オンラインでnodeのスケールアップやディスク容量リサイズもできる
  - 業務で苦労したこれらの問題に対してすでにノウハウがあります。
- ESのzen.discovery設定が楽
  - AWSではec2-discoveryのプラグインが必要で、設定にハマりましたが、こちらは不要です。

## 参考資料
こちらのESのマニフェストファイルを参考にしました。本記事ではより簡易なものに修正し、かつcerebroのPodを追加しています。
[A Guide to Deploy Elasticsearch Cluster on Google Kubernetes Engine](https://medium.com/google-cloud/a-guide-to-deploy-elasticsearch-cluster-on-google-kubernetes-engine-52f67743ee98)

やや複雑ですが、公式で出されているkibana+Elasticsearchも参考にしました。
[click-to-deploy/README.md at master · GoogleCloudPlatform/click-to-deploy](https://github.com/GoogleCloudPlatform/click-to-deploy/blob/master/k8s/elastic-gke-logging/README.md)