本記事の概要
Kubernetesクラスター上にNFSサーバを作ることで、Kubernetesクラスター内でReadWriteMany(RWX)なボリューム、つまり「複数のPodから同時に読み書きできるボリューム」を割と簡単に利用可能にする方法を紹介します。
課題
Kubernetes上で、複数のPodから同時アクセスするRWXなボリュームを用意するのは意外と面倒です。例えばオンプレミスのKubernetesだとNFSサーバ等を用意する必要があります。パブリッククラウドのマネージドKubernetesの場合、EKSだとAmazon EFS、AKSだとAzure Files等が使えますが、リソース作成やKubernetesで利用可能にするためのセットアップにだいぶ手間がかかります。
特に検証用や小規模な環境の場合は、「なるべく手間とお金をかけずにReadWriteManyなボリュームを使える状態にしたい!」と思う方も多いでしょう。本記事はそのような時に活用できそうな内容を書いてみました。
背景
実は過去に同じようなことを試して、以下の記事にまとめたことがあります。
この方法だと、NFSサーバのコンテナイメージのビルドやKubernetesリソースの自分で作成する必要があり結構手間でした。また、NFSサーバのサービス公開手段に関してもいくつかの課題がありました。
「もっといい方法があればいいのに...」と消化不良な気持ちを抱えて記事を書き終えたのを覚えています。
それから一年とちょっと経過。改めて調べてみたら、nfs-server-provisionerという「NFSサーバ用のHelmチャート」が公開されていることに気づきました。
これを使うと、helm install
相当のコマンドで、あっさりとNFSサーバを作れてしまいました。なぜ一年前にこれに気づけなかったのだろう...w
nfs-server-provisionerのHelmチャートのインストール
以後は、nfs-server-provisionerのインストール手順を記載します。
前提条件
- Kuberenetesクラスターが構築済であること
-
helm
とhelmfile
が使えること
事前準備
Kubernetesの各ノードに nfs-common をインストールしておく必要があります。ノードのOSがUbuntu系の場合は、以下のコマンドでインストールします。
sudo apt install nfs-common -y
ノードに nfs-common をインストールしないと、どうなるか?
nfs-common をインストールし忘れていると、NFSをマウントするPodを起動する際に次のようなメッセージがEventに出力されてマウントに失敗します。
Warning FailedMount 21s (x7 over 53s) kubelet MountVolume.SetUp failed for volume "pvc-756add44-f386-4714-956e-1a15028636ed" : mount failed: exit status 32
Mounting command: mount
Mounting arguments: -t nfs -o retrans=2,timeo=30,vers=3 10.107.158.61:/export/pvc-756add44-f386-4714-956e-1a15028636ed /var/lib/kubelet/pods/f479f261-76ca-410a-b33f-093e865f3216/volumes/kubernetes.io~nfs/pvc-756add44-f386-4714-956e-1a15028636ed
Output: mount: /var/lib/kubelet/pods/f479f261-76ca-410a-b33f-093e865f3216/volumes/kubernetes.io~nfs/pvc-756add44-f386-4714-956e-1a15028636ed: bad option; for several filesystems (e.g. nfs, cifs) you might need a /sbin/mount.<type> helper program.
※ 本当はノードで個別設定をするのは避けたいのですが、現状回避策が見当たりませんでした。もし見つかった場合は追記or修正予定です。
Helmfileの用意
事前準備ができたら、以下のHelmfile形式のファイルを用意します。
repositories:
- name: nfs-ganesha-server-and-external-provisioner
url: https://kubernetes-sigs.github.io/nfs-ganesha-server-and-external-provisioner
releases:
- name: nfs-server-provisioner
namespace: nfs-server-provisioner
createNamespace: true
chart: nfs-ganesha-server-and-external-provisioner/nfs-server-provisioner
version: 1.8.0
values:
- persistence: # 必須ではないが、設定を強く推奨(後述)
enabled: true
storageClass: "openebs-hostpath" # あくまで一例
最後の values[0].persistence
は、NFSサーバのコンフィグやマウント後の作成ファイルの永続化に関する設定をします。
enabled: true
enabled
はデフォルトが false
です。デフォルトから変えないと、NFSマウントで作成したファイルが emptyDir
のボリュームに作られてしまい、永続化が保証されないので注意が必要です。(この辺の話は本記事の最後の方にも詳しく書いています)
storageClass: "openebs-hostpath"
本例では上のように書くことで、永続化ボリュームとして openebs-hostpath
のStorageClassリソースを使う設定にしていますが、ここは環境に応じて適宜変更ください。StorageClassではなくて、直接PVCを永続化先として指定する場合は以下のように書きます。
values:
- persistence:
enabled: true
existingClaim: "export-pvc" # あくまで一例
Helmチャートのインストール
helmfileコマンドで、nfs-server-provisionerのHelmチャートをインストールします。
helmfile apply -f nfs-server-provisioner.yaml
リソースが作成されたのを確認します。
$ kubectl get all -n nfs-server-provisioner
NAME READY STATUS RESTARTS AGE
pod/nfs-server-provisioner-0 1/1 Running 0 12s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/nfs-server-provisioner ClusterIP 10.106.177.244 <none> 2049/TCP,2049/UDP,32803/TCP,32803/UDP,20048/TCP,20048/UDP,875/TCP,875/UDP,111/TCP,111/UDP,662/TCP,662/UDP 12s
NAME READY AGE
statefulset.apps/nfs-server-provisioner 1/1 12s
NFSを利用可能なStorageClassも以下のように作られます。
$ kubectl get storageclass -l app=nfs-server-provisioner
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
nfs cluster.local/nfs-server-provisioner Delete Immediate true 33s
これで、Kubernetesクラスター内でNFSのボリュームが使える状態になりました。
NFSのボリュームをマウントしてみる
実際にNFSボリュームが作れるのかを試してみます。
PVCの作成
まずはボリューム作成のためのPVCを以下のマニフェストで作ってみます。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-pvc
namespace: default
spec:
storageClassName: nfs # ここがポイント
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
実行コマンドは以下
kubectl apply -f nfs-pvc.yaml
PVCが作られて、ダイナミックプロビジョニングでPVも作られていることを確認できます。
$ kubectl get pvc -n default
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
nfs-pvc Bound pvc-c0b29986-7679-4f90-b49d-d1f7a2da99ab 1Gi RWX nfs <unset> 7s
Podにマウントしてみる
続いて、上記のボリュームをマウントするDeploymentリソースをレプリカ数3で作ります。以下がマニフェストです。
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
実行コマンドは以下
kubectl apply -f mnt-nfs-deploy.yaml
各PodでNFSボリュームがマウントされて、以下のように起動するのを確認します。
$ kubectl get pod -n default
NAME READY STATUS RESTARTS AGE
mnt-nfs-65f866fc8b-8kfg2 1/1 Running 0 18s
mnt-nfs-65f866fc8b-cs99m 1/1 Running 0 18s
mnt-nfs-65f866fc8b-msm6h 1/1 Running 0 18s
確かに、/data
ディレクトリがNFSでマウントされていることがわかります。
$ kubectl exec -n default -it deploy/mnt-nfs -- mount | grep /data
10.107.158.61:/export/pvc-c0b29986-7679-4f90-b49d-d1f7a2da99ab on /data type nfs (rw,relatime,vers=3,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=30,retrans=2,sec=sys,mountaddr=10.107.158.61,mountvers=3,mountport=20048,mountproto=udp,local_lock=none,addr=10.107.158.61)
NFSボリュームのマウント元の確認
上記の/data
ディレクトリの実体は、最初に作った nfs-server-provisioner-0
というPod(以下)の /export
ディレクトリ配下に存在します。
$ kubectl get pod -n nfs-server-provisioner
NAME READY STATUS RESTARTS AGE
nfs-server-provisioner-0 1/1 Running 0 120m
それを確認すべく、試しにマウント先で適当なファイルを作ってみます。
kubectl exec -n default -it deploy/mnt-nfs -- \
sh -c 'echo hello >> /data/tmp.txt'
これと同じファイルを、nfs-server-provisioner-0
の以下のディレクトリで確認できます。
$ kubectl exec -n nfs-server-provisioner -it nfs-server-provisioner-0 -- \
ls -l /export/pvc-c0b29986-7679-4f90-b49d-d1f7a2da99ab
(以下が実行結果)
total 4
-rw-r--r-- 1 root root 6 Oct 27 06:08 tmp.txt
さらに言うと、/export
ディレクトリも特定のボリュームをマウントして作られているので、本当の実体はさらに別のところにあります。この部分の設定は、最初に nfs-server-provisioner のHelmチャートを作った時の value の中で、persistence
配下の設定によって決まります。
何だか文章だけではわけがわからなくなってきたので、今の状況を図にしてみました。
この図を見ると、マウントポイントとマウント元が多重になっているのがわかります。
また障害耐性やパフォーマンスの観点では、一番右の「nfs-server-provisionerの設定に依存」の部分が、実際のデータが格納されている箇所でかなり重要であることがわかります。
繰り返しになりますが、nfs-server-provisionerのHelmチャートをデプロイする際に全てデフォルトのvalueを使うと、/export
ディレクトリのボリュームがKubernetesのemptyDirと呼ばれる場所に作られます。この場合、もしnfs-server-provisioner-0
のPodが何らかの原因で消えてしまうと、NFSのマウントポイントで作成したデータも全て消えてしまいます。
したがってnfs-server-provisionerのHelmチャートをデプロイする際は、何かしらの永続的なストレージリソースを persistence
配下のvalueで指定しておくことを強くお勧めします。
上手くいかなかった点
nfs-server-provisionerのStatefulSetのレプリカ数がデフォルト1なので、冗長性を上げるために2にしてみましたが、NFSをマウントするPodがRunningにならずに上手く動作しませんでした。
ドキュメントの Persistence の部分に次のように書かれているので、動かなかったのも一応納得です。
If this chart is deployed with more than 1 replica, storageClass.defaultClass=true and persistence.storageClass, then the 2nd+ replica will end up using the 1st replica to provision storage - which is likely never a desired outcome.