おうちで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かつ動的にボリュームを確保する実装を動作させてみるのが本記事の内容です。

環境

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"}

実験

そもそも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のイニシエータとして動作させるコマンドをインストールします。具体的にはiscsiadmmkfs.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がほしい方にはオススメの選択肢となるのではないでしょうか。


  1. 記事を書いている時にこのリポジトリを見つけたんですが、息してなさそうですね… https://github.com/kubernetes-csi/csi-driver-iscsi 
  2. ボリュームを作りたいときは毎回iSCSI ターゲット側で事前にブロックデバイスを作成する必要があった 
  3. https://twitter.com/whywaita/status/1261915457654677505 

コメントを残す