Metabaseのダッシュボードをエクスポート/インポートする

この記事は[NTTコムウェア Advent Calendar 2023](https://qiita.com/advent-calendar/2023/nttcomware)の記事です。

# はじめに
　こんにちは。NTTコムウェア コーポレート革新本部の熊谷です。普段は社内のシステム開発プロジェクトが利用する開発ツールの調査・提供を行っています。

　業務の中でオープンソースの可視化ツールであるMetabaseを取り扱うことがあり、ダッシュボードのテンプレートを他チームに配布したいというケースがありました。しかし、Metabase標準のGUI操作ではエクスポート/インポートが対応しておらず、代替手段の情報も中々見当たらずで少々苦戦しましたので今回はその実現方法を紹介したいと思います。同じような状況の方の一助になればと思います。

# Metabaseについて
　Metabaseはオープンソースの可視化ツールです。↓のような感じで、データソースを集計し様々なグラフを作成することができ、それを一つのダッシュボードにまとめて表示することができます。無料にもかかわらず割と簡単に可視化ができてしまいます。このダッシュボードも10分ほどで作れてしまいました。基本的な使い方は[公式サイト](https://www.metabase.com/)や他の方の記事を参照いただければと思います。

<!--metabaseサンプルダッシュボード -->
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3622903/ef880f6b-98d1-8aed-4052-60be45f29ac5.png)

上記には以下の要素を含みます。
- コレクション
    - 画像左側のフォルダ。ダッシュボードやカードを管理する箱のようなもの。
- カード
    - データソースの集計・グラフ作成までを記述したもの。（クエリとも呼びます）
- ダッシュボード
    - 画像右側。複数のカードやテキストを1つの画面に表示できる。

今回の記事では、これら要素をエクスポート/インポートすることを目標にします。


# 実現方法
　MetabaseのGUI標準機能としてエクスポート/インポート機能は提供されていません。（厳密には、グラフ結果をCSVや画像を出力することはできますが、それを再度インポートすることはできません。）
　代わりの方法を模索したところ、API経由でダッシュボードやカードの操作を行うことができるとのことで、活用できそうです。（→[Metabase APIの公式ドキュメント](
https://www.metabase.com/docs/latest/api-documentation.html)）

実現の流れは以下の通りです。
- Metabase APIを利用できるようにする
- ダッシュボードの構成情報を取得する（エクスポート）
- インポート先の情報を取得する
- ダッシュボードを登録する（インポート）

## Metabase APIを利用できるようにする
:::note
本記事ではAPI実行のHTTPクライアントとしてcurlを利用します。
API実行可能な環境であれば何でもOKです。
:::

Metabase APIを利用するために、はじめにセッションIDの取得が必要になります。
- 実行するAPI：[POST /api/session/](https://www.metabase.com/docs/latest/api/session#post-apisession)
- API実行例
    ```bash
    $ curl -X POST \ 
        -H "Content-Type: application/json" \
        -d "{\"username\": \"${USER_NAME}\", \"password\": \"${PASSWORD}\"}" \
        https://${METABASE_DOMAIN}/api/session
    ```
    ```bash
    {"id":"55bdfecb-b883-4608-9927-dac9e23e11e8"} ※値の例です
    ```
以降、APIを利用する際にヘッダー`"X-Metabase-Session"`に取得したIDを設定することで使えるようになります。

 

## ダッシュボードの構成情報を取得する（エクスポート）
　配布したいダッシュボードと、使用されているカード情報をAPI経由でjson形式で取得します。ダッシュボード→カードの順で取得します。


#### ダッシュボード
- 実行するAPI：[GET /api/dashboard/:id](https://www.metabase.com/docs/latest/api/dashboard#get-apidashboardid)
- API実行例
```
curl -X GET \
  -H "Content-Type: application/json" \
  -H "X-Metabase-Session: ${SESSION_ID}" \
  https://${METABASE_DOMAIN}/api/dashboard/${DASHBOARD_ID} > dashboard.json
```
※`${SESSION_ID}`は先ほど取得したセッションIDを指定します。
※`${DASHBOARD_ID}`はダッシュボードIDの値を指定します。対象のダッシュボードのURL末尾の確認できます。

`dashboard.json`に、ダッシュボードの構成情報がjsonで出力されるので、これを後のインポートで利用します。また、次のカード取得のために、実行結果からダッシュボードで利用しているカードID（Metabaseでの管理ID）を抽出します。複雑なダッシュボードで行数が多い場合は、jqなどのjsonパーサーで抽出すると楽です。

```json
{
  "description": null,
  "archived": false,
  "collection_position": null,
  "ordered_cards": [
    {
      "size_x": 9,
      "dashboard_tab_id": null,
      "series": [],
      "action_id": null,
      "collection_authority_level": null,
      "card": {
        "description": null,
        ～略～
        "id": 1, ←★この値をメモしておく
        ～略～
    },
    {
        ～以降、カードの数だけ繰り返し～
```

#### カード
- 実行するAPI：[GET /api/card/:id](https://www.metabase.com/docs/latest/api/card#get-apicardid)
- API実行例
```bash
curl -X GET \
  -H "Content-Type: application/json" \
  -H "X-Metabase-Session: ${SESSION_ID}" \
  https://${METABASE_DOMAIN}/api/card/${CARD_ID} > card_${CARD_ID}.json 
```
※`${CARD_ID}`は、先ほどダッシュボードで抽出したカードIDの値を指定します

`card_${CARD_ID}.json`に、カードの構成情報がjsonで出力されるので、これを後のインポートで利用します。ダッシュボードで使用しているカードの数だけこのファイルを作成しておきます。



## インポート先の情報を取得する
　前項で取得したjsonを登録APIにそのまま流そうとすると、インポート先Metabaseで接続するDB情報や保存先コレクションの指定で不整合が起きる可能性があります。そのため、登録用のjsonへ加工する必要があります。
　以下の流れで登録まで実施します。
- インポート先のDB情報を取得
- インポート先のコレクションを作成
- 取得したjsonから必要な要素を抽出し、登録用のjsonに加工

#### インポート先のDB情報を取得
- 実行するAPI：[GET /api/database/](https://www.metabase.com/docs/latest/api/database#get-apidatabase)
- API実行例
```bash
curl -X GET \
  -H "Content-Type: application/json" \
  -H "X-Metabase-Session: ${SESSION_ID}" \
  https://${METABASE_DOMAIN}/api/database
```
実行結果からデータベースIDを抽出します。
(実行結果一部抜粋)
```json
{
  "data": [
    {
    ～省略～
      "is_sample": true,
      "id": 1,  ←★この値をメモ
      "is_on_demand": false,
    ～省略～
```

#### インポート先のコレクションを作成
　インポート先Metabaseで保存先のコレクションを作成しておきます。GUIで作成した場合は、コレクションのURL末尾にコレクションIDがあるのでメモしておきます。

　APIで作成する場合は以下を参考にしてください。
- 実行するAPI：[POST /api/collection/](https://www.metabase.com/docs/latest/api/collection#post-apicollection)
- API実行例

```bash
curl -X POST \
  -H "Content-Type: application/json" \
  -H "X-Metabase-Session: ${SESSION_ID}" \
  -d @collection.json \
  https://${METABASE_DOMAIN}/api/collection
```
(データ部のjson例)
```json
{
  "name": "コレクション名",
  "description": "説明",
  "color": "カラーコード（#509EE3等）"
}
```
(レスポンス抜粋)
```json
{
  "authority_level": null,
  "description": "説明",
  ～略～
  "id": 2,　←★この値をメモ
  ～略
}
```


## ダッシュボードを登録する（インポート）
ここまででようやく準備完了です。いよいよダッシュボードを登録します。
カード作成→ダッシュボード作成→ダッシュボードのカード更新の順でAPIを実行します。

#### カード作成
- 実行するAPI：[POST /api/card/](https://www.metabase.com/docs/latest/api/card#post-apicard)
- API実行例
```bash
curl -X POST \
  -H "Content-Type: application/json" \
  -H "X-Metabase-Session: ${SESSION_ID}" \
  -d @card_${CARD_ID}.json \
  https://${METABASE_DOMAIN}/api/card
```

`card_${CARD_ID}.json`はGETで取得したものを使い、`collection_id`の値を保存先コレクションのIDに、`dataset_query.database`の値を接続先データベースのIDに書き換えておきます。他のパラメータはそのまま流用してOKです。ただし、自前のSQLやフィルタ機能等を使ったカードを作っていると、不要なパラメータが入ってしまいPOSTがNGになる場合があります。その時はエラーメッセージに表示されますので、jsonから取り除いてあげてください。解決できない場合は、ブラウザの開発者ツール等で実際にGUIから登録した際のPOSTリクエストのパラメータを覗いて、必要なパラメータが何かを調べると早いです。

API実行が成功すると、カードが対象のコレクションに作成されます。


#### ダッシュボード作成
- 実行するAPI：[POST /api/dashboard/](https://www.metabase.com/docs/latest/api/dashboard#post-apidashboard)
- API実行例

```bash
curl -s -X POST \
  -H "Content-Type: application/json" \
  -H "X-Metabase-Session: ${SESSION_ID}" \
  -d @dashboard.json \
  https://${METABASE_DOMAIN}/api/dashboard
```

`dashboard.json`はGETで取得したものそのまま使用すればOKです。

API実行が成功すると、ダッシュボードが対象のコレクションに作成されます。この段階ではカードの配置はされず、ダッシュボードの箱のみが作成されます。


#### ダッシュボードのカード更新
- 実行するAPI： [POST /api/dashboard/:id/cards](https://www.metabase.com/docs/latest/api/dashboard#put-apidashboardidcards)
- API実行例
```bash
curl -s -X PUT \
  -H "Content-Type: application/json" \
  -H "X-Metabase-Session: ${SESSION_ID}" \
  -d @dashboard_putdata.json \
  https://${METABASE_DOMAIN}/api/dashboard/1/cards
```

`dashboard_putdata.json`は、GETで取得した`dashboard.json`から`ordered_cards`と`orderd_tabs`を抽出し、 `ordered_cards`のキー名を`cards`に変更したものを使用します。以下はサンプルです。
```json
{
  "cards": [
    {
      "size_x": 9,
      "dashboard_tab_id": null,
      "series": [],
      "action_id": null,
      "collection_authority_level": null,
      "card": {
        "description": null,
        ～略～
    }
    ～カードの数だけ繰り返し～
  ],
  "ordered_tabs": []
}
```

API実行が成功すると、カードが前項で作成したダッシュボードに追加されます。

# 終わりに
　ということで、無事、MetabaseのAPIでエクスポート/インポートを実現できました。
今回はテンプレートの配布でしたが、「別環境に再構築したい」「バックアップ取得/復元したい」といったユースケースにも適用できるかと思います。また、紹介したAPI経由以外の方法としては、dockerイメージ化して丸ごと渡したり、オープンソースなので自分でMetabaseに機能を作りこんだり（そんなことする人はいるのか...？）といった手段も選択肢としてはあります。コストや環境の制限から採用しませんでしたが...。本記事が参考になれば幸いです。

※記載されている会社名、製品名、サービス名は、各社の商標または登録商標です。