LIVESENSE ENGINEER BLOG

リブセンスエンジニアの活動や注目していることを発信しています

Redashのデータをスプレッドシートに定期的に出力する内製WebアプリとそのGradio化

概要

技術部データプラットフォームグループでデータエンジニアをしている富士谷です。

リブセンスでは、Redashがよく使われています。 接続先はデータプラットフォームグループで管理しているRedshiftや、プロダクトのデータベース*1が中心です。 一方で、より柔軟な分析などのため、Google スプレッドシートもよく使われています。

業務によっては「定期的にクエリを実行し、取得したデータをスプレッドシートに書き込みたい」というニーズがあり、これを実現するために、Webアプリを内製しています。 このWebアプリは、Redashからスプレッドシートにデータをエクスポートするものなので「redash2ss」と呼んでいます。

このシステムとそのGradio化について紹介します。

redash2ssについて

redash2ssの歴史は長く、何度かリプレイスしています。

GASなどを使っても類似のものはできると思いますが、Webアプリとして提供することで個々人が開発するより手軽です。 このような背景から、社内にはそれなりの数の利用者がいます。 今は2022年頃に私が作ったシステムをベースに稼働しているので、まずはこのアーキテクチャを簡単に紹介します。

redash2ss(2022年版)

データプラットフォームグループで利用する技術を揃えて開発効率を高めることを主眼に、利用方法や要件を整理して、Dockerfile1つで動くようにするなど、よりシンプルな構成に変更しました。 大まかなアーキテクチャは以下のとおりです。CDなどは省略しています。

redash2ss(2022年版)のアーキテクチャ

redash2ssを利用するとき、利用者はあらかじめRedashでクエリの定期実行の設定を行っておきます。なお、社内のRedashの多くは技術部インフラグループのAWSで管理されています。 利用者はredash2ssにアクセスし、Google OAuth2.0のリフレッシュトークンやスケジュールの設定(実行開始時刻、読み込み元RedashクエリURL、書き込み先スプレッドシートなど)を行います。 これらの情報はCloud SQLに保存されます。

定期実行の際には、Argo WorkflowsのCron Workflowによりワークフローを起動し、Cloud SQLの情報を元にRedashからデータを読み出し、スプレッドシートに定期的に書き込んでいます。 実装では、Redash APIスプレッドシートのAPIを使っています。

画面は、ユーザ管理、スケジュール一覧、スケジュール設定の主に3つです。 TypeScript、React、MUIなどを使って実装していました。

redash2ss(2022年版)のスケジュール画面

課題とGradio化のモチベーション

リプレイスでシンプルな構成にはなったのですが、フロントエンドの実装には課題がありました。 私を含めデータプラットフォームグループのメンバーの多くはデータエンジニアであり、普段はPythonを利用しています。 チームにはTypeScriptやReactを扱える人もいますが、その習熟やアップデートを追いかけるのは大変で、どうしても属人化してしまいます。 加えて、内部ではcreate-react-appを利用していましたが、2022年頃から更新がほぼ止まってしましました。 Viteに変える事も考えましたが、もっと簡単に実装したいと考えていました。

そんな中、最近は、Gradioをはじめとして、StreamlitDashなど、Pythonでフロントエンドを扱う方法が増えています。 チームメンバーの何人かが個人的に試してみたところ、乗り換えられそう、TypeScriptやReactより楽そう、ということがわかり、Gradio に移行することにしました。

実装、検証部分はチームメンバーの田中さんに行っていただきました。以下の記事の内容は田中さんにも協力いただきました。

Gradioの選定理由と工夫、大変だったこと

Gradioは、StreamlitやDashと比べて「将来性」と「FastAPIとの連携」に強みを感じたため、選定しました。

機械学習アプリケーションのデモアプリの開発でよく利用されており、生成AIをローカルで動かしてみたことがあれば、一度は目にしたことがあるかもしれません。 最近、Gradio 5が出るなど、アクティブに開発されており、今後の改善にも期待できます。

他にも、GradioはFastAPIと連携がしやすくmountを使うと既存の API エンドポイントと共存できるため、従来の実装を極力壊さずに段階的な移行ができます。 また、RequestもFastAPIと共通なので必要に応じて、HTTPヘッダやURL のクエリパラメータを扱うことできます。

工夫

Gradioに移植するにあたり、いくつか工夫した点を紹介します。 実装では、コンポーネントのレイアウトとイベントハンドラの実装、コンポーネントへのイベントハンドラの紐付けがなるべくわかりやすいようコーディングしました。 実装例は以下のとおりです。

