Kubernetes でデータを保存する仕組みが難しかったのでまとめました。Kubernetes では、ロードバランサーの背後にコンテナを複数立ち上げて、一つのコンテナが落ちても勝手に復旧する一人データセンターみたいな事が簡単に手元の開発機で実現出来ます。コンテナの入れ物である Pod は使い捨てなので、データをとっておく事が出来ません。データを保存するにはちょっと工夫が必要です。
また、ストレージの機構は本番サーバで利用するサービスによってもまちまちなので、アプリから見てストレージの詳細を隠蔽するような仕組みも必要です。とりあえずサンプルコードです。
# 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
- このサンプルでは
volumeClaimTemplates:
の中で作成しました。 - Pod から見て、ここにこういうストレージがほしいと要求する物です。
- このサンプルでは
-
Persistent Volumes
- ストレージの実体です。
- このサンプルでは自動的に生成されます。
- node のようにクラスタ側であらかじめ作っておく事も出来ます。
-
Storage Classes
- GCP や AWS などのプロバイダによって異なるストレージの種類を示します。
- この例ではデフォルトの
hostpath
(ホストの適当な場所に置かれる) が選択されます。hotpath
はデバッグ目的なので実用には向かないです。
ようするに、ストレージの定義を PersistentVolumeClaims と PersistentVolume に分割する事によってストレージの違いを吸収出来るというのが味噌です。開発時には hostpath で作っておいて本番は AWS EBS を使うというような事が出来ます。