CLOVER🍀

That was when it all began.

OKD/Minishift上で、Storageを使ってみる(PersistentVolume/PersistentVolumeClaim+NFS)

これは、なにをしたくて書いたもの?

  • OKD/Minishift…というかKubernetes上で、ストレージに関する機能を試してみたい
  • PersistentVolume、PersistentVolumeClaimというものがあるらしいので、こちらを使ってみようと
  • お題はNFS

そんな感じで、OKD/Minishift上、Kubernetes上で、ストレージを扱うお勉強という感じです。題材として、手元で扱えそうな
NFSを利用することにしました。

PersistentVolume/PersistentVolumeClaim?

コンテナにおけるファイルシステムは、コンテナごとに独立しているため、そのままではファイルの共有などといったことは
できず、またコンテナのプロセスの停止時には消失してしまうものになります。

この課題に対処できるのが、Volumeという仕組みだそうです。

Volumes - Kubernetes

ただ、Volumeを直接コンテナにマウントしてしまうと、Podの定義が環境に依存してしまうことになります。

こちらに対処するのが、PersistentVolume、PersistentVolumeClaimという仕組みになるそうです。

Persistent Volumes - Kubernetes

Using Persistent Volumes | Developer Guide | OKD 3.10

PersistentVolume、PersistentVolumeClaimを利用することで、Podの定義から環境依存の部分を取り除き、より汎用的に
扱えるようになる、と。

両者の関係としては、PersistentVolumeがクラスタ内のストレージリソースを指し、クラスタの管理者によってプロビジョニング
されます。つまり、PersistentVolumeは管理者権限がないと作れない、と。

PersistentVolumeClaimは、PersistentVolumeに対して必要なストレージリソースを要求します。このPersistentVolumeClaimを
Podに紐付けることで、Pod側からストレージが利用できるようになります。

とまあ、書いていてもよくわからない感じもするので、とにかく試してみましょう。

環境

今回の環境は、こちら。

$ minishift version
minishift v1.25.0+90fb23e


$ oc version
oc v3.10.0+dd10d17
kubernetes v1.10.0+b81c8f8
features: Basic-Auth GSSAPI Kerberos SPNEGO

Server https://192.168.42.132:8443
openshift v3.10.0+456651d-57
kubernetes v1.10.0+b81c8f8

NFSサーバー側。IPアドレスは、192.168.0.3とします。

$ rpcinfo | grep nfs
    100003    3    tcp       0.0.0.0.8.1            nfs        superuser
    100003    4    tcp       0.0.0.0.8.1            nfs        superuser
    100003    3    udp       0.0.0.0.8.1            nfs        superuser
    100003    3    tcp6      ::.8.1                 nfs        superuser
    100003    4    tcp6      ::.8.1                 nfs        superuser
    100003    3    udp6      ::.8.1                 nfs        superuser

サーバー側はNFS 4.2で、Ubuntu Linux 18.04 LTS。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.1 LTS
Release:    18.04
Codename:   bionic

エクスポートしているディレクトリの定義は、こんな感じです。
/etc/exports

/var/nfs/exports 192.168.0.0/24(rw,sync,fsid=0,crossmnt,no_subtree_check,insecure,all_squash)
/var/nfs/exports/openshift 192.168.122.0/24(rw,sync,no_subtree_check,insecure,all_squash)

サンプルアプリケーション

最初に、ストレージを使うようなサンプルアプリケーションを作成します。

Node.jsで、Expressを使った簡単なサーバーを書いてみることにします。

$ npm i --save express

Expressのバージョンは、こちら。

  "dependencies": {
    "express": "^4.16.4"
  }

作成したソースコード。 server.js

const express = require('express');
const bodyParser = require('body-parser');

const os = require('os');
const util = require('util');
const fs = require('fs');

const app = express();
app.use(bodyParser.json());

const docRoot = process.env.DOC_ROOT;

if (!docRoot) {
    console.log('not set Environment Variable, DOC_ROOT');
    process.exit(1);
}

app.post('/', async (req, res) => {
    const request = req.body;

    const path = docRoot + request.path;
    const content = request.content;

    const paths = request.path.split('/');
    [docRoot].concat(paths.slice(0, paths.length - 1)).reduce((acc, cur) => {
        const dir = acc + '/'  + cur;

        if (! fs.existsSync(dir)) {
            fs.mkdirSync(dir);
        }

        return dir;
    });

    await util.promisify(fs.writeFile)(path, content);

    res.send(`OK!! created file = ${request.path}, from ${os.hostname()}`);
});

