そもそもVolumeとは
コンテナはいわゆる1揮発性であり、かつコンテナは基本的に作って壊してを繰り返す、再利用をすることが一般的です。
そこでVolumeと呼ばれる外部ストレージ(物理機器で言えばHDDのようなもの)を使用してコンテナにマウントすることでデータを永続化することができます。
前提
- 本記事で紹介している検証環境はkindを使用したローカルのクラスター環境で検証しております。
- Windows10 Pro 64bitのWSL2で動作検証をしております。
PersistentVolumeについて解説する前に、Kubernetesで使用できるボリュームには以下の種類があるため、順を追って解説いたします。
- Podのディスクを使用する方法 (emptyDir)
- Nodeのディスクを使用する方法 (hostPath)
- 外部ストレージを利用する方法 (PersistentVolume)
Podのディスクを利用する方法 (emptyDir)
文字通り、Pod内のディスク領域を使用する方法です。
ただし、これは一時的な利用領域となるとため、Podが削除されると保存されたデータも一緒に削除されてしまうため、Volumeとは言いつつもデータの永続化ができません。
マニフェストファイルで定義する場合、volumesセクションにemptyDir: {}
という空のディレクトリを作成し、一時的なストレージをPod内に作成します。
emptyDir
ボリュームを作成し、それをコンテナの/cache
ディレクトリにマウントします。
apiVersion: v1
kind: Pod
metadata:
name: test-emptydir
spec:
containers:
- image: nginx:latest
name: test-emptydir
volumeMounts:
- mountPath: /cache
name: cache-volume
volumes:
- name: cache-volume
emptyDir: {}
実際に確認してみましょう。
適当なディレクトリにemptyDir.yamlを作成し、以下コマンドでapplyします。
kubectl apply -f emptyDir.yaml
pod/test-emptydir created
# コンテナが起動するのを確認します
kubectl get pod -w -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
test-emptydir 1/1 Running 0 97s 10.1.8.43 docker-desktop <none> <none>
コンテナに接続し、ボリュームを確認してみます。
kubectl exec -it test-emptydir -- /bin/bash
# dfコマンドでマウントされたディレクトリの情報を確認します、-kで1024(K)バイト単位で表示します
df -k
root@test-emptydir:/# df -k
Filesystem 1K-blocks Used Available Use% Mounted on
overlay 1055762868 14390924 987668472 2% /
tmpfs 65536 0 65536 0% /dev
tmpfs 2007700 0 2007700 0% /sys/fs/cgroup
/dev/sde 1055762868 14390924 987668472 2% /cache
shm 65536 0 65536 0% /dev/shm
tmpfs 4015400 12 4015388 1% /run/secrets/kubernetes.io/serviceaccount
tmpfs 2007700 0 2007700 0% /proc/acpi
tmpfs 2007700 0 2007700 0% /sys/firmware
root@test-emptydir:/#
/cache
がdev/sde
にマウントされていることが確認できます。
実際に/cache
に適当なファイルを作成し、一時的なファイルであることを確認していきます。
以下コマンドで/cache
にtestfile.txtを作成します。
root@test-emptydir:/cache# echo "Hello, emptyDir!" > /cache/testfile.txt
root@test-emptydir:/cache# ls -l
total 4
-rw-r--r-- 1 root root 17 Mar 23 05:23 testfile.txt
root@test-emptydir:/cache# cat testfile.txt
Hello, emptyDir!
exitでPodを削除して、再作成して/cache
の中身がないことを確認します。
kubectl delete pod test-emptydir
kubectl apply -f emptyDir.yaml
kubectl exec -it test-emptydir -- /bin/bash
root@test-emptydir:/# cd /cache/
root@test-emptydir:/cache# ls -l
total 0
何もなくなっていることが確認できました。
Nodeのディスクを利用する方法 (hostPath)
Kubernetesの2Nodeのディスク領域を使用する方法です。
Nodeのディスク領域をPodにマウントすることで、Nodeのディスクにデータが残るため、データは永続化されます。
ただし、特定のNode上のディレクトリにマウントするため、Podが別のNodeにスケジュールされるとデータにアクセスできなくなります。
スケジュールされるとは、Kubernetesのコントロールプレーンに存在するkube-scheduler
がPodが検出されると、どのNodeに対してPodを割り当てるかをスケジューリングすることを言います。
上記サイトの図を拝借しながら処理の流れを以下に記載します。
- ユーザが
kubectl apply
コマンドでPodの作成をリクエストすると、そのリクエストがkube-apiserver
に送られます -
kube-apiserver
はリクエストを検証し、etcd
にPodの情報を保存します -
kube-scheduler
はkube-apiserver
を監視し、Pending
状態のPodを検知します -
kube-scheduler
はNodeの空き状況を確認し、適切なNodeに割り当てます -
kube-scheduler
は選択したNodeをkube-apiserver
に通知します -
kube-apiserver
はetcd
に上記の情報を保存し、対象のkubelet
(Node上のエージェント)にPodの作成を指示します -
kubelet
はマニフェストファイルの仕様に従ってコンテナを起動します -
kubelet
はPodのステータスをRunning
に更新し、kube-apiserver
を通じてetcd
に反映します
「Node上のディレクトリ」であることから、同じノード上のPod間ではデータを共有することはできますが、異なるNode間でデータを共有することはできません。
実際にhostPath
の動作確認をしてみます。
以下マニフェストファイルを使用してNodeの特定のディレクトリ (/mnt/data
)がPod内の/host-data
にマウントします。
apiVersion: v1
kind: Pod
metadata:
name: test-hostpath
spec:
containers:
- image: nginx:latest
name: test-hostpath
volumeMounts:
- mountPath: /host-data
name: hostpath-volume
volumes:
- name: hostpath-volume
hostPath:
path: /mnt/data
type: DirectoryOrCreate # ディレクトリが存在しなければ作成
以下コマンドでapply
を実行します。
kubectl apply -f hostPath.yaml
pod/test-hostpath created
# コンテナが起動するのを確認します
kubectl get pod -w -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
test-hostpath 1/1 Running 0 4s 10.244.0.6 sandbox-cluster-control-plane <none> <none>
コンテナに接続し、ボリュームを確認してみます。
kubectl exec -it test-hostpath -- /bin/bash
# dfコマンドでマウントされたディレクトリの情報を確認します、-kで1024(K)バイト単位で表示します
df -k
Filesystem 1K-blocks Used Available Use% Mounted on
overlay 1055762868 14643780 987415616 2% /
tmpfs 65536 0 65536 0% /dev
tmpfs 2007700 0 2007700 0% /sys/fs/cgroup
overlay 1055762868 14643780 987415616 2% /host-data
/dev/sde 1055762868 14643780 987415616 2% /etc/hosts
shm 65536 0 65536 0% /dev/shm
tmpfs 4015400 12 4015388 1% /run/secrets/kubernetes.io/serviceaccount
tmpfs 2007700 0 2007700 0% /proc/acpi
tmpfs 2007700 0 2007700 0% /sys/firmware
Podのhost-data
ディレクトリがoverlay
にマウントされていますね。
このoverlay
についてはこれ一つで一つの記事ができてしまいそうなので、非常にわかりやすく解説をしている記事を載せておきます。
Podのhost-data
ディレクトリがNodeに作成したmnt/data
にマウントされており、データの共有ができているかを確認します。
以下コマンドでNodeの名前を確認します。
kubectl get nodes
NAME STATUS ROLES AGE VERSION
sandbox-cluster-control-plane Ready control-plane 3d22h v1.27.3
KindのノードはDockerコンテナとして動作しているため、ノードコンテナ内のパスをhostPath
と指定しています。
そのためKindノードへの接続の仕方は以下のコマンドを使用します。
docker container exec -it sandbox-cluster-control-plane bash
root@sandbox-cluster-control-plane:/#
上記でノード内に接続することができました。
マニフェストファイルで作成した/mnt/data
が存在するか確認してみます。
ls -l /mnt/data/
total 0
ノード内にマニフェストで指定したディレクトリが存在することが確認できます。
/mnt/data
内に適当なファイルを作成してみます。
root@sandbox-cluster-control-plane:/mnt/data# echo > "test" /mnt/data/
root@sandbox-cluster-control-plane:/mnt/data# ls -l
total 4
-rw-r--r-- 1 root root 11 Mar 26 21:53 test
今度はPod側からtest
ファイルが共有されているかを確認します。
kubectl exec -it test-hostpath -- /bin/bash
root@test-hostpath:/# cd /host-data/
root@test-hostpath:/host-data# ls -l
total 4
-rw-r--r-- 1 root root 11 Mar 26 21:53 test
Pod側からもtest
ファイルが存在することが確認できます。
永続性の確認をしてみます。
kubectl delete pod test-hostpath # 一度Podを削除します
pod "test-hostpath" deleted
kubectl apply -f hostPath.yaml # Podを再作成します
pod/test-hostpath created
kubectl get pod -o wide # Podの情報を確認します
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS
GATES
test-hostpath 1/1 Running 0 33s 10.244.0.6 sandbox-cluster-control-plane <none> <none>
kubectl exec -it test-hostpath -- /bin/bash # Podに接続します
root@test-hostpath:/# cd host-data/
root@test-hostpath:/host-data# ls -l # testファイルの存在確認をします
total 4
-rw-r--r-- 1 root root 11 Mar 26 21:53 test
Podの再作成で再度ノードに存在するファイルを確認した結果、ちゃんと永続化されていることが確認できました。
Kindクラスターの場合デフォルト構成では単一のコントロールプレーンノードしか存在しないため、すべてのPodがそのノードにスケジュールされるため、ノード上にファイルが存在すれば、そのファイルを確認することが可能です。
AWS EKSクラスターでFargateプロファイルでサーバーレスにPodを実行する場合、
ノードという概念がなくなります。
どのFargateプロファイルでPodを実行するか?を定義することになります。
FargateプロファイルにはNamespaceを定義することができ、Pod側はどのNamespaceで実行するか?をマニフェストファイルに記述することで、マッチするFargateプロファイルにプロビジョニングを行います。
外部ストレージを利用する方法
ここから本題のPersistentVolumeについて解説します。
これまで解説してきたemptyDir
とhostPath
についてはPodの中にストレージ領域を作成する方法とノード上のディレクトリを作成してファイルを共有する方法でした。
PersistentVolume (PV)は外部の永続ボリュームと連携して永続化領域を確保するボリュームです。
AWSを例にすると外部の永続ボリュームとしてEFSと連携します。
以下図を見てただけるとわかりやすいかと思います。
ただし、このPVを使用するためにはPersistentVolumeClaim (PVC)が必要になります。
PersistentVolumeClaim (PVC)とはコンテナとPVを紐づける役割を担っています。
Claim = 要求する といった意味になりますが、PVCはPodとPVの仲介役となり、Podが必要としているPVをPVCが要求し、PodとPVを紐づけます。
PVとPVCは1:1の関係である必要があります。
Kubernetesクラスター内にはNamespaceを作って境界を分けることがありますが、NamespaceごとにPVとPVCをapplyしてEFSと連携する必要があります。
EFSにおいてもNamespaceごとのEFSが必要となります。
マニフェストファイルを例に解説してみます。
sc.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: aws-efs-sc
provisioner: efs.csi.aws.com
pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: aws-pv-share
spec:capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: aws-efs-sc
csi:
driver: efs.csi.aws.com
volumeHandle: fs-xxxxxx # EFSのDNS名のファイルシステムのID
volumeAttributes:
path: /01 # EFS内のマウントパス
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: aws-pvc-share
namespace: aws-01
spec:
accessModes:
- ReadWriteMany
storageClassName: aws-efs-sc
resources:
requests:
storage: 5Gi
PV作成に当たり、どのStorageClass
を使用するかを宣言する必要があります。
以下の場合、AWSのEFSを使用することを宣言しています。
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: aws-efs-sc
provisioner: efs.csi.aws.com
provisioner: efs.csi.aws.com
の記載により、どのストレージと連携するか?を示しています。
この場合、Kubernetesがストレージリソースを作成する際に、外部のプロビジョニングシステム (AWS CSI ドライバー)を呼び出すことを宣言しています。
Amazon EFS CSIドライバーとは
CSIとはContainer Storage Interface
の略です。
Container Storage Interface
という名前からKubernetesとEFSが連携する際の橋渡しをする役目をします。
このEFS CSIドライバーはEKSクラスターのアドオンとして提供されています。
以下AWS公式にEFS CSIドライバーに関する解説がありますが、EKSクラスターがCSIドライバーをアドオンとして実装することで、KubernetesがEFSストレージを利用可能となります。
EFS CSIドライバーを使用する前提条件
- CSIドライバーを使用するためのポリシーがアタッチされたIAMロールをServiceAccountに関連付け
- CSIドライバーのインストール
IAMロールをServiceAccountに関連付けするには以下コマンドを実行します。
以下のコマンドを実行する上での前提条件はeksctl, kubectlコマンドが実行できることです。
eksctl, kubectlのインストールについては以下AWS公式に手順があります。
https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/install-kubectl.html
## IAMポリシードキュメントのダウンロード
curl -S https://raw.githubusercontent.com/kubernetes-sigs/aws-efs-csi-driver/v1.2.0/docs/iam-policy-example.json -o iam-policy.json
## IAMポリシーの作成
aws iam create-policy \
--policy-name EFSCSIControllerIAMPolicy \
--policy-document file://iam-policy.json
## Kubernetesサービスアカウントの作成
eksctl create iamserviceaccount \
--cluster=<cluster> \ # EKSクラスターの名前を指定
--region <AWS Region> \
--namespace=kube-system \ # Namespaceの指定 -> CSIドライバーがインストールされるNamespaceと一致させる必要がある
--name=efs-csi-controller-sa \ # ServiceAccountの名前を指定
--override-existing-serviceaccounts \
--attach-policy-arn=arn:aws:iam::<AWS account ID>:policy/EFSCSIControllerIAMPolicy \
--approve
helmを使用してEFS CSIドライバーをインストール
helmを使用する場合にはhelmをインストールしている必要があります。
以下AWS公式に手順があります。
https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/helm.html
helm repo add aws-efs-csi-driver https://kubernetes-sigs.github.io/aws-efs-csi-driver
helm repo update
helm upgrade -i aws-efs-csi-driver aws-efs-csi-driver/aws-efs-csi-driver \
--namespace kube-system \ # ServiceAccountを作成したNamespaceと一致させる必要がある
--set image.repository=602401143452.dkr.ecr.us-west-2.amazonaws.com/eks/aws-efs-csi-driver \
--set serviceAccount.controller.create=false \
--set serviceAccount.controller.name=efs-csi-controller-sa
ここまでStorageClass
からAWS CSIドライバーまで話までしました。
PersistentVolume
次にPVです。
以下がPVを作成するマニフェストとなります。
apiVersion: v1
kind: PersistentVolume
metadata:
name: aws-pv-share
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: aws-efs-sc
csi:
driver: efs.csi.aws.com
volumeHandle: fs-xxxxxx # EFSのDNS名のファイルシステムのID
volumeAttributes:
path: /01 # EFS内のマウントパス
spec.capacity
spec
セクションでボリュームの容量を5GiBを宣言しています。
しかし、EFSの容量制限はないですが、必須の記載項目となっていますので形式的に記載しています。
spec.accessModes
accessModes
はボリュームのマウント方法をKubernetesに伝えます。
ReadWriteMany
は「同時に複数のPodから同時に読み込み/書き込みが可能」を意味します。
例えばPodAとPodBが同じEFSの/data
に同時にファイルを書き込みすることができます。
spec.persistentVolumeReclaimPolicy
Retain
はPVのライフサイクルポリシーを指定するもので、PVCが削除されたとしてもPVとEFS(およびストレージ内のデータ)は保持する、という指定です。
PersistentVolumeClaim
以下はPVCの内容ですが、PVCはどのようなストレージを要求するか、を宣言しPVをバインドします。
storageClassName: aws-efs-sc
により、StorageClassで定義している外部ストレージを使用することを宣言しています。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: aws-pvc-share
namespace: aws-01
spec:
accessModes:
- ReadWriteMany
storageClassName: aws-efs-sc
resources:
requests:
storage: 5Gi
再掲しますが、StorageClassではprovisioner: efs.csi.aws.com
とすることで、AWSのEFSと連携することを宣言しています。
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: aws-efs-sc
provisioner: efs.csi.aws.com
PV, PVCとEFSを連携した動作確認をしたいところですが、そのためにはEKSクラスターの構築が必要になってくるため、AWS環境を利用した動作確認は割愛します。
ローカルクラスターで代替できる方法で動作確認を行っていきたいと思います。
EFSの代替としてローカルクラスター上のノードをストレージとしてマウントします。
1.クラスターのノードに接続し、適当なファイルを作成します。
docker container exec -it sandbox-cluster-control-plane bash
root@sandbox-cluster-control-plane:/#
root@sandbox-cluster-control-plane:~# mkdir -p /mnt/data
root@sandbox-cluster-control-plane:~# echo "Hello from Node!" > /mnt/data/test.txt
root@sandbox-cluster-control-plane:~# ls -l /mnt/data/test.txt
-rw-r--r-- 1 root root 17 Apr 2 22:09 /mnt/data/test.txt
root@sandbox-cluster-control-plane:~# cat /mnt/data/test.txt
Hello from Node!
root@sandbox-cluster-control-plane:~# exit
exit
2.以下のPVとPVCのマニフェストファイルを作成します。
# pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: node-pv
spec:
storageClassName: node-sc # 明示的に指定
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /mnt/data # ノード内の絶対パス
type: Directory
# pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: node-pvc
spec:
storageClassName: node-sc # PVと同じストレージクラス名
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
PVとPVCのマニフェストファイルにおいてstorageClassName: node-sc
と明示的に記載することで、PVとPVCをマッチングさせています。
PVとPVCに記載するstorageClassNameは完全一致させる必要があります。
ローカルクラスターの場合、外部ストレージを使用しないことから、StorageClass
のマニフェストファイルは不要となります(hostPathを使用してクラスターノードを使用するためです)
3.applyしてきます。
kubectl apply -f pv.yaml
persistentvolume/node-pv created
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
node-pv 1Gi RWO Retain Available node-sc <unset> 13s
kubectl apply -f pvc.yaml
persistentvolumeclaim/node-pvc created
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
node-pvc Bound node-pv 1Gi RWO node-sc <unset> 9s
PVのSTATUSがAvailable
となっているのは利用可能な状態を表しています。
PVCのSTATUSがBound
となっているのは利用可能なPVとバインド済みであることを表しています。
PVC作成後に再度kubectl get pv
をしてSTATUSがBound
状態になっていることを確認します。
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
persistentvolume/node-pv 1Gi RWO Retain Bound default/node-pvc node-sc <unset> 22m
これでPVとPVCが問題なくバインドされていることが確認できます。
PVとPVCの準備が整いましたのでPodと連携していきましょう。
以下のPodのマニフェストファイルを使用してapplyしていきます。
# pod-test.yaml
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
containers:
- name: nginx
image: nginx:latest
volumeMounts:
- name: node-storage # volumesで定義したnameと一致させる
mountPath: /usr/share/nginx/html # PVCにマウントされるディレクトリ
volumes:
- name: node-storage
persistentVolumeClaim:
claimName: node-pvc
PodはPVCを介してPVと連携することから、どのPVCを指定するかを記載する必要があります。
volumes.persistentVolumeClaim
がその箇所です。
PVCと連携する際にPod内のどのディレクトリにマウントするのか?を記載しているのが、volumeMounts
の箇所になります。
連携の全体像は以下となります。
ではPodをapplyしていきます。
kubectl apply -f pod-test.yaml
kubectl get pod
NAME READY STATUS RESTARTS AGE
test-pod 1/1 Running 0 9s
Podに接続してmountPath内にtest.txtが存在しているかを確認してみます。
kubectl exec -it test-pod -- /bin/bash
root@test-pod:~# cat /usr/share/nginx/html/test.txt
Hello from Node!
クラスターのノードに作成したtest.txtが存在することが確認できました。
以上でPersistentVolume/PersistentVolumeClaimに関する解説を終わりたいと思います。
PVをEFSと連携して使用するための補足
EFSと連携して使用する場合にはEFSにポート2049を許可するインバウンドルールが必要となります。
参考
https://kubernetes.io/ja/docs/concepts/storage/volumes/
https://www.ios-net.co.jp/blog/20230628-1190/