ローカルのkubernetes環境にRocketChatを構築する（Local Volume使用）

#概要
ローカルのkubernetes環境に、RocketChatというSlackライクなOSSチャットツールを構築します。

RocketChatではmongoDBが使用されており、使用するボリュームについては今回はLocalVolumeを使用します。
mongoDBが起動しているNodeのローカルストレージにデータが保存されるシンプルな形になります。

この状態だとNode障害に対応できなかったり、mongoDBのレプリカセット（レプリケーションと自動フェイルオーバー機能）が使えない、などの問題があるのですが、今回はこの形で進めようと思います。

後ほど、LocalVolumeではなく、NFSを使用した場合の手順についても記事にしようと思います。

#構成
以下のように、workerNode1にRocketChatとmongoDB用のPODを作成します。
NodePortを作成しておき、ブラウザからアクセスする際は、NodeのIPアドレスとNodePortで設定したポートに対してhttpで接続します。

![local図.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355563/576cff39-77a3-25e6-5fb6-9101df14bafc.png)

#前提
OS : Ubuntu 16.04.6 LTS

```
vagrant@master:~$ kubectl version
Client Version: version.Info{Major:"1", Minor:"14", GitVersion:"v1.14.1", GitCommit:"b7394102d6ef778017f2ca4046abbaa23b8
8c290", GitTreeState:"clean", BuildDate:"2019-04-08T17:11:31Z", GoVersion:"go1.12.1", Compiler:"gc", Platform:"linux/amd
64"}
Server Version: version.Info{Major:"1", Minor:"14", GitVersion:"v1.14.2", GitCommit:"66049e3b21efe110454d67df4fa62b08ea7
9a19b", GitTreeState:"clean", BuildDate:"2019-05-16T16:14:56Z", GoVersion:"go1.12.5", Compiler:"gc", Platform:"linux/amd
64"}
```
#SC作成
それではまず、LocalVolumeに対応したStorageClassを作成します。

