0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブラウザで DuckDB が動く BI as Code ツール Evidence: その仕組みと運用を整理する

0
Posted at

はじめに

Evidence は、SQL と Markdown だけでダッシュボードを組み立てられる BI as Code のフレームワークです。

仕組みは少し変わっています。ブラウザの中で DuckDB を動かし、SQL がそこで実行されます。ビルド成果物は HTML + JavaScript + Parquet の静的コンテンツで、サーバプロセスを持たずに静的配信だけで動きます。

ただし、データが丸ごとブラウザに渡る構造なので、ホスティング設計には固有の考え方が必要になります。

本記事では、ブラウザの中で DuckDB が動く仕組みと、その前提でのセキュアなホスティング設計を、サンプルリポジトリおよびサンプルダッシュボードを題材に整理します。

こんな方におすすめ

  • 社内ダッシュボードをコードで管理する選択肢を探している方
  • AI エージェントにダッシュボード構築を任せたい方
  • Evidence を動かしてみて、その仕組みや運用設計まで把握したい方

BI as Code とは何か

BI as Code は、データ・モデル・可視化のすべてをコードで管理するアプローチです。SQL / YAML / Markdown / 設定ファイルで定義を完結させ、CI/CD でデプロイします。

UI 操作主導の従来型 BI には根深い問題があります。

  • 編集しても差分も変更履歴も追いづらい (UI の中だけで完結)
  • 変更内容をレビューする手段がほぼない
  • 本番に出す前にステークホルダーに見せて確認してもらう場を用意しづらい
  • 不具合が出ても前の状態に戻すのが一苦労
  • 結果、「触ると壊れそうだから放置」が広がる

ダッシュボードは作って終わりではなく、改修や拡張が日常的に発生します。BI as Code はそれを Git の運用ループに乗せます。PR で差分をレビューして、ブランチごとに自動で立ち上がる検証環境でステークホルダーが見た目を確認、main にマージすれば本番が自動更新、git revert 一発で戻せる。アプリケーション開発と同じ運用習慣を BI に持ち込めます。

データ整備側の dbt もコード + Git で動いており、BI as Code と組み合わせれば raw → marts → ダッシュボード を同じリポジトリ・同じ PR レビューに乗せられます。SQL から Markdown、PR、デプロイまで一連で扱えるので、AI エージェントとの相性も良い。

Evidence もこの流れの上にあります。次節からは、Evidence の特徴である「ブラウザの中で SQL を実行する」仕組みを見ていきます。

Evidence とは

Evidence は SvelteKit (Svelte ベースのフルスタック Web フレームワーク) ベースの静的サイトジェネレータで、Markdown ファイルにクエリ結果や可視化コンポーネントを埋め込むだけでダッシュボードを組み立てられます。生成物は静的サイト (HTML + JavaScript + Parquet) で、サーバプロセスを持ちません。

その動作モデルが特徴的です。

  • ビルド時: ソース (CSV / DWH の SELECT 結果) を Parquet に変換して同梱
  • 閲覧時: ブラウザに DuckDB の WebAssembly 版 (DuckDB WASM、詳細は後述) が読み込まれ、Parquet を参照して SQL を実行

つまり「クエリエンジンがブラウザの中にある」構造です。これによりサーバ往復なしのインタラクションが成立します。仕組みの詳細は次節以降で見ていきます。

加えて、Evidence の中では DuckDB がデータソースの抽象化層としても効いています。ビルド時、CSV / PostgreSQL / BigQuery / Snowflake 等のソースアダプタが何であっても、最終的な SQL の実行エンジンは DuckDB に統一されます。

開発者は同じ SQL を書くだけで済み、ソースを後から差し替えても (BigQuery → Snowflake に乗り換える等) クエリやページの書き直しは不要です。「ソースは差し替えられる、クエリは資産として残る」という構造は、データ基盤を入れ替えながら長く使い続けたい組織には地味に効きます。

そしてもう一つ、Evidence は「すべてがテキストで完結する」という性質を最大限に活かしたツールです。AI エージェントとの相性が良いのもこの構造ゆえです。

レイヤー 管理対象 主な選択肢
レポート ページ・チャート Markdown
データ クエリ SQL
品質 Lint / Format SQLFluff (SQL)、Prettier (Markdown) 等
インフラ ホスティング Terraform 等の IaC ツール
CI/CD ビルド・デプロイ GitHub Actions、CircleCI 等

GUI 操作が一切不要で、リポジトリの中身だけで BI の全レイヤーが完結する。Claude Code のような AI コーディングエージェントに「月別売上の推移と前年比較を表示して」と頼めば、SQL → Markdown → Terraform → GitHub Actions まで一連で生成・修正してもらえます。従来の UI BI ではこれが圧倒的に難しく、画面操作を再現する RPA 的なアプローチに落ちるしかなかった領域です。

ちなみに雑に「いい感じのダッシュボードを作って」と指示を出しても超低品質なダッシュボードが出てきたので、ダッシュボード設計の領域はまだ楽はできないようです(苦笑)

サンプルダッシュボードに触ってみる

仕組みの解説に入る前に、まず触ってみてください。デプロイ済の URL を用意しています。

ページにアクセスすると、DuckDB WASM と Parquet ファイルを取得しているのがわかります。

image.png

ページ間を移動したりテーブルをソートしたりしても、ネットワーク往復なしで反映されます。これが次節からの仕組み編で扱う中身です。本記事の論点はすべて「触って見えた挙動の解明」として読めます。

ローカルで動かしたい場合の手順、ディレクトリ構成、SQL の置き方、データソースの詳細はサンプルリポジトリの dashboard/README.md に書いてあります。記事では仕組みと運用に集中します。

Evidence の仕組み: DuckDB と WebAssembly がすべて

