Raspberry PiでKubernetesクラスター用のNFSを作る

# 概要
- Raspberry Pi 4 Model B + M.2 SSDでNFSサーバーを構築
- KubernetesのPersistent Volumeとして登録

# 背景
自宅内で複数端末から接続できる [Jupyter](https://jupyter.org/) 環境欲しいなあ。

→　例によって**ラズパイk8sクラスター**上に作ろう。

→ ラズパイk8sクラスターに永続ボリューム無いからデータ保存どうしよう...

→ NFS追加しよう！せっかくだから見栄え意識してこれもラズパイで構築しよう！


# Before After
Before
<img width="400px" alt="P_20200210_085613.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214526/3e496f53-e867-8133-9a1b-3daad7548ebf.jpeg">

After
最下段のハブを上に移動し、空いたスペースにラズパイ + SSDを追加しました。
<img width="400px" alt="P_20210512_191122.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214526/b559f3c8-1797-5e53-4138-0521f11bdc86.jpeg">

※ ラズパイk8sクラスター作成に関してはこちらで記事にしています。

https://qiita.com/reireias/items/0d87de18f43f27a8ed9b


# そもそも何故ラズパイk8sに別途永続ボリュームが必要なのか？
端的に言うと、k8sのPodは揮発性だから。
Pod中に保存してもデプロイ等でPodが再作成されると消えます。

なので、Persistent Volumeという概念がk8sにはあります。

https://kubernetes.io/ja/docs/concepts/storage/persistent-volumes/

図にするとこんな感じです。（実際にはPVCをつかってPVをPodに割り当てますが割愛します）

![persistent_volume.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214526/62b852e1-9c14-8167-71e8-8fa28003775a.png)


Dockerと名前つきボリュームの関係をイメージするとわかりやすいかも。

# ハードウェア
今回追加したハードウェアは以下になります。

- Raspberry Pi 4 Model B 8GB
- [Geekworm X873](https://wiki.geekworm.com/X873)
- M.2 SSD (2280) 128GB NVMe

ラズパイのストレージにはmicroSDを使うのが一般的ですが、NFSとして永続ストレージに使用するには適していません。
そのため、SSDをストレージに採用します。
普通の2.5インチSSDでもいいのですが、クラスターの積層ケースに積むことを考慮してM.2 SSDを採用しました。

<img width="400px" alt="P_20210511_211740.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214526/71b26348-713e-2269-4605-7b6d8388838d.jpeg">
上記は X873 に M.2 SSD を取り付けた状態です。
これをラズパイの下に固定します。

# 構築
ここからはOS、NFS、PersistentVolumeの構築を順に紹介していきます。

## OSイメージの書き込み
まずは X873 をラズパイではなく、作業マシンにUSB接続します。
その状態で[Raspberry Pi Imager](https://www.raspberrypi.org/software/) を使用してSSDにOSイメージを書き込みます。

Raspberry Pi Imagerはシンプルなので公式ドキュメントを読めばわかります。
OSはせっかくメモリ8GBモデルにしているので、64bit版にします。
まあ、1プロセスで3GB以上メモリを使う予定はないのですが。
（32bitだと1プロセスのメモリが3GBに制限される）

64bit版は2021/05/23時点ではRaspberry Pi Imagerから選択できないため、以下の公式ダウンロードページから取得し、解凍し、 `Use custom` から指定して利用します。

http://downloads.raspberrypi.org/raspios_arm64/images/raspios_arm64-2021-04-09/

![スクリーンショット 2021-05-23 21-56-52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214526/129c8cc9-0c7d-1ebf-b361-a8a2aa2a1b5d.png)

あとは `WRITE` ボタンを押せばOSイメージがSSDに書き込まれます。

OSイメージが書き込めたら、追加でファイルを設置してsshを起動する設定にします。
再度SSDをUSB接続しなおして、 `boot/ssh` ファイルを生成します。

```bash
# 起動時にsshを有効にする設定
# パスは私の環境の例
$ touch /media/reireias/boot/ssh
```

## OS初期設定
ラズパイにSSDを接続し、電源を入れます。
ルーター等からIPさえ特定できれば、ラズパイにディスプレイもキーボードも接続せずに初期ユーザー + 初期パスワードでssh接続できるはずです。
（我が家にmicroHDMI端子が無くてつながるディスプレイはなかった...）

ssh鍵の設置、パスワード変更、swapの無効化、ホスト名の設定等を行っていきます。
最後にパッケージを更新して再起動をかけます。

```bash
# authorized_keysの設定
$ scp ~/.ssh/id_rsa.pub pi@ip:/tmp/authorized_keys
$ ssh pi@ip
$ mkdir ~/.ssh
$ cp /tmp/authorized_keys ~/.ssh/authorized_keys

# 初期パスワードを変更
$ sudo passwd pi

# swapを無効化
$ sudo systemctl disable dphys-swapfile.service

# ホスト名を設定
# 今回は pikube01 ~ pikube03 という名前にしました
$ sudo vi /etc/hosts
$ sudo vi /etc/hostname

# 何らかの手段でIPを固定化しておくと便利でしょう
# 私はルーターの機能で固定DHCPを設定しています

# packageの更新
$ sudo apt update
$ sudo apt upgrade

# もろもろを反映させるために再起動
$ sudo reboot
```

あとはお好みでrsyslogやnode_exporter等設定していくのもいいでしょう。
私は新しいラズパイに毎回設定するのが面倒なのでAnsibleのスクリプトを用意して流しています。

https://github.com/reireias/raspberrypi-ansible

## NFSの設定
「ラズパイ NFS」等でググると山ほど記事がでてきます。
通常の説明はそちらに譲るとして、本記事ではせっかくなので私が使ったAnsibleでのスクリプトを紹介します。
これさえ用意しておけば、何度故障したって新規構築は楽ですね。

ホスト名を `pikube04` にしているので、一部ファイル名もホスト名に合わせています。

```console
$ tree
.
├── inventories
│   └── pikube04.yml
├── pikube04.yml
└── roles
    └── nfs
        ├── files
        │   └── exports
        └── tasks
            └── main.yml
```

```yml:inventories/pikube04.yml
---
raspberrypi:
  hosts:
    pikube04:
  vars:
    ansible_user: pi
    ansible_ssh_private_key_file: ~/.ssh/id_rsa
    ansible_become: yes
```

```yml:pikube04.yml
---
- hosts: raspberrypi
  roles:
    - nfs
```

```conf:roles/nfs/files/exports
# /etc/exports: the access control list for filesystems which may be exported
#               to NFS clients.  See exports(5).
#
# Example for NFSv2 and NFSv3:
# /srv/homes       hostname1(rw,sync,no_subtree_check) hostname2(ro,sync,no_subtree_check)
#
# Example for NFSv4:
# /srv/nfs4        gss/krb5i(rw,sync,fsid=0,crossmnt,no_subtree_check)
# /srv/nfs4/homes  gss/krb5i(rw,sync,no_subtree_check)
#
/opt 192.168.0.0/255.255.255.0(rw,sync,no_subtree_check,all_squash)
```

※利用する場合はLANのCIDRに応じて `192.168.0.0/255.255.255.0` を修正してください。

```yml:roles/nfs/tasks/main.yml
---
- name: install nfs-server
  apt:
    name: nfs-kernel-server
    update_cache: yes

- name: create nfs directory
  file:
    path: /opt/nfs
    owner: nobody
    group: nogroup
    mode: 0755
    state: directory

- name: create nfs sub directory
  file:
    path: "/opt/nfs/{{ item }}"
    owner: nobody
    group: nogroup
    mode: 0755
    state: directory
  with_sequence: start=1 end=10 format=pv%02d

- name: copy exports
  copy:
    src: exports
    dest: /etc/exports
    owner: root
    group: root
    mode: 0644

- name: enable nfs-server
  systemd:
    name: nfs-server
    enabled: yes
    state: started
    daemon_reload: yes
```

NFS用のディレクトリ配下にPersistentVolume用のディレクトリを `/opt/nfs/pv01` 〜 `/opt/nfs/pv10` まで作成しています。

以上でNFSの設定ができたはずなので、試しに作業マシンからマウントして確認してみます。

```console
$ sudo mount 192.168.xx.xx:/opt/nfs/pv001 /mnt/nfs

# マウントできたら読み書きできるか適当に試す
```

## Persistent Volumeの登録
マニュフェストを書いてk8sにPVを登録します。

```yml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv01
spec:
  capacity:
    storage: 5Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle
  storageClassName: slow
  nfs:
    server: 192.168.xx.xx
    path: /opt/nfs/pv01
```

## Persistent Volumeの利用
最終的にはJupyterのストレージとして以下のようにPodを構成しています。
Persistent Volume ClaimでPVを払い出してもらう形になります。

以下では自作のJupyterイメージ中でWORKDIRを `/opt` に指定しているため、Podの `/opt` にPVをマウントしています。
これでJupyter上で保存したファイルがNFSに保存されます。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jupyter-deployment
spec:
  selector:
    matchLabels:
      app: jupyter
  replicas: 1
  template:
    metadata:
      labels:
        app: jupyter
    spec:
      containers:
        - name: jupyter
          image: reireias/jupyterlab:0.0.3
          ports:
            - containerPort: 8080
          volumeMounts:
            - name: opt
              mountPath: /opt
      volumes:
        - name: opt
          persistentVolumeClaim:
            claimName: jupyter-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: jupyter-pvc
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 5Gi
  storageClassName: slow
---
apiVersion: v1
kind: Service
metadata:
  name: jupyter-service
spec:
  selector:
    app: jupyter
  ports:
    - port: 80
      targetPort: 8080
  type: LoadBalancer
```

# まとめ
お家k8sクラスターに永続ストレージが増えた！
今までmicroSDの耐久性を気にして保存できなかったデータも保存できるようになった！

# 参考
https://note.com/ryoma_0923/n/nb7c26336a057

https://denor.jp/raspberry-pi%E3%81%A7nfs%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%B5%E3%83%BC%E3%83%90%E4%BD%9C%E6%88%90

https://qiita.com/YasuhiroABE/items/2e18bb546beea4660a99