MetabaseをKubernetes上で冗長化してみる 

## 概要

MetabaseのコンテナをKubernetesを使って複数台構成にして冗長化できたので、その方法を共有する。
冒頭で言ってしまうと身も蓋もないのだが、同じようなことを下記サイトにあるHelmを使ってもできそうなので、Helmに詳しい方はこちらを参考にしたほうが簡単にできるかも。
https://github.com/helm/charts/tree/master/stable/metabase

## Metabaseコンテナの構造

今回、以下のMetabaseコンテナを利用する。
https://www.metabase.com/docs/latest/operations-guide/running-metabase-on-docker.html

Metabaseの中身をざっくりと見たのが下図。コンテナの中にアプリケーション本体と、設定情報（ユーザアカウントやコレクション等）を格納したアプリケーションDBが存在する。アプリケーションDBはJavaプラットフォーム上で動くH2データベース上で起動している。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370519/498881cc-a753-904c-df04-2439ab52266a.png" width=65%>

このままの状態でMetabaseコンテナを複数立ち上げてしまうと、各コンテナの中に別個にアプリケーションDBが出来てしまうので、アプリの設定が適切に反映されなくなってしまう。そこでちょっと工夫が必要。

## 手順の概要

上記の問題を解決するために、まずはコンテナ内に入っているアプリケーションDBをコンテナ外に予め作成したPostgreSQLのDBに変換する。以下がそのイメージ。
Metabaseのコンテナ起動時にオプションを使うとPostgreSQL形式に変換してくれるので、今回はそれを利用してKubernetesのJobを作成する。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370519/c92af14a-8291-1193-b7a3-fe748c150e0e.png">

その後、サービス用のMetabaseコンテナを立ち上げる。デフォルトだとH2データベースのアプリDBが作られてしまうので、環境変数でポスグレのアプリDB情報を指定する必要あり。下図が冗長化構成にしたイメージ。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370519/4f554366-7119-19f2-9ba6-3453f6d9691a.png">

それでは具体的な方法を記載する。

## データベースサーバーの用意

アプリケーションデータを格納するためのDBサーバ（今回はPostgreSQL）を用意する。

* DBサーバーは各環境に合わせて用意してもらえればいいが、本例ではKubernetesで作成（永続化はしていないので注意）
* コンテナイメージは下記Docker Hubのpostgresを利用
    * https://hub.docker.com/_/postgres

マニフェストは以下

```yml:postgres.yml
# PostgreSQLのServiceリソースに関する定義
# このリソースがあることで、同じKubernetesクラスター内の他のPodから
#「metadata.name」で指定したホスト名(=postgres)でアクセス可能となる
apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  type: ClusterIP
  selector:
    app: postgres
  ports:
  - protocol: TCP
    port: 5432
    targetPort: 5432
---
# パラメータの定義。後でMetabaseのマニフェストを書く際にも利用する。
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-vars
data:
  POSTGRES_DB: postgres
  POSTGRES_USER: postgres
  POSTGRES_PASSWORD: pgpasswd
---
# postgresサーバーのデプロイ方法に関する定義
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: postgres
  name: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      # コンテナ本体の定義
      containers:
      - image: postgres
        name: postgres
        env:
        envFrom:
        - configMapRef:
            name: postgres-vars
```

* リソース作成

```
$ kubectl apply -f postgres.yml
```

* 各リソースが出来ていることを確認

```
$ kubectl get pod -l app=postgres
NAME                       READY   STATUS    RESTARTS   AGE
postgres-ff5bfdccf-2wgvl   1/1     Running   0          14m
$ kubectl get deployments -l app=postgres
NAME       READY   UP-TO-DATE   AVAILABLE   AGE
postgres   1/1     1            1           14m
$ kubectl get service
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP    76d
postgres     ClusterIP   10.107.54.211   <none>        5432/TCP   16m
```

## H2データベースをPostgreSQLに変換

手順の概要で述べたとおり、Metabaseの設定情報（ユーザアカウントやコレクション等）はデフォルトではH2データベースに格納されていため、先ほど作成したPostgreSQLのDBに移行する。
以下サイトにMetabaseのDockerコンテナを使ってH2データベースをPostgreSQLに変換する方法が記載されており、これを参考に「H2データベース→PostgreSQL変換」のJobを実行するマニフェストを書く。

* https://www.metabase.com/docs/latest/operations-guide/running-metabase-on-docker.html

