Kubernetesでコンテナを削除しても消えないPersistentVolumeを作る

この記事は、「[オンプレミス、Kubernetesで、マイクロサービスを意識したWebアプリをデプロイするまでの軌跡](https://qiita.com/soumi/items/2ef65b0d6e3fb562451e)」の一部です。

# はじめに

Dockerをはじめとするコンテナには**Immutable(不変)であるべき**、という思想があり、各インスタンスは立ち上がるごとに初期化され、綺麗な状態に戻ります。
ここで一点困るのが、ファイルサーバーやデータベースなど、永続的に保持しておきたい情報をどうしておくか？という事です。

KubernetesにはPersistentVolumeという機能があって、これを使う事でコンテナとは独立したボリュームを作ることが出来ます。

今回は、このPersistentVolumeの使い方を紹介します。

# NFSサーバーをセットアップする

PersistentVolumeで使えるボリュームの形態はいくつかあるようなのですが、今回は手軽そうなNFSを使う事にします。
まずは任意のサーバーにNFSをインストールする必要があるので、用意します。専用であれば良いでしょうが、僕は[以前の記事](https://qiita.com/soumi/items/7736ac3aabbbe4fb474a)で構築したKubernetesのWorkerノードをNFSサーバーとしてセットアップしました。

Ubuntuであれば、以下のコマンドでインストール可能です。

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

共有するフォルダを作成し、権限を設定します。

```shell
$ sudo mkdir -p /export/nfs
$ sudo chmod 1777 /export/nfs/.
$ sudo nano /etc/exports

...略

/export/nfs xxx.xxx.xxx.0/24(rw,async,no_root_squash)

$ sudo service nfs-server restart
$ sudo service rpcbind restart
```

参考サイトにあるように、複数のサービスで同じフォルダを共有する事は難しいようです。
必要に応じて、サブフォルダを作成しておきます。

```shell
$ sudo mkdir -p /export/nfs/serviceA
$ sudo mkdir -p /export/nfs/serviceB
```

今回はテストのため、```serviceA```の方に以下のようなファイルを仕込んでおきます。

```
$ nano /export/nfs/serviceA/index.html

hello world!
```

# NFSマウント準備

クライアント側も準備が必要です。
全てのノードで以下を実行して、NFSが使えるようにしておきます。

```shell
sudo apt install nfs-common
```

# PeersistentVolumeを設定する

NFSサーバーは設定できたので、PeersistentVolumeを設定します。

```yaml:service-a-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: service-a-pv
  namespace: some-name-space
  annotations:
    volume.beta.kubernetes.io/storage-class: "slow"
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  nfs:
    server: 192.168.0.xxx        <-- 実際のipアドレス 
    path: /export/nfs/serviceA   <-- 共有したいNFSサーバー上のフォルダ 
```

設定の中で```name```、```namespace```、```storage```、```accessMode```等、必要であれば適宜変更してください。

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

設定を元に、PersistentVolumeを作成します。

```shell
kubectl apply -f service-a-pv.yaml
```

これで作成できました。
確認します。

```shell
$ kubectl get pv

NAME             CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM     STORAGECLASS   REASON   AGE
service-a-pv     5Gi        RWO            Retain           Available          slow                    1h
```


問題がある場合はStatusがAvailableにならないので、設定を見直してください。

# PersistentVolumeClaimを設定する

間違い探しみたいですが、PersistentVolume **Claim** です。
この設定で、PersistentVolumeを実際のアプリに紐づけます。

```yaml:service-a-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: service-a-pvc
  namespace: some-name-space
  labels:
    app: service-a         　　　<-- ボリュームを紐づけるデプロイメント
  annotations:
    "volume.beta.kubernetes.io/storage-class": "slow"
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
```

これで作成する準備は出来ましたが、今回はnamespaceを指定しているので、namespaceが無い状態ではエラーが出ます。

```shell
$ kubectl create namespace some-name-space
```

namespaceを作成したら、pvcを作成しましょう。

```shell
$ kubectl apply -f service-a-pvc.yaml
```

改めて状態を確認します。

```shell
$ kubectl get pv

NAME           CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM           STORAGECLASS   REASON   AGE
service-a-pv   5Gi        RWO            Retain           Bound    service-a-pvc   slow                    32h
```

service-a-pvcにバインドされていますね。
PersistentVolumeClaimの状態も確認します。

```shell
$ kubectl get pvc -n some-name-space

NAME            STATUS   VOLUME         CAPACITY   ACCESS MODES   STORAGECLASS   AGE
service-a-pvc   Bound    service-a-pv   5Gi        RWO            slow           32h
```

これで、ボリュームの準備は完了しました。

一点注意ですが、今回の設定ではnamespaceを分けているので単に```kubectl get pvc```では```No resources found.```となってしまいます。オプションでnamespaceを指定してください。

# コンテナに紐づける

いよいよ、実際のコンテナに紐づけていきます。
以下のようなyamlファイルで、サービスとデプロイメントを定義します。

```yaml:service-a.yaml
apiVersion: v1
kind: Service
metadata:
  name: service-a
  namespace: some-name-space
  labels:
    app: service-a
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
  selector:
    app: service-a
---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: service-a
  namespace: some-name-space
spec:
  replicas: 1
  revisionHistoryLimit: 3
  template:
    metadata:
      namespace: some-name-space
      labels:
        app: service-a
        version: latest
    spec:
      containers:
      - name: service-a
        image: nginx:latest
        ports:
        - containerPort: 80
        volumeMounts:
        - name: service-a-storage   <-- ※ 同じ名前を指定する
          mountPath: /usr/share/nginx/html
      volumes:
    app: service-a
      - name: service-a-storage     <-- ※ 同じ名前を指定する
        persistentVolumeClaim:
          claimName: service-a-pvc
```

この例ではnginxのコンテナを起動して、デフォルトの公開ディレクトリである```/usr/share/nginx/html```に先ほど作ったボリュームをマウントしています。
これにより、サービスにアクセスした際に先ほど作った```index.html```が見えるようになります。

```shell
$ kubectl apply -f service-a.yaml
```

デプロイ後、このサービスが起動しているポートを調べます。

```shell
$ kubectl get services -n some-name-space
NAME        TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service-a   NodePort   10.107.212.223   <none>        80:30380/TCP   71m
```

この例では30380で起動しているようなので、以下のコマンドで内容を確認できます。

```shell
$ curl <サーバーのip>:30380
hello world!
```

無事に確認できれば、全てうまく設定できています。
お疲れさまでした！


# 注意

今回はデモのために```index.html```をPersistentVolumeの中にいれてnginxで参照しましたが、この使い方では、ポッドをたくさん作ってスケールしても結局単一のファイルを見に行くので、Kubernetesの恩恵もあまり受けられないと思われます。検証等はしていないですが、ご注意ください。

# 参考

[「Docker」を全く知らない人のために「Docker」の魅力を伝えるための「Docker」入門](https://qiita.com/bremen/items/4604f530fe25786240db)
[KubernetesでNFSを利用してPersistentStorageを準備する](https://qiita.com/YasuhiroABE/items/2e18bb546beea4660a99)