おうちでDynamic Provisioning! kubernetes-incubator/external-storage iSCSIクライアントを試してみる
こんにちは。 whywrite.it Kubernetes班のwhywaitaです。
皆さんも一家に一クラスタはKubernetes のクラスタがあると思いますが、永続化ストレージ (Persistent Volume)はいかがでしょうか。
近年はCSIという様々なストレージを同じように扱えるインターフェースが定義されているため、様々なベンダーがCSIに対応したストレージ製品やCSIに対応させるゲートウェイアプリケーションを公開していたりします。
ここにKubernetesコミュニティが認識しているCSI Driverの一覧が載っているのですが、見事にクラウド上のブロックストレージサービスのDriverが沢山ありますね。オンプレミス向けだと高価なハードウェアアプライアンス製品がちらほら。
自宅はクラウドではないので(?)、オープンソースで動作するiSCSIなどで解決したいのですが、CSIには実装がなく1、CSI以前から存在しているiSCSIを用いる実装はKubernetesにおけるPersistent Volume Claimを用いて動的にボリュームが作れない状況にありました2。
そこでkubernetes-incubator/external-storageを用いて、iSCSIかつ動的にボリュームを確保する実装を動作させてみるのが本記事の内容です。
[ ※ 2021/07/31 追記 ]
執筆から1年以上経過し状況が変わっているため追記しておきます。
kubernetes-incubator Organization 配下にあったexternal-storageは現在 kubernetes-retired Organization にtransferされており、リポジトリ自体もRead-onlyとなっています。
Kubernetes v1.20以降では互換性が無くなってしまい動作しなくなりました。
有志がforkしたリポジトリでKubernetes v1.20以降でも動作するよう修正しているようなので、今から試す方はこちらをご利用ください。
[ 追記ここまで ]
環境
iSCSI ターゲット側 (ストレージを提供する側)
$ cat /etc/centos-release CentOS Linux release 8.1.1911 (Core) $ targetcli --version /usr/bin/targetcli version 2.1.fb49 $ docker --version Docker version 19.03.8, build afacb8b
iSCSI イニシエータ側 (ストレージを利用する側)
$ cat /etc/os-release | grep -A1 NAME NAME="Ubuntu" VERSION="18.04.3 LTS (Bionic Beaver)" $ kubectl version Client Version: version.Info{Major:"1", Minor:"16", GitVersion:"v1.16.2", GitCommit:"c97fe5036ef3df2967d086711e6c0c405941e14b", GitTreeState:"clean", BuildDate:"2019-10-15T19:18:23Z", GoVersion:"go1.12.10", Compiler:"gc", Platform:"linux/amd64"} Server Version: version.Info{Major:"1", Minor:"16", GitVersion:"v1.16.2", GitCommit:"c97fe5036ef3df2967d086711e6c0c405941e14b", GitTreeState:"clean", BuildDate:"2019-10-15T19:09:08Z", GoVersion:"go1.12.10", Compiler:"gc", Platform:"linux/amd64"}
- kubernetes-incubator/external-storage: 5c38d738d49dd6c7b1f1ad9de2583f56693bd18d
- incubatorプロジェクトなので仕様変更などが激しいかもしれません、ご了承ください
実験
そもそもiSCSIとは?という方には、InternetWeek2011のでiSCSI再入門の資料を一読しておくことを推奨します。
iSCSI ターゲットを用意する
まずは一般的な iSCSIターゲットサーバを起動させるのに必要なパッケージをインストールします。
$ sudo yum install targetcli $ sudo firewall-cmd --permanent --add-port=3260/tcp $ sudo firewall-cmd --permanent --add-port=18700/tcp $ sudo firewall-cmd --reload
targetcli はiSCSIターゲットデーモンを管理するCLIアプリケーションです。一般的な利用方法についてはenakaiさんのiSCSIイニシエータ/ターゲット構成手順の記事が参考になります。
インストール直後はこのような出力が得られます。
$ sudo targetcli ls / o- / ........................................................... [...] o- backstores ................................................ [...] | o- block .................................... [Storage Objects: 0] | o- fileio ................................... [Storage Objects: 0] | o- pscsi .................................... [Storage Objects: 0] | o- ramdisk .................................. [Storage Objects: 0] o- iscsi .............................................. [Targets: 0] o- loopback ........................................... [Targets: 0]
特にボリュームなども存在していない状態ですが、いったんこれで置いておきます。
次に、targetdから切り出す用のLVMボリュームを作成します。自分はOS用とは別にストレージを切り出す用のディスクをマシンに刺しました。
同じ容量のディスクを2枚刺し、ソフトウェアRAID1で束ねておきます。
LVMのVolume Groupが出来ればよいので、RAIDなどは任意で行ってください。
$ sudo yum install mdadm $ mdadm --create /dev/md0 --level=raid1 --name=md0 --raid-devices=2 /dev/sdb /dev/sdc $ mdadm --detail --scan > /etc/mdadm.conf
できたRAIDデバイスをそのままVolume Groupとして登録します。ここからはexternal-storageのREADMEに沿って進めます。
$ sudo pvcreate /dev/md0 $ sudo vgcreate vg-targetd /dev/md0 $ sudo vgs VG #PV #LV #SN Attr VSize VFree cl 1 3 0 wz--n- <222.57g 0 vg-targetd 1 6 0 wz--n- <3.64t <3.59t
無事にLVMのVolume Groupができました。
動的にボリュームを作成する秘訣として、LVMを用いて動的にiSCSI向けのボリュームを切り出すopen-iscsi/targetdというアプリケーションをターゲットサーバで動作させることが必要になります。
CentOS7まではCentOSの標準パッケージに入っていたのですが、CentOS8からは削除されてしまった模様3なので、リポジトリにあるDockerfileを使って動作させてみます。
事前にDockerは利用できるようになっている前提です(参考)。
# CentOS標準のターゲット用デーモンが3260番ポートを競合してしまうので先に止める $ sudo systemctl stop target.service $ sudo systemctl disable target.service # targetdの設定ファイルを書く $ sudo vim /etc/target/targetd.yaml $ cat /etc/target/targetd.yaml password: ciao pool_name: vg-targetd user: admin ssl: false target_name: iqn.2003-01.org.linux-iscsi:targetd # 実際にビルドしてみる $ git clone https://github.com/open-iscsi/targetd # targetd:latest というdocker imageを作って起動 $ docker build -t targetd -f docker/Dockerfile . $ docker run -d \ --name targetd \ --restart=always \ --net=host \ --privileged \ -v /etc/target:/etc/target \ -v /sys/kernel/config:/sys/kernel/config \ -v /run/lvm:/run/lvm \ -v /lib/modules:/lib/modules \ -v /dev:/dev \ -p 3260:3260 \ -p 18700:18700 \ targetd
--net=host
オプションがミソで、これを付けないとKubernetesからボリュームにアクセスするリクエストが来た時にDockerコンテナを持つIPを返却してしまい繋がりません。
targetd用の18700ポートだけDockerコンテナにフォワードしてtarget.serviceにiSCSIのアクセスを任せようかと思っていましたが、
なぜか運用してしばらく経つと3260番ポートのLISTENがいつの間にかなくなっている事象が起きたので3260番ポートもtargetdコンテナにフォワードするようにし、今のところ安定して動いています。
Linux上で3260番、18700番ポートをLISTENしていることを確認すれば、ターゲット側の準備は完了です。
$ sudo ss -antp | grep -E "3260|18700" LISTEN 0 5 0.0.0.0:18700 0.0.0.0:* users:(("targetd",pid=11585,fd=3)) LISTEN 0 256 0.0.0.0:3260 0.0.0.0:*
Kubernetes上にiSCSIイニシエータを作成する
Kubernetesのworker node、正確にはPVCをマウントするPodが動作するホストに対して、iSCSIのイニシエータとして動作させるコマンドをインストールします。具体的にはiscsiadm
と mkfs.xfs
が使えるようにインストールします。(参考)
# Ubuntu の場合 $ sudo apt install open-iscsi xfsprogs # CentOS の場合 (XFSは標準に入っているので不要) $ sudo yum install iscsi-initiator-utils
また、イニシエータ側のIQN (ノードごとにある固有の認識名) も変更しておきます。こちらのIQNも仕様に合わせたものを自分で決めます。
$ cat /etc/iscsi/initiatorname.iscsi InitiatorName=iqn.2017-04.com.example:node1 $ sudo systemctl restart iscsid
そしてexternal-storageのマニフェストをapplyしますが、いくつか修正事項があるのでマニフェストを手元にダウンロードした上で修正します。パッチをとりあえず置いておきます。
このパッチの上で、イニシエータやターゲットのIQN、ターゲット側のIPアドレスなどの認証情報を変更しておきます。
変更した上で必要な情報をapplyします。
$ kubectl create secret generic targetd-account --from-literal=username=admin --from-literal=password=ciao -n kube-system $ kubectl apply -f iscsi-provisioner-d.yaml $ kubectl apply -f iscsi-provisioner-storageclass.yaml
無事コンテナが起動すれば、イニシエータ側の準備も完了です。
動作試験
では実際にボリュームを作成し、コンテナにマウントしてみましょう。
# リポジトリにあるサンプルのPVCマニフェスト $ cat iscsi-provisioner-pvc.yaml kind: PersistentVolumeClaim apiVersion: v1 metadata: name: myclaim namespace: kube-system annotations: volume.beta.kubernetes.io/storage-class: "iscsi-targetd-vg-targetd" spec: accessModes: - ReadWriteOnce resources: requests: storage: 100Mi $ kubectl apply -f iscsi-provisioner-pvc.yaml persistentvolumeclaim/myclaim created $ kubectl get pvc -n kube-system NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE myclaim Bound pvc-xxx-xx-xx-xx-xx 100Mi RWO iscsi-targetd-vg-targetd 7s $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pvc-xxx-xx-xx-xx-xx 100Mi RWO Retain Bound kube-system/myclaim iscsi-targetd-vg-targetd 10s
実際にPVCが切り出されていることは、ターゲット側でも確認できます。
$ sudo targetcli ls / o- / ......................................................................................................................... [...] o- backstores .............................................................................................................. [...] | o- block .................................................................................................. [Storage Objects: 1] | | o- vg-targetd:pvc-xxx-xx-xx-xx-xx [/dev/vg-targetd/pvc-xxx-xx-xx-xx-xx (100.0MiB) write-thru activated] | | o- alua ................................................................................................... [ALUA Groups: 1] | | o- default_tg_pt_gp ....................................................................... [ALUA state: Active/optimized] | o- fileio ................................................................................................. [Storage Objects: 0] | o- pscsi .................................................................................................. [Storage Objects: 0] | o- ramdisk ................................................................................................ [Storage Objects: 0] o- iscsi ............................................................................................................ [Targets: 1] | o- iqn.2003-01.org.linux-iscsi:targetd ...........,,,,,,,............................................................. [TPGs: 1] | o- tpg1 ............................................................................................... [no-gen-acls, no-auth] | o- acls .......................................................................................................... [ACLs: 1] | | o- iqn.2017-04.com.example:node1 ........................................................................ [Mapped LUNs: 1] | | o- mapped_lun0 ........................................................ [lun4 block/vg-targetd:pvc-xxx-xx-xx-xx-xx (rw)] | o- luns .......................................................................................................... [LUNs: 1] | | o- lun0 [block/vg-targetd:pvc-xxx-xx-xx-xx-xx (/dev/vg-targetd/pvc-xxx-xx-xx-xx-xx) (default_tg_pt_gp)] | o- portals .................................................................................................... [Portals: 1] | o- 0.0.0.0:3260 ..................................................................................................... [OK] o- loopback ......................................................................................................... [Targets: 0]
ターゲット側の出力から確認できるように、iSCSIの接続に必要なLVMボリュームの切り出し、aclの挿入などはtargetdが自動で行ってくれます。
lvs コマンドを実行することで、こちらからもブロックデバイスが切り出されていることが分かります。
$ sudo lvs LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert home cl -wi-ao---- <164.78g root cl -wi-ao---- 50.00g swap cl -wi-ao---- <7.79g pvc-xxx-xx-xx-xx-xx vg-targetd -wi-ao---- 100.00m
もしpvcがBoundにならない場合は、Kubernetes Node側から直接 iscsiadm コマンドを実行してデバッグすることができます。ブロックデバイス(LUN)を作成したあとに1度 sendtargets
を送信した後にloginコマンドを実行します。
$ sudo iscsiadm -m discovery -t sendtargets -p <ターゲット側のIP> <ターゲット側のIP>:3260,1 iqn.2003-01.org.linux-iscsi:targetd $ sudo iscsiadm -m node --portal <ターゲット側のIP> --login
あとは通常のPodマウントと同じようにマウントすることができます(参考)。
$ cat mysql.yaml --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: data-mysql namespace: middleware spec: accessModes: - ReadWriteOnce volumeMode: Filesystem resources: requests: storage: 20Gi storageClassName: iscsi-targetd-vg-targetd --- apiVersion: apps/v1 kind: Deployment metadata: name: mysql namespace: middleware labels: app: mysql spec: replicas: 1 selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: containers: - name: mysql image: mysql:8.0.13 env: - name: MYSQL_ALLOW_EMPTY_PASSWORD value: "true" ports: - containerPort: 3306 volumeMounts: - name: mysql-storage mountPath: /var/lib/mysql volumes: - name: mysql-storage persistentVolumeClaim: claimName: data-mysql $ kubectl apply -f mysql.yaml
クラウド上に存在するブロックデバイスサービスのようにマウントすることができます。
まとめ
Kubernetesの実験的プロジェクトであるkubernetes-incubatorからexternal-storageを、その中にあるiSCSIクライアントを試してみました。
非常に安価で汎用的なストレージでKubernetesのDynamicなPersistent Volumeが利用できるようになったので満足しています。
何日か運用した上で比較的安定していそうな雰囲気だったため、このブログやMySQLなどKubernetes上に乗っていたPVCを全て今回のiSCSI経由でのボリュームに載せ替えました。
iSCSIやLVMは非常に枯れたプロトコルであり、全てOSSで用意することができました。
もし何らかの要因でPVCとボリュームとを紐付けるメタデータが壊れたとしても、iSCSIのブロックデバイスなのでレガシーなオペレーションでデータロストを避けられるというのはかなり嬉しいポイントです。
incubatorプロジェクトのため今後どのような立ち位置になるかは分かりませんが、安価に自宅KubernetesにDynamic PVCがほしい方にはオススメの選択肢となるのではないでしょうか。
- 記事を書いている時にこのリポジトリを見つけたんですが、息してなさそうですね… https://github.com/kubernetes-csi/csi-driver-iscsi ↩
- ボリュームを作りたいときは毎回iSCSI ターゲット側で事前にブロックデバイスを作成する必要があった ↩
- https://twitter.com/whywaita/status/1261915457654677505 ↩