```yml:load-from-h2.yml
apiVersion: v1
kind: ConfigMap
metadata:
  name: metabase-load-vars
# Metabaseの設定ファイルをPostgreSQLに変換するためのパラメータを定義
data:
  MB_DB_FILE: /metabase.db
  MB_DB_TYPE: postgres
  MB_DB_PORT: "5432"
  MB_DB_HOST: postgres
---
apiVersion: batch/v1
kind: Job
metadata:
  name: load-from-h2
spec:
  completions: 1
  template:
    spec:
      restartPolicy: Never
      containers:
      - image: metabase/metabase # ★自作でイメージを作った場合はここを変更
        name: load-from-h2
        args: ["load-from-h2"]
        envFrom:
        - configMapRef:
            name: metabase-load-vars
        # DB名、DBユーザー名、パスワードはpostgresの
        # コンテナと同じパラメータを利用
        env:
        - name: MB_DB_DBNAME
          valueFrom:
            configMapKeyRef:
              name: postgres-vars
              key: POSTGRES_DB
        - name: MB_DB_USER
          valueFrom:
            configMapKeyRef:
              name: postgres-vars
              key: POSTGRES_USER
        - name: MB_DB_PASS
          valueFrom:
            configMapKeyRef:
              name: postgres-vars
              key: POSTGRES_PASSWORD
        # 以下はMetabaseのH2データベース形式の設定ファイル
        # (metabase.db.mv.db,metabase.db.trace.db)を
        # PersistentVolumeを経由してマウントする例
        # Metabaseを一から新しく作る場合や、コンテナ内にカスタマイズ済みの
        # 設定ファイルが既に格納されている場合は、以後の記載は不要
        volumeMounts:
        - name: settings-vol
          mountPath: /metabase.db
      volumes:
      # ここには記載しないが、persistentVolumeと
      # persistentVolumeClaimを別途作る必要あり
      - name: settings-vol
        persistentVolumeClaim:
          claimName: metabase-pvc
```

このマニフェストでは、Metabaseの設定ファイルをPersistentVolumeを使って反映している。この手順が面倒な場合は、予め設定ファイルをコンテナ内にコピーしたイメージをDockerfile等で作っておき、自作イメージをコンテナ化するマニフェストを書いても差し支えない。（上記マニフェストの``# ★自作でイメージを``とコメントした部分のコンテナ名を書き換える）

* 以下コマンドで、変換Jobを実行

```
$ kubectl apply -f load-from-h2.yml
```

* Jobが完了していることの確認（筆者の環境では１分ほどかかった）

```
$ kubectl get jobs
NAME           COMPLETIONS   DURATION   AGE
load-from-h2   1/1           75s        76s
```



## Metabaseのマニフェストを書いてデプロイ

以下がMetabaseのサービス化用マニフェスト

```yml:deploy.yml
apiVersion: v1
kind: Service
metadata:
  name: metabase
spec:
  type: ClusterIP
  selector:
    app: metabase
  ports:
  - protocol: TCP
    port: 3000 # 公開するポート
    targetPort: 3000
  # 環境に応じて適宜設定。
  # サービス公開するポート番号が任意でよければ、
  # NodePortを使ってexternalIPsは書かなくても問題ない
  externalIPs:
    - <ホストOSのIP>
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: metabase
  name: metabase
spec:
  replicas: 2 # デプロイするPodの数を記載
  selector:
    matchLabels:
      app: metabase
  template:
    metadata:
      labels:
        app: metabase
    spec:
      containers:
      - image: metabase/metabase
        name: metabase
        ports:
        - containerPort: 3000
          protocol: TCP
        # ポスグレに格納したアプリデータにアクセスするための設定
        # load-from-h2のJobで設定した環境変数と全く同じ
        envFrom:
        - configMapRef:
            name: metabase-load-vars
        env:
        - name: MB_DB_DBNAME
          valueFrom:
            configMapKeyRef:
              name: postgres-vars
              key: POSTGRES_DB
        - name: MB_DB_USER
          valueFrom:
            configMapKeyRef:
              name: postgres-vars
              key: POSTGRES_USER
        - name: MB_DB_PASS
          valueFrom:
            configMapKeyRef:
              name: postgres-vars
              key: POSTGRES_PASSWORD
```

* Metabaseのリソース作成と稼働状況の確認

```
$ kubectl apply -f deploy.yml 
service/metabase created
deployment.apps/metabase created
$ kubectl get all -l app=metabase
NAME                            READY   STATUS    RESTARTS   AGE
pod/metabase-6c8b6d84bb-gb47j   1/1     Running   0          106s
pod/metabase-6c8b6d84bb-8m8rf   1/1     Running   0          106s


NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/metabase   2/2     2            2           106s

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/metabase-6c8b6d84bb   2         2         2       106s
```

* http://hostname:3000 でアクセスできることを確認

## おまけ

最初はアプリDBをポスグレに移行せずに、下図のようにH2データベースが格納されたボリュームを別途作って、各コンテナのH2データベースがここを共通にアクセスすることを試みていたのだが、上手く行かなかった。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370519/b994bbc6-719a-0781-14a8-85cb10a6d2a3.png">


具体的には、一方のコンテナから以下のようなエラーが出力。どうやらデータには一つのH2データベースからしかアクセスできない模様。

```
2020-11-07 07:45:18,797 ERROR driver.util :: Database connection error
org.h2.jdbc.JdbcSQLException: Database may be already in use: null. Possible solutions: close all other connection(s); use the server mode [90020-197]
	at org.h2.message.DbException.getJdbcSQLException(DbException.java:357) ~[metabase.jar:?]
```

複数のH2データベースからアクセス可能する方法を調べたら、下記サイトで「H2データベースのパス指定でAUTO_SERVER=trueを追加すればOK」という情報を見つける。だがMetabaseのコンテナ起動時のオプションではそのような指定は不可能。（自分で新たにDockerfileから作ればいけるかもしれないが…）

* http://www.h2database.com/html/features.html?highlight=AUTO_SERVER&search=auto_server#auto_mixed_mode

そもそも一つのデータを複数のDBサーバから共有して使おうとすること自体間違っていたのかも。