はじめに
自前で構築したK8sクラスターでReadWriteMany(RWX)なPersistentVolume(PV)を作りたい場合の一つの方法として、Kubernetesクラスター外にNFSサーバーを立ててPVやPVCを作ってPodにマウントするのが考えられます。けど「それってもしかしてファイルサーバーそのものもKubernetesクラスター内で作ってもいけるんじゃない!?」と思って試してみました。
今回はNFSのコンテナイメージを使ってやってみたのでご紹介します。
実現したいことの具体化
あるノードのディレクトリ/data
をNFS ServerのPodにマウントして、マウントしたディレクトリを「NFS ServerのServiceリソース」を使ってクラスター内に公開します。
また、こちらで公開されているNFS用のCSIドライバー(csi-nfs-driver)を使ってNFS ServerのServiceリソースにアクセスすることで、PVCが作成された際に動的プロビジョニングによりPVを作成し、Kuberenetes上で利用可能なボリュームを提供できる状態にします。
以下がやりたいことを図示したものです。
今回の構成ではNFS Serverに利用するボリュームはhostPath
によりノードのディレクトリを直接指定していますが、既に何らかのCSIドライバー(Amazon EBS, OpenEBS等)がインストール済であれば、hostPath
ではなくてpersistentVolumeClaim
で動的プロビジョニングによりボリュームを用意した方がいいでしょう。
準備:NFS Serverのコンテナイメージの準備
上の図でNFS ServerのPodを作るコンテナイメージは何らかの手段で自分で用意する必要があります。docker.ioなどのコンテナレジストリから持ってこれれば一番楽ですが、すぐに見つけられませんでした。そこで下記ソースをgit clone
してローカルでビルドすることにしました。
ソース取得
git clone https://github.com/GoogleCloudPlatform/nfs-server-docker.git
コンテナイメージのビルド
執筆時点だと1/debian11/1.3/
のディレクトリにそのままビルドできるソースがあったので、以下のコマンドでビルドします。
cd 1/debian11/1.3/
docker build -t nfs-server1:1.3.4-debian11 .
上記はローカルPC上でのみコンテナイメージを使う場合の例ですが、コンテナレジストリにpushする場合は以下コマンドを実行すればいいはずです。
docker build -t [hostname-registry]/nfs-server1:1.3.4-debian11 .
docker push [hostname-registry]/nfs-server1:1.3.4-debian11
ここまで説明しておきながらの後出しですみませんが、筆者もDocker HubにNFSサーバのコンテナイメージを置いたので、よかったらお使いください。
NFS Serverのデプロイ
StatefulSetリソース
NFS ServerのStatefulSetリソースのマニフェストを次のように作成します。
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
app: nfs-server
name: nfs-server
spec:
replicas: 1 # 解説(1)
selector:
matchLabels:
app: nfs-server
template:
metadata:
labels:
app: nfs-server
spec:
nodeName: "[マウントポイントを提供するノード名]" # 解説(2)
containers:
- name: nfs-server
image: "showchan33/nfs-server1:1.3.4-debian11" # 適宜変更
securityContext:
privileged: true # 解説(3)
ports:
- containerPort: 2049
volumeMounts:
- mountPath: "/exports"
name: exports
volumes:
- name: exports
hostPath: # 解説(4)
path: /data
type: DirectoryOrCreate
解説
- (1)
spec.replicas : 1
- 2以上にして冗長化しても問題なさそうです
- (2)
nodeName: "[マウント元を提供するノード名]"
- NFSのマウントポイントを提供するノードでデプロイされる必要があります
- 未調査ですが、AWS等パブリッククラウドが提供するストレージを利用する場合は(おそらく)記載不要です
- (3)
spec.securityContext.privileged : true
-
capabilities.add
でもう少し権限を限定できるかもしれませんが、少なくともDockerで["NET_ADMIN", "SYS_ADMIN", "SYS_MODULE"]
を指定してもダメでした
-
- (4)
hostPath:
- マウントポイントをK8sクラスター上のノードから提供する場合の書き方です
補足:マウント時に指定するパスについて
上記で作ったNFS Serverの共有ディレクトリは/exports
になっていますが、今回利用したコンテナイメージの設定ファイル/etc/exports
には次のように、fsid=0
が記載されています。
/etports *(rw,fsid=0,sync,no_subtree_check,no_root_squash)
今回はNFSv4を使ってクライアントからマウントしますが(v3ではランダムなポートを利用するためK8sでは使えませんでした...)、NFSv4ではfsidがついている共有ディレクトリはクライアントからは/exports
ではなくルート/
として見えます。(以後に記載するマウント先のパスも全て/
となります)
参考情報
Serviceリソース
以下はServiceリソースのマニフェストです。
今回はNFSv4でマウントを行いますが、その場合は2049/TCPのみを開放すればいいようです。
apiVersion: v1
kind: Service
metadata:
labels:
app: nfs-server
name: nfs-server-svc
spec:
type: ClusterIP
ports:
- name: nfs-2049
port: 2049
protocol: TCP
targetPort: 2049
selector:
app: nfs-server
NFS Serverのリソース作成と確認
以下コマンドでデプロイします。
kubectl apply -f deployment.yaml -f service.yaml
作成したリソースが動いているのを確認します。
$ kubectl get all -l app=nfs-server
NAME READY STATUS RESTARTS AGE
pod/nfs-server-5bd88d64b6-qwknc 1/1 Running 0 10s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/nfs-server-svc ClusterIP 10.96.108.121 <none> 2049/TCP 10s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/nfs-server 1/1 1 1 10s
NAME DESIRED CURRENT READY AGE
replicaset.apps/nfs-server-5bd88d64b6 1 1 1 10s
csi-driver-nfs のHelmチャートのインストール
NFSサーバを使ってPodからボリュームをマウントできるようにするため、NFSのCSIドライバーをインストールします。
上記のGitHubリポジトリには、NFS CSIドライバーのHelmチャートも用意されています。そこで以下のHelmfileを使って、Helmチャート経由でインストールすることにします。
以下を実行するには、KubernetesのクライアントにHelmとHelmfileがインストールされている必要があります。
repositories:
- name: csi-driver-nfs
url: https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts
releases:
- name: csi-driver-nfs
namespace: kube-system
chart: csi-driver-nfs/csi-driver-nfs
version: v4.9.0
values:
- storageClass:
create: true
name: nfs-csi
parameters:
server: nfs-server-svc.default.svc.cluster.local # NFSサーバのServiceリソースのホスト名を指定
share: / # /exports ではなくて / を指定する
mountOptions:
- nfsvers=4.1
インストールコマンド
helmfile apply -f csi-driver-nfs.yaml
各種リソースができているのを確認します。
$ kubectl get deploy,ds -n kube-system -l helm.sh/chart=csi-driver-nfs-v4.9.0
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/csi-nfs-controller 1/1 1 1 30m
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
daemonset.apps/csi-nfs-node 2 2 2 2 2 kubernetes.io/os=linux 30m
$ kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
nfs-csi nfs.csi.k8s.io Delete Immediate false 32m
PVCを作って、PVが動的プロビジョニングされることの確認
以下のマニフェストでPVCを作ってみます。StorageClassの指定で、nfs-csi
と書くのがポイントです。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-pvc
namespace: default
spec:
storageClassName: nfs-csi
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
実行コマンド
$ kubectl apply -f nfs-pvc.yaml
persistentvolumeclaim/nfs-pvc created
PVCだけでなく、動的プロビジョニングによりPVも作成されていることを確認できます。
$ kubectl get pvc -n default
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
nfs-pvc Bound pvc-c08920a6-238a-4635-91b8-aa31cd8730c0 1Gi RWX nfs-csi <unset> 24s
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
pvc-c08920a6-238a-4635-91b8-aa31cd8730c0 1Gi RWX Delete Bound default/nfs-pvc nfs-csi <unset> 28s
ボリュームをマウントするPodの作成
PodやDeployment等のワークロードリソースを作成時に、マニフェストで上記のPVCを指定すると、PVのマウントしてReadWriteManyなボリュームとして使えます。
以下にマニフェストの例だけ載せておきます。
NFSボリュームをマウントするPodのマニフェスト
apiVersion: apps/v1
kind: Deployment
metadata:
name: mnt-nfs
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: mnt-nfs
template:
metadata:
labels:
app: mnt-nfs
spec:
containers:
- name: mnt-nfs
image: busybox
command: ["tail", "-f", "/dev/null"]
volumeMounts:
- name: nfs-volume
mountPath: /data
volumes:
- name: nfs-volume
persistentVolumeClaim:
claimName: nfs-pvc
まとめ
NFSのコンテナイメージとNFSのCSIドライバーを使うことで、Kubernetesクラスター内に良い感じにRWXのボリュームが作れることを確認できました。
以降は過去に書いた記事 (CSIドライバーを使わないケース)
過去に書いていた内容は、「NFSのCSIドライバーを使わずに自分でPVとPVCの両方を作ってNFSボリュームを用意する」ものでした。当時やりたかったことを図示したものが以下です。
この場合だと、PVの定義でNFS ServerのServiceリソースにクラスター内ネットワークでアクセスできないため、一工夫必要になります。以下で具体的に見ていきましょう。
NFS Serverのマウントポイントにマウント
NFS Serverのマウントポイントにマウントできるかを確認してみます。
PV,PVCを作成してもアクセスできない問題...
以下のPVとPVCのマニフェストを作れば問題なさそうに見えます。
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-client-pv
labels:
pv: nfs-client-pv
spec:
capacity:
storage: 1Gi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
path: /
server: nfs-server-svc # (推測)クラスター内のサービスにはアクセスできない(?)
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-client-pvc
spec:
accessModes:
- ReadWriteMany
volumeMode: Filesystem
resources:
requests:
storage: 1Gi
selector:
matchLabels:
pv: nfs-client-pv
しかしながら上のマニフェストで作ったPVCをvolumesやvolumeMountsでマウントしようとすると、次のエラーが出てマウントできません。
Output: mount.nfs: Failed to resolve server nfs-server-svc: Name or service not known
うまくいかない理由は推測ですが、PVのspec.nfs.server
はクラスター外のサービスを参照することを前提としているからと思われます。nfs-server-svc
はクラスター内(かつ同じNamespace)でしか認識されないホスト名なのでマウント時に見つからずに失敗しているようです。
そこで(あまりスマートとは言えないですが)、以下の2つの方法でこの問題が解決できることを確認しました。
- (1) NFS ServerのServiceリソースに
externalIPs
を追加する - (2) Podに直接 NFS ServerのServiceをマウントする
(1)の方法で最初はNodePortを使えばいいと思ったのですが、PVのspec.nfs.server
にIP:[NodePort番号]
を書いても上手くいかないことが判明しました...
以下順番に詳細説明していきます。
(1) NFS ServerのServiceリソースにexternalIPs
を追加する
以下のように、specにexternalIPs
を追加します。追加するIPアドレスは例えばノードのIPなど、クラスターの外からアクセス可能なIPアドレスを記載する必要があります。
apiVersion: v1
kind: Service
metadata:
labels:
app: nfs-server
name: nfs-server-svc-external
spec:
type: ClusterIP
externalIPs:
- [クラスター外からアクセス可能なIPアドレス]
ports:
- name: nfs-2049
port: 2049
protocol: TCP
targetPort: 2049
selector:
app: nfs-server
上記Serviceリソースをデプロイします。
kubectl apply -f service-externalIPs.yaml
PVとPVCに関しては、こちらで記載したマニフェストで、PVのspec.nfs.server
の部分を次のように変更します。
nfs:
path: /
server: [クラスター外からアクセス可能なIPアドレス]
PVとPVCのリソースをデプロイします。
kubectl apply -f pv-pvc.yaml
上記で作成したPVCをマウントするリソースを作ります。複数のPodからマウントできることを確認するため、レプリカ数3のDeploymentリソースで作ります。
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: mount-pvc
name: mount-pvc
spec:
replicas: 3
selector:
matchLabels:
app: mount-pvc
template:
metadata:
labels:
app: mount-pvc
spec:
containers:
- name: mount-pvc
image: debian
command:
- bash
- -c
- tail -f /dev/null
volumeMounts:
- mountPath: "/mnt"
name: nfs
volumes:
- name: nfs
persistentVolumeClaim:
claimName: nfs-client-pvc
Deploymentのリソースを作成します。
kubectl apply -f mount-pvc.yaml
問題なく動いていることを確認します。
$ k get all -l app=mount-pvc
NAME READY STATUS RESTARTS AGE
pod/mount-pvc-6fdc8568c-656nw 1/1 Running 0 3m23s
pod/mount-pvc-6fdc8568c-8ddbt 1/1 Running 0 3m23s
pod/mount-pvc-6fdc8568c-t94mc 1/1 Running 0 3m23s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/mount-pvc 3/3 3 3 3m23s
NAME DESIRED CURRENT READY AGE
replicaset.apps/mount-pvc-6fdc8568c 3 3 3 3m23s
以下のように、Podの/mnt
ディレクトリにPVCがマウントされていることを確認できます。
$ kubectl exec -it mount-pvc-6fdc8568c-656nw -- df -h
Filesystem Size Used Avail Use% Mounted on
overlay 30G 19G 9.1G 68% /
tmpfs 64M 0 64M 0% /dev
tmpfs 988M 0 988M 0% /sys/fs/cgroup
192.168.1.4:/ 75G 60G 12G 84% /mnt
/dev/vda1 30G 19G 9.1G 68% /etc/hosts
shm 64M 0 64M 0% /dev/shm
tmpfs 1.9G 12K 1.9G 1% /run/secrets/kubernetes.io/serviceaccount
(2) Podに直接 NFS ServerのServiceをマウントする
もう一つのアプローチは、PV,PVCを介さずにPodから直接NFS Serverをマウントする方法です。以下がマニフェストです。
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: mount-direct
name: mount-direct
spec:
replicas: 3
selector:
matchLabels:
app: mount-direct
template:
metadata:
labels:
app: mount-direct
spec:
containers:
- name: mount-direct
image: debian
securityContext:
privileged: true # これがないとマウントできない
command:
- bash
- -c
- >- # nfsのマウントコマンドをそのまま書く
apt-get update &&
apt-get install -y nfs-common &&
mount nfs-server-svc:/ /mnt -t nfs -o vers=4.2 &&
tail -f /dev/null
Deploymentのリソースを作成します。
kubectl apply -f mount-direct.yaml
問題なく動いていることを確認します。
$ kubectl get all -l app=mount-direct
NAME READY STATUS RESTARTS AGE
pod/mount-direct-5bbf476cc5-jxzb6 1/1 Running 0 6s
pod/mount-direct-5bbf476cc5-pp597 1/1 Running 0 6s
pod/mount-direct-5bbf476cc5-q5xr6 1/1 Running 0 6s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/mount-direct 3/3 3 3 7s
NAME DESIRED CURRENT READY AGE
replicaset.apps/mount-direct-5bbf476cc5 3 3 3 6s
こちらはnfs-server-svc
という名前でNFS Serverを参照してマウントできているのを確認できます。
$ kubectl exec -it mount-direct-5bbf476cc5-jxzb6 -- df -h
Filesystem Size Used Avail Use% Mounted on
overlay 75G 60G 12G 84% /
tmpfs 64M 0 64M 0% /dev
tmpfs 5.8G 0 5.8G 0% /sys/fs/cgroup
/dev/sda5 75G 60G 12G 84% /etc/hosts
shm 64M 0 64M 0% /dev/shm
tmpfs 12G 12K 12G 1% /run/secrets/kubernetes.io/serviceaccount
nfs-server-svc:/ 75G 60G 12G 84% /mnt
障害耐性の確認
nfs-serverのPodが何らかの原因で稼働しなくなった場合、当然ながらクライアントでマウントしたディレクトリはアクセスできなくなってしまいます。nfs-serverがその後復旧しても、上記で試した2ケースで以下のようにアクセスできないようでした。
$ kubectl exec -it mount-pvc-6fdc8568c-656nw -- df -h
(レスポンス無し)
$ kubectl exec -it mount-direct-5bbf476cc5-jxzb6 -- df -h
(レスポンス無し)
この場合、Deploymentを再起動すると復旧することを確認できました。
kubectl rollout restart deploy/mount-pvc
kubectl rollout restart deploy/mount-direct
「nfs-server
のレプリカ数を2以上にすればいいのでは?」と思ってトライしてみましたが、たとえ複数にしても、あるnfs-server
のPodがダウンすると、クライアント側でもNFS Serverにアクセス不可になるPodが一定割合で出てくるようでした。なので今回の場合は冗長構成にする恩恵があんまりないのかもしれません。
例えばHTTPのServiceリソースであればこんな事にならずにちゃんと生きているPodのみにリクエストが行くようになると思いますが、NFSをService経由でマウントする時の仕組み的な問題なのかもしれません。まあ色々と課題が多い印象...