ここから「なぜそう動くのか」を解いていきます。Evidence の挙動はビルド時と閲覧時の 2 段階に分かれており、それぞれに DuckDB が登場します。

動作モデルの俯瞰

Evidence の動作はビルド時と閲覧時の 2 段階に分かれます。境界がどこにあるかを最初に押さえると、後続の節 (Parquet がなぜ選ばれたか、WebAssembly がどう絡むか) が立体的に読めます。

ビルド時は、各データソース (CSV、BigQuery、Snowflake 等) からデータを取得し、DuckDB Node.js を介して Parquet に変換します。Parquet は列指向の圧縮フォーマットです。evidence sources コマンドが走ると、結果が dashboard/build/data/<source>/<table>/<hash>/<table>.parquet のような構造で書き出されます。<hash> 部分はソースの内容ハッシュで、incremental refresh のキーとしても使われます。サンプルリポジトリで npm run sources && ls dashboard/build/data/retail/ を実行すると、CSV と 1:1 対応する Parquet が並んでいるのが見えます。

サイズ感も実際に見ておくと挙動が掴みやすいです。サンプルリポジトリの 100 本ノック CSV (約 10 万行の POS 明細を含む計 6 ファイル) は元 CSV 合計が約 22MB ですが、Parquet 化すると 4MB 程度まで縮みます。列指向 + 圧縮の効果で 1/5〜1/10 になるのは普通の挙動です。これがブラウザに送られるデータの実体です。

閲覧時は、ブラウザがビルド成果物を取りに行きます。最初にロードされるのは HTML と JavaScript バンドル、続いて DuckDB WASM (33〜38MB のバイナリ)、それから対応する Parquet ファイルです。これがブラウザの中で組み上がると、Markdown に書かれた SQL がブラウザ内で実行される、という状態になります。サーバプロセスは介在しません。CDN や GitHub Pages のような静的ホスティングだけで動くのはこの構造のためです。

ここで重要なのは「ダッシュボード側の集計やフィルタはビルド時に固定しない」ことです。Parquet に入るのはソース SQL の粒度で決まるデータで、チャート単位の最終的な集計やフィルタはブラウザで実行されます。これによりインタラクション (フィルタ・ドリルダウン) が再ビルドなしに成立する、というのが Evidence の特徴の核心です。

これは「ブラウザ DuckDB が現実的に速い」という前提があってこそ成立する設計判断です。ソース処理の結果 (CSV ならファイルそのまま、DWH ならソース SQL の結果セット) を Parquet として丸ごとブラウザに配布し、ダッシュボード側の集計やフィルタはすべてクライアント側に寄せる、という割り切り方です。次節以降ではその前提が何に支えられているかを見ていきます。

ソース処理パイプライン: CSV と DWH

Evidence のソース処理は、データの種類によって挙動が違います。CSV と DWH で何が違うかを表にまとめると次のようになります。

