Elastic Cloud on Kubernetes (ECK)の仕組みと各構成変更における挙動について

本記事は[ZOZOテクノロジーズ アドベントカレンダー#2](https://qiita.com/advent-calendar/2020/zozo_tech2)の17日目の記事です。

# TL;DR;
- ECK動作確認環境構築手順
- ECKの仕組み
- 各構成・設定変更における挙動解説

# 動作確認環境の構築
本記事の解説で使う環境構築方法を簡単に解説します。なお、Kubernetes環境としてはAmazon Elastic Kubernetes Service（以下、EKS）を利用してますが、EKSセットアップとk8sクラスタ操作必要なkubectl等の設定手順については省略します。また、既にk8sクラスタに接続できる状態になっていることを前提とします。

## 環境

- クライアントOS: macOS
- Kubernetes versions:
  - client: v1.19.4
  - server: v1.18.9
- ECK: 1.3

## セットアップ

既に下記のようにkubectlコマンドでk8sクラスタにアクセスできる状態になっていることを前提とします。

```bash

kubectl get nodes

NAME                                                STATUS   ROLES    AGE    VERSION
ip-192-168-27-58.ap-northeast-1.compute.internal    Ready    <none>   3d4h   v1.18.9-eks-d1db3c
ip-192-168-38-215.ap-northeast-1.compute.internal   Ready    <none>   3d4h   v1.18.9-eks-d1db3c
ip-192-168-85-80.ap-northeast-1.compute.internal    Ready    <none>   3d4h   v1.18.9-eks-d1db3c
```

## ECKインストール

ECK 1.3.0（Elastic Operatorと関連リソース）のマニフェストを次のようにk8sクラスタにapplyします。

```bash
VERSION=1.3.0
kubectl apply -f https://download.elastic.co/downloads/eck/${VERSION}/all-in-one.yaml
```

ECKのコアリソースは`elastic-system`名前空間にデプロイされます。次のコマンドでElastic Operator podがrunningになっていることを確認します。

```bash
kubectl get all -n elastic-system

NAME                     READY   STATUS    RESTARTS   AGE
pod/elastic-operator-0   1/1     Running   0          14m

NAME                             TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
service/elastic-webhook-server   ClusterIP   10.100.111.185   <none>        443/TCP   14m

NAME                                READY   AGE
statefulset.apps/elastic-operator   1/1     14m
```

Elastic Operatorのログは次のように確認できます。

```bash
kubectl -n elastic-system logs -f statefulset.apps/elastic-operator
```

## Elasticsearchクラスタのデプロイ

上記の手順でElastic Operatorのインストールが完了したら、続いてElasticsearchクラスタをk8sにデプロイします。既にインストールされているElastic OperatorがElasticsearchクラスタをk8sに自動的にデプロイして宣言した通りの状態が維持されるようにk8sリソースを管理します。

まずは、本記事の動作確認で利用する名前空間`testeck`を作成します。

```
kubectl create namespace testeck
```

作成した名前空間`testeck`に、次のようにmasterノード x 3、dataノード x 1な構成のElasticsearchクラスタ`ecksandbox`をデプロイします。

```yaml
cat <<EOF | kubectl apply -n testeck -f -
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: ecksandbox
spec:
  version: 7.10.0
  nodeSets:
  - name: master-nodes
    count: 3
    config:
      node.store.allow_mmap: false
      node.roles: ["master"]
    volumeClaimTemplates:
    - metadata:
        name: elasticsearch-data
      spec:
        accessModes:
        - ReadWriteOnce
        resources:
          requests:
            storage: 1Gi
        storageClassName: gp2
  - name: data-nodes
    count: 1
    config:
      node.store.allow_mmap: false
      node.roles: ["data", "ingest"]
    volumeClaimTemplates:
    - metadata:
        name: elasticsearch-data
      spec:
        accessModes:
        - ReadWriteOnce
        resources:
          requests:
            storage: 2Gi
        storageClassName: gp2
EOF
```
> NOTE: volumeClaimTemplatesで、masterノードにそれぞれ1GBの[EBS volume(gp2)](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html)を、dataノードに2GBの[EBS volume(gp2)](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html)を割り当てる宣言をしています。

次のようにデプロイしたElasticsearchリソースの状態を取得します。問題なければHEALTH状態がgreen、NODESが4、PHASEがreadyになっているはずです。

```bash
kubectl get elasticsearch -n testeck

NAME         HEALTH   NODES   VERSION   PHASE   AGE
ecksandbox   green    4      7.10.0    Ready   64s
```

## Elasticsearchクラスタにアクセス

デプロイしたElasticsearchクラスタにアクセスします。まずはsecretsに自動的に保存されている`elastic`ユーザー用パスワードを次のように取得します。

```bsah
export NAMESPACE=testeck
export PASSWORD=$(kubectl get secret ecksandbox-es-elastic-user -n ${NAMESPACE} -o go-template='{{.data.elastic | base64decode}}')

echo $PASSWORD
2LIh4PY5XUAejd85d9c3q461
```

次に、Elasticsearchクラスタ用に作成されたserviceを取得します。`ecksandbox-es-http`が接続対象のserviceになります。

```bash
kubectl get svc -n testeck

NAME                         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
ecksandbox-es-data-nodes     ClusterIP   None             <none>        9200/TCP   2m50s
ecksandbox-es-http           ClusterIP   10.100.237.243   <none>        9200/TCP   2m52s
ecksandbox-es-master-nodes   ClusterIP   None             <none>        9200/TCP   2m50s
ecksandbox-es-transport      ClusterIP   None             <none>        9300/TCP   2m52s
```


`kubectl port-forward`を使ってローカルマシンからElasticsearchの`ecksandbox-es-http`にポート9200にアクセスできるようにポートフォワードします。

```bash
kubectl port-forward service/ecksandbox-es-http 9200 -n testeck

Forwarding from 127.0.0.1:9200 -> 9200
Forwarding from [::1]:9200 -> 9200
```

最後に、さきほど取得した`elastic`ユーザー用パスワードを指定してElasticsearchクラスタにリクエストを送信すると、次のような結果が得られます。

```bash

curl -u "elastic:$PASSWORD" -k "https://localhost:9200"

{
  "name" : "ecksandbox-es-master-nodes-0",
  "cluster_name" : "ecksandbox",
  "cluster_uuid" : "Jdcikd2dSPqlPGR9uj2hqA",
  "version" : {
    "number" : "7.10.0",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "51e9d6f22758d0374a0f3f5c6e8f3a7997850f96",
    "build_date" : "2020-11-09T21:30:33.964949Z",
    "build_snapshot" : false,
    "lucene_version" : "8.7.0",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}
```

## テストデータをIngestして検索してみる

Elastic社の[公式クイックスタートガイド](https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-index.html)を参考に上記で作成したElasticsearchクラスタに次のようにテストデータをIngestします。

さきほどと同じようにローカルマシンからElasticsearchの`ecksandbox-es-http`にアクセスできるようにポートフォワードします。既にポートフォワードされている場合はこちらのステップは省略ください。

```bash
kubectl port-forward service/ecksandbox-es-http 9200 -n testeck
```

次に、テストデータを取得して、[bulk API](https://www.elastic.co/guide/en/elasticsearch/reference/7.10/docs-bulk.html)を使ってElasticsearchクラスタにデータをBulkでインジェストします。ここではターゲットindexを`bank`としています。

```bash
# テストデータ取得
curl -o accounts.json https://raw.githubusercontent.com/elastic/elasticsearch/master/docs/src/test/resources/accounts.json

# Bulkインジェスト
curl -u "elastic:$PASSWORD" -k -H "Content-Type: application/json" -XPOST "https://localhost:9200/bank/_bulk?pretty&refresh" --data-binary "@accounts.json"
```

[cat indices API](https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-indices.html)でインデックスを確認するとプライマリとレプリカが１つずつの構成で、1000件のドキュメントが`bank`インデックスに格納されていることがわかります。

```bash
curl -u "elastic:$PASSWORD" -k "https://localhost:9200/_cat/indices?v"

health status index uuid                   pri rep docs.count docs.deleted store.size pri.store.size
yellow open   bank  TyjMvOOCQDiNZNqwNHSNQA   1   1       1000            0      394kb          394kb
```

# ECKの仕組み
## Elastic Operator

ECKは、Elastic Operatorを使ってKubernetes上でのElasticsearchやKinanaの設定・管理を自動化します。Elastic Operatorが行う処理に、クラスタのデプロイメント、クラスタ設定変更やアップグレード、ノード数のスケールアウトやスケールイン、バックアップなどがあります。機能について詳しくは[こちら](https://www.elastic.co/jp/elastic-cloud-kubernetes)を参照ください。

次のイメージは、Elastic OperatorによるElasticsearchや、Kibanaリソースのデプロイまでの流れを表しています。利用ユーザーがElasticsearchや、Kibanaリソースを定義するCRD（Custom Resource Definition）のYAMLを作成してk8sにapplyすると、Elastic Operatorがそれに基づいて、Elasticsearchや、Kibanaリソースをデプロイ、もしくは変更します。さきほど行ったElasticsearchクラスタ`ecksandbox`のデプロイメントも、まさにこの流れで実施されます。

![eck-elastic-operator.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103420/c033dcf0-a6fb-ac27-bedb-d92d25917354.png)


## NodeSetsとStatefulSetについて

Elastic Operatorは、クラスタ内の同一Elasticsearch構成のノードグループを表すNodeSetsをつかってElasticsearchクラスターのトポロジー設定を行います。このNodeSetsの実体はStatefulSetです。通常、Elasticsearchのmasterノードやdataノードは個別のNodeSetsを設定しますが、この場合は別々のStatefulSetでそれぞれのPodが管理されることになります。以下、NodeSetsの定義と、定義ごとに別々のStatefulSetが作られるイメージになります。NodeSetsについて詳しくは公式ページの[Node orchestration](https://www.elastic.co/guide/en/cloud-on-k8s/current/k8s-orchestration.html)を参照ください。

![eck-nodesets.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103420/90e991cb-1404-b445-5d42-9910cd485fb2.png)

上述の通り、NodeSetsは[StatefulSet](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/)リソースとしてデプロイされるので、ECKによる各構成変更の挙動を理解するにはStatefulSetを理解しておく必要があります。 以下、構成や設定変更時の挙動理解のために抑えておくべきポイントを簡単に解説します。

### ポイント1: StatefulSetにおける各Podへの個別のPVC、PVの割当て

StatefulSetは[volumeClaimTemplate](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/)の定義にもとづき、各Podに個別のPVC（[PersistentVolumeClaim](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims)）、PV（[Persistent Volume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/)）を割り当てます。
例えば、さきほど行った`ecksandbox`クラスタの`data-nodes`ノードでは、次のように`volumeClaimTemplates`で各Podが２GBのEBS volume(gp2)を割り当てる設定をしています。

```yaml
  nodeSets:
  - name: data-nodes
    count: 1
    config:
      node.store.allow_mmap: false
      node.roles: ["data", "ingest"]
    volumeClaimTemplates:
    - metadata:
        name: elasticsearch-data
      spec:
        accessModes:
        - ReadWriteOnce
        resources:
          requests:
            storage: 2Gi
        storageClassName: gp2
```

これで、各Podには個別のPVC、PVが割り当てられます。一方、k8sにはReplicaSetがありますが、ReplicaSetの場合は、同一ReplicaSetのすべてのPodには同じPVC、PVが割り当てられます。以下、ReplicaSetとStatefulSetのPVC、PV割り当ての比較イメージになります。

![eck-replicaset-vs-statefulset.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103420/62002290-e8dc-60f8-9834-4b84895e14b4.png)

参考までに、さきほど行ったElasticsearchクラスタ`ecksandbox`のデプロイメントでは`master-nodes`（3ノード）と`data-nodes`（1ノード）の２種類のNodeSets、つまり２つのStatefulSetをデプロイしてます。それぞれのPodとPVC、PVの割当状況は次のとおりとなります。

```bash
kubectl get po,sts,pv,pvc -n testeck

NAME                               READY   STATUS    RESTARTS   AGE
pod/ecksandbox-es-data-nodes-0     1/1     Running   0          7h10m
pod/ecksandbox-es-master-nodes-0   1/1     Running   0          7h10m
pod/ecksandbox-es-master-nodes-1   1/1     Running   0          7h10m
pod/ecksandbox-es-master-nodes-2   1/1     Running   0          7h10m

NAME                                          READY   AGE
statefulset.apps/ecksandbox-es-data-nodes     1/1     7h10m
statefulset.apps/ecksandbox-es-master-nodes   3/3     7h10m

NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                                     STORAGECLASS   REASON   AGE
persistentvolume/pvc-265d9044-5111-4e54-9667-a741c7537043   1Gi        RWO            Delete           Bound    testeck/elasticsearch-data-ecksandbox-es-master-nodes-2   gp2                     7h10m
persistentvolume/pvc-299aff52-cfff-4b5b-a847-5f4bdf528f8f   1Gi        RWO            Delete           Bound    testeck/elasticsearch-data-ecksandbox-es-master-nodes-0   gp2                     7h10m
persistentvolume/pvc-d7ff409d-f731-4f02-b26e-c67fd3662941   2Gi        RWO            Delete           Bound    testeck/elasticsearch-data-ecksandbox-es-data-nodes-0     gp2                     7h10m
persistentvolume/pvc-e40bcf99-0e57-48e7-acc8-d19224b67665   1Gi        RWO            Delete           Bound    testeck/elasticsearch-data-ecksandbox-es-master-nodes-1   gp2                     7h10m

NAME                                                                    STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/elasticsearch-data-ecksandbox-es-data-nodes-0     Bound    pvc-d7ff409d-f731-4f02-b26e-c67fd3662941   2Gi        RWO            gp2            7h10m
persistentvolumeclaim/elasticsearch-data-ecksandbox-es-master-nodes-0   Bound    pvc-299aff52-cfff-4b5b-a847-5f4bdf528f8f   1Gi        RWO            gp2            7h10m
persistentvolumeclaim/elasticsearch-data-ecksandbox-es-master-nodes-1   Bound    pvc-e40bcf99-0e57-48e7-acc8-d19224b67665   1Gi        RWO            gp2            7h10m
persistentvolumeclaim/elasticsearch-data-ecksandbox-es-master-nodes-2   Bound    pvc-265d9044-5111-4e54-9667-a741c7537043   1Gi        RWO            gp2            7h10m
```


### ポイント2: Pod実行順序（作成は番号が低いものから、更新・削除は高いものから）

StatefulSetにおいて各Pod名には0からはじまりN-1まで（Nはレプリカ数）の整数値が順番に割り当てられます。
例えば、先ほどデプロしたElasticsearchクラスタ`ecksandbox`のmasterノード用のStatefulSetはレプリカ数3で、つまり3Podで構成されますが、次のように各Pod名には0〜2 (3-1)の整数値が割りあてられています。

```bash
kubectl get pods -n testeck

NAME                           READY   STATUS    RESTARTS   AGE
ecksandbox-es-master-nodes-0   1/1     Running   0          6h25m
ecksandbox-es-master-nodes-1   1/1     Running   0          6h25m
ecksandbox-es-master-nodes-2   1/1     Running   0          6h25m
...
```

同一StatefulSetにおいてPodの作成は番号が低いものから順番に１つずつ行われます。一方、更新・削除はPodの番号が高いもの順に１つずつ行われます。

例えば、3レプリカのStatefulSetが0にスケールインするときは、Podの更新・削除にあたるのでPodの番号が高いものから順（2->1->0）に削除されます。一方、PodがスケールアウトするときはPodが番号の低いもの順（0->1->2）で作成されます。

もう一点、スケールインしてからスケールアウトするようなケースで、抑えておくべき重要なポイントに、同一PVC、PVは切り離されたあともそのままで、再びスケールアウト時に同じ番号のPodに割り当てられることです。以下のイメージは、3レプリカのStatefulSetが１Podにスケールインしてから再び３Podにスケールアウトする処理の流れを表します。スケールイン時に1と2のPodはPVC、PVCが切り離されますが、それらはそのままで、再びスケールアウトの際に同じ番号のPodに割り当てられます。

![eck-scale-in-out.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103420/f4f59425-7d34-4485-10ff-e82561a79dfb.png)


これらの挙動は、データを失うことなくPodの変更、すなわち構成変更を可能にするための基本動作になります。


# 各構成変更とその挙動

## ノードのスケールアウト・スケールイン

ノードのスケールアウト・スケールインは基本的に、NodeSetsのcountを変更して行います。これは上述の通りStatefulSetのスケールアウト・スケールインになります。よって、クラスタは無停止で１つずつレプリカが更新されます。ただし、以下の条件においてはクラスタ可用性が保証されないのでご注意ください。

- シングルノードのElasticsearchクラスタ
- Elasticsearchクラスタのインデックスにレプリカがない


それでは以下のYAMLをapplyして、先ほどデプロしたElasticsearchクラスタ`ecksandbox`の`data-nodes`ノードを3レプリカにスケールアウトさせてみます。

```yaml

cat <<EOF | kubectl apply -n testeck -f -

apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: ecksandbox
spec:
  version: 7.10.0
  nodeSets:
  - name: master-nodes
    count: 3
    config:
      node.store.allow_mmap: false
      node.roles: ["master"]
    volumeClaimTemplates:
    - metadata:
        name: elasticsearch-data
      spec:
        accessModes:
        - ReadWriteOnce
        resources:
          requests:
            storage: 1Gi
        storageClassName: gp2
  - name: data-nodes
    count: 3
    config:
      node.store.allow_mmap: false
      node.roles: ["data", "ingest"]
    volumeClaimTemplates:
    - metadata:
        name: elasticsearch-data
      spec:
        accessModes:
        - ReadWriteOnce
        resources:
          requests:
            storage: 2Gi
        storageClassName: gp2
EOF
```

上記を実行すると、次のようにdata-nodesのStatefulSetが３レプリカに拡張し、また順番に各PodにPVC、PVが割り当てられることがわかります。

```bash
kubectl get elasticsearch,po,sts,pv,pvc -n testeck

NAME                                                    HEALTH   NODES   VERSION   PHASE   AGE
elasticsearch.elasticsearch.k8s.elastic.co/ecksandbox   yellow   4       7.10.0    Ready   7h38m

NAME                               READY   STATUS    RESTARTS   AGE
pod/ecksandbox-es-data-nodes-0     1/1     Running   0          7h38m
pod/ecksandbox-es-data-nodes-1     0/1     Running   0          29s
pod/ecksandbox-es-data-nodes-2     0/1     Running   0          29s
pod/ecksandbox-es-master-nodes-0   1/1     Running   0          7h38m
pod/ecksandbox-es-master-nodes-1   1/1     Running   0          7h38m
pod/ecksandbox-es-master-nodes-2   1/1     Running   0          7h38m

NAME                                          READY   AGE
statefulset.apps/ecksandbox-es-data-nodes     1/3     7h38m
statefulset.apps/ecksandbox-es-master-nodes   3/3     7h38m

NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                                     STORAGECLASS   REASON   AGE
persistentvolume/pvc-1acf4843-0563-4d2d-95d3-0332515c985d   2Gi        RWO            Delete           Bound    testeck/elasticsearch-data-ecksandbox-es-data-nodes-2     gp2                     23s
persistentvolume/pvc-265d9044-5111-4e54-9667-a741c7537043   1Gi        RWO            Delete           Bound    testeck/elasticsearch-data-ecksandbox-es-master-nodes-2   gp2                     7h38m
persistentvolume/pvc-299aff52-cfff-4b5b-a847-5f4bdf528f8f   1Gi        RWO            Delete           Bound    testeck/elasticsearch-data-ecksandbox-es-master-nodes-0   gp2                     7h38m
persistentvolume/pvc-31440203-06d5-4d00-947d-3f431b84a381   2Gi        RWO            Delete           Bound    testeck/elasticsearch-data-ecksandbox-es-data-nodes-1     gp2                     23s
persistentvolume/pvc-d7ff409d-f731-4f02-b26e-c67fd3662941   2Gi        RWO            Delete           Bound    testeck/elasticsearch-data-ecksandbox-es-data-nodes-0     gp2                     7h38m
persistentvolume/pvc-e40bcf99-0e57-48e7-acc8-d19224b67665   1Gi        RWO            Delete           Bound    testeck/elasticsearch-data-ecksandbox-es-master-nodes-1   gp2                     7h38m

NAME                                                                    STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/elasticsearch-data-ecksandbox-es-data-nodes-0     Bound    pvc-d7ff409d-f731-4f02-b26e-c67fd3662941   2Gi        RWO            gp2            7h38m
persistentvolumeclaim/elasticsearch-data-ecksandbox-es-data-nodes-1     Bound    pvc-31440203-06d5-4d00-947d-3f431b84a381   2Gi        RWO            gp2            29s
persistentvolumeclaim/elasticsearch-data-ecksandbox-es-data-nodes-2     Bound    pvc-1acf4843-0563-4d2d-95d3-0332515c985d   2Gi        RWO            gp2            29s
persistentvolumeclaim/elasticsearch-data-ecksandbox-es-master-nodes-0   Bound    pvc-299aff52-cfff-4b5b-a847-5f4bdf528f8f   1Gi        RWO            gp2            7h38m
persistentvolumeclaim/elasticsearch-data-ecksandbox-es-master-nodes-1   Bound    pvc-e40bcf99-0e57-48e7-acc8-d19224b67665   1Gi        RWO            gp2            7h38m
persistentvolumeclaim/elasticsearch-data-ecksandbox-es-master-nodes-2   Bound    pvc-265d9044-5111-4e54-9667-a741c7537043   1Gi        RWO            gp2            7h38m
```

### ノードのスケール変更に合わせてレプリカ数を自動伸縮

ノードのスケール変更に合わせてレプリカ数を自動で伸縮させることができます。レプリカの自動拡張は、主にインデックスにシャードが１つの場合には純粋にノード数に合わせて線形増加させるだけでよいのでとても有効です。

次のようにインデックスに`index.auto_expand_replicas`を設定することで、クラスター内のdataノードの数に基づいて、レプリカの数を自動拡張します。設定については、ダッシュで区切られた下限値-上限値（例：0-5）に設定するか、上限に全てを意味する`all`を使用します（例：0-all）。ここでは、先ほど作成した`bank`インデックスに完全にdataノード数に基づいてレプリカの数を自動拡張することを意味する`0-all`を設定します。

```bash

INDEX_NAME="bank"
curl -u "elastic:$PASSWORD" -k -XPUT "https://localhost:9200/${INDEX_NAME}/_settings" -H 'Content-Type: application/json' -d'
{
 "index" : {
    "auto_expand_replicas": "0-all"
  }
}'
```
なお、インデックス設定について詳しくは[こちら](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html)のページを参照ください。

これで、ノードのスケールアウト・スケールインでやったようにdataノード数を変更すると、それに基づきレプリカ数がN-1で増減します。レプリカ数の確認は次のコマンドで確認ください（ポートフォワードすることをお忘れなく）。`rep`のフィールドがレプリカ数になります。

```bash
curl -u "elastic:$PASSWORD" -k "https://localhost:9200/_cat/indices?v"

health status index uuid                   pri rep docs.count docs.deleted store.size pri.store.size
yellow open   bank  TyjMvOOCQDiNZNqwNHSNQA   1   3       1000            0      394kb          394kb
```


## NodeSetの追加

ElasticsearchクラスタへのNodeSetの新規追加はクラスタ無停止で行われます。
例えば`data-nodes2`のような新しいNodeSetsをElasticsearchクラスタに追加すると、Elastic Operatorはそれに応じた新しいStatefulSetを作成します。また、TLS証明書やElasticsearch設定のためのSecretsやConfigMapを自動的に作成します。

## Volumeサイズの拡張

2020年12月時点では、[こちらのStatefulSet実装の制限](https://github.com/kubernetes/enhancements/issues/661)のためStatefulSetにアタッチされているPVCのVolumeサイズをダイナミックに変更することはできないようです。つまり`volumeClaimTemplates`のストレージサイズを変更してもPVCがストレージ拡張をサポートしない、PVCストレージ拡張がサポートされていてもStatefulSetが最初のVolumeを掴むためにいづれにせよできないということのようです。

```yaml
  - name: data-nodes
    count: 2
    config:
      node.store.allow_mmap: false
      node.roles: ["data", "ingest"]
    volumeClaimTemplates:
    - metadata:
        name: elasticsearch-data
      spec:
        accessModes:
        - ReadWriteOnce
        resources:
          requests:
            storage: 3Gi  <<<ここを増やす
        storageClassName: gp2
```

ワークアラウンドとして共有されている方法は、次のようにNodeSetの名前を変更（`data-nodes2`）して、ストレージサイズを変更して、そのYAMLをapplyする方法になります。Elastic Operatorは新規NodeSet`data-nodes2`のためにPodを作成して、旧Podのデータを自動的に新Podに移行します。データ移行が終了すると、旧ポッドが自動的に削除されます。

```yaml
  - name: data-nodes2
    count: 2
    config:
      node.store.allow_mmap: false
      node.roles: ["data", "ingest"]
    volumeClaimTemplates:
    - metadata:
        name: elasticsearch-data
      spec:
        accessModes:
        - ReadWriteOnce
        resources:
          requests:
            storage: 3Gi
        storageClassName: gp2
```

## NodeSet.specの更新

既存のNodeSetの仕様が更新されます。 Elasticsearch設定や、podTemplateを更新するといったNodeSet.specの更新がされる場合は、Elastic Operatorは、対応するElasticsearchノードの[ローリングアップグレード](https://www.elastic.co/guide/en/elasticsearch/reference/current/rolling-upgrades.html)を実行します。Elasticsearchのローリングアップグレードに従い、可能な限りElasticsearchクラスターの可用性を維持しながら、関連するPodを更新します。通常、この処理ではElasticsearchノードが1つずつ再起動されます。

NodeSet.spec更新例
- ES Configs like JVMヒープサイズ
- PodTemplateの更新（仮想メモリ設定、Pluginインストール設定）

なお、ノードスケール変更と同様に、以下の条件下においてはクラスタ可用性が保証されないのでご注意ください。
- シングルノードのElasticsearchクラスタ
- Elasticsearchクラスタのインデックスにレプリカがない

### 仮想メモリ設定例

次のようにpodTemplateを変更して仮想メモリを設定することができます。仮想メモリ設定について詳細は[こちら](https://www.elastic.co/guide/en/cloud-on-k8s/current/k8s-virtual-memory.html)を参照ください。

```yaml
  nodeSets:
  - name: master-nodes
    count: 3
    podTemplate:
      spec:
        initContainers:
        - name: sysctl
          securityContext:
            privileged: true
          command: ['sh', '-c', 'sysctl -w vm.max_map_count=262144']
```

### pluginインストール例
 
次のようにpodTemplateを変更してpluginをインストールすることができます。pluginインストール設定について詳細は[こちら](https://www.elastic.co/guide/en/cloud-on-k8s/current/k8s-init-containers-plugin-downloads.html)を参照ください。

```yaml
  - name: data-nodes
    count: 2
    config:
      node.store.allow_mmap: false
      node.roles: ["data", "ingest"]
    podTemplate:
      spec:
        initContainers:
        - name: install-plugins
          command:
          - sh
          - -c
          - |
            bin/elasticsearch-plugin install --batch analysis-icu
```

## Elasticsearchクラスタのアップグレード

ノードのスケールアウト・スケールインと同様に、新規Podが１つずつ作成され、無停止でアップグレードを行うことが可能です。ただし、バージョンを下げることはできません。

先ほどデプロしたElasticsearchクラスタ`ecksandbox`は `7.10.0`でしたが、次のようにspec.versionで新しいバージョン`7.10.1`を指定することで自動アップグレード処理が開始されます。

```yaml 
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: ecksandbox
spec:
  version: 7.10.1
  nodeSets:
  - name: master-nodes
    count: 3
    config:
      node.store.allow_mmap: false
      node.roles: ["master"]
    volumeClaimTemplates:
    - metadata:
        name: elasticsearch-data
      spec:
        accessModes:
        - ReadWriteOnce
        resources:
          requests:
            storage: 1Gi
        storageClassName: gp2
```

# まとめ
ECKはかなりの部分がElastic Operatorで自動化されていて、運用管理が単純化されることが理解できたのではないかと思います。また、自動化されている処理についてはStatefulSetが重要な役割を担っており、この挙動の理解が必須かと思います。今回ストレージ周りの挙動については触れませんでしたが安定した運用を行うためにこちらについても十分な理解が必要かと思います。
以上長くなってしまいましたが、ECKの仕組みと各構成変更における挙動について簡単に説明させていただきました。ご一読いただきありがとうございました。

Have a happy ECK life!