KubernetesでNFSを利用してPersistentStorageを準備する

# はじめに

:::note warn
【2022/03/18追記】
NFSを利用したPVの構築は、RWX(ReadWriteMany)モードを簡単に実現できるので便利です。以前は、Rook/Ceph がRWXモードをサポートしなかったり、CephFilesystem が一つしか使えないなどの制約があって、NFSを利用していました。

現在では Rook/Ceph がRWXをサポートするようになっていますので、既存のNFSサーバーを再利用したい、各ワーカー・ノードに空のドライブが接続されていない、といった理由がなければ Rook/Ceph の利用をお勧めします。

Rook/Cephを利用すれば、BlockStorageだけでなく、CephFilesystemでも、任意のサイズでコンテナから利用できる永続化デバイスを作成でき、後からサイズを拡張(resize)することも簡単にできます。
:::

いろいろアプリケーションをデプロイするため、cephfsを利用しようと思ったのですが、まずはお手軽なNFSから手をつけてみました。

お題として、k8sのドキュメントにあるWordpressのデプロイをkubesprayで準備したクラスターに適用してみます。

* 公式ドキュメント [Example: Deploying WordPress and MySQL with Persistent Volumes](https://kubernetes.io/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume/)

# 試してみてわかった事

1つのPV(PersistentVolumes)に複数のPVC(PV-Claim)を割り当てるといった事ができません。
そのためNFSサーバーで公開(export)した領域にサブディレクトリを作成し、k8s側ではこのサブディレクトリをPVとして定義しています。

パフォーマンス上の懸念はありますので、本番環境では物理ディスクを分けて複数のNFS領域を公開したり、専用マシンを準備したりする必要があると思われます。

テストで利用する分には、この方法はそれなりに有用だと思います。特に小さい領域を複数PVCとして確保したい場合には、便利です。それでも、Rook/Cephに慣れてしまうと、もう戻れないと思います。
以前のRook/CephではRWXな領域の確保ができなかったのですが、最新版では複数のファイルシステムが定義できるのでRWX接続が必要であってもNFS接続を積極的に利用する理由はなくなりました。

それでもiSCSI環境が整備されていたり、既設のNFSサーバーインフラがあれば、この方法もまだ役に立つかもしれません。


:::note alert
2020/03/23追記: ".metadata.annotations.volume.beta.kubernetes.io/storage-class"に"slow" を指定する方法は古く、現在では".spec.storageClassName"で"slow"を指定する方法が正しい表記です。詳しくは本家のドキュメントを参照してください。 [In the past, the annotation volume.beta.kubernetes.io/storage-class was used instead of the storageClassName attribute. This annotation is still working; however, it will become fully deprecated in a future Kubernetes release.](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistent-volumes)
:::

# NFS Serverの準備

外部に専用のNFSサーバーを準備した例と、k8sノードの1台をNFSサーバーにした例の2つで試しています。
それぞれUbuntu 16.04 LTSが前提です。

> 2020/03/23追記: 18.04 LTSをNFSサーバーとする場合でも同様に動作します。

```bash
$ sudo apt-get install nfs-kernel-server
```

## 適当なディレクトリのexport

今回は `/export/nfs/` を利用することにします。IPアドレス(```192.168.0.0/16```)の部分は環境に合わせて、192.168.1.0/24や10.0.0.0/8などに適宜変更してください。

```bash
$ sudo mkdir /export/nfs
$ sudo chmod 1777 /export/nfs/.
$ echo "/export/nfs 192.168.0.0/16(rw,async,no_root_squash) 127.0.0.1/32(rw,async,no_root_squash)" | tee -a /etc/exports
$ sudo systemctl restart nfs-server rpcbind
$ sudo systemctl enable nfs-server rpcbind
```

## PVとして利用するサブディレクトリの作成

例として100個分のディレクトリを作成します。

```bash
$ i=1;i_max=100; while test $i -le ${i_max}; do j=$(printf "%04d" $i); sudo mkdir /export/nfs/pv$j ; i=$(($i+1)) ; done
```

次のように大量の空ディレクトリができました。

```bash
$ ls /export/nfs/
pv0001/  pv0010/  pv0019/  pv0028/  pv0037/  pv0046/  pv0055/  pv0064/  pv0073/  pv0082/  pv0091/  pv0100/
pv0002/  pv0011/  pv0020/  pv0029/  pv0038/  pv0047/  pv0056/  pv0065/  pv0074/  pv0083/  pv0092/
pv0003/  pv0012/  pv0021/  pv0030/  pv0039/  pv0048/  pv0057/  pv0066/  pv0075/  pv0084/  pv0093/
...
```

## k8sノードへのnfs-commonパッケージの導入
後の工程で```kubectl describe pods <podsname>```を実行したところ、k8sのノードからNFSサーバーにアクセスできていないことが分かりました。

Ubuntu 16.04 LTSを利用して、Kubesprayでインスタンスを構築した場合には、nfs-commonパッケージの導入が必要になります。

nfs-commonパッケージをk8sの全ノードにインストールしておきます。
実際にはansibleコマンドを利用して全台に導入しています。

```bash
$ sudo apt-get install nfs-common
or
$ ansible all -i inventory/mycluster/hosts.ini -b -m apt -e "name='nfs-common'"
```

# PV(PersistentVolume)の割り当て

NFSサーバーのIPアドレスは適宜変更してください。
サーバー上ではあらかじめ/export/nfs/pv0001を作成しています。

``volume.beta.kubernetes.io/storage-class: "slow"`` を指定しているのは、SSDで準備した領域やRook/Cephなどと区別するためで、**"slow"** である必要はありませんが、何らかのannotationを加えることをお勧めします。

```yaml:01.create-persistent-volume.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv0001
  annotations:
    volume.beta.kubernetes.io/storage-class: "slow"
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle
  nfs:
    server: 192.168.1.1
    path: /export/nfs/pv0001
```

ここで作成したファイルをkubectlで適用してみます。

```bash
$ kubectl create -f 01.create-persistent-volume.yaml
```

終わったら必ず ```$ kubectl get pv``` で確認します。
pv0001ディレクトリが存在しないなど、何か問題があるとStatusがAvailableになりません。
ただStatusがAvailableになるまで、少し時間がかかる場合もあるようです。

```bash
$ kubectl get pv
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS    CLAIM                      STORAGECLASS   REASON    AGE
pv0001    5Gi        RWO            Recycle          Available                           slow                     18h
```

続いて、PVCを作成していきます。
この定義は公式ドキュメントのExampleから抜き出していますが、annotationsを追加しているのと、割り当てサイズを20Giから5Giに変更しています。

```yaml:02.create-persitent-claim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pv-claim
  labels:
    app: wordpress
  annotations:
    "volume.beta.kubernetes.io/storage-class": "slow"
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
```

PVと同様に適用しています。
annotationsやaccessModesがPV作成時の定義とマッチしないと割り当てに失敗します。今回は"slow"を指定しているので、必ずこの例と同じようにannotationsを指定してください。

```bash
$ kubectl create -f 02.create-persitent-claim.yaml
```

ここまで進めて、kubectlで割り当て状況を確認します。

```bash
$ kubectl get pv
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS    CLAIM                      STORAGECLASS   REASON    AGE
pv0001    20Gi       RWO            Recycle          Bound     wordpress/mysql-pv-claim   slow                     25m

$ kubctl get pvc
NAME             STATUS    VOLUME    CAPACITY   ACCESS MODES   STORAGECLASS   AGE
mysql-pv-claim   Bound     pv0001    20Gi       RWO            slow           25m6
```

# 作成したmysql-pv-claimを利用したWordpress用のMysqlのデプロイメント

公式ドキュメントのExampleにあるYAMLファイルを実行していきますが、PVCの割り当ても同時行なっているので、その箇所は手元で削ったファイルを実行しています。

```yaml:03.wordpress-mysql-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress-mysql
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
      tier: mysql
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: wordpress
        tier: mysql
    spec:
      containers:
      - image: mysql:5.6
        name: mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-pass
              key: password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-persistent-storage
          mountPath: /var/lib/mysql
      volumes:
      - name: mysql-persistent-storage
        persistentVolumeClaim:
          claimName: mysql-pv-claim
---
apiVersion: v1
kind: Service
metadata:
  name: wordpress-mysql
  labels:
    app: wordpress
spec:
  ports:
    - port: 3306
  selector:
    app: wordpress
    tier: mysql
  clusterIP: None
```

同様に```kubectl create -f ```の引数に渡して実行します。

nfs-commonパッケージが各ノードに導入されていれば問題はないと思いますが、Pendingのまま有効にならないといった事があれば、```$ kubectl describe```を利用してデバッグすることが必要になります。

# 公式ドキュメントのExampleとの相違点

この後、ExampleはWordpressの導入に進みますが、その中でもPVCの定義があるので、同様にannotationsセクションの追加とサイズの変更が必要になりますが、それ以外はほぼExampleのままです。

軽微な違いとして、最後のEXTERNAL-IPの割り当てとアクセス用URLのところで、別の記事でMetalLBを準備しているので、自動的に割り当てられ、ポート番号は80番となっています。

ちなみに、公式ドキュメントには、この他にもStatless Applicationの例としてPHPのMessageBoardをデプロイする記事がありますが、こちらは、そのままYAMLファイルを順番に実行していけば準備が完了します。

# 【追記】ReadWriteOnce, ReadWriteManyなど数種類のPVを準備してみた

実際に公式のExampleなどを実施しようとすると、20Giを要求していたり、Webサーバーのクラスターを構成したりすると、ReadWriteOnceに加えてReadWriteManyも欲くなったり、いくつかの種類のPVをあらかじめ準備する必要がでてきました。

今回のようにpv0000-pv0099まで100ディレクトリを準備しておいて、その中にReadWriteOnly(RWO)、ReadWriteMany(RWX)を混在させようとして次のようなテンプレートとスクリプトファイルを準備しました。

```text:template01.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv__N__
  annotations:
    volume.beta.kubernetes.io/storage-class: "slow"
spec:
  capacity:
    storage: 20Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle
  nfs:
    server: 192.168.1.1
    path: /export/nfs/pv__N__
```

```text:template02.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv__N__
  annotations:
    volume.beta.kubernetes.io/storage-class: "slow"
spec:
  capacity:
    storage: 20Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Recycle
  nfs:
    server: 192.168.1.1
    path: /export/nfs/pv__N__
```

作成したファイルを次のようなスクリプトの冒頭の変数に並べて、ファイルの数に応じたPVの定義を作成するYAMLファイルを生成させます。

```bash:genpvconf.sh
#!/bin/bash

## variable definition
declare -a TEMPLLIST=("template01.yaml" "template02.yaml")
i=0
i_max=100

## main-loop
while test "${i}" -lt "${i_max}"
do
  r="$((${i} % ${#TEMPLLIST[@]}))"  ## r <= 0 or 1
  n="$(printf %04d ${i})"           ## n <= 0001 ... 0099
  echo "---"
  sed -e s/__N__/${n}/ "${TEMPLLIST[${r}]}"
  i=$(($i+1))
done
```

作成したファイルは```$ kubectl apply -f ```で適用します。

Rook/Cephを良く使っていますが、このBlock StorageではRWXなアクセスモードが機能しないので、StatefulSetやDeploymentで作成した複数のレプリカでディスクを共有したい場合にはNFS Storageもまだまだ使えると思います。

以上