app.get('/*', async (req, res) => {
    const path = docRoot + req.path;

    if (await util.promisify(fs.exists)(path)) {
        res.send(`found, from ${os.hostname()}\r\n` + await util.promisify(fs.readFile)(path, 'utf-8'));
    } else {
        res.sendStatus(404).send(`file not found, from ${os.hostname()}`);
    }
});

app.listen(8080);

環境変数DOC_ROOTから、このサーバーのルートディレクトリを取得し

const docRoot = process.env.DOC_ROOT;

POSTで、JSONで指定されたパスと内容でディレクトリ、ファイルを作成し

app.post('/', async (req, res) => {
    const request = req.body;

    const path = docRoot + request.path;
    const content = request.content;

    const paths = request.path.split('/');
    [docRoot].concat(paths.slice(0, paths.length - 1)).reduce((acc, cur) => {
        const dir = acc + '/'  + cur;

        if (! fs.existsSync(dir)) {
            fs.mkdirSync(dir);
        }

        return dir;
    });

    await util.promisify(fs.writeFile)(path, content);

    res.send(`OK!! created file = ${request.path}, from ${os.hostname()}`);
});

GETで作成されたファイルの内容を読み出す、というアプリケーションになります。

app.get('/*', async (req, res) => {
    const path = docRoot + req.path;

    if (await util.promisify(fs.exists)(path)) {
        res.send(`found, from ${os.hostname()}\r\n` + await util.promisify(fs.readFile)(path, 'utf-8'));
    } else {
        res.sendStatus(404).send(`file not found, from ${os.hostname()}`);
    }
});

package.jsonでは、このスクリプトを指定して起動するように作成。

  "scripts": {
    "start": "node server.js"
  },

これを、Gitリポジトリに登録しておきます。

まずはemptyDirで試してみる

最初は、VolumeのemptyDirを使って試してみましょう。emptyDirはPodと同じライフサイクルを持つVolumeで、Podが作成される時に
作られ、Podがなくなると削除されます。Nodeに割り当てられるものみたいですね。

Volumes - Kubernetes

An emptyDir volume is first created when a Pod is assigned to a Node, and exists as long as that Pod is running on that node. As the name says, it is initially empty.

「oc new-app」で、アプリケーションをデプロイ。環境変数DOC_ROOTには、/var/doc-rootというディレクトリを指定することに
します。

$ oc new-app [GitリポジトリのURL] -e DOC_ROOT=/var/doc-root
$ oc expose svc/node-use-storage

ファイルを作ろうとしてみます。

$ curl -XPOST -H 'Content-Type:application/json' node-use-storage-myproject.192.168.42.74.nip.io -d '{"path": "/foo/bar/test.txt", "content": "Hello OpenShift!!"}'

環境変数で指定したディレクトリもなければ、/var配下にディレクトリを作成する権限もないので、実行に失敗します。

(node:23) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: EACCES: permission denied, mkdir '/var/doc-root/'
(node:23) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

ここでDeploymentConfigに対して、emptyDirでvolumeを追加してみます。Volumeの種類は「--type」で指定し、
どこにマウントするかは「--mount-path」で指定するようです。

$ oc volume dc/node-use-storage --add --name=doc-root-dir --type=emptyDir --mount-path=/var/doc-root
deploymentconfig "node-use-storage" updated

しばらく待っていると、デプロイが終わり、今度はディレクトリ、ファイルが作成できるようになります。

$ curl -XPOST -H 'Content-Type:application/json' node-use-storage-myproject.192.168.42.132.nip.io -d '{"path": "/foo/bar/test.txt", "content": "Hello OpenShift!!"}'
OK!! created file = /foo/bar/test.txt, from node-use-storage-2-ltjcw

確認。

$ curl node-use-storage-myproject.192.168.42.132.nip.io/foo/bar/test.txt
found, from node-use-storage-2-ltjcw
Hello OpenShift!!

ディレクトリ、ファイルが作成され、参照できましたね。

DeploymentConfigに割り当てられている、Volumeの情報を見てみましょう。

$ oc volume dc/node-use-storage
deploymentconfigs/node-use-storage
  empty directory as doc-root-dir
    mounted at /var/doc-root

YAMLでも。

$ oc get dc/node-use-storage -o yaml
apiVersion: apps.openshift.io/v1
kind: DeploymentConfig
metadata:

...

