これは、なにをしたくて書いたもの?
- OKD/Minishift…というかKubernetes上で、ストレージに関する機能を試してみたい
- PersistentVolume、PersistentVolumeClaimというものがあるらしいので、こちらを使ってみようと
- お題はNFSで
そんな感じで、OKD/Minishift上、Kubernetes上で、ストレージを扱うお勉強という感じです。題材として、手元で扱えそうな
NFSを利用することにしました。
PersistentVolume/PersistentVolumeClaim?
コンテナにおけるファイルシステムは、コンテナごとに独立しているため、そのままではファイルの共有などといったことは
できず、またコンテナのプロセスの停止時には消失してしまうものになります。
この課題に対処できるのが、Volumeという仕組みだそうです。
ただ、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に割り当てられるものみたいですね。
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)
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クラスタの管理者が定義するストレージの種類を表すものです。サービスレベル、バックアップポリシー、
性能など、クラスタ管理者が定義します。
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サーバー側のオプションの指定だったり、
プロトコルのバージョンの指定だったりが原因でした。いやぁ、てこずりました…。
それ以外は、割とすんなりと。
これで、ストレージの扱いも少しは慣れる…といいなぁ…。