Kubernetes でデータ保存

Kubernetes でデータを保存する仕組みが難しかったのでまとめました。Kubernetes では、ロードバランサーの背後にコンテナを複数立ち上げて、一つのコンテナが落ちても勝手に復旧する一人データセンターみたいな事が簡単に手元の開発機で実現出来ます。コンテナの入れ物である Pod は使い捨てなので、データをとっておく事が出来ません。データを保存するにはちょっと工夫が必要です。

また、ストレージの機構は本番サーバで利用するサービスによってもまちまちなので、アプリから見てストレージの詳細を隠蔽するような仕組みも必要です。とりあえずサンプルコードです。

```yaml
# sticky.yaml

apiVersion: v1
kind: Service
metadata:
  name: sticky
  labels:
    app: sticky
spec:
  type: LoadBalancer
  ports:
  - name: web
    port: 3333
    targetPort: 80
  selector:
    app: sticky

---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: sticky
spec:
  serviceName: sticky
  replicas: 3
  selector:
    matchLabels:
      app: sticky
  template:
    metadata:
      labels:
        app: sticky
    spec:
      containers:
      - name: nginx
        image: k8s.gcr.io/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: sticky-data # マウントしたいボリュームの名前
          mountPath: /usr/share/nginx/html # コンテナからみたストレージの位置
  volumeClaimTemplates:
  - metadata:
      name: sticky-data # ボリュームの名前
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi
```

実行方法は以下です。

    kubectl apply -f https://gist.github.com/propella/ba888509001a1e12ffe91227350fff1a/raw/b6c0fd530f7852cd4b4863ceebe85b9d8c06a833/sticky.yaml

`---` の前半は LoadBalancer の定義なので気にしなくていいです。ブラウザから http://localhost:3333 で見るために使います。

後半の StatefulSet で複数の Pod を作りそれぞれ nginx を起動させます。nginx から見た /usr/share/nginx/html がストレージです。現在まだ何もストレージに置かれていないので、ブラウザでアクセスすると 403 Forbidden が表示されるはずです。

ランダムな名前の Pod を生成する Deployment や ReplicaSet とは違って、StatefulSet は順番に安定した名前を付けます。

```
$ kubectl get pods
NAME       READY     STATUS    RESTARTS   AGE
sticky-0   1/1       Running   0          4m
sticky-1   1/1       Running   0          4m
sticky-2   1/1       Running   0          3m
```

sticky-2 の `/usr/share/nginx/html/index.html` に何か書き込んでみます。

    kubectl exec sticky-2 -- sh -c 'echo hello > /usr/share/nginx/html/index.html'

Pod が全部で三個あるので、三回に一度程度 `hello` が表示されるはずです。

    $ curl http://localhost:3333/
    hello

ここで、おもむろに Pod を全部消してしまいます！

    $ kubectl delete pod -l app=sticky

当然 http://localhost:3333/ は反応しなくなりますが
    
    $ curl http://localhost:3333/
    curl: (52) Empty reply from server

Service のおかげで勝手に復旧します。また、通常コンテナが再起動すると古い内容は消えてしまうのですが、sticky-2 の /usr/share/nginx/html/ だけはホストに保存されているので、そのうち hello も返ってくるようになります。

    $ curl http://localhost:3333/
    hello

ちなみに、このファイルの実体はどこにあるかと言うと、まず `kubectl get persistentvolumeclaim` で sticky-data-sticky-2 に対応するボリュームを探して

```
$ kubectl get persistentvolumeclaim 
NAME                   STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
sticky-data-sticky-0   Bound     pvc-30ca019d-b1ab-11e8-9055-025000000001   1Gi        RWO            hostpath       16m
sticky-data-sticky-1   Bound     pvc-3aa2bc34-b1ab-11e8-9055-025000000001   1Gi        RWO            hostpath       16m
sticky-data-sticky-2   Bound     pvc-44f72ce0-b1ab-11e8-9055-025000000001   1Gi        RWO            hostpath       16m
```

`kubectl describe persistentvolume` で詳細を表示させると出てきます。

```
$ kubectl describe persistentvolume pvc-44f72ce0-b1ab-11e8-9055-025000000001 
Name:            pvc-44f72ce0-b1ab-11e8-9055-025000000001
...
    Path:          /Users/tyamamiya/.docker/Volumes/sticky-data-sticky-2/pvc-44f72ce0-b1ab-11e8-9055-025000000001
```

ためしにホスト側からファイルを変えてみます。

```
$ echo byebye > /Users/tyamamiya/.docker/Volumes/sticky-data-sticky-2/pvc-44f72ce0-b1ab-11e8-9055-025000000001/index.html 
$ curl http://localhost:3333/
byebye
```

## 仕組み

ここまで全く仕組みについて触れずに動作を紹介しましたが、使われている概念について書きます。

* [PersistentVolumeClaims](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims)
  * このサンプルでは `volumeClaimTemplates:` の中で作成しました。
  * Pod から見て、ここにこういうストレージがほしいと要求する物です。
* [Persistent Volumes](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistent-volumes)
  * ストレージの実体です。
  * このサンプルでは自動的に生成されます。
  * node のようにクラスタ側であらかじめ作っておく事も出来ます。
* [Storage Classes](https://kubernetes.io/docs/concepts/storage/storage-classes/)
  * GCP や AWS などのプロバイダによって異なるストレージの種類を示します。
  * この例ではデフォルトの `hostpath` (ホストの適当な場所に置かれる) が選択されます。`hotpath` はデバッグ目的なので実用には向かないです。

ようするに、ストレージの定義を PersistentVolumeClaims と PersistentVolume に分割する事によってストレージの違いを吸収出来るというのが味噌です。開発時には hostpath で作っておいて本番は AWS EBS を使うというような事が出来ます。

## 参考

* [StatefulSet Basics](https://kubernetes.io/docs/tutorials/stateful-application/basic-stateful-set/)