spec:
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    app: node-use-storage
    deploymentconfig: node-use-storage

...

  template:

...

    spec:
      containers:
      - env:
        - name: DOC_ROOT
          value: /var/doc-root
        image: 172.30.1.1:5000/myproject/node-use-storage@sha256:2180b0a0fec264504326a0d1c75af1ac347f8b674997221c90c0bfa571503ba2
        imagePullPolicy: Always
        name: node-use-storage
        ports:
        - containerPort: 8080
          protocol: TCP
        resources: {}

...

        volumeMounts:
        - mountPath: /var/doc-root
          name: doc-root-dir

...

      volumes:
      - emptyDir: {}
        name: doc-root-dir

...

抜粋すると、こんな感じですね。

      containers:
        volumeMounts:
        - mountPath: /var/doc-root
          name: doc-root-dir
      volumes:
      - emptyDir: {}
        name: doc-root-dir

では、今度はスケールアウトしてPodを増やしてみましょう。

$ oc scale dc/node-use-storage --replicas=3

Route越しに各Podに順次アクセスしていくと、最初に作成されたPodしかファイルを読み出すことができないことがわかります。

$ curl node-use-storage-myproject.192.168.42.132.nip.io/foo/bar/test.txt
found, from node-use-storage-2-ltjcw
Hello OpenShift!!


$ curl node-use-storage-myproject.192.168.42.132.nip.io/foo/bar/test.txt
Not Found


$ curl node-use-storage-myproject.192.168.42.132.nip.io/foo/bar/test.txt
Not Found

ここで、2つ目のPodに対してファイル作成。

$ curl -XPOST -H 'Content-Type:application/json' node-use-storage-myproject.192.168.42.132.nip.io -d '{"path": "/foo/bar/test.txt", "content": "Hello OpenShift!!"}'
OK!! created file = /foo/bar/test.txt, from node-use-storage-2-fqpkf

2つ目のPodについても、ファイルを読み出すことができるようになりましたね。

$ curl node-use-storage-myproject.192.168.42.132.nip.io/foo/bar/test.txt
found, from node-use-storage-2-ltjcw
Hello OpenShift!!


$ curl node-use-storage-myproject.192.168.42.132.nip.io/foo/bar/test.txt
found, from node-use-storage-2-fqpkf
Hello OpenShift!!


$ curl node-use-storage-myproject.192.168.42.132.nip.io/foo/bar/test.txt
Not Found

これで、Volumeを使うとPodに対してストレージを割り当てられること、Volumeの種類がemptyDirであればPod内でしか
共有できていないことがわかりました。

また、Volumeの定義は、直接DeploymentConfigに記載されることもわかりました。emptyDirといった種類などの情報も
含めて、YAMLに記載されていたことは確認できましたね。今回は使っていませんが、NFSを使ったりした場合は
NFSサーバーの情報といった環境に依存した内容などもYAMLに書くことになります。

確認が終わったので、いったん全部削除しておきます。

$ oc delete all --all

PersistentVolume/PersistentVolueClaimで、NFSを使う

では、続いてDeploymentConfigの定義からストレージについての定義を切り離すために、PersistentVolumeとPersistentVolumeClaimを
使ってみましょう。

PersistentVolumeとしては、NFSを利用することにします。

Using Persistent Volumes | Developer Guide | OKD 3.10

Sharing an NFS PV Across Two Pods - Persistent Storage Examples | Configuring Clusters | OKD 3.10

Using NFS - Configuring Persistent Storage | Configuring Clusters | OKD 3.10

それぞれ、YAMLで定義します。

PersistentVolume。 nfs-pv.yml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv 
spec:
  capacity:
    storage: 50Gi 
  accessModes:
    - ReadWriteMany 
  persistentVolumeReclaimPolicy: Retain
#  storageClassName: 
  mountOptions:
    - nfsvers=4.1
  nfs: 
    path: /openshift
    server: 192.168.0.3
    readOnly: false

各オプションの説明は、Kubernetesのドキュメントを見ることになります。

Persistent Volumes - Kubernetes

今回は、以下の内容で作成。

  • ストレージ容量(spec.capacity.storage) … 50G
  • アクセスモード(spec.accessModes) … 複数マシンからRead/Writeモードでマウント可能(ReadWriteMany)
    • なお、単一マシンからRead/Writerモードでマウント可能な場合はReadWriteOnce、複数マシンからReadOnlyモードでマウント可能なのはReadOnlyMany
  • PersistentVolumeClaimによるバインド解除後のコンテンツ保持ポリシー(spec.persistentVolumeReclaimPolicy) … コンテンツを保持(Retain)
    • ボリューム内のファイルを削除する場合はRecycle(NFSおよびHostPathのみ可)、関連するストレージリソース全体を削除する場合はDelete(AWS EBS、GCE PDなどが対応)