LocalVolumeは動的にVolumeを作成してくれる[Dynamic Volume Provisioning](https://kubernetes.io/ja/docs/concepts/storage/dynamic-provisioning/)という機能がサポートされないため使えません。
そのためLocalVolumeを使用する場合、[provisioner](https://kubernetes.io/docs/concepts/storage/storage-classes/#provisioner)は「kubernetes.io/no-provisioner」を選択します。

```yaml:sc_chat.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
```
実行結果

```
vagrant@master:/vagrant/yaml_file$ kubectl create -f sc_chat.yaml
storageclass.storage.k8s.io/local-storage created
vagrant@master:/vagrant/yaml_file$ kubectl get sc
NAME            PROVISIONER                    AGE
local-storage   kubernetes.io/no-provisioner   6s
```
#PV,PVC作成
PersistentVolumeを作成します。
[nodeAffinity](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity)にて、node1にPVが作成されるように設定します。
アクセスモードはReadWriteOnceで単一ノードで読み書きが可能にします。

```yaml:pv_chat.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv
spec:
  capacity:
    storage: 2Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-storage
  local:
    path: /chat_data
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - node1
```
実行結果

```
vagrant@master:/vagrant/yaml_file$ kubectl create -f pv_chat.yaml
persistentvolume/local-pv created
vagrant@master:/vagrant/yaml_file$ kubectl get pv -o wide
NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM                STORAGECLASS    REASON   AGE
local-pv   2Gi        RWO            Retain           Available                        local-storage            6s
```
次はPersistentVolumeClaimを作成します。
storageClassNameで先程作成したSCの`metadata.name`を指定します。


```pvc_chat.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: local-claim
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: local-storage
  resources:
    requests:
      storage: 2Gi
```
実行結果

```
vagrant@master:/vagrant/yaml_file$ kubectl create -f pvc_chat.yaml
persistentvolumeclaim/local-claim created
vagrant@master:/vagrant/yaml_file$ kubectl get pvc
NAME          STATUS   VOLUME     CAPACITY   ACCESS MODES   STORAGECLASS    AGE
local-claim   Bound    local-pv   2Gi        RWO            local-storage   3s
```
#mongoDBのPOD作成
`spec.replicas: 1` で、レプリカセットをシングルノードで作成します。
スタンドアロンモードでの作成も可能ですが、レプリカセットを使うことでトランザクション機能が使用できるようになります。
>Starting in version 4.0, MongoDB provides the ability to perform multi-document transactions against replica sets.
https://docs.mongodb.com/manual/release-notes/4.0/


```mongodb.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mongodb-deployment
spec:
  replicas: 1 #シングルノードで作成
  selector:
    matchLabels:
      app: mondb
  template:
    metadata:
      labels:
        app: mondb
    spec:
      containers:
      - name: mongodb
        image: mongo:4.0.18-xenial
        args: ["--smallfiles","--replSet","rs0","--oplogSize","128"]
        env:
        volumeMounts:
        - name: vol
          mountPath: /data/db
      volumes:
      - name: vol
        persistentVolumeClaim:
          claimName: local-claim
```
spec.template.spec.containers.argsで指定している引数は以下の通りです。
`"--smallfiles"`：少ない容量で作成
`"--replSet","rs0"`：レプリカセットをrs0という名前で作成
`"--oplogSize"`：オペログのサイズ

実行結果

```
vagrant@master:/vagrant/yaml_file$ kubectl apply -f mongodb.yaml
deployment.apps/mongodb-deployment created
vagrant@master:/vagrant/yaml_file$ kubectl get po -o wide
NAME                                  READY   STATUS    RESTARTS   AGE   IP             NODE    NOMINATED NODE   READINESS GATES
mongodb-deployment-7b75b6447b-hn5xt   1/1     Running   0          6s    10.244.1.248   node1   <none>           <none>
vagrant@master:/vagrant/yaml_file$ kubectl get deployment -o wide
NAME                 READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS   IMAGES                SELECTOR
mongodb-deployment   1/1     1            1           13s   mongodb      mongo:4.0.18-xenial   app=mondb
```
#mongoDBのレプリカセットを初期化

レプリカセットの初期化を行います。
作成したmongoDBのメンバーへ接続し、[rs.initiate()](https://docs.mongodb.com/manual/reference/method/rs.initiate/)コマンドで初期化します。
これを忘れてRocketChatのPODを作成しようとすると、エラーが出て正常に作成できません。

```
・mongoDBに接続
# mongo 10.244.1.248

・初期化前確認
> rs.status()
{
        "operationTime" : Timestamp(0, 0),
        "ok" : 0,
        "errmsg" : "no replset config has been received",
        "code" : 94,
        "codeName" : "NotYetInitialized",
        "$clusterTime" : {
                "clusterTime" : Timestamp(0, 0),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}

・初期化実行
> rs.initiate()
{
        "info2" : "no configuration specified. Using a default configuration for the set",
        "me" : "mongodb-deployment-7b75b6447b-hn5xt:27017",
        "ok" : 1,
        "operationTime" : Timestamp(1588334257, 1),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1588334257, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}

・実行後、Enterを押すとプロンプトが変化します。
旧）>
新）rs0:PRIMARY>

・初期化後確認
rs0:PRIMARY> rs.status()
{
        "set" : "rs0",
        "date" : ISODate("2020-05-01T11:57:52.337Z"),
        "myState" : 1,

***長いので省略***

        "members" : [
                {
                        "_id" : 0,
                        "name" : "mongodb-deployment-7b75b6447b-hn5xt:27017",
                        "health" : 1,
                        "state" : 1,
                        "stateStr" : "PRIMARY",
                        "uptime" : 945,
                        "optime" : {
                                "ts" : Timestamp(1588334257, 7),
                                "t" : NumberLong(1)
                        },
                        "optimeDate" : ISODate("2020-05-01T11:57:37Z"),
                        "syncingTo" : "",
                        "syncSourceHost" : "",
                        "syncSourceId" : -1,
                        "infoMessage" : "",
                        "electionTime" : Timestamp(1588334257, 2),
                        "electionDate" : ISODate("2020-05-01T11:57:37Z"),
                        "configVersion" : 1,
                        "self" : true,
                        "lastHeartbeatMessage" : ""
                }
        ],
        "ok" : 1,
        "operationTime" : Timestamp(1588334257, 7),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1588334257, 7),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}
```

#ClusterIPの作成
`spec.selector.app`に、mongoDBの`spec.selector.matchLabels.app`を記載します。
また、`spec.ports.port`には27017を指定します。

```service_ClusterIP.yaml
apiVersion: v1
kind: Service
metadata:
  name: mongodb-service
spec:
  type: ClusterIP
  selector:
    app: mondb
  ports:
  - protocol: TCP
    port: 27017
```
実行結果

```
vagrant@master:/vagrant/yaml_file$ kubectl apply -f service_chat.yaml
service/mongodb-service created
vagrant@master:/vagrant/yaml_file$ kubectl get svc -o wide
NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)     AGE    SELECTOR
mongodb-service   ClusterIP   10.244.181.86   <none>        27017/TCP   7s     app=mondb
```
#RocketChatの作成
・**MONGO_URL**：mongoDBに接続するURL
→`spec.template.spec.env.value`に`mongodb://<ClusterIPのサービス名>:<ClusterIPのポート>/<RocketChat用にmongoDBで使用するDB名>`で設定します。

・**ROOT_URL**：RocketChatのURL
→環境によって変わってきますが、ここでは`http://localhost:3000/`で設定しました。

・**MONGO_OPLOG_URL**：mongoDBのOPLOGを格納する
→`spec.template.spec.env.value`に`mongodb://<ClusterIPのサービス名>:<ClusterIPのポート>/local`で設定する。

```rocketchat.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: rocket-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: rocket
  template:
    metadata:
      labels:
        app: rocket
    spec:
      containers:
      - name: rocket
        image: rocket.chat:3.0.12
        ports:
        - containerPort: 3000
        env:
        - name: MONGO_URL
          value: mongodb://mongodb-service:27017/rocket
        - name: ROOT_URL
          value: http://localhost:3000/
        - name: MONGO_OPLOG_URL
          value: mongodb://mongodb-service:27017/local
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: kubernetes.io/hostname
                operator: In
                values:
                - node1
```
実行結果

```
vagrant@master:/vagrant/yaml_file$ kubectl apply -f rocketchat.yaml
deployment.apps/rocket-deployment created
vagrant@master:/vagrant/yaml_file$
vagrant@master:/vagrant/yaml_file$ kubectl get deployment -o wide
NAME                 READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS   IMAGES                SELECTOR
mongodb-deployment   1/1     1            1           18m   mongodb      mongo:4.0.18-xenial   app=mondb
rocket-deployment    1/1     1            1           4s    rocket       rocket.chat:3.0.12    app=rocket
vagrant@master:/vagrant/yaml_file$ kubectl get po -o wide
NAME                                  READY   STATUS    RESTARTS   AGE   IP             NODE    NOMINATED NODE   READINESS GATES
mongodb-deployment-7b75b6447b-hn5xt   1/1     Running   0          18m   10.244.1.248   node1   <none>           <none>
rocket-deployment-77596dcf9f-c9w9z    1/1     Running   0          6s    10.244.1.251   node1   <none>           <none>
```
結果確認
ログを確認すると、SERVER RUNNINGになっていることが確認できます。

```
vagrant@master:/vagrant/yaml_file$ kubectl logs rocket-deployment-77596dcf9f-c9w9z
Setting default file store to GridFS
LocalStore: store created at
LocalStore: store created at
LocalStore: store created at
ufs: temp directory created at "/tmp/ufs"
Loaded the Apps Framework and loaded a total of 0 Apps!
Updating process.env.MAIL_URL
Using GridFS for custom sounds storage
Using GridFS for custom emoji storage
Browserslist: caniuse-lite is outdated. Please run next command `npm update`
➔ System ➔ startup
➔ +--------------------------------------------------+
➔ |                  SERVER RUNNING                  |
➔ +--------------------------------------------------+
➔ |                                                  |
➔ |  Rocket.Chat Version: 3.0.12                     |
➔ |       NodeJS Version: 12.14.0 - x64              |
➔ |      MongoDB Version: 4.0.18                     |
➔ |       MongoDB Engine: wiredTiger                 |
➔ |             Platform: linux                      |
➔ |         Process Port: 3000                       |
➔ |             Site URL: http://172.16.20.12:3000/  |
➔ |     ReplicaSet OpLog: Enabled                    |
➔ |          Commit Hash: 15ac7670be                 |
➔ |        Commit Branch: HEAD                       |
➔ |                                                  |
➔ +--------------------------------------------------+
```
#NodePortの作成
spec.portsについては以下の方針で記載していきます。

[KubernetesのDiscovery＆LBリソース（その1）](https://thinkit.co.jp/article/13738?page=0%2C1)から引用

```
spec.ports[x].port            ClusterIPで受け付けるPort番号
spec.ports[x].targetPort      転送先のコンテナのPort番号
spec.ports[x].nodePort        全Kubernetes NodeのIP Adressで受け付けるPort番号
```
今回は以下のように作成しました。

```service_NodePort.yaml
apiVersion: v1
kind: Service
metadata:
  name: rocket-service
spec:
  type: NodePort
  selector:
    app: rocket
  ports:
  - protocol: TCP
    port: 3000
    targetPort: 3000
    nodePort: 31000
```
実行結果

```
vagrant@master:/vagrant/yaml_file$ kubectl apply -f service_nodeport_chat.yaml
service/rocket-service created
vagrant@master:/vagrant/yaml_file$ kubectl get svc -o wide
NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE    SELECTOR
mongodb-service   ClusterIP   10.244.181.86   <none>        27017/TCP        23m    app=mondb
rocket-service    NodePort    10.244.175.43   <none>        3000:31000/TCP   2s     app=rocket
```

#サービス接続
ブラウザから`http://172.16.20.12:31000/`に接続します。

初回は初期設定を実施します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355563/8ff35aca-d8e2-dc0f-c005-fa1c961f9504.png)

完了すると以下のようにチャットすることができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355563/bb4e6ce2-5df8-4152-602a-3c4e9f1138b7.png)

#参考
・mongoDB関連
[Kubernetes 上に 単独 MongoDB サーバー を構築する方法](https://garafu.blogspot.com/2019/07/k8s-single-mongodb.html)
[俺でもわかるシリーズ: MongoDBのレプリケーション](https://qiita.com/y13i/items/78c9f45acd07a9478d2d)
[MongoDBでReplica Setsを試した時のメモ](https://qiita.com/toshirot/items/b5e99bf4923b84b2bb40)
・RocketChat
[公式リファレンス](https://hub.docker.com/_/rocket-chat)
[公式インストールドキュメント](https://docs.rocket.chat/installation/docker-containers/)