cli-kintoneでCloudStorageからkintoneアプリにリストア・おまけのBIツール（LookerStudio）連携 (2/2)

この記事は、[kintoneアドベントカレンダー2022](https://qiita.com/advent-calendar/2022/kintone)の記事です。

### はじめに 
[前編](https://qiita.com/hiroki_kaneko/items/57434f41922030b85901)ではカーネマン社の金子さんにcli-kintoneを利用してkintoneアプリをCloudStorageとcloudsqlに自動バックアップする関数を紹介してもらいました。

https://qiita.com/hiroki_kaneko/items/57434f41922030b85901

後編では、そのバックアップしたデータをkintoneに戻す関数の実装を紹介しています。
おまけ編では、Google製の無償BIツール[Looker Studio](https://cloud.google.com/looker-studio?hl=ja)を利用したBIツール連携に挑戦してみます。

### Cloud Storageからkintoneへデータを復帰させる
#### 構成図
試験実装のため、関数をキックするのはユーザーの操作からではなくTerminalからHTTP経由で行います。
そのため現状では処理の起点はCloud Functionsからということになります。
※本来はバックアップ一覧からリストアするファイルをユーザーに選択させるべきですが、今回はCloud StorageにバックアップしたCSVファイルをリストアできることを確認することが最優先だったため、最新のバックアップを戻す実装にしています。
![clikinton構成図.003.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3060358/a4333bdb-90f4-f4f1-15d4-c84f81ed4e34.jpeg)

#### プロジェクト構成
プロジェクト構成は先に紹介した `clikinton-py-gcp` と同じです。

```
import-to-kintone
├── README.md
├── .env.yaml // 環境変数
├── cli-kintone-linux
│   ├── LICENSE
│   ├── NOTICE
│   └── cli-kintone
├── deploy
│   ├── cli-kintone
│   ├── main.py
│   └── requirements.txt
├── deploy.sh
├── main.py
└── requirements.txt

```
#### コード
このコードでは、Cloud Storageに保存されたCSVファイルをcli-kintoneを使ってkintoneアプリにリストアします。本来でしたらkintoneプラグインを作って、Cloud Storageに保存されているバックアップのリストを表示して、ユーザーにリストアさせるバージョンを指定させるような画面を作ると便利なのですが、今回は一時的な仕様として「バックアップファイルのうち最新のものをリストアする」実装になっています。
また、cli-kintoneでデータをリストアさせる際、ソースファイルはローカルのパスで指定しなければならないため、Cloud Storage内のファイルを読み出してから一時ファイル `tmp.csv` を作成してそれを指定しています。この一時ファイルは毎回新しいデータで内容を上書きしているため、削除はしません。

``` main.py
import io

import functions_framework
import os
import subprocess
from google.cloud import storage

@functions_framework.http
def import_to_kintone(request):
    # 環境変数を読み込む
    domain = os.environ.get('DOMAIN')
    app_id = os.environ.get('APPID')
    token = os.environ.get('TOKEN')
    bucket_name = os.environ.get('BUCKET')

    storage_client = storage.Client()
    file_list = list_backups(storage_client, bucket_name)

    bucket_name = storage_client.get_bucket(bucket_name)
    # TODO ユーザーがkintoneプラグインでバックアップファイルを選択できるようになるまでは最新バックアップを自動的に指定する
    file_name = file_list[-1]
    blob = bucket_name.blob(file_name)
    buffer = blob.download_as_string()
    # ファイルを削除すること
    with open('./tmp.csv', 'w', encoding='utf-8') as f:
        f.write(buffer.decode('utf-8'))

    args = f"record import --base-url https://{domain}.cybozu.com --app {app_id} --api-token {token}" \
           f" --file-path {'./tmp.csv'}"
    res = subprocess.run([f"./cli-kintone {args}"], shell=True, encoding='utf-8', capture_output=True, text=True)

    return 'success'


# 指定バケット内のファイルリストを取得
def list_backups(storage_client, bucket_name):
    files = storage_client.list_blobs(bucket_name)
    file_list = []
    for file in files:
        file_list.append(file.name)
    return file_list
```
Cloud Storageに保存されていたCSVデータがkintoneにリストアされました。
レコード番号の重複を避けるため、CSVデータがCloud Storageに保存される前の処理でレコード番号カラムは削除されています。下記スクリーンショットのレコード番号は、データのリストア時に新たに採番されたものです。
![スクリーンショット 2022-12-18 10.38.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3060358/515931eb-1c82-993a-f3e8-fa98f513e69c.png)

### まとめ
上記の試験実装では、バックアップしたCSVファイルから元のkintoneアプリにリストアすることができました。このように、 cli-kintoneを使用すると簡単にバックアップが取れる他、その作業をクラウドサービス上に構築してしまえば、バックアップ作業を自動化できることがわかりました。

### BIツール連携（おまけ）
Cloud StorageにkintoneデータをCSVファイルとして保存できれば、これをデータソースとして[Looker Studio](https://cloud.google.com/looker-studio?hl=ja)を使って簡単なBIツール連携を実現できます。
![レポート画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073813/3bebeabe-775c-bd23-2ad7-d49bae7f3a08.png)

#### 構成図
今回はkintoneのwebhook機能を利用して、レコードの追加/更新/削除のタイミングでCSVファイルを更新して、レポートに反映されるようにしてみます。
![BI構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073813/09b32d4d-084b-686c-e304-5ebcb677f7de.png)

#### コード
CSV出力するプログラムをHTTPトリガーとしてCloud Functionsにデプロイします。[前編](https://qiita.com/hiroki_kaneko/items/57434f41922030b85901)の`main.py`を以下のように書き換えました。
```main.py
import base64
import subprocess
import os
import datetime
import io
import pandas
from google.cloud import storage


# Triggered from a message on a Cloud Pub/Sub topic.
# @functions_framework.cloud_event
def entry_point_name(request):
    # Print out the data from Pub/Sub, to prove that it worked
    # print(base64.b64decode(cloud_event.data["message"]["data"]))

    domain = os.environ.get('DOMAIN')
    app_id = os.environ.get('APPID')
    token = os.environ.get('TOKEN')
    bucket_name = os.environ.get('BUCKET')

    args = f"record export --base-url https://{domain}.cybozu.com --app {app_id} --api-token {token}"
    res = subprocess.run([f"./cli-kintone {args}"], shell=True, encoding='utf-8', capture_output=True, text=True)
    # TODO "レコード番号"カラムを削除するがRDBにinsertする際もそれでよいか？
    record_num_deleted = delete_record_number(res.stdout)
    # とりあえず時間帯はGMT+9=JST決め打ち
    t_delta = datetime.timedelta(hours=9)
    jst = datetime.timezone(t_delta, 'JST')
    now = datetime.datetime.now(jst)
    fmt = now.strftime('%Y%m%d%H%M%S')
    # googlecloudapi ではCloudSQLへデータをインポートする際にヘッダ行を無視するオプションが存在しないため
    # CloudSQLへimportするためのヘッダなしCSVを保存する（再利用しないので後で消すこと）
    file_name_headless = f"{domain}/{app_id}/{fmt}_headless.csv"
    csv = '\n'.join(record_num_deleted.splitlines()[1:])
    upload_csv(csv, bucket_name, file_name_headless)
    # データをKintoneへimportする際はヘッダ付きCSVファイルを利用する
    file_name = f"{domain}/{app_id}/backup.csv"
    upload_csv(record_num_deleted, bucket_name, file_name)
    # TODO file_name_headless を指定するのではないか？
    # import_csv_to_rdb(bucket_name, file_name)
    # ...headless.csv は不要になったので削除する
    delete_csv(bucket_name, file_name_headless)

    request_json = request.get_json()
    if request.args and 'message' in request.args:
        return request.args.get('message')
    elif request_json and 'message' in request_json:
        return request_json['message']
    else:
        return f'Uploaded CSV completed.'


# CSVデータから"レコード番号"カラムを削除して返す
def delete_record_number(csv_buffer):
    df = pandas.read_csv(io.StringIO(csv_buffer))
    # Kintoneのデフォルト名が"レコード番号"のため
    res = df.drop(['レコード番号'], axis=1)
    buffer = io.StringIO()
    res.to_csv(buffer, index=False)
    return buffer.getvalue()


# 指定したファイルを削除する
def delete_csv(bucket_name, blob_name):
    storage_client = storage.Client()
    bucket = storage_client.bucket(bucket_name)
    blob = bucket.blob(blob_name)
    blob.delete()


# Cloud Storageにファイルをアップロードする
def upload_csv(csv, bucket_name, file_name):
    storage_client = storage.Client()
    bucket_name = storage_client.get_bucket(bucket_name)
    blob = bucket_name.blob(file_name)
    # TODO byte convertエラーが出るためstr()をしているがこの方法は新しい変数を生成して返しているため無駄が多い
    blob.upload_from_string(data=str(csv), content_type='text/csv')


# import_csv_to_rdb()は利用しないので省略
```
:::note info
ポイント
・entry_point_name()の引数をrequestに変える
・CSVファイル名は固定（`backup.csv`）にする
・import_csv_to_rdb()の呼出しをコメントアウトする
:::
#### デプロイスクリプト
[前編](https://qiita.com/hiroki_kaneko/items/57434f41922030b85901)の`deploy.sh`を以下のように書き換えました。
``` deploy.sh
#!/bin/sh

cp main.py deploy/main.py
cp requirements.txt deploy/
cp .env.yaml deploy/
cp cli-kintone-linux/cli-kintone deploy/
cd deploy

gcloud functions deploy  insert-to-rdbms-http \
--region=asia-northeast1 \
--runtime=python310 \
--entry-point=entry_point_name \
--env-vars-file=.env.yaml \
--trigger-http
```
:::note info
ポイント
・HTTPトリガー、第1世代関数としてデプロイする
:::

#### HTTPトリガー
下記スクリーンショットはGCPでHTTP関数としてデプロイされた状態です。
試しにトリガーURLをクリックすると画面に「Uploaded CSV completed.」と表示されれば成功です。「Error: Forbidden」と表示される場合は権限設定で「allUsers」に「Cloud Functions 起動元」のロールを割当てます。詳しい設定方法は[公式ドキュメントのこのあたり](https://cloud.google.com/functions/docs/securing/managing-access-iam#after_deployment)が参考になります。
![Cloud Functions http_trigger.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073813/b9118597-7f52-b9b0-93d9-2eac86fb8143.png)
成功すると指定バケットに`backup.csv`が出力されます。次の設定で利用するため、CSVファイルのパスをクリップボードにコピーしておきましょう。
![クリップボード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073813/17ba590d-94f9-8dd8-c3f6-cbccdf08a0aa.png)

#### Looker Studio設定
次に`backup.csv`をLooker Studioのデータソースに指定します。
[Google Cloud Storage に接続する](https://support.google.com/looker-studio/answer/7511998?hl=ja&ref_topic=7332343)を参考に設定を進めます。
>1. Looker Studio にログインします。
>2. 左上にある 作成 をクリックして、[データソース] を選択します。
>3. リストから、[Google Cloud Storage コネクタ] を選択します。
>4. 承認を求めるメッセージが表示された場合は、[承認] をクリックします。
>5. データへのパスを入力します。
>6. 右上の [接続] をクリックします。

5の「データへのパス」は先程クリップボードにコピーしたCSVファイルのパスを貼り付けます。パスを入力すると右上の「接続」が有効になるので押します。
![looker studio データソース接続.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073813/f29049e4-5cf1-4744-c356-5923a127cf3d.png)
 これで`backup.csv`のフィールド（項目）が取り込まれました。
このまま「レポートを作成」を押します。
※レポートに追加するか確認されるので「レポートに追加」を押します。
![looker studio フィールド設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073813/d6ea4334-f45d-8279-ce9e-306b4dab0ef4.png)

レポート画面になり、左にCSVデータが取り込まれています。
右の「グラフ」を押します。
![データソース作成⑤.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073813/280666b7-9c3a-4a05-9473-b17f9b45cfa5.png)
表の中から円グラフを選択すると、追加したデータが円グラフに変わります。右上の「表示」を押すとそのままレポートが作成されます。詳しい設定方法は[レポートのチュートリアル](https://support.google.com/looker-studio/answer/6292570)を参照してください。
![データソース作成⑥.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073813/b331e104-fc7c-e95b-6117-34507a1c4344.png)

#### Webhook設定
最後にkintoneでWebhookの設定を行います。
「Webhook URL」の部分に先程のHTTPトリガーURLを設定し、「通知を送信する条件」でレコード追加、編集、削除にチェックをつけて保存します。これで設定は完了です。
![kintoneのWebhook設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073813/db0dc526-b0a4-72d3-8916-625a36eb801f.png)
#### 動作確認
kintoneのレコードの追加/更新/削除のタイミングでCSVファイルが更新されてレポートに反映されます。レポートに反映される更新頻度は1分単位で設定できます。詳しくは[公式ヘルプのこちら](https://support.google.com/looker-studio/answer/7020039?hl=ja&ref_topic=6370331#set-data-freshness-for-a-data-source)をご確認ください。レポートの閲覧者が[手動で更新](https://support.google.com/looker-studio/answer/7020039?hl=JA#refresh-report-data-manually)することもできます。

### 最後に
今回の主な目的は既存DBにkintoneのデータを挿入する手段を探ることでした。その過程でcli-kintoneを活用すると随時バックアップやBIツール連携も行えることがわかりました。kintoneのCSV連携はとても奥が深いです。次はAPI連携に挑戦してみます。

続きの記事
[Kintoneのデータをクラウドからバックアップ→リストアする処理を作りました！](https://note.com/vips_kintone/n/n6e026e9ec148)