IBM Cloud Kubernetes Service(IKS)上でNode-REDをノード情報を永続化して稼働させる

# やりたいこと

IBM Cloud Kubernetes Service (IKS) の Kubernetes クラスター上に Node-RED を稼働させ、ノードの構成情報を永続化します。

### 作成する環境
1. IBM Cloud Kubernetes Service (IKS) の Kubernetes クラスター上に Node-RED の環境を構築
2. Node-RED は公式の[イメージ](https://hub.docker.com/r/nodered/node-red/) を使用します。
3. Node-RED の [doc](https://nodered.org/docs/getting-started/docker) (以下に引用) によると、`/data` 以下に構成データが保存されるとあるので、Node-RED の pod が永続ボリュームをマウントするよう構成します。
4. 永続ボリュームには IBM Cloud 上で利用できるストレージサービスのうち、IBM Cloud Object Storage を利用します。

> Node-RED uses the /data directory inside the container to store user configuration data.

### 今回はやらないこと

1. アプリケーションの公開方法については特に考えません。この記事では http プロトコルでアクセスできることまでを考えます。
2. Node-RED 自体も管理者以外がアクセスできないよう、パスワード等で環境を保護するべきですが、その部分も扱いません。
3. 高可用性などの非機能要件も特別には考えません。
4. Node−RED アプリを IKS クラスター内でどの namespace に配置するかも考えません。`Default` を使用しました。


# 用意する必要があるもの

 - IBM Cloud のリソース
   -  IBM Cloud Kubernetes Service の Kubernetes Cluster (Classic/VPC どちらでも可)
   -  IBM Cloud Object Storage
 - IBM Cloud Kubernetes Service で利用できるアドオン
   -  IBM Cloud Object Storage plug-in

### リソース準備時の考慮点
- IBM Cloud Kubernetes Service
  - 特になし
- IBM Cloud Object Storage
  - `サービス資格情報` を作成します。今回は `HMAC認証` を使用したいので、 `HMAC認証` を使用するように構成します。


# 構成手順
### IKS クラスターの構築

- カタログから 「Kubernetes Service」を選択する (https://cloud.ibm.com/kubernetes/catalog/create)
- 必要な環境を指定し、クラスターを作成します。すでにお持ちのクラスターを利用する場合には作成不要です。私の環境では国内にデータが保持されるよう大阪リージョンを使って構成しました。
　以下は私の環境で使用した設定値です。記載がない項目はデフォルト値のままとしました。
  - インフラストラクチャー: クラシック (適宜変更ください)
  - リージョン: アジア太平洋
  - 可用性: マルチゾーン
  - メトロ: 大阪
  - ワーカーゾーンとVLAN: 大阪22と大阪23を選択。(VLANはそれぞれのゾーンでデフォルトのものとした。お客様のアカウントの状態によってはVLANの選択もしくは作成が必要となる)
  - バージョン: 1.28.3
  - ワーカーノード: 「b3c.4x16 | 仮想 - 共有 | x86-64 | Ubuntu 20」とし、ゾーンあたり1台を指定
  - クラスター名: mfcluster12

### ICOS インスタンスの作成
インスタンスを作成します。ここでは指定しませんが、国内にデータが保持されるように大阪リージョンのリージョナルのバケットを利用するようにします。
- カタログから　「Object Storage」を選択してインスタンスの作成画面にアクセスします。
(https://cloud.ibm.com/objectstorage/create)
- 必要な環境を指定してインスタンスを作成します。
  - 以下は私の環境で使用した設定値です。
  - インフラストラクチャーの選択: IBM Cloud
  - 料金プランの選択: Standard
  - サービス名: Cloud Object Storage-mfcos12
  - リソースグループの選択: Default
- 作成されたインスタンスに資格情報を設定します。

### ICOS 資格情報の IKSクラスターへの登録
- IBM Cloudのコンソール画面からICOSのインスタンスにアクセスします。(https://cloud.ibm.com/objectstorage でリストされるインスタンスから対象のインスタンスを選択)
- 資格情報の作成を行います。
  - 「サービス資格情報」タブを選択し、「新規資格情報」をクリックします。
    <img width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83689/03f0c464-4072-11c9-ddf0-e119153d8404.png" />
  - 名前を指定、ロールを「ライター」、HMAC資格情報を含めるを「オン」として「追加」をクリックします
    <img width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83689/ca9f4ec5-5225-5e51-cfc9-231c5655c16f.png" />

  - マスクした箇所に現れる `access_key_id` と `secret_access_key`を控えておきます。
    <img width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83689/b2620b40-1468-c220-d199-9238f56f016f.png" />

- ここからは IKSクラスター側の作業を始めます。
- コマンドラインでIBM Cloudにログイン (ibmcloud login) して構成ファイルをダウンロード (ibmcloud ks cluster config --cluster <CLUSTER_NAME>)します。
- シークレットを作成します。access-keyとsecret-keyには先ほどひかえた`access_key_id` と `secret_access_key` を使用します。以下のコマンド実行例のkeyはそれっぽい値ですが、すでに使用できない値です。
```
$ kubectl create secret generic cos-write-access --type=ibm/ibmc-s3fs --from-literal=access-key="13f94d936f5b485289d2650cc4444444" --from-literal=secret-key="b7151990cc12bd99ba329650494a9f239a3d011111111111"
secret/cos-write-access created
```
- シークレットが作成できているか確認します。cos-write-access のエントリーがあればOKです。
```
$ kubectl get secret
NAME                                                TYPE                             DATA   AGE
all-icr-io                                          kubernetes.io/dockerconfigjson   1      16h
cos-write-access                                    ibm/ibmc-s3fs                    2      14s
mfcluster12-c82dcc5867b41450588f49284670f9a8-0000   kubernetes.io/tls                2      16h
```



### ICOS plugin の導入
- 続いてKubernetesクラスター上にCloud Object Storageプラグインを導入します。
HELMをつかって導入します。
- 最初にノードのKubernetesバージョンを確認します。更新版があるというようなアイコン、メッセージが表示されないのでこのまま進めます。
```
$ ibmcloud ks worker ls --cluster mfcluster12
OK
ID                                                       Public IP       Private IP   Flavor              State    Status   Zone    Version
kube-clkklb8o0484faqs0h70-mfcluster12-default-000001ee   163.73.64.235   10.10.5.54   u3c.2x4.encrypted   normal   Ready    osa23   1.28.3_1535
kube-clkklb8o0484faqs0h70-mfcluster12-default-00000226   163.69.66.194   10.9.20.76   u3c.2x4.encrypted   normal   Ready    osa22   1.28.3_1535
```
- 利用できるhelmの環境が3系列であることを確認して、ibm-helmレポジトリを追加します。
```
$ helm version
version.BuildInfo{Version:"v3.12.3", GitCommit:"3a31588ad33fe3b89af5a2a54ee1d25bfe6eaa5e", GitTreeState:"clean", GoVersion:"go1.20.7"}
$ helm repo add ibm-helm https://raw.githubusercontent.com/IBM/charts/master/repo/ibm-helm
"ibm-helm" has been added to your repositories
```
- レポジトリをアップデートします。
```
$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "ibm-helm" chart repository
Update Complete. ⎈Happy Helming!⎈
```
-　プラグインを環境にインストールします。
```
$ helm plugin install ./ibm-object-storage-plugin/helm-ibmc
Installed plugin: ibmc
```
- インストール終了後に以下のコマンドを実行してエラーとなることがあります。
```
$ helm ibmc --help
Error: fork/exec <PATH>/.local/share/helm/plugins/helm-ibmc/ibmc.sh: permission denied
```
- エラーとなった場合には以下のコマンドでパーミッションを変更して改めて実行します。
```
$ chmod 755 <PATH>/.local/share/helm/plugins/helm-ibmc/ibmc.sh
$ helm ibmc --help
Helm version: v3.12.3+g3a31588
Install or upgrade Helm charts in IBM K8S Service(IKS)
Usage:
  helm ibmc [command]
Available Commands:
  install           Install a Helm chart
  upgrade           Upgrade the release to a new version of the Helm chart
Available Flags:
  -h, --help        (Optional) This text.
  -u, --update      (Optional) Update this plugin to the latest version
Example Usage:
    Install: helm ibmc install ibm-object-storage-plugin ibm-helm/ibm-object-storage-plugin
    Upgrade: helm ibmc upgrade [RELEASE] ibm-helm/ibm-object-storage-plugin
Note:
    1. It is always recommended to install latest version of ibm-object-storage-plugin chart.
    2. It is always recommended to have 'kubectl' client up-to-date.
```
- プラグインをクラスター上にインストールします。
```
$ helm ibmc install ibm-object-storage-plugin ibm-helm/ibm-object-storage-plugin --set license=true --set quotaLimit=false --set bucketAccessPolicy=false --set allowCrossNsSecret=false
Helm version: v3.12.3+g3a31588
Checking cluster type
Installing the Helm chart...
PROVIDER: IBMC
WORKER_OS: debian
PLATFORM: k8s
KUBE_DRIVER_PATH: /usr/libexec/kubernetes
CONFIG_BUCKET_ACCESS_POLICY: false
CONFIG_QUOTA_LIMIT: false
ALLOW_CROSS_NS_SECRET: false
DC: osa22
Region: jp-osa
Chart: ibm-helm/ibm-object-storage-plugin
namespace/ibm-object-s3fs created
NAME: ibm-object-storage-plugin
LAST DEPLOYED: 
NAMESPACE: ibm-object-s3fs
STATUS: deployed
REVISION: 1
NOTES:
Thank you for installing: ibm-object-storage-plugin.   Your release is named: ibm-object-storage-plugin

1. Verify that the storage classes are created successfully:

   $ kubectl get storageclass | grep 'ibmc-s3fs'

2. Verify that plugin pods are in "Running" state:

   $ kubectl get pods -n ibm-object-s3fs -o wide | grep object

   The installation is successful when you see one `ibmcloud-object-storage-plugin` pod and one or more `ibmcloud-object-storage-driver` pods.
   The number of `ibmcloud-object-storage-driver` pods equals the number of worker nodes in your cluster. All pods must be in a `Running` state
   for the plug-in to function properly. If the pods fail, run `kubectl describe pod -n ibm-object-s3fs <pod_name>`
   to find the root cause for the failure.
######################################################
Additional steps for IBM Kubernetes Service(IKS) only:
######################################################

  1. If the plugin pods show an "ErrImagePull" or "ImagePullBackOff" error, copy the image pull secret 'all-icr-io' from "default" namespace to ibm-object-s3fs namespace of your cluster. The image pull secret 'all-icr-io' provides access to IBM Cloud Container Registry.

     a. Check the secret exists in "default" namespace

     $ kubectl get secrets -n default | grep icr-io

     Example output:
     ------------------------------------------------------------------
     all-icr-io         kubernetes.io/dockerconfigjson        1      2d
     ------------------------------------------------------------------

     b. Copy secret to ibm-object-s3fs  namespace

     $ kubectl get secret -n default all-icr-io -o yaml | sed 's/default/<namespace>/g' | kubectl -n <namespace> create -f -

     c. Verify that the image pull secret is available in the ibm-object-s3fs  namespace.

     $ kubectl get secrets -n ibm-object-s3fs | grep icr-io

  2. Verify that the state of the plugin pods changes to "Running".

     $ kubectl get pods -n ibm-object-s3fs | grep object
```
- 実行後数十秒で以下のようにpodが起動します。ワーカーノードの数+1つのpodができると考えられます。
```
$ kubectl get pod --all-namespaces -o wide | grep object
ibm-object-s3fs   ibmcloud-object-storage-driver-cbcw8                   1/1     Running   0          92s   10.10.5.54      10.10.5.54   <none>           <none>
ibm-object-s3fs   ibmcloud-object-storage-driver-cdw7r                   1/1     Running   0          92s   10.9.20.76      10.9.20.76   <none>           <none>
ibm-object-s3fs   ibmcloud-object-storage-plugin-69dcbb5747-8hz99        1/1     Running   0          92s   172.30.22.210   10.9.20.76   <none>           <none>
```
- その後ストレージクラスが作成されていることも確認します。
```

$ kubectl get storageclass | grep s3
ibmc-s3fs-cold-cross-region            ibm.io/ibmc-s3fs    Delete          Immediate           false                  108s
ibmc-s3fs-cold-regional                ibm.io/ibmc-s3fs    Delete          Immediate           false                  108s
ibmc-s3fs-smart-cross-region           ibm.io/ibmc-s3fs    Delete          Immediate           false                  108s
ibmc-s3fs-smart-perf-cross-region      ibm.io/ibmc-s3fs    Delete          Immediate           false                  108s
ibmc-s3fs-smart-perf-regional          ibm.io/ibmc-s3fs    Delete          Immediate           false                  108s
ibmc-s3fs-smart-regional               ibm.io/ibmc-s3fs    Delete          Immediate           false                  108s
ibmc-s3fs-standard-cross-region        ibm.io/ibmc-s3fs    Delete          Immediate           false                  108s
ibmc-s3fs-standard-perf-cross-region   ibm.io/ibmc-s3fs    Delete          Immediate           false                  108s
ibmc-s3fs-standard-perf-regional       ibm.io/ibmc-s3fs    Delete          Immediate           false                  108s
ibmc-s3fs-standard-regional            ibm.io/ibmc-s3fs    Delete          Immediate           false                  108s
ibmc-s3fs-vault-cross-region           ibm.io/ibmc-s3fs    Delete          Immediate           false                  108s
ibmc-s3fs-vault-regional               ibm.io/ibmc-s3fs    Delete          Immediate           false                  108s

```
- PVCを作成するためのマニフェストファイルを作成します。
  - バケットを自動生成し、アプリ削除時に自動削除する設定としています。容量は1GB,　ストレージクラスは「ibmc-s3fs-smart-regional」つまりRegionalのSmart Tierのバケットとしています。

```pvcmf12.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvcmf12
  namespace: default
  annotations:
    ibm.io/auto-create-bucket: "true"
    ibm.io/auto-delete-bucket: "true"
    ibm.io/bucket: ""
    ibm.io/quota-limit: "false"
    ibm.io/tls-cipher-suite: "default"
    ibm.io/secret-name: "cos-write-access" 
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: ibmc-s3fs-smart-regional
```
- 作成したファイルを引数にして PVC を作成します。

```
$ kubectl apply -f pvcmf12.yaml
persistentvolumeclaim/pvcmf12 created
```

- PVCの作成状況を確認します。最下部 `Events` に　`ProvisioningSucceeded`　が現れれば成功です。これが5分以上立っても表示されないときには、ここまでの構成に問題があるケースがほとんどです。

```
$ kubectl describe pvc pvcmf12
Name:          pvcmf12
Namespace:     default
StorageClass:  ibmc-s3fs-smart-regional
Status:        Bound
Volume:        pvc-d5963154-f70b-4f23-bcaa-0a01c1f80dfd
Labels:        <none>
Annotations:   ibm.io/auto-create-bucket: true
               ibm.io/auto-delete-bucket: true
               ibm.io/bucket: 
               ibm.io/quota-limit: false
               ibm.io/secret-name: cos-write-access
               ibm.io/tls-cipher-suite: default
               pv.kubernetes.io/bind-completed: yes
               pv.kubernetes.io/bound-by-controller: yes
               volume.beta.kubernetes.io/storage-provisioner: ibm.io/ibmc-s3fs
               volume.kubernetes.io/storage-provisioner: ibm.io/ibmc-s3fs
Finalizers:    [kubernetes.io/pvc-protection]
Capacity:      1Gi
Access Modes:  RWO
VolumeMode:    Filesystem
Used By:       <none>
Events:
  Type    Reason                 Age                    From                                                                                                   Message
  ----    ------                 ----                   ----                                                                                                   -------
  Normal  ExternalProvisioning   2m48s (x2 over 2m48s)  persistentvolume-controller                                                                            Waiting for a volume to be created either by the external provisioner 'ibm.io/ibmc-s3fs' or manually by the system administrator. If volume creation is delayed, please verify that the provisioner is running and correctly registered.
  Normal  Provisioning           2m48s                  ibm.io/ibmc-s3fs_ibmcloud-object-storage-plugin-69dcbb5747-8hz99_ed768111-26f1-43af-ae57-0c264221bd1c  External provisioner is provisioning volume for claim "default/pvcmf12"
  Normal  ProvisioningSucceeded  2m44s                  ibm.io/ibmc-s3fs_ibmcloud-object-storage-plugin-69dcbb5747-8hz99_ed768111-26f1-43af-ae57-0c264221bd1c  Successfully provisioned volume pvc-d5963154-f70b-4f23-bcaa-0a01c1f80dfd
```


### Node-RED の導入

- マニフェスト・ファイルの作成
  - Service と Deployment を作成します。
  - IKS のクラシックインフラストラクチャ上での稼働のケースを想定して、NLB 1.0 を単一のゾーン(任意)に作成し、公開します。(「今回はやらないこと」節にも記載した通り、セキュアなアクセス方法や高可用性は考慮していませんが、考慮する場合にはこのマニフェスト・ファイルの定義を変更します。)
  - 使用するコンテナイメージは Node-RED 公式イメージの latest タグがついたものです。
  - Podが `/data` に PV をマウントする構成としています。
  - セキュリティコンテキストとして非ルートユーザーとして稼働させます。[IBM Cloud Docs](https://cloud.ibm.com/docs/containers?topic=containers-storage-cos-understand#cos_limitations) (以下に引用) に記載されている注意事項に合わせて runAsUser と fsGroup を同一の値としています。

> IBM Cloud Object Storage のファイルに非 root ユーザーとしてアクセスするには、デプロイメント内の runAsUser および fsGroup の各値を同じ値に設定する必要があります。


```noderedmanifest.yaml
apiVersion: v1
kind: Service
metadata:
  name: node-red
  labels:
    app: node-red
spec:
  type: LoadBalancer
  ports:
  - port: 1880
  selector:
    app: node-red
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: node-red
spec:
  replicas: 1
  selector:
    matchLabels:
      app: node-red
  template:
    metadata:
      labels:
        app: node-red
    spec:
      containers:
      - name: node-red
        image: nodered/node-red:latest
        ports:
        - containerPort: 1880
        volumeMounts:
        - name: data
          mountPath: /data
      securityContext:
        runAsNonRoot: true
        runAsUser: 2000
        fsGroup: 2000
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: pvcmf12

```

- 作成したマニフェスト・ファイルを使用してアプリをデプロイします。
```
$ kubectl apply -f noderedmanifest.yaml
service/node-red created
deployment.apps/node-red created
```

- 作成したサービスを確認します
```
$ kubectl get service
NAME         TYPE           CLUSTER-IP       EXTERNAL-IP    PORT(S)          AGE
kubernetes   ClusterIP      172.21.0.1       <none>         443/TCP          18h
node-red     LoadBalancer   172.21.160.238   163.73.69.50   1880:32675/TCP   2m39s
```
node-red というサービスが作成できています。EXTERNAL-IP と PORT を使うとアクセスできますね。
実際にhttp://163.73.69.50:1880 にアクセスすると、、、

<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83689/1ea6cb44-5eca-e333-0b45-4c29328f3111.png" />

このように Node-RED にアクセスできました。


# Node情報を保持できるか確認してみる

- Nodeを追加して、デプロイ（保存）した後に、別のブラウザーからNode-REDにアクセスしてみましょう。

Node-RED チュートリアルの Your First Flowのようなフローをつくってデプロイしてみます。

<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83689/38903f25-6874-b56e-58fa-5a40f3ea2d74.png" />

### Node-RED のPodを削除してみる
- Node-REDのPodを削除してみます。
Deployentに定義した `replicas : 1` を満たすように別のPodが起動されるはずです。このときボリュームが永続化されていなければノードの設定情報は消失するはずですが、永続化できていれば新しいアプリに引き継がれるはずです。

```
$ kubectl get pod
NAME                        READY   STATUS    RESTARTS   AGE
node-red-5dd9f958f6-5pvb9   1/1     Running   0          14m

$ kubectl delete pod/node-red-5dd9f958f6-5pvb9
pod "node-red-5dd9f958f6-5pvb9" deleted

$ kubectl get pod
NAME                        READY   STATUS    RESTARTS   AGE
node-red-5dd9f958f6-xsmfn   1/1     Running   0          6s
```

- 再び Node-RED にアクセスしてみます。念のため別の端末からアクセスしてみましたが、フローは保存され、永続化されていました。

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83689/bbe17ffd-06e0-a58f-abac-2aeb3017351d.png" />

### Object Storageのバケットをみてみる

保存されているデータがどのように見えるのか最後に確認しましょう。
IBM Cloudコンソールからバケットにアクセスします。

<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83689/b6eac5f2-791e-0d0b-61aa-5bb300247253.png" />

ICOSインスタンス内に作成されたバケットにアクセスするといくつかのオブジェクトが保存されています。`flow.json`をダウンロードしてエディタで開いてみると、先ほどのフローの要素が表示され保存できていることが確認できると思います。フローの情報がICOS内のバケットにオブジェクトとして保存できていました。

# まとめ

Node-REDをIKS上にデプロイし、ICOS上にノード情報を保存してみました。ICOSプラグインを使うと手軽に実現できますね。ICOSは並行した読み書きが少し苦手だと言われていますが、Node-REDの編集の頻度を考えると、そういった特性の影響はほとんど受けないと考えられます。Block Storage/File Storageを使うような方法ももちろんありますが、個人的にはICOSを利用するのが一番手軽かなと思いました。

今回紹介した環境ではNode-RED上でそのままアプリを開発することができます。一方でこの構成ではNode-REDアプリがインターネット上に公開され、誰でもアクセスできてしまうので、システムの要件を勘案した対策が求められます。その部分は追ってまたの機会に説明したいと思います。

# 参考文献

- IBM Cloud Docs
  - [IBM Cloud Object Storage のセットアップ](https://cloud.ibm.com/docs/containers?topic=containers-storage-cos-understand)
  - [IBM Cloud Object Storage プラグインのインストール](https://cloud.ibm.com/docs/containers?topic=containers-storage_cos_install)
  - [アプリへのオブジェクト・ストレージの追加](https://cloud.ibm.com/docs/containers?topic=containers-storage_cos_apps)
  - [クラシック: NLB 1.0 による基本的なロード・バランシングのセットアップ](https://cloud.ibm.com/docs/containers?topic=containers-loadbalancer)
- Node-RED
  - [Tutorial - First Flow](https://nodered.org/docs/tutorials/first-flow)
- Docker Hub
  - [Node-RED 公式イメージ](https://hub.docker.com/r/nodered/node-red)