# イベントハンドラの実装
def handle_page_load():
    schedule_id = request.path_params["id"]
    schedule = get_schedule(schedule_id)
    return [schedule.name, schedule.description]


def handle_submit_click(name: str, description: str):
    schedule_id = request.path_params["id"]
    schedule = update_schedule(schedule_id, name, description)
    return [schedule.name, schedule.description]


def handle_delete_click():
    schedule_id = request.path_params["id"]
    delete_schedule(schedule_id)
    # 削除後はボタンを無効化する
    return [
        gr.Button(value="更新", interactive=False),
        gr.Button(value="削除", interactive=False)
    ]


with gr.Blocks(title="スケジュール編集画面",) as page:
    # コンポーネントのレイアウト
    name = gr.Textbox(label="名前")
    description = gr.Textbox(label="説明")
    with gr.Row():
        with gr.Column(scale=1):
            submit = gr.Button(value="更新")
        with gr.Column(scale=1):
            delete = gr.Button(value="削除")

    # コンポーネントへのイベントハンドラの紐付け
    page.load(
        fn=handle_page_load,
        inputs=None,
        outputs=[name, description],
    )
    submit.click(
        fn=handle_submit_click,
        inputs=[name, description],
        outputs=[name, description],
    )
    delete.click(
        fn=handle_delete_click,
        inputs=None,
        outputs=[submit, delete],
    )

また、「スケジュールの削除」などで利用する確認ダイアログは、Gradioでは用意されていなかったため、代わりに、実行ボタンとその可視性を制御するボタンの2つのアクションに分けて実装しました。

def handle_delete_click():
    gr.Warning(message="スケジュールを削除すると元に戻せません。本当に削除する場合は「削除を実行」をクリックしてください。")
    delete = gr.Button(value="削除", visible=False)
    do_delete = gr.Button(value="削除を実行", visible=True)
    return [delete, do_delete]


def handle_do_delete_click(request: gr.Request):
    ...


with gr.Blocks(title="スケジュール編集画面",) as page:
    ...
    delete = gr.Button(value="削除")
    do_delete = gr.Button(value="削除を実行", visible=False)

    delete.click(
        fn=_handle_delete_click,
        inputs=None,
        outputs=[delete, do_delete],
    )
    delete.click(
        fn=_handle_do_delete_click,
        ...
    )

他にも、スケジュール一覧を表示する画面において、個別スケジュールへのリンクはDataFrameとmarkdownの組み合わせで実装しています。 Gradioではpolarsも使えます。

import polars as pl

...
df = (
    pl.from_dicts(schedules, schema=dict(id=pl.Int64, name=pl.String))
    .select([
        pl.format("[{}](/schedule/{}/)", pl.col("name"), pl.col("id")).alias("名前"),
    ])
)
gr.DataFrame(value=df, datatype="markdown")

redash2ss(2024年版)のスケジュール一覧画面

大変だったこと

Gradioで従来の機能をほとんど実現できたものの、大変なところもいくつかありました。

  • コンポーネントのinput/outputとして複雑な構造体を扱うのが難しい
    • 複数のフィールドからなるフォームの送信データをdata classで受け取ったりしにくい
  • レイアウトが基本的なものだけで、Navbar, Sidebar, Breadcrumbsなどはない
  • 細かいスタイルの調整がしにくい
    • フォームのdescriptionやplaceholderなどはraw textでしか指定できない

いずれも致命的な部分ではありませんでしたが、Gradio 5ではマルチページへの対応や、Sidebarの追加の予定もあるようなので、将来的な改善に期待したいと思います。

結果

9月にUIの切り替えを行いましたが、特に利用者の混乱もなく行えました。改変後のアーキテクチャは以下のとおりです。 画面の見た目のスタイリッシュさは少し減ったかもしれませんが、機能性は変わらず、Pythonだけで済むので開発効率は飛躍的に向上したと考えています。

redash2ss(2024年版)のアーキテクチャ

redash2ss(2024年版)のスケジュール

今後

データプラットフォームグループには、TypeScriptとReactを使っているシステムもまだあります。 今後新しく作りたいWebアプリもいくつかあります。Gradioをいろんな場面で利用していこうと考えています。

一方で、redash2ssが気軽に利用できるがゆえに、本来はスプレッドシートを使わずに専用のシステムを組んだほうがよさそうな業務も、スプレッドシートで行われていることがあります。 Redashも便利ですが、管理面やより柔軟な可視化など、あまり得意でないところもあります。 便利ツールの開発など目の前の業務改善を行いつつも、より長期的な視点を持って技術選定を行い、引き続き、データに関わる業務そのものを改善していこうと考えています。

*1:個人情報にアクセスできないようにマスキングなどを行ったもの