観点 CSV ソース DWH ソース
入力 ファイル (sources/<source>/*.csv) DWH 接続 + SQL (sources/<source>/<query>.sql)
変換時の処理 1:1 のフォーマット変換 (SQL 介在なし) DWH 側で SQL を実行、結果セットを Parquet 化
粒度の決定 ファイル粒度に固定 ソース SQL の粒度で自由に決められる
データ最小化の場所 ファイル作成時 (Evidence の枠外) ソース SQL 内 (Evidence の枠内で完結)
実運用 不向き 標準

CSV ソースは設定ファイルだけで完結します。sources/<source>/connection.yamltype: csv を書いておけば、evidence sources コマンドが配下の CSV をすべて Parquet に変換します。ここに SQL は登場しません。サンプルリポジトリでは 100 本ノックの CSV をそのまま放り込んでいるだけで、変換結果は CSV と 1:1 対応した Parquet になります。

DWH ソースになると挙動が一段豊かになります。connection.yaml に DWH 接続情報 (BigQuery なら type: bigquery、Snowflake なら type: snowflake 等) を書き、<query>.sql ファイルにソースクエリを書きます。SELECT * FROM raw_events と書けば全件配布になり、SELECT date, store_id, SUM(amount) FROM raw_events GROUP BY 1, 2 と書けば集約済み Parquet になります。ソース SQL の粒度が、そのままブラウザに渡るデータの粒度になる、という構造です。これは後段のホスティング章で重要な意味を持つので、頭の隅に置いておいてください。

ここで効いているのが、前節で触れた DuckDB の抽象化層としての役割です。BigQuery でも Snowflake でも、Evidence のアダプタが結果セットを取得し、DuckDB が Parquet 化します。閲覧時のクエリエンジンも DuckDB なので、開発者から見ると「SQL の方言は DuckDB 一本」になります。BigQuery の SAFE_CAST や Snowflake の TRY_CAST のような方言にロックインされず、ソースを差し替えても閲覧側のクエリは無傷で残る、という構造です。

サンプルリポジトリは CSV ソースを採用しています。これは「データサイエンス 100 本ノック」の公開 CSV を題材にしているためで、Evidence の動作確認には CSV で十分です。一方、本記事の後段ホスティング議論は DWH 接続前提で進めます。理由はシンプルで、本番運用では定期ビルドで DWH の最新状態を取得する流れが標準だからです。

Parquet 化されたあとの動作 (DuckDB WASM 配布、ブラウザ内 SQL 実行) は CSV / DWH で完全に同じです。ここから先は両者を区別せずに進めます。

なぜブラウザで SQL が動かせるのか

従来の BI ツールは、ブラウザのリクエストを受けてサーバ側で DB にクエリを投げ、結果を JSON で返す構造でした。クエリ実行は常にサーバ側です。

Evidence はこの構造を反転させます。データを丸ごとブラウザに配布し、クエリエンジンをブラウザに置きます。サーバは静的ファイルを配るだけです。

これを成立させているのは DuckDBWebAssembly です。DuckDB は OLAP (集計や分析クエリに最適化された処理形態) 用途のインプロセス DB で、SQLite が OLTP (トランザクション処理) 寄りなのに対し、こちらは集計クエリや列指向処理に最適化されています。「組み込み DB の OLAP 版」というのが立ち位置として近いです。

そしてその DuckDB の C++ ソースを emscripten (C/C++ を WebAssembly にコンパイルするツールチェーン) でコンパイルしたのが DuckDB WASM です。ブラウザの JavaScript エンジンの隣で、ネイティブに近い速度で SQL を実行できます。WebAssembly そのものは次節で扱います。

WebAssembly とは

WebAssembly は、ブラウザが直接実行できる低レベルのバイナリフォーマットです。JavaScript と並んでブラウザの実行エンジン側で動くコードで、JavaScript とは違う実行モデルを持ちます。

JavaScript との違いを整理します。

観点 JavaScript WebAssembly
形式 ソースコード (テキスト) 事前コンパイル済バイナリ
起動 パース + JIT コンパイル ストリーミングコンパイルで起動が速い
動的型 i32, i64, f32, f64 などの低レベル型
メモリ GC 管理の Object/Array 線形メモリ (連続バイト配列)
得意な処理 UI 操作、I/O、文字列処理 数値計算、CPU バウンド処理

形式の違いがそのまま起動コストに効きます。JavaScript はブラウザに渡されてからソースコードをパースし、JIT コンパイラが実行時に最適化しながら走らせる必要があります。WebAssembly は事前にコンパイル済のバイナリ形式で渡されるので、ストリーミングコンパイルでロード中から実行に入れます。

型とメモリの違いが実行速度に効きます。JavaScript は動的型で、変数の型を実行時に判定する必要があります。WebAssembly は型が事前に確定しています。メモリも、JavaScript は GC 管理の Object / Array で間接参照が多いのに対し、WebAssembly は連続したバイト配列 (線形メモリ) を直接扱う構造です。配列を端から処理するような計算では、CPU のキャッシュに乗りやすく、命令パイプラインも回しやすいです。

これらの結果、JavaScript では現実的でない速度の重い計算 (動画コーデック、ゲームエンジン、画像処理、そして DB の集計クエリ) を WebAssembly に任せて、UI 操作や I/O は JavaScript で扱う、という役割分担ができます。Evidence の場合は SQL の実行が前者で、これを DuckDB WASM に任せている、という構造です。

代償として、JavaScript と WebAssembly の間でデータをやり取りするときに「境界コスト」が発生します。WebAssembly の線形メモリと JavaScript の Object/Array は別空間にあり、橋渡しの際にコピーや変換が要るためです。

境界をまたぐ呼び出しが頻繁だと、WebAssembly 化のメリットが相殺されることもあります。例えば 1 行ずつ JS から WebAssembly に値を渡して計算させると、コピーのオーバーヘッドが計算時間を超えることもあります。一方、大きな塊 (バイト列、テーブル) を渡して中で計算させ、結果だけ返すパターンなら境界往復が少なく済みます。

なぜ WebAssembly が DuckDB のような OLAP エンジンに効くかというと、DuckDB の処理は「比較的大きなデータをまとめて処理する」CPU バウンドな計算が中心だからです。WebAssembly の最適化された数値演算と線形メモリの恩恵を最大限受けやすく、JS では現実的でない処理速度が出ます。

次節では、Evidence がこの境界コストをどう避けているかを見ます。

Evidence における WebAssembly の役割

Evidence では DuckDB WASM が「閲覧時のクエリエンジン」として動きます。具体的には次のような流れです。

ここで効いているのが「境界コストが問題化しない構造」です。前節で触れたとおり、JavaScript と WebAssembly の境界をまたぐ呼び出しが頻繁だとオーバーヘッドが大きくなります。Evidence は Parquet を DuckDB WASM に渡し、SQL の実行はその内側で完結させ、結果セットだけを JavaScript 側に返します。境界往復が SQL のクエリ単位で済むため、行単位や列単位で境界をまたぐような設計に比べてオーバーヘッドが少なく済みます。

実装としては @evidence-dev/universal-sql というパッケージが DuckDB WASM をラップしていて、Markdown 内で書かれた SQL がこのパッケージ経由で DuckDB WASM 側に渡る形になっています。

DuckDB WASM のバイナリそのものは vite の ?url import で参照されているので、サーバ側に置かれた .wasm ファイルをそのまま fetch してきます。サンプルリポジトリでは dashboard/patches/@evidence-dev+universal-sql+3.0.1.patch で、この参照先を jsdelivr CDN に書き換える patch を当てています (Cloudflare Pages の単一ファイル容量制限 25MB を回避するための調整、後段で扱います)。

インタラクションも同じ構造で動きます。Dropdown コンポーネントの選択値は Markdown 内で ${inputs.xxx.value} のように参照でき、これが SQL に埋め込まれます。

<Dropdown name="store" data={stores} value=store_id />

```sql filtered_sales
select * from sales where store_id = '${inputs.store.value}'
```

値が変わると DuckDB WASM がブラウザ内でクエリを再実行し、可視化が更新されます。サーバ往復は発生せず、実時間はクエリの複雑さとデータ量で決まります。

この設計が生む制約と利点

「サーバなし、ブラウザ内クエリエンジン」という選択は、いくつかの制約と利点を同時にもたらします。

観点 利点 制約
攻撃面 サーバプロセスを持たない データそのものがブラウザに渡る
アクセス制御 (利点なし) DWH のロール・行/列レベル制御を引き継げない
ホスティング 静的配信向けに選択肢が広い (無料の CDN から Evidence Cloud のような公式有料サービスまで) (制約なし)
インタラクション サーバ往復が発生しない データ量が大きいと DWH の分散処理に劣ることがある
データ鮮度 (利点なし) ビルド頻度で決まる、リアルタイム不向き
データサイズ (利点なし) ブラウザに渡せる上限あり (数十万行が実用域)
DWH への負荷 閲覧時はゼロ (ビルド時のみ) (制約なし)
運用 IaC でデプロイまで完結 (制約なし)

サーバ側の攻撃ベクトルが大幅に減るのは大きな利点です。サーバプロセスがないので、SQL インジェクションも、サーバ側のセッション乗っ取りも、認証バイパスのバックドアも、原理的に存在しません。守るのは静的ホスティングだけです。一方、データそのものがブラウザに渡るという配布型 BI 特有の攻撃面はむしろ広く、これは後段の章で詳しく扱います。

加えて、DWH 側の RBAC、Row-level Security、Column マスキングといった行・列単位の認可は、ビルド成果物に引き継がれません。ビルド時のアカウントの権限で評価された結果が、全閲覧者に同じ Parquet として配られる構造です。ユーザーごとに見せるデータを変えたい要件は、ホスティング層 (どのページを誰に見せるか) で表現することになります。

「DWH への負荷が閲覧時ゼロ」も効きます。サーバ往復型 BI でもクエリキャッシュで負荷を下げる工夫はありますが、Evidence は構造的に「ビルド時にしか DWH を叩かない」ので、負荷とクエリ料金 (BigQuery のスキャン課金等) のピークがビルドのタイミングに固定されます。閲覧者数が増えても DWH 負荷は変わらず、スパイクが読みやすい。DWH 側の権限設計も、ビルド用のリードオンリーアカウント 1 つあれば足ります。

データサイズの上限は実務上の壁になります。体感では数十万行 / 数十 MB の Parquet までが快適に動く目安で、それを超えると初期ロードに数秒かかったり、メモリ消費でブラウザが重くなったりします。「全部ブラウザに渡す」設計のコストです。サンプルリポジトリの 100 本ノックは 10 万行ほどで、Parquet にして約 4MB なので、まだ余裕があるサイズ感です。

データ鮮度の制約も重要です。Evidence のデータはビルド時にしか更新されないので、リアルタイムにデータを見たい要件 (1 秒前の数字が見たい) には合いません。1 時間ごと / 5 分ごとのビルドで吸収できる要件なら、Cloudflare Pages の Deploy Hook を cron で叩く運用で十分です。

そしてもう一つの制約が「データそのものがブラウザに渡る」点です。これは伝統的な BI のサーバ側認可モデルを反転させ、データ粒度の設計とホスティング側の認証層に責任を分配する必要を生みます。次の章の主題です。

セキュアなホスティング: データが同梱される前提で守る

仕組み章で見たとおり、Evidence は「データを丸ごとブラウザに配布する」設計です。サーバ往復型の BI とは脅威モデルが根本的に違うので、ホスティングの考え方も切り替える必要があります。

データ粒度設計が安全境界を決める

ホスティングの議論に入る前に、前提を 1 つ押さえます。配布される Parquet の中身がソース SQL の粒度で決まる、という事実です。

Evidence 固有の配信特性を表にまとめます。

特性 影響
ビルド成果物に Parquet (データ) が含まれる 配信先 = データの所在地。CDN に載せるとエッジノードにデータが分散する
認証突破 = 全データアクセス サーバサイドでリクエスト単位のフィルタができない。認証の信頼性がそのままデータ保護の強度になる
ブラウザの DevTools でデータ閲覧可能 正規の閲覧者であっても、画面に表示していないカラムや行まで取得できる
SELECT * の結果が全部キャッシュされる SQL クエリが返した全データが Parquet に入る。画面に出していないデータも含む

「DevTools で画面に表示していない列まで取得可能」というのは具体的にどういうことか。実際にやってみると、ブラウザの Network タブで data/<source>/<table>/<hash>/<table>.parquet を見つけて URL を直接叩くだけで、Parquet ファイルがダウンロードできてしまいます。あとはローカルで DuckDB を立ち上げて SELECT * FROM read_parquet('downloaded.parquet') と打てば、画面に出ていない列も含めて全部見えます。配布型 BI のデータは「事実上、認証通過者には全部見える」と考えるしかありません。

これが配布型 BI の脅威モデルの本質です。クエリ往復型 BI は「ユーザがどの行を見たか」をサーバ側で制御できますが、配布型は「最初の配布以降の挙動」に介入できません。

ここからデータ最小化の責任分担が見えてきます。DWH 接続前提で書くと、原則は次のようになります。

  • sources/<source>/<query>.sql の中で集約・マスキング・サンプリングを済ませる
  • 認証は粒度設計の代替にならない (Parquet は静的アセット、認証通過後は単純にダウンロード可能)
  • 機密データを扱う場合は「ソース SQL でデータ最小化」+「認証で配信制御」の二段構成
  • パス単位アクセス制御も部門分離にはならない (詳細は後述)

ソース SQL でのデータ最小化は、具体的には以下のような工夫を入れます。個別レコードではなく集計済みテーブルだけを Parquet 化する、必要列だけ SELECT する (SELECT * を避け、PII を含むカラムはそもそも含めない)、低頻度イベントは抽象化してロングテールを残さない、など。要は、ブラウザに渡って困るデータをソース SQL の段階で削っておく、という発想です。

-- 例: 個別ユーザは見せたくないので集計済みで配信
SELECT
  date_trunc('day', purchased_at) AS day,
  store_id,
  COUNT(DISTINCT customer_id) AS unique_customers,
  SUM(amount) AS total_amount
FROM raw_events
WHERE purchased_at >= current_date - interval '90 days'
GROUP BY 1, 2;

「パス単位アクセス制御は部門分離にならない」という話は補足が要ります。HTTP request 単位で認可するパス単位の機構 (Cloudflare Access、IAP、Azure Easy Auth、nginx の auth_request 等) を /products/*/marketing/* に当てる構成は理屈の上では可能ですが、Evidence (SvelteKit) の SPA (Single Page Application) 性質に阻まれます。

SPA では認可の効き方が 2 段階に分かれます。初回ロードやリロード時は HTTP request が走るので、Cloudflare Access のようなパス単位の認可機構が効きます。

一方、ページ間リンクのクリックは client-side routing (history.pushState で URL だけ書き換える方式) で完結し、サーバへの新規 HTTP request が発生しません。HTTP request 単位で評価する認可機構は、内部遷移に対して評価機会を持たないため、結果として直接 URL 入力時のみ拒否される、という限定的な保護にとどまります。

パス別ポリシーを当てた構成で実際に試すと、ブラウザの URL バーに別部門のパスを直打ちすれば拒否されますが、ナビゲーションリンク経由で同じパスに飛ぶと普通に開いてしまいます。これは認可機構側の設定では解決できない構造的な話です。

加えて、Cloudflare の Zero Trust モデルには注意の必要な挙動があります。

Cloudflare Pages / Workers は「公開ホスティング」が前提で、Zero Trust (Access) はその上に allow-list 方式で認証層をかぶせる構造です。保護したい host を Access Application で明示しないと、デフォルト公開のまま動きます。<project>.pages.dev (production) だけ保護して *.<project>.pages.dev (preview / branch deployment) を見落とすと、preview URL が無防備になる典型ケースです。これは AWS IAM の deny by default や Vercel のプロジェクト単位保護 ON/OFF とは異なる設計思想で、host のバリエーションを把握していないと無防備な穴を作ります。

サンプルリポジトリでは production URL 用と preview URL 用の 2 つの Access Application を立てて両者を保護しています。

5 つの構成パターン

Evidence (に限らず配布型 BI) のホスティングを並べると、おおむね次の 5 パターンに分類できます。

パターン 配信経路 データの所在 サーバ管理 想定する機密度
A. CDN 公開 (認証なし) Internet → CDN → ブラウザ CDN エッジ (世界各地) 不要 なし (公開データ)
B. CDN 公開 + 認証 Internet → CDN → ブラウザ CDN エッジ (世界各地) 不要 低〜中
C. クラウド PaaS + マネージド認証 Internet → PaaS → ブラウザ クラウドの特定リージョン 不要 中〜高
D. トンネル経由 Internet → ZTNA/VPN → 内部サーバ → ブラウザ 内部サーバのみ 必要 中〜高
E. 閉域ホスティング 社内 NW → 内部サーバ → ブラウザ 社内 NW 内のみ 必要

選び方のフローを Mermaid で示します。

データの機密度が上がるほど構成パターンも上に登っていきます。本記事のサンプルは A (認証なし公開) と B (CDN 公開 + 認証) を実装しています。C 以降は実装は持っていませんが、選択肢として位置づけを整理しておきます。

A. CDN 公開 (認証なし)

ビルド成果物を CDN に配置し、認証なしでインターネットに公開する構成です。最もシンプルですが、データは誰でも閲覧可能になります。サンプルリポジトリの「GitHub Pages デプロイ」「Cloudflare Pages 公開」がこれに該当します。

観点 内容
データの所在 CDN エッジノード (世界各地に分散)
運用負荷 最低。サーバ管理不要、デプロイは push のみ
アクセス制御 なし。URL を知っていれば誰でも閲覧可能
コスト $0 (Cloudflare Pages、Vercel、Netlify、GitHub Pages いずれも無料枠あり)

リスクは「データが全公開される」「SELECT * 等で意図しないデータが混入する」「公開後の取り消しが困難 (CDN キャッシュやクローラに残る)」など。OSS のデモ、IR / 公開統計、教材のような公開前提のダッシュボードに向きます。社内 KPI のような「公開を意図しないデータ」は、集約済みであっても認証付き (B 以降) にすべきです。

B. CDN 公開 + 認証

ビルド成果物を CDN に配置し、認証レイヤーで保護する構成です。Cloudflare Pages + Access が代表例で、サンプルリポジトリの「Cloudflare + Google 認証」がこれに該当します。本記事のサンプルの主力構成です。

観点 内容
データの所在 CDN エッジノード (世界各地に分散)
運用負荷 低。サーバ管理不要、デプロイは push or API
認証方式 Cloudflare Access、Vercel Password Protection 等
コスト Cloudflare Pages + Access なら 50 ユーザまで $0

リスクは「CDN エッジへのデータ残存 (デプロイ更新後も TTL 分残る)」「認証設定ミスによる意図せぬ公開 (*.pages.dev や preview URL の保護漏れ)」「認証突破時の影響範囲の広さ (CDN 上の全 Parquet にアクセス可能)」「CDN 事業者へのデータ信託」。集約済み KPI、売上推移など、漏洩しても直接的な損害が限定的なデータに向きます。個人情報、給与、財務詳細など漏洩時に法的リスクがあるデータは C 以降を検討。

料金は執筆時点 (2026 年 5 月) のもので、各サービスの料金体系は変わる可能性があります。

プラットフォーム 認証 備考
Cloudflare Pages + Access Google Workspace、OTP、SAML 等 50 人無料、Terraform 管理可能
Vercel パスワード保護 (Pro $20/月〜) IdP 連携不可、パスワード共有になりがち
Netlify パスワード保護 (Pro $19/月〜) 同上
AWS Amplify Cognito / パスワード Cognito 設定が煩雑

認証の柔軟性とコストの両面で Cloudflare Pages + Access が優位です。本記事のサンプルもこの組み合わせを採用しています。

C. クラウド PaaS + マネージド認証

ビルド成果物をクラウドの PaaS (Cloud Run、App Service 等) に配置し、クラウド側のマネージド認証で保護する構成です。データは特定リージョンに留まり、CDN エッジには分散しません。サーバ管理が不要な点が D との違いです。

データを CDN エッジに分散させたくないが、サーバ管理もしたくない場合の解です。クラウド別に有名な構成があります。

観点 Google Cloud Run + IAP AWS S3 + CloudFront + Cognito Azure App Service + Easy Auth
データのリージョン限定 可 (Cloud Run リージョン) 不可 (CDN 分散) 可 (App Service リージョン)
マネージド認証 IAP (コード変更不要) Lambda@Edge (実装必要) Easy Auth (コード変更不要)
自然な IdP Google Workspace Cognito User Pool Entra ID (Microsoft 365)
小規模コスト $0 〜数ドル $0 〜$1 $13 〜 18/月
構成の複雑さ 高 (Lambda@Edge の管理)
パス単位の制御 不可 限定的 不可

既に利用しているクラウドと IdP の組み合わせで選ぶのが自然です。Google Workspace なら Google Cloud、Microsoft 365 なら Azure、AWS に寄せている組織なら AWS。データをリージョン内に留める要件があるなら Google Cloud または Azure が有利。AWS で完全リージョン縛りを満たすには CloudFront を使わない構成 (ALB + S3 + VPC Endpoint) になり、ALB の固定費 ($16〜24/月) がかかってきます。

Google Cloud の Cloud Run + IAP は、IAP が Cloud Run に直接統合できるためロードバランサーなしで認証をかけられます。Workforce Identity Federation を使えば外部 IdP (SAML/OIDC) も繋げます。Azure の App Service + Easy Auth は、コード変更なしで Entra ID 認証を付加できる手軽さが効きます。Static Web Apps を選ぶとデフォルトで CDN 配信されるので、データレジデンシー要件があるなら Private Endpoint (Standard プラン) が必要です。

D. トンネル経由

C のサーバを内部に置き、ZTNA トンネル (Cloudflare Tunnel、Tailscale Funnel) や VPN 経由でインターネットからアクセスする構成です。データが CDN に分散せず、サーバ管理は必要ですがリモートアクセスは可能です。

C との使い分けが論点になります。どちらも「データを CDN に分散させずリモートアクセスを提供する」点で似ていますが、D を新規に構築するメリットは限定的です。サーバ管理が必要、運用負荷が中、コストは サーバ費 + ZTNA/VPN 費。新規構築するくらいなら C のほうがシンプルかつ低コストで同等の要件を満たせます。

D を選ぶ合理性があるのは、以下のようなケースに絞られます。

  • データをクラウド事業者にも預けられないポリシーがある (ただし、そこまで求めるなら E のほうが一貫性がある)
  • 既に ZTNA / VPN 基盤とサーバ運用体制があり、追加コストが小さい

代表的な実装は Cloudflare Tunnel + Access (内部サーバから Cloudflare へのアウトバウンド接続だけで動作、ポート開放不要) や、Tailscale + 内部サーバ (Mesh VPN、サーバはプライベート IP のみ、インターネット非露出) があります。

E. 閉域ホスティング

社内ネットワーク内に内部サーバを配置し、社内からのみアクセスする構成です。インターネットを一切経由しません。

観点 内容
データの所在 社内ネットワーク内のみ
運用負荷 中〜高。サーバ管理 + ネットワーク管理
リモートアクセス 不可 (社内 VPN 経由なら可能だが、それは D に近づく)
認証方式 社内 AD 連携、Basic 認証、ネットワーク境界による暗黙の認証
コスト 既存インフラ流用なら追加コスト小、新規構築なら大

データが社外に出ることを一切許容しないポリシー、個人情報や財務詳細を含むダッシュボードで採用される構成です。リスクは「運用属人化」「単一障害点 (社内サーバの冗長構成がなければ)」「ビルドパイプラインの設計」。SaaS 系 CI (GitHub Actions 等) を社内から使う場合、ビルド成果物の転送経路を別途設計する必要があります。

D / E 共通: サーバの実体

D / E では「内部サーバ」にビルド成果物を置きます。Evidence の成果物は HTML + JavaScript + Parquet の静的ファイル群なので、必要なのは静的ファイルを配信できる HTTP サーバだけ。アプリケーションランタイム (Node.js 等) は不要です。

方式 実体 運用負荷 備考
クラウド VM + nginx EC2 / GCE / Azure VM 上の nginx OS・nginx のパッチ管理が必要
コンテナ nginx コンテナを ECS / Cloud Run / k8s で実行 低〜中 イメージ更新でデプロイ、OS 管理はランタイムに委譲
社内物理 / 仮想サーバ + nginx 既存の社内サーバに nginx を追加 中〜高 既存インフラ流用なら追加コスト小
Windows Server + IIS 既存の Windows Server で IIS を利用 Evidence が公式サポート、.NET 不要

デプロイ方法も配信経路によって工夫が要ります。CI でコンテナレジストリにイメージを push してサーバが pull する、CI で rsync/scp で転送、CI でオブジェクトストレージに置いてサーバが cron pull、サーバ内でリポジトリ clone してビルド (CI が使えない閉域)、など。

パターン横断: パス単位のアクセス制御

部門ごとに閲覧できるレポートを分けたい場合、パス単位のアクセス制御が必要になります。パターンによって対応状況が大きく異なります。

パターン パス単位の制御 方法
B. Cloudflare Pages + Access 実質不可 Access のパス別ポリシーは設定できるが、SPA 内部遷移で素通りするため部門分離には使えない
C. Google Cloud Run + IAP 不可 IAP はサービス単位、パス単位のポリシー分離機能なし
C. Azure App Service + Easy Auth 不可 アプリ全体に適用、パス単位はアプリコード側の実装が必要
C. AWS CloudFront + Cognito 限定的 Behavior ごとに Lambda@Edge を分けることは可能だが煩雑
D. Cloudflare Tunnel + Access 実質不可 B と同じ理由 (SPA 内部遷移で素通り)
D. Tailscale 不可 ACL はホスト単位、パス単位はサーバ側実装が必要
E. nginx 自前実装 location ディレクティブ + Basic 認証 / IP 制限で対応

表で「実質不可」が並ぶとおり、HTTP request 単位の認可機構をパス別に当てる方式は SPA 内部遷移で素通りするため、配布型 BI では部門分離として機能しません。本格的な部門隔離が必要なら、ダッシュボードをホスト単位で複数サービスに分割するのが筋です。

プロジェクト分離が最もシンプルです。Evidence の成果物は軽量な静的ファイルなので、サービス分割のデプロイコストは低くて済みます。例えば Google Cloud なら Cloud Run サービスを部門別に作成し、それぞれに IAP を設定すれば Google Workspace のグループ単位でアクセス制御が成立します。サンプルリポジトリでも採用している Cloudflare Pages なら、部門ごとに別 Pages Project を立てて、それぞれに Access Application を当てる構成です。

本サンプルで実装した 3 パターン

サンプルリポジトリでは、A と B のバリエーションとして 3 パターンを提供しています。Evidence プロジェクト本体 (dashboard/) は共通で、ホスティング先の設定 (Terraform / GitHub Actions Workflow) のみ切り替えます。

パターン デプロイ先 認証 設定ファイル
① GitHub Pages github.io なし .github/workflows/deploy-pages.yml
② Cloudflare Pages 公開 pages.dev なし terraform/cloudflare/ (enable_google_auth = false)
③ Cloudflare + Google 認証 pages.dev 許可リスト型 terraform/cloudflare/ (enable_google_auth = true)

GitHub Pages と Cloudflare Pages の差別化を整理します。

観点 GitHub Pages Cloudflare Pages
単一ファイル容量上限 100MB 25MB
DuckDB WASM (33〜38MB) バンドル可 不可 (CDN 経由が必要)
認証機能 なし Cloudflare Access (Zero Trust)
URL 構造 <user>.github.io/<repo>/ (Project Pages) <project>.pages.dev
basePath 対応 必要 (Project Pages のとき) 不要

DuckDB WASM の 25MB 制限は具体的な落とし穴です。サンプルでは dashboard/patches/@evidence-dev/universal-sql の DuckDB WASM 参照先を jsdelivr CDN に書き換える patch を当てて回避しています。これがないと Cloudflare Pages の deploy がエラーで止まります。

GitHub Pages 側はそのままバンドルできるので patch は不要です。ただし、サンプルでは差分を最小化するため、3 パターン共通で patch を適用する構成にしました。

サンプル③の Terraform 設計は許可リスト型です。var.viewer_emails 1 つで host 全体を保護します。Cloudflare Access のパス単位制御は SPA 内部遷移で素通りするため、本格的な部門隔離には使えないからです。

部門ごとに独立した配信が必要な本番運用では、部門ごとに別 Cloudflare Pages Project を立てて Cloudflare Access で個別保護するパターンを採ります (前節「パス単位のアクセス制御」のプロジェクト分離方針)。

basePath の扱いも運用上のトリッキーなポイントです。GitHub Project Pages では URL 構造が /<repo-name>/ になります。HTML 内の絶対パス参照を /<repo-name>/... に書き換える必要があります。

Evidence では evidence.config.yamldeployment.basePath で指定します。これを固定値で書くと、ローカル npm run dev や Cloudflare Pages デプロイ (basePath なし) と整合しません。3 環境で挙動が分岐します。

サンプルでは GitHub Actions の build ステップ直前に evidence.config.yaml へ動的に append する形で、CI 限定の設定にしています。

- name: Configure basePath for GitHub Pages
  run: |
    cat >> evidence.config.yaml <<EOF
    deployment:
      basePath: /${{ github.event.repository.name }}
    EOF

ちなみに Evidence の公式 docs には BASE_PATH 環境変数を渡せば良い、と書かれています。しかし、現行の sdk (執筆時点の v40.1.8、これが最新版) はこの環境変数を読みませんでした。リポジトリ全体を検索しても BASE_PATH の参照は docs ページに 1 箇所あるだけで、sdk のソースコードには存在しないことを確認済です。evidence.config.yamldeployment.basePath だけが効きます。

加えて、npm の lock file 整合の話も書いておきます。Evidence のようにそれなりに多い依存を持つプロジェクトだと、npm のバージョン違い (Node 22 同梱の npm 10 vs Node 24 同梱の npm 11) で peer dependency の解決アルゴリズムが微妙に違って、ローカルで生成した lock を CI 上で npm ci すると Missing エラーになるケースがあります。

サンプルでは GitHub Actions と Cloudflare Pages の両方で Node 24 を使う設定 (Cloudflare Pages は NODE_VERSION = "24" を Terraform で指定) で揃えています。Cloudflare Pages Build Image v3 は「Any version」サポートなので Node 24 も動きます。

構成パターン横断比較

5 パターンを横断で比較すると、選び方の輪郭がさらにはっきり見えます。

観点 A. CDN 公開 B. CDN + 認証 C. クラウド PaaS D. トンネル経由 E. 閉域
データの社外配置 あり (全公開) あり (CDN エッジ) あり (クラウドリージョン) なし (内部サーバ) なし (社内 NW のみ)
データのリージョン限定 不可 不可
運用負荷 最低 中〜高
リモートアクセス 可 (誰でも) 可 (ZTNA / VPN 経由) 不可
サーバ管理 不要 不要 不要 必要 必要
アクセス制御 なし 可 (Cloudflare Access 等) 可 (クラウド IdP) 可 (ZTNA) 環境次第 (AD 連携等)
初期構築コスト 最低 低〜中 既存インフラ次第
データ漏洩の攻撃面 該当なし (公開前提) 広い (CDN + 認証バイパス) 中 (PaaS + 認証バイパス) 中 (サーバ + トンネル) 狭い (社内 NW 境界)

「機密度が高いほど運用負荷が上がる」という相関がそのまま見えます。本記事のサンプルは A〜B (運用負荷 = 最低〜低) を実装し、C〜E は構成案として位置づけだけ示しています。

共通の運用設計

配信経路に関わらず、以下はどのパターンでも検討が必要です。

ビルドパイプラインの段階整理:

段階 内容
初期 手動ビルド + 手動デプロイ (or push トリガー)
標準 CI/CD (GitHub Actions 等) で自動ビルド + 自動デプロイ
成熟 PR レビュー必須 + build:strict + SQLFluff CI を経てからデプロイ

変更管理の段階整理:

段階 内容
初期 main への直接 push
標準 ブランチ保護 + PR レビュー
成熟 ワークフロー / Terraform の変更にもレビュー必須

データガバナンスの観点:

  • SELECT * を避け、表示に必要なカラムだけ返す
  • 集約済みデータを返し、生の明細データを含めない
  • 表示しないカラム (ID や内部コード等) も Parquet に入ることを認識する
  • ダッシュボードの用途に応じて、機密度の異なるデータは別プロジェクトに分離する

運用上のチェックリスト

運用に乗せたあとに気をつけたい項目を影響度順で並べます。

項目 影響度 内容と対策
Cloudflare Pages + Access の wildcard 保護漏れ preview / branch deployment が無防備になる典型ケース。production の Access Application だけでなく、*.<project>.pages.dev も別 Application で保護する。Terraform で production 用 / preview 用の 2 リソースを定義しておく
DuckDB WASM の容量制限 Cloudflare Pages は 25MB 超で deploy エラー。@evidence-dev/universal-sql の DuckDB WASM 参照先を jsdelivr CDN に書き換える patch を当てる (サンプルの dashboard/patches/ 参照)
SELECT * によるデータ過剰露出 画面に表示していない列まで Parquet に書き出される。ソース SQL は必要列だけ書く。レビュー時に SELECT * をチェックするのが現実的
npm の lock file 整合 Node のバージョン差で npm ci が Missing エラーになるケースあり。CI と本番ホスティングの Node version を揃える

拡張に向けての論点

本サンプルは「最小限で仕組みと運用を理解する」目的で組んでいるので、本番運用に向けてはいくつかの拡張ポイントがあります。

検証本番のフロー

サンプルでは Cloudflare Pages の preview deployment を dev / preview/* ブランチで有効化しています。これを使うと、PR ごとに <branch>.<project>.pages.dev の URL が自動生成され、コードレビューと並行して動作確認ができます。SQL や Markdown のレビューを PR で上げつつ、preview URL で実際の見た目とデータも確認する、という二段階の検証フローが組めます。

ステークホルダー (営業、企画、経営層) に preview URL を共有してデータの中身まで確認してもらえば、main マージ前に手戻りを潰せます。production と preview は別の Cloudflare Access Application で保護されているので、認証の同期も自然に回ります。

さらに進めて staging 専用の Pages Project を別に立てる選択肢もあります。本番リリース前に時間を取って動作確認したい場合や、データソースを stg / prd で分けたい場合に向きます。

部門単位の隔離

「データ粒度設計が安全境界を決める」節で扱ったとおり、Cloudflare Access のパス単位制御は SPA 内部遷移で素通りします。本格的な部門隔離が必要になったら、部門ごとに別 Pages Project を立てて個別の Access で保護するパターンに切り替えます。本サンプルの Terraform は単一プロジェクト構成なので、ここを複数プロジェクトに展開する形になります。

認証の強化

Cloudflare Access の許可リスト型から、Google Workspace のグループ単位の制御に切り替えるのは自然な拡張です。さらに進めると、Workforce Identity Federation や SAML / OIDC 連携でエンタープライズ IdP に繋ぐパターンに発展します。

ライブクエリ要件

ライブクエリ要件 (リアルタイムにデータを見たい) が出た場合、Evidence の構造とは合いません。Evidence のデータ鮮度はビルド頻度で決まるからです。選択肢は次のとおりです。

アプローチ 内容 向いている場面
ビルド頻度を上げる 1 日 1 回から数時間ごと / 30 分ごとに頻度を上げる。Cloudflare Pages の Deploy Hook を cron で叩く 数十分のラグを許容できる業務
サーバ往復型 BI と組み合わせる サーバ往復型 BI を別途立てて、リアルタイム要件はそちらに リアルタイム要件と Evidence の利便性を両立したい
アーキテクチャ変更 Evidence をやめて、別の BI ツールに リアルタイム要件が支配的

選び方は「鮮度の要件 (数十分前のデータで足りるのか、もっと新しい数字が要るのか)」と「インフラ運用負荷の許容範囲」のバランスで決まります。

おわりに

Evidence の特徴は「ブラウザの中に DuckDB を持ち込んで SQL を動かす」という設計判断に集約されます。前半で掘ったとおり、これは WebAssembly + Parquet + DuckDB という 3 要素の組み合わせと、境界コストを避けるための「一括ロード + DuckDB WASM 内完結」という構造で支えられています。

後半では、この設計が生む独自の脅威モデル (データが丸ごとブラウザに渡る、配布型 BI の限界) と、それを踏まえた運用設計 (データ粒度、Cloudflare Zero Trust モデルの理解、5 つの構成パターン、許可リスト型認証) を整理しました。配布型 BI 特有の留意点を理解した上で、サンプルリポジトリと一緒に手元で挙動を確かめてもらえると、各論点の具体像が見えやすくなるはずです。

ホスティング 3 パターンの切り替え方法、Terraform 設計、basePath の扱いなどはリポジトリの README を参照してください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?