NFSサーバをKubernetesのServiceで作り、ReadWriteManyなPersistentVolumeを作る

# はじめに

自前で構築したK8sクラスターでReadWriteMany(RWX)なPersistentVolume(PV)を作りたい場合の一つの方法として、Kubernetesクラスター外にNFSサーバーを立ててPVやPVCを作ってPodにマウントするのが考えられます。けど「それってもしかしてファイルサーバーそのものもKubernetesクラスター内で作ってもいけるんじゃない！？」と思って試してみました。

今回はNFSのコンテナイメージを使ってやってみたのでご紹介します。

# 実現したいことの具体化

あるノードのディレクトリ``/data``をNFS ServerのPodにマウントして、マウントしたディレクトリを「NFS ServerのServiceリソース」を使ってクラスター内に公開します。
また、[こちら](https://github.com/kubernetes-csi/csi-driver-nfs)で公開されているNFS用のCSIドライバー(csi-nfs-driver)を使ってNFS ServerのServiceリソースにアクセスすることで、PVCが作成された際に動的プロビジョニングによりPVを作成し、Kuberenetes上で利用可能なボリュームを提供できる状態にします。

以下がやりたいことを図示したものです。

![nfs-server-pvc-rwx-new.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370519/d656f840-24a9-acc4-d527-cd152d2feb1b.png)

:::note info
今回の構成ではNFS Serverに利用するボリュームは``hostPath``によりノードのディレクトリを直接指定していますが、既に何らかのCSIドライバー（Amazon EBS, OpenEBS等）がインストール済であれば、``hostPath``ではなくて``persistentVolumeClaim``で動的プロビジョニングによりボリュームを用意した方がいいでしょう。
:::

# 準備：NFS Serverのコンテナイメージの準備

上の図でNFS ServerのPodを作るコンテナイメージは何らかの手段で自分で用意する必要があります。docker.ioなどのコンテナレジストリから持ってこれれば一番楽ですが、すぐに見つけられませんでした。そこで下記ソースを``git clone``してローカルでビルドすることにしました。

https://github.com/GoogleCloudPlatform/nfs-server-docker

#### ソース取得

```
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サーバのコンテナイメージを置いたので、よかったらお使いください。

https://hub.docker.com/r/showchan33/nfs-server1

# NFS Serverのデプロイ

## StatefulSetリソース

NFS ServerのStatefulSetリソースのマニフェストを次のように作成します。
```yaml:deployment.yaml
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``が記載されています。
```/etc/exports
/etports *(rw,fsid=0,sync,no_subtree_check,no_root_squash)
```

今回はNFSv4を使ってクライアントからマウントしますが（v3ではランダムなポートを利用するためK8sでは使えませんでした...）、NFSv4ではfsidがついている共有ディレクトリはクライアントからは``/exports``ではなくルート``/``として見えます。（以後に記載するマウント先のパスも全て``/``となります）

### 参考情報

https://qiita.com/hana_shin/items/7b31750bf899ee73b5d2

## Serviceリソース

以下はServiceリソースのマニフェストです。
今回はNFSv4でマウントを行いますが、その場合は2049/TCPのみを開放すればいいようです。
```yaml:service.yaml
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ドライバーをインストールします。

https://github.com/kubernetes-csi/csi-driver-nfs

上記のGitHubリポジトリには、NFS CSIドライバーのHelmチャートも用意されています。そこで以下のHelmfileを使って、Helmチャート経由でインストールすることにします。

:::note info
以下を実行するには、KubernetesのクライアントにHelmとHelmfileがインストールされている必要があります。
:::

```yaml:csi-driver-nfs.yaml
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`` と書くのがポイントです。

```yaml:nfs-pvc.yaml
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なボリュームとして使えます。

以下にマニフェストの例だけ載せておきます。

<details><summary>NFSボリュームをマウントするPodのマニフェスト</summary>

```yaml:mount-pvc.yaml
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
```
</details>

# まとめ

NFSのコンテナイメージとNFSのCSIドライバーを使うことで、Kubernetesクラスター内に良い感じにRWXのボリュームが作れることを確認できました。

<br>
<br>

---
# 以降は過去に書いた記事 （CSIドライバーを使わないケース）

過去に書いていた内容は、「NFSのCSIドライバーを使わずに自分でPVとPVCの両方を作ってNFSボリュームを用意する」ものでした。当時やりたかったことを図示したものが以下です。

![nfs-server-pvc-rwx.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370519/e95b4286-1347-6554-1682-20a4fd9cc2a2.png)

この場合だと、PVの定義でNFS ServerのServiceリソースにクラスター内ネットワークでアクセスできないため、一工夫必要になります。以下で具体的に見ていきましょう。

## NFS Serverのマウントポイントにマウント

NFS Serverのマウントポイントにマウントできるかを確認してみます。

## PV,PVCを作成してもアクセスできない問題...

以下のPVとPVCのマニフェストを作れば問題なさそうに見えます。

```yaml: pv-pvc.yaml
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をマウントする**

:::note info
(1)の方法で最初はNodePortを使えばいいと思ったのですが、PVの``spec.nfs.server``に``IP:[NodePort番号]``を書いても上手くいかないことが判明しました...
:::

以下順番に詳細説明していきます。

### (1) NFS ServerのServiceリソースに``externalIPs``を追加する

以下のように、specに``externalIPs``を追加します。追加するIPアドレスは例えばノードのIPなど、クラスターの外からアクセス可能なIPアドレスを記載する必要があります。
```yaml: service-externalIPs.yaml
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に関しては、[こちら](https://qiita.com/showchan33/items/fa3dadc546d4ae5e8c09#pvpvc%E3%82%92%E4%BD%9C%E6%88%90%E3%81%97%E3%81%A6%E3%82%82%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84%E5%95%8F%E9%A1%8C)で記載したマニフェストで、PVの``spec.nfs.server``の部分を次のように変更します。

```yaml
  nfs:
    path: /
    server: [クラスター外からアクセス可能なIPアドレス]
```

PVとPVCのリソースをデプロイします。
```
kubectl apply -f pv-pvc.yaml
```

上記で作成したPVCをマウントするリソースを作ります。複数のPodからマウントできることを確認するため、レプリカ数3のDeploymentリソースで作ります。

```yaml: mount-pvc.yaml
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をマウントする方法です。以下がマニフェストです。
```yaml:mount-direct.yaml
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が一定割合で出てくるようでした。なので今回の場合は冗長構成にする恩恵があんまりないのかもしれません。

:::note info
例えばHTTPのServiceリソースであればこんな事にならずにちゃんと生きているPodのみにリクエストが行くようになると思いますが、NFSをService経由でマウントする時の仕組み的な問題なのかもしれません。まあ色々と課題が多い印象...
:::