Metabase Dashboardのexport&import 2024

## TR; DL

Metabaseはフリーで使えるのにも関わらずとても強力なBIツールである。
ローカル環境に構築したMetabaseのDashboardを運用環境に引越したい。しかしいくら探してもExport機能とImport機能についての記事が見当たらない。どうもそういうユーザーにはPro版やEnterprise版を使えということのようだ。
一方Metabase APIでなんとかできるとの記事もあり、それで四苦八苦してなんとかした記録です。

## 前提条件

- Starter & Open Source版を使う（Pro or Enterprise版ではない）
- local開発環境（Mac、Linux & Docker）で開発する
    - Windowsの場合はWSLをインストールする必要あり。Curl文で@によるJSONファイルインポートが使えないので、ファイルの中身を@ファイル名の箇所にコピペする
- AWS等にアップして運用する
    - ここでMetabaseのDashboardのExportとImportをする必要がある
- Metabaseに関して
    - ダッシュボードやその中のカードは全て初期状態で作られる「分析」コレクション（フォルダのようなもの）に入っている

## 手順

### 概要

以下の段取りでひたすらREST APIを叩く作業です。[Postman](https://www.postman.com/)を準備するといいでしょう。

1. ExportするMetabaseの作業
    1. セッションIDの取得
    1. ダッシュボード情報（JSON）の取得とカードIDの取得
    1. カード情報（JSON）の取得
1. カードIDがExport元とImport先で違った場合の対応（必要に応じて）
1. ImportするMetabaseの操作
    1. セッションIDの取得
    1. カードの登録
    1. ダッシュボードの登録

### ExportするMetabaseの作業

#### セッションIDの取得

MetabaseのAPIで操作をするためには最初にログイン情報を用いてセッションIDを取得する必要があります。
以下は、セッションIDを取得する手順です。usernameとpasswordの値にはMetabaseにログイン時に使用するユーザー（メアド）とパスワードを使用してください。

```terminal: input
curl --location 'http://<MetabaseのURL>/api/session' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username": "<Metabase UIで入力するユーザー（メアド）>",
    "password": "<パスワード>"
}' > dashboard.json
```

すると下記のようなUUID v4が出力されます。**IDは例です。実行するたびにランダムに生成されます。**

```terminal: output
{
    "id": "590aa73c-60bb-424e-8d0a-8602b1b7ef4b"
}
```

このidをセッションIDとしてExportする側のMetabaseへのHTTP Headerに設定してリクエストを投げます。

#### ダッシュボード情報（JSON）の取得とカードIDの取得

ダッシュボード情報はコピー先でダッシュボードを復元させるために必要な情報です。
またこちらにはダッシュボードを構成するカードの情報も含まれているので、こちらを先に取得します。
**この中に含まれるカード情報は使いません。**

ダッシュボードの情報を取得するにあたり、ダッシュボードのIDを調べておく必要があります。IDを調べるためには必したいダッシュボードのリンクの上にマウスポインタをホバーさせると飛び先URLが表示されます。（Google Chromeの場合は左下に表示されます。）/dashboard/のすぐ後ろの数値がダッシュボードIDになります。

![Screenshot 2024-05-02 at 20.59.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13252/610099c6-6c1a-791d-2b5a-05a60215c189.png)

以下はダッシュボードIDが1のダッシュボード情報の取得方法になります。文末のリダイレクト（>）は後ほどこのデータを引越し先で取り込む時にファイルでデータを指定するために保存するための命令です。

```terminal: input
curl --location 'http://<MetabaseのURL>/api/dashboard/1' \
--header 'Content-Type: application/json' \
--header 'X-Metabase-Session: <<ここに先ほどのIDで置き換える>>'
```

```terminal: output
{
    "description": "A look at events over time and by several categories.",
    "archived": false,
    "collection_position": 1,
    "dashcards": [
        {
            ・・・中略・・・
            "card": {
            ・・・中略・・・
                "id": 1 ←これがカードID
            ・・・以下、略・・・
```

```root->dashcards->card```に```id```が含まれない場合は無視して大丈夫ですので、このidの値を控えておいてください。他にも```card_id```等紛らわしいものがありますが、ここでは併せて無視して大丈夫です。

#### カード情報（JSON）の取得

カード情報も引越しに必要な情報です。ダッシュボード情報に似たようなものが含まれているために必要なさそうですが、1カードずつデータを保存していきましょう。以下はカード情報の取得方法になります。```{id}```という箇所が2箇所あるので、それを先ほど取得したカードIDで置き換えてください。カードの枚数だけこれを繰り返します。文末のリダイレクト（>）は後ほどこのデータを引越し先で取り込む時にファイルでデータを指定するために保存するための命令です。

```terminal: input
curl --location 'http://<MetabaseのURL>/api/card/{id}' \
--header 'Content-Type: application/json' \
--header 'X-Metabase-Session: <<ここに先ほどのIDで置き換える>>'
> card_{id}.json
```

```terminal: output
{
    "description": "A look at events over time and by several categories.",
    "archived": false,
    "collection_position": 1,
    "dashcards": [
        {
            ・・・中略・・・
            "card": {
            ・・・中略・・・
                "id": 
            ・・・以下、略・・・
```
### ImportするMetabaseの操作
#### セッションIDの取得

この手順はExportするMetabaseの操作と同じですので説明は省略します。

```terminal: input
curl --location 'http://<MetabaseのURL>/api/session' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username": "<Metabase UIで入力するユーザー（メアド）>",
    "password": "<パスワード>"
}' > dashboard.json
```

```terminal: output
{
    "id": "590aa73c-60bb-424e-8d0a-8602b1b7ef4b"
}
```

#### カードの登録

設定をインポートするときはダッシュボードよりカードを先にインポートします。

```terminal: input
curl --location 'http://localhost:3000/api/card' \
--header 'Content-Type: application/json' \
--header 'X-Metabase-Session: <<ここに先ほどのIDで置き換える>>' \
--data-raw '@card_{id}.json'
```

すると登録されたカード情報が出力されます。
```terminal: output
{
    "description": null,
    "archived": false,
    "collection_position": null,
    "table_id": 10,
    ・・・中略・・・
    "id": 2,　←これがカードID
    ・・・以下、省略・・・
}
```

このとき注意してほしいのが、必ずしもインプットデータとして使用したJSONに記載されたIDが再利用されるわけではないということです。それを確認するためには、上記output例で示したカードIDを確認してください。これが元々のカードのIDと違う場合は、ダッシュボード情報（dashboard.jsonに保存したデータ）を修正する必要があります。（結構面倒臭い。）おそらく全てのカード情報において新旧のマッピングをする必要があります。
#### カードIDがExport元とImport先で違った場合の対応

dashboard.jsonを開き、次の項目を古いカードIDから新しいカードIDに修正します。地味に面倒くさいです。

```
root
- dashcards
    - card
        - parameter_mappings
            - card_id ← ここ（parameter_mappingsが配列なので複数箇所）
``` 
   
#### ダッシュボードの登録

ダッシュボードの情報の登録は、ダッシュボード本体というより外側とフィルターなどの登録になります。Export元から取得したダッシュボード情報がそのまま使えますので、以下のコマンドを実行してください。

```terminal: input
curl --location --request POST 'http://localhost:3000/api/dashboard' \
--header 'X-Metabase-Session: <<ここに先ほどのIDで置き換える>>' \
--header 'Content-Type: application/json' \
--data-raw '@dashboard.json'
```

```terminal: output
{
    "description": "xxxxxxxxxxxx",
    "archived": false,
    "collection_position": 1,
    "dashcards": [], 　←dashcardsはこの時点では登録されていないことがわかる
    ・・・中略・・・
    "id": 1,　←これがダッシュボードID
    ・・・以下、省略・・・
}
```

このダッシュボードIDは次の手順で必要になるので控えておいてください。

```terminal: input
curl --location --request PUT 'http://localhost:3000/api/dashboard/{ダッシュボードID}' \
--header 'X-Metabase-Session: <<ここに先ほどのIDで置き換える>>' \
--header 'Content-Type: application/json' \
--data-raw '@dashboard.json'
```

```terminal: output
{
    "description": "xxxxxxxxxxxx",
    "archived": false,
    "collection_position": 1,
    "dashcards": [ 　←今度はdashcardsが登録されていることがわかる
        {
    ・・・以下、省略・・・
}
```

以上になります。

## 楽したい人向けPython Script

弊社のGitHubリポジトリにこれら一連の作業をするPythonスクリプトを公開しています。

https://github.com/Universal-Micro-Networks/apigw-log-visualizer/blob/develop/for_develop/metabase/util/copy_dashboard_settings.py

## 作業中はまったこと

### Metabase APIの仕様変更

Metabaseのバージョンが48になる時、ダッシュボード情報に含まれるordered_card属性の名前が、dashcard属性になりました。参考にした資料ではまだ古いままだったのでそのキーをJSONの中で探すもなかなか出てこず、公式を読んだらそうだとのことでした。やはり公式が最強である。

### Cardを作るAPIを叩く時に間違えてJSONにPython辞書をぶち込んだ

そんなことある？と言われるかもしれないが、楽したい人向けPython Scriptを作っていたときに遭遇しました。
APIのレスポンスとしてHTTP Status 400、下記のようなエラーメッセージが返ってきました。

```json
{"errors":{"visualization_settings":"Valueはマップである必要があります。"},"specific-errors":{"visualization_settings":["Valueはマップである必要があります。, しょき: nil"]}}
```

ちょっと何を言ってるのかわからず、ググっても公式見ても碌なヒントが見つかりませんでした。眠たい時にこういう作業をしてはいけませんね。（という教訓と言い訳です。）

## 参考資料

https://qiita.com/comware_kumagai/items/a82483ab9ed1f5d22188