spec.nfs以下は、NFSサーバーとエクスポートされたパスの設定です。spec.mountOptionsでマウント時のオプションが指定できる
ようなので、NFS 4.1を使うようにしておきました。

続いて、PersistentVolumeClaim。 nfs-pvc.yml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc  
spec:
  accessModes:
  - ReadWriteMany      
  resources:
     requests:
       storage: 20Gi
#  storageClassName: 
  volumeName: nfs-pv

spec.volumeNameで、PersistentVolumeを紐付けています。

リクエストする容量は20Gにしたのですが、ドキュメントを読むとPersistentVolumeとPersistentVolumeClaimは1対1の
関係になるので、今回の例ではPersistentVolumeと同じ容量にしてもよかったかも…。

A PVC to PV binding is a one-to-one mapping.

Persistent Volumes - Kubernetes

実際、PersistentVolumeに2つ以上のPersistentVolumeClaimを紐付けようとしても、2つ目以降はバインドできずにPendingと
なります。

では、これらを作成していきましょう。

PersistentVolumeの作成。PersistentVolumeの作成には、管理者権限が必要です。

$ oc create -f nfs-pv.yml --as system:admin
persistentvolume "nfs-pv" created

PersistetVolumeClaimの方は、管理者権限は不要です。

$ oc create -f yaml/nfs-pvc.yml
persistentvolumeclaim "nfs-pvc" created

先ほどのNode.jsでのアプリケーションを、もう1度デプロイします。

$ oc new-app [GitリポジトリのURL] -e DOC_ROOT=/var/doc-root
$ oc expose svc/node-use-storage

DeploymentConfigと、PersistentVolumeClaimの紐付け。

$ oc volume dc/node-use-storage --add --name=doc-root-dir --type=pvc --claim-name=nfs-pvc --mount-path=/var/doc-root
deploymentconfig "node-use-storage" updated

スケールアウト。

$ oc scale dc/node-use-storage --replicas=3

確認してみます。まずは、ファイルを作成。

$ curl -XPOST -H 'Content-Type:application/json' node-use-storage-myproject.192.168.42.132.nip.io -d '{"path": "/foo/bar/test.txt", "content": "Hello OpenShift!!"}'
OK!! created file = /foo/bar/test.txt, from node-use-storage-2-4nm7f

成功しました。

では、各Podにアクセス。

$ curl node-use-storage-myproject.192.168.42.132.nip.io/foo/bar/test.txt
found, from node-use-storage-2-pzn9l
Hello OpenShift!!


$ curl node-use-storage-myproject.192.168.42.132.nip.io/foo/bar/test.txt
found, from node-use-storage-2-ld9xl
Hello OpenShift!!


$ curl node-use-storage-myproject.192.168.42.132.nip.io/foo/bar/test.txt
found, from node-use-storage-2-4nm7f
Hello OpenShift!!

いずれも書き出したファイルを読み出せていることが、確認できます。

NFSサーバー側では、ちゃんとディレクトリとファイルが作成できていることが確認できます。
/var/nfs/exports/openshift/foo/bar/test.txt

Hello OpenShift!!

では、PersistentVolumeやPersistentVolumeClaimの情報を確認してみましょう。

PersistentVolumeClaimから。

$ oc get pvc/nfs-pvc
NAME      STATUS    VOLUME    CAPACITY   ACCESS MODES   STORAGECLASS   AGE
nfs-pvc   Bound     nfs-pv    50Gi       RWX                           4m

YAMLでも確認。

$ oc get pvc/nfs-pvc -o yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  annotations:
    pv.kubernetes.io/bind-completed: "yes"
  creationTimestamp: 2018-10-15T14:36:51Z
  finalizers:
  - kubernetes.io/pvc-protection
  name: nfs-pvc
  namespace: myproject
  resourceVersion: "10974"
  selfLink: /api/v1/namespaces/myproject/persistentvolumeclaims/nfs-pvc
  uid: c11eab02-d087-11e8-ba6f-525400f36957
spec:
  accessModes:
  - ReadWriteMany
  resources:
    requests:
      storage: 20Gi
  volumeName: nfs-pv
