Kubernetesに触れる: データの永続化についてとKafkaクラスターのデプロイ

前回まで:

* [KubernetesからみたDocker](https://qiita.com/kenji-kondo/items/d80c1be71853b3fac276)
* [Kubernetesに触れる](https://qiita.com/kenji-kondo/items/a5d497007f1b2099d29b)
* [Kubernetesに触れる: Kubernetes内のネットワークとDNS](https://qiita.com/kenji-kondo/items/a673ec09fe397b8a7e7f)

今回はKubernetes上のデータの永続化(Volume, PersistentVolumeClaim)について説明したあと、KafkaとZookeeperクラスターをKubernetes上にデプロイするManifestファイルを眺めながら、そこで出てくるリソースについて軽く説明する。

# データの永続化について

## Kubernetesに不向きなこと

いわゆる状態を持つことをKubernetesでもそのまま**stateful**という言葉で表現する。Kubernetesにおける"stateful"の明確な定義を私はまだ述べられないけれど、少なくともDBやKafkaのような、永続化されたデータを扱うタイプのPodのことをstatefulと呼んでいる。これはNginxがstatelessと呼ばれることと対比して考えるとわかりやすい。

[Cloud Native DevOps with Kubernetes](https://www.oreilly.com/library/view/cloud-native-devops/9781492040750/)という書籍では、はっきりと「データベース(のようなstatefulなPod)をKubernetesで動かすことはお勧めしない」と記載されている。似たようなことを言う人はWeb上にも沢山見受けられる。例えば -> https://www.quora.com/Is-it-a-good-way-to-run-Kafka-on-Kubernetes

勘違いのないように述べておくと、Kubernetesでもデータの永続化をしたり、それを操作するDBをデプロイすることはできるし、さらに完全にプロダクションレベルでそれを動かすことはできる。

その上でお勧めしない理由としては、Manifestが非常に技巧的でまた煩雑になることにある。Kubernetesをそもそも導入したい動機の中には「インフラに構う時間を抑えて、コアな仕事に集中すること」があったはず。それを鑑みると、Kubernetesでデータベースを動かすことに多大な労力をかける価値を見いだせないかもしれない。素朴にホスト上で動かしたほうが、管理も障害時の調査も比較的に楽な可能性すらある。

Kubernetes上のアプリケーションでデータベースを使う際の代替案として挙げられているのが、クラウドプロバイダーが提供するPaaSを利用することである。

このような背景がありながらも、これからKubernetesでデータを永続化する方法についてみてゆく。

## Volume

Dockerのときと同じように、Podでもホスト上のファイルやディレクトリをマウントできる。例えば以下のようにManifestファイルを記述すればよい。

```yaml:mount-hostpath.yaml
apiVersion: v1
kind: Pod
metadata:
  name: cent-hostpath
  labels:
    app: myapp
spec:
  containers:
  - name: myapp-container-1
    image: centos:7
    command: ['sleep', '3600']
    volumeMounts:
    - mountPath: "/tmp"
      name: sample-hostpath
  volumes:
    - name: sample-hostpath
      hostPath:
        path: /tmp
        type: DirectoryOrCreate
```

これはCentOS7のcotainerを`sleep 3600`コマンドを実行しているだけの無意味なものである。`spec.volumeMounts`のフィールドにあるところで、Container内の`/tmp`のディレクトリを`sample-hostpath`という名前のVolumeで定義されたところにマウントすることを指定している。そのあとの`spec.volumes`で`sample-hostpath`という名前で指定されたホストの領域を指定している。

このようにホストの領域を使うことはできるけれど、これは明らかなアンチパターンなのでこれ以上深入りはしない。

ただ以下の`emptyDir`は使い道があるかもしれない。

```yaml:emptydir-sample.yaml
apiVersion: v1
kind: Pod
metadata:
  name: cent
  labels:
    app: myapp
spec:
  containers:
  - name: myapp-container-1
    image: centos:7
    command: ['sleep', '3600']
    volumeMounts:
    - mountPath: "/tmp"
      name: sample-emptydir
  - name: myapp-container-2
    image: centos:7
    command: ['sleep', '3600']
    volumeMounts:
    - mountPath: "/tmp"
      name: sample-emptydir
  volumes:
    - name: sample-emptydir
      emptyDir: {}
```

`spec.volumes`の中に`emptyDir`が定義された。これはPod内に作成される疑似的な"ホスト上のディレクトリ"で、Podが削除されたらこの領域も削除される。従ってデータの永続化はできない。とはいえ、上のようにPodの中に複数のContainerがある場合には、この領域を互いのContainerで共有できることにある。例えばPodの中でPub/Subを実現したい場合に、Pub用のContainerがこの領域に何かを書き込んで、Sub用のContainerがこの領域内のファイルをtailするような構成にすればそれができる。こうした使い方は有意義かもしれない。



## SC, PV, PVC

まずKubernetesでデータを永続化する場合、データの保存場所はKubernetesクラスターの外部にあることが前提となる。Podはその保存場所にネットワーク越しにアクセスする。

データを永続化したい場合は、**StorageClass(SC)**, **PersistentVolumeClaim(PVC)**と呼ばれる概念を知る必要がある。(今回はDynamic Provisioningを扱う。PersistentVolumeを手動で定義する方法は扱わないので、この2つの概念しか説明しない)

### StorageClass

```azure-storageclass-sampla.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: managed-premium-retain
provisioner: kubernetes.io/azure-disk
reclaimPolicy: Retain
parameters:
  storageaccounttype: Premium_LRS
  kind: Managed
```

* クラウドプロバイダーが提供するディスク(例えばAzureであればAzure File)をKubernetesで扱うための設定。
* この例では`managed-premium-retain`というStorageClassリソース名で「AzureのPremium_LRSというタイプのディスク(SSDのやつ)を」を宣言している。
* クラウドプロバイダー別にパラメータが変わる。Kubernetes公式ドキュメントには利用できる`provisioner`が一覧となっている => https://kubernetes.io/docs/concepts/storage/storage-classes/

### PersistentVolumeClaim

```azure-persistentvolumeclaim-sample.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: azure-managed-disk
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: managed-premium-retain
  resources:
    requests:
      storage: 5Gi
```

* 使用するディスクのサイズや、アクセスモード(リードオンリーや色々なPodから書き込みもできるかどうか)といったことを定義するもの。
* この例では`azure-managed-disk`というPersistentVolumeClaimリソース名で「`managed-premium-retain`というディスクを使って、一つのPodからしかアクセスできず(`ReadWriteOnce`)、サイズは`5Gi`使う」ということを宣言している。

### 実際にこれを利用するには

Podの定義の`spec.volumes`に、上で定義した`persistentVolumeClaim`を指定すればよい。

```azure-persistent-pod-sample.yaml
kind: Pod
apiVersion: v1
metadata:
  name: mypod
spec:
  containers:
  - name: mypod
    image: nginx:1.15.5
    resources:
      requests:
        cpu: 100m
        memory: 128Mi
      limits:
        cpu: 250m
        memory: 256Mi
    volumeMounts:
    - mountPath: "/mnt/azure"
      name: volume
  volumes:
    - name: volume
      persistentVolumeClaim:
        claimName: azure-managed-disk
```

以上をまとめると

* **Pod**は**PersistentVolumeClaim**を指定する。
* **PersistentVolumeClaim**は**StorageClass**を指定する。

となる。つまりPodからは実際になんのディスクを使っているからはわかっていない。ここでPodと実際のディスクとの疎結合が実現できている。

またStatefulSetならば、Podの定義と同じファイルにPersistentVolumeClaimを記述できる。

```statefulset-persistentvolumeclaim-sample.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: cent
spec:
  selector:
    matchLabels:
      app: cent
  replicas: 3
  template:
    metadata:
      labels:
        app: cent
    spec:
      containers:
      - name: cent
        image: centos:7
        command: ['sleep', '3600']
        volumeMounts:
        - name: instatefulset
          mountPath: /tmp
  volumeClaimTemplates:
  - metadata:
      name: instatefulset
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "managed-premium-retain"
      resources:
        requests:
          storage: 1Gi
```

# KafkaとZookeeperクラスターをデプロイする

上記を踏まえてKafkaとZookeeperのクラスターをデプロイするyamlを眺める。

Zookeeper: https://github.com/kubernetes/contrib/blob/6530ce7c043610a18bdcfbfccda0984d3b068659/statefulsets/zookeeper/zookeeper.yaml

Kafka: https://github.com/kubernetes/contrib/blob/6a70f578cb71d04992ecc0543aef291b30614d00/statefulsets/kafka/kafka.yaml

なかなか長大なManifestである。以下のことがケアされている:

* ClusterIPをつけない、HeadlessなServiceを作る。ホストに対して直接名前でアクセスできるように。前回のCNAME云々の話を参照
* StatefulSetとしてPodをデプロイしている。
* 例えばNodeのメンテナンスなどであるNodeからZookeeperを退避させないときに、動いているZookeeperの数を少なくとも2つは保っておくことを定義する -> `PodDisruptionbudget`
* 同じNodeにZookeeperが立ってはならない -> `podAntiAffinity`
* Podが正常に動いているかの確認 -> `readinessProbe`と`livvenessProbe`
* zookeeperの`myid`やkafkaの`beokerId`を、Podのhostnameから動的につけられるようにしている。
* データの永続化