Kubernetesなどのコンテナ系を利用していると、Pod内にデータをためておくことができないため、ファイルを保存しておく外部のストレージが必要になってくる。昔のようにローカルの/var/dataとかに適当に保存すればええべって話にはなりまてん。
そんな時に使われるのが、k8sのStorageClass, PersistentVolume(PV), PersistentVolumeClaim(PVC)。
ログなどは適当な既存のソリューションを使えばいいし、設定ファイルレベルのものであればconfigmapというものがあるのでそちらを使えばいい。
今回は、geoipのDataMind?のmmdbファイルを複数のpodで共有する必要がでてきたためまとめてみた。mmdbファイルは定期的に更新されているので、とってきて更新する必要がある。
TL;DR
1台のworker nodeでLocalのディレクトリを複数のpodから参照する流れでまとめています。
StorageClassでどんなストレージ(ボリューム)を使うかを定義し、PersistentVolume(PV)で永続化ボリュームとして定義する、PersistentVolumeClaim(PVC)で利用要求をする感じ。
環境
murata:~ $ kubectl version
Client Version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.3", GitCommit:"06ad960bfd03b39c8310aaf92d1e7c12ce618213", GitTreeState:"clean", BuildDate:"2020-02-11T18:14:22Z", GoVersion:"go1.13.6", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"16", GitVersion:"v1.16.2", GitCommit:"c97fe5036ef3df2967d086711e6c0c405941e14b", GitTreeState:"clean", BuildDate:"2019-10-15T19:09:08Z", GoVersion:"go1.12.10", Compiler:"gc", Platform:"linux/amd64"}
StorageClass
下記のような感じで定義ファイルを作成する。
ローカルディレクトリだったり、AWSのEBSやNFSサーバ、GCPのGCEPersistentDiskなどを利用できる。
公式: https://kubernetes.io/docs/concepts/storage/storage-classes/
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: geoip-db-local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: Immediate
reclaimPolicy: Retain
volumeBindingModeは実際にPodでPVCを使うときにバインドするか、即時にバインドしてしまうかの違い。
describeなどはこんな感じ。
murata:/var/lib $ kubectl get sc -o wide
NAME PROVISIONER AGE
geoip-db-local-storage kubernetes.io/no-provisioner 32s
murata:/var/lib $ kubectl describe storageclass.storage.k8s.io/geoip-db-local-storage
Name: geoip-db-local-storage
IsDefaultClass: No
Annotations: kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"storage.k8s.io/v1","kind":"StorageClass","metadata":{"annotations":{},"name":"geoip-db-local-storage"},"provisioner":"kubernetes.io/no-provisioner","reclaimPolicy":"Retain","volumeBindingMode":"Immediate"}
Provisioner: kubernetes.io/no-provisioner
Parameters: <none>
AllowVolumeExpansion: <unset>
MountOptions: <none>
ReclaimPolicy: Retain
VolumeBindingMode: Immediate
Events: <none>
PersistentVolume(PV)
永続化ボリューム、↑のStrageClassで定義されたものを利用したり、ほかにもいろいろあるらしい。
読み書きのアクセスを設定したり、マウントオプションなどを設定できる。
公式: https://kubernetes.io/docs/concepts/storage/persistent-volumes/
apiVersion: v1
kind: PersistentVolume
metadata:
name: geoip-db-pv
namespace: ns-test
spec:
storageClassName: geoip-db-local-storage # ↑のStorageClassのnameを入れる。
volumeMode: Filesystem
capacity:
storage: 10Gi
accessModes:
- ReadOnlyMany
persistentVolumeReclaimPolicy: Retain
hostPath:
path: "/var/lib/k8s-geoip-db" # ローカルの適当なパスを指定
type: DirectoryOrCreate # ローカルにディレクトリがなければ作ってくれる。
describeなどはこんな感じ。
murata:~ $ kubectl get pv -o wide
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE VOLUMEMODE
geoip-db-pv 10Gi ROX Retain Available geoip-db-local-storage 11s Filesystem
murata:~ $ kubectl describe persistentvolume/geoip-db-pv
Name: geoip-db-pv
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"v1","kind":"PersistentVolume","metadata":{"annotations":{},"name":"geoip-db-pv"},"spec":{"accessModes":["ReadOnlyMany"],"ca...
Finalizers: [kubernetes.io/pv-protection]
StorageClass: geoip-db-local-storage
Status: Available
Claim:
Reclaim Policy: Retain
Access Modes: ROX
VolumeMode: Filesystem
Capacity: 10Gi
Node Affinity: <none>
Message:
Source:
Type: HostPath (bare host directory volume)
Path: /var/lib/k8s-geoip-db
HostPathType: DirectoryOrCreate
Events: <none>
この時点では/var/lib/k8s-geoip-db
はできていない。
PersistentVolumeClaim(PVC)
利用要求をする。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
namespace: ns-test
name: geoip-db-pvc
spec:
storageClassName: geoip-db-local-storage
accessModes:
- ReadOnlyMany
volumeMode: Filesystem
resources:
requests:
storage: 10Gi
murata:~ $ kubectl get pvc -o wide
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE VOLUMEMODE
geoip-db-pvc Bound geoip-db-pv 10Gi ROX geoip-db-local-storage 114s Filesystem
murata:~ $ kubectl describe persistentvolumeclaim/geoip-db-pvc
Name: geoip-db-pvc
Namespace: ns-test
StorageClass: geoip-db-local-storage
Status: Bound
Volume: geoip-db-pv
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"v1","kind":"PersistentVolumeClaim","metadata":{"annotations":{},"name":"geoip-db-pvc","namespace":"ns-test"},"spec":{"acces...
pv.kubernetes.io/bind-completed: yes
pv.kubernetes.io/bound-by-controller: yes
Finalizers: [kubernetes.io/pvc-protection]
Capacity: 10Gi
Access Modes: ROX
VolumeMode: Filesystem
Mounted By: <none>
Events: <none>
3点セット確認
StorageClass, pv,pvcをまとめてみるとこんな感じになる。
murata:~ $ kubectl get sc,pv,pvc -o wide
NAME PROVISIONER AGE
storageclass.storage.k8s.io/geoip-db-local-storage kubernetes.io/no-provisioner 13m
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE VOLUMEMODE
persistentvolume/geoip-db-pv 10Gi ROX Retain Bound ns-test/geoip-db-pvc geoip-db-local-storage 7m59s Filesystem
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE VOLUMEMODE
persistentvolumeclaim/geoip-db-pvc Bound geoip-db-pv 10Gi ROX geoip-db-local-storage 3m51s Filesystem
murata:~ $ kubectl describe storageclass.storage.k8s.io/geoip-db-local-storage persistentvolume/geoip-db-pv persistentvolumeclaim/geoip-db-pvc
Name: geoip-db-local-storage
IsDefaultClass: No
Annotations: kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"storage.k8s.io/v1","kind":"StorageClass","metadata":{"annotations":{},"name":"geoip-db-local-storage"},"provisioner":"kubernetes.io/no-provisioner","reclaimPolicy":"Retain","volumeBindingMode":"Immediate"}
Provisioner: kubernetes.io/no-provisioner
Parameters: <none>
AllowVolumeExpansion: <unset>
MountOptions: <none>
ReclaimPolicy: Retain
VolumeBindingMode: Immediate
Events: <none>
Name: geoip-db-pv
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"v1","kind":"PersistentVolume","metadata":{"annotations":{},"name":"geoip-db-pv"},"spec":{"accessModes":["ReadOnlyMany"],"ca...
pv.kubernetes.io/bound-by-controller: yes
Finalizers: [kubernetes.io/pv-protection]
StorageClass: geoip-db-local-storage
Status: Bound
Claim: ns-test/geoip-db-pvc
Reclaim Policy: Retain
Access Modes: ROX
VolumeMode: Filesystem
Capacity: 10Gi
Node Affinity: <none>
Message:
Source:
Type: HostPath (bare host directory volume)
Path: /var/lib/k8s-geoip-db
HostPathType: DirectoryOrCreate
Events: <none>
Name: geoip-db-pvc
Namespace: ns-test
StorageClass: geoip-db-local-storage
Status: Bound
Volume: geoip-db-pv
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"v1","kind":"PersistentVolumeClaim","metadata":{"annotations":{},"name":"geoip-db-pvc","namespace":"ns-test"},"spec":{"acces...
pv.kubernetes.io/bind-completed: yes
pv.kubernetes.io/bound-by-controller: yes
Finalizers: [kubernetes.io/pvc-protection]
Capacity: 10Gi
Access Modes: ROX
VolumeMode: Filesystem
Mounted By: <none>
Events: <none>
Podから利用してみる。
コンテナの/data
ディレクトリを↑で作ったPVCでマウントしてみる。
apiVersion: v1
kind: Pod
metadata:
namespace: ns-test
name: geoip-pv-test
spec:
containers:
- image: alpine
name: alpine
command: ["tail", "-f", "/dev/null"]
volumeMounts:
- name: claim-volume
mountPath: /data
volumes:
- name: claim-volume
persistentVolumeClaim:
claimName: geoip-db-pvc
terminationGracePeriodSeconds: 0
describeしてみるとMountsのところに項目がある。
(ReadOnlyManyにしたのにrwとは・・・?→AccessModeはノード間のアクセスモードでpods間ではないので注意)
murata:~ $ kubectl describe pods/geoip-pv-test
Name: geoip-pv-test
Namespace: ns-test
Priority: 0
Node: ip-10-0-12-97.ap-northeast-1.compute.internal/10.0.12.97
Start Time: Wed, 11 Mar 2020 17:26:35 +0900
Labels: <none>
Annotations: cni.projectcalico.org/podIP: 192.168.0.125/32
kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"geoip-pv-test","namespace":"ns-test"},"spec":{"containers":[{"command...
Status: Running
IP: 192.168.0.125
IPs:
IP: 192.168.0.125
Containers:
alpine:
Container ID: docker://c88f4ea644a4da03d600eac6fd12ba48a1eebafcdc0dac3d946bb8b8af7f7860
Image: alpine
Image ID: docker-pullable://alpine@sha256:ab00606a42621fb68f2ed6ad3c88be54397f981a7b70a79db3d1172b11c4367d
Port: <none>
Host Port: <none>
Command:
tail
-f
/dev/null
State: Running
Started: Wed, 11 Mar 2020 17:26:39 +0900
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/data from claim-volume (rw)
/var/run/secrets/kubernetes.io/serviceaccount from default-token-2sr2p (ro)
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
claim-volume:
Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
ClaimName: geoip-db-pvc
ReadOnly: false
default-token-2sr2p:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-2sr2p
Optional: false
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s
node.kubernetes.io/unreachable:NoExecute for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled <unknown> default-scheduler Successfully assigned ns-test/geoip-pv-test to ip-10-0-12-97.ap-northeast-1.compute.internal
Normal Pulling 20s kubelet, ip-10-0-12-97.ap-northeast-1.compute.internal Pulling image "alpine"
Normal Pulled 17s kubelet, ip-10-0-12-97.ap-northeast-1.compute.internal Successfully pulled image "alpine"
Normal Created 17s kubelet, ip-10-0-12-97.ap-northeast-1.compute.internal Created container alpine
Normal Started 17s kubelet, ip-10-0-12-97.ap-northeast-1.compute.internal Started container alpine
確認
ディレクトリは勝手に出来上がるので、適当にファイルを保存してみてコンテナ側から参照できるか確認してみましょう。
[root@ip-10-0-12-97 ~]# ll /var/lib | grep k8s-geoip-db
drwxr-xr-x 2 root root 6 Mar 11 17:26 k8s-geoip-db
[root@ip-10-0-12-97 ~]# echo 'Oreha Worker Node.' > /var/lib/k8s-geoip-db/test.txt
[root@ip-10-0-12-97 ~]# cat /var/lib/k8s-geoip-db/test.txt
Oreha Worker Node.
murata:/var/lib $ kubectl exec -it geoip-pv-test sh
/ # ls -al /data/
total 4
drwxr-xr-x 2 root root 22 Mar 11 08:35 .
drwxr-xr-x 1 root root 41 Mar 11 08:26 ..
-rw-r--r-- 1 root root 19 Mar 11 08:35 test.txt
/ # cat /data/test.txt
Oreha Worker Node.
大丈夫できてる。
でもコンテナからでもファイルに追記できてしまった。。。
/ # echo 'FF7 hayaku!' >> /data/test.txt
/ # cat /data/test.txt
Oreha Worker Node.
FF7 hayaku!
[root@ip-10-0-12-97 ~]# cat /var/lib/k8s-geoip-db/test.txt
Oreha Worker Node.
FF7 hayaku!
ほかのPodからもアクセスできるか確認してみよう。
geoip-pv-test2という名前でpodをもう一つ立てた。
murata:~ $ kubectl exec -it pod/geoip-pv-test2 sh
/ # cd /data
/data # ls
test.txt
/data # cat test.txt
Oreha Worker Node.
FF7 hayaku!
/data # echo 'Oreha Cloud no Clone da!' >> /data/test.txt
/data # cat /data/test.txt
Oreha Worker Node.
FF7 hayaku!
Oreha Cloud no Clone da!
[root@ip-10-0-12-97 ~]# cat /var/lib/k8s-geoip-db/test.txt
Oreha Worker Node.
FF7 hayaku!
Oreha Cloud no Clone da!
/ # cat /data/test.txt
Oreha Worker Node.
FF7 hayaku!
Oreha Cloud no Clone da!
共有されいた。
PVのpathを変えてみる
オンラインの状態でPVのhostPathを挿げ替えたらどうなるだろう。
murata:~ $ cat <<EOF | kubectl apply -f -
> apiVersion: v1
> kind: PersistentVolume
> metadata:
> name: geoip-db-pv
> namespace: ns-test
> spec:
> storageClassName: geoip-db-local-storage
> volumeMode: Filesystem
> capacity:
> storage: 10Gi
> accessModes:
> - ReadOnlyMany
> persistentVolumeReclaimPolicy: Retain
> hostPath:
> path: "/var/lib/tmp-k8s-geoip-db" # ここ変える
> type: DirectoryOrCreate
>
> EOF
The PersistentVolume "geoip-db-pv" is invalid: spec.persistentvolumesource: Forbidden: is immutable after creation
怒られまーした。
まとめ
これでmmdbファイルを取ってきて保存するPodを作って、cronjobで定期実行し、利用するPodsからはそこを参照すればよさそう。
k8sはやっぱり難しいが、それぞれの役割がわかってくればそれほど難しい話ではないのかも?
スナップショットが取れたりクローンできたりもする。
注意点としてはStorageClassによって制約があり、EBSを使った場合には複数からマウントできないなどの制約は出てくるよ。
Pods間でファイルを共有する方法としてほかに、Podから直接EBSなどのボリュームをマウントしたり、サイドカーと言ってPodに複数のコンテナを定義して読めるようにしたりする方法もあるみたい。
PVのpersistentVolumeReclaimPolicyで使い捨て(要求時に毎回空にされるなど)できたりもする模様。
今回の場合、更新Pod以外は読み取り専用でよいのでs3などを利用したほうがよいのかも。。。
逆にMysqlなどのDBのdatadirなどでは使える。(本番ではRDSとか使うんだぞ。)
注意 ネームスペースをまたいでの多重のPVClaimはできない模様です。同じPATHでpv,pvc,storageClassを作ればいけました。(ファイル壊れる可能性あり得る
参考: Kubernetes道場 12日目 - PersistentVolume / PersistentVolumeClaim / StorageClassについて (いつも参考にさせていただいています。ありがとう。