status:
  accessModes:
  - ReadWriteMany
  capacity:
    storage: 50Gi
  phase: Bound

ちょこっと、PersistentVolumeの情報が見えますね。

続いて、PersistentVolumeの確認。

$ oc get pv/nfs-pv --as system:admin
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS    CLAIM               STORAGECLASS   REASON    AGE
nfs-pv    50Gi       RWX            Retain           Bound     myproject/nfs-pvc                            5m

YAMLでも。

$ oc get pv/nfs-pv --as system:admin -o yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  annotations:
    pv.kubernetes.io/bound-by-controller: "yes"
  creationTimestamp: 2018-10-15T14:36:51Z
  finalizers:
  - kubernetes.io/pv-protection
  name: nfs-pv
  resourceVersion: "10972"
  selfLink: /api/v1/persistentvolumes/nfs-pv
  uid: c11d46bb-d087-11e8-ba6f-525400f36957
spec:
  accessModes:
  - ReadWriteMany
  capacity:
    storage: 50Gi
  claimRef:
    apiVersion: v1
    kind: PersistentVolumeClaim
    name: nfs-pvc
    namespace: myproject
    resourceVersion: "10970"
    uid: c11eab02-d087-11e8-ba6f-525400f36957
  mountOptions:
  - nfsvers=4.1
  nfs:
    path: /openshift
    server: 192.168.0.3
  persistentVolumeReclaimPolicy: Retain
status:
  phase: Bound

PersistentVolumeClaimから紐付けられていることが、確認できますね。

spec:
  accessModes:

...

  claimRef:
    apiVersion: v1
    kind: PersistentVolumeClaim
    name: nfs-pvc
    namespace: myproject
    resourceVersion: "10970"
    uid: c11eab02-d087-11e8-ba6f-525400f36957

さらに、DeploymentConfigから見た情報を確認してみましょう。

$ oc get dc/node-use-storage -o yaml
apiVersion: apps.openshift.io/v1
kind: DeploymentConfig
metadata:
  annotations:
    openshift.io/generated-by: OpenShiftNewApp

...

spec:
  replicas: 3
  revisionHistoryLimit: 10
  selector:
    app: node-use-storage
    deploymentconfig: node-use-storage

...

  template:
    metadata:
      annotations:
        openshift.io/generated-by: OpenShiftNewApp
      creationTimestamp: null
      labels:
        app: node-use-storage
        deploymentconfig: node-use-storage
    spec:
      containers:
      - env:
        - name: DOC_ROOT
          value: /var/doc-root

...

        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
        volumeMounts:
        - mountPath: /var/doc-root
          name: doc-root-dir

...

      volumes:
      - name: doc-root-dir
        persistentVolumeClaim:
          claimName: nfs-pvc

...

spec.spec.containers.volumesに、紐付けられているPersistentVolumeClaimの情報が登場しています。

動作確認もできましたし、OKですね。

StorageClass

今回のPersistentVolume/PersistentVolumeClaimを扱った時に、コメントアウトして飛ばしたものにstorageClassがあります。

#  storageClassName: 

これは、Kubernetesクラスタの管理者が定義するストレージの種類を表すものです。サービスレベル、バックアップポリシー、
性能など、クラスタ管理者が定義します。

Storage Classes - Kubernetes

Change the default StorageClass - Kubernetes

Kubernetes PVC と Deployment, StatefulSet の関係を検証してみた - Qiita

今回は、なにもないので指定していません。実際、PersistentVolumeやPersistentVolumeClaminでのstorageClassの部分は、
空でしたね。

$ oc get pv/nfs-pv --as system:admin
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS    CLAIM               STORAGECLASS   REASON    AGE
nfs-pv    50Gi       RWX            Retain           Bound     myproject/nfs-pvc                            5m

クラスタ上も、なにも定義していないので。

$ oc get sc
No resources found.

こちらについては、ドキュメント内にちょこちょこと指定してある記述が出てくるので、調べてみました。

まとめ

OKD…というか、KubernetesのVolume、PersistentVolume/PersistentVolumeClaimを試してみました。

実は、だいぶハマったのはKubernetesからのNFSのマウントだったのですが、NFSサーバー側のオプションの指定だったり、
プロトコルのバージョンの指定だったりが原因でした。いやぁ、てこずりました…。

それ以外は、割とすんなりと。

これで、ストレージの扱いも少しは慣れる…といいなぁ…。