😸

Cloudflare上のデータをR2経由でBigQueryに反映する

に公開

こんにちは。Sun* でバックエンドエンジニアとして働いているMizukiです。
Cloudflareで作った小規模アプリでのデータ分析基盤の試みについて書きました。

背景

個人的にCloudflareでアプリを開発している時に、「ユーザー数が少ない小規模アプリでのデータ分析とデータ分析基盤はどうしよう」と考えたのがきっかけです。
趣味プロジェクトや小規模スタートアップでは、Snowflakeのような大規模なDWHは明らかにオーバースペックです。かといって、分析基盤なしでは「どの機能が使われているか」「どこでユーザーが離脱しているか」といった基本的な分析すらできません。
特にCloudflare Pagesでホスティングしていると、データベースはD1(SQLite)、Web Analyticsはcloudflare.aiと、データソースが分散しています。これらを統合して分析できる環境が欲しい、でも月数千円の予算内で実現したいと考えてました。

そこで行き着いたのが「R2をデータレイクにして、BigQueryを分析層に使う」構成です。本記事では、実際にこの構成を採用した理由と実装方法を紹介します。

なぜこの構成にしたのか

検討した構成の選択肢

データ分析基盤として、以下の3つの構成を比較検討しました。

構成1: Workers → R2 → BigQuery → Looker Studio(採用案)

Cloudflare D1/Analytics
      ↓
Workers (ETL)
      ↓
R2 (データレイク)
      ↓
BigQuery (DWH)
      ↓
Looker Studio (可視化)

構成2: Workers → BigQuery → Looker Studio

Cloudflare D1/Analytics
      ↓
Workers (ETL)
      ↓
BigQuery (DWH)
      ↓
Looker Studio (可視化)

R2を経由せず、直接BigQueryにデータを投入。

構成3: AWS Glue → S3 → Redshift → Redash

Cloudflare D1/Analytics
      ↓
AWS Glue (ETL)
      ↓
S3 (データレイク)
      ↓
Redshift (DWH)
      ↓
Redash (可視化)

AWSエコシステムで統一した構成。

4つの観点での比較

想定するアプリ規模:小規模ECサイト(MAU 5,000人、月間購入者100人、データ量100MB/月、イベント数5万/月)

コストの観点

構成 ETL レイク/DWH 可視化 合計
構成1 (採用案) $0 R2: $0 + BQ: $0-1 $0 $0-1
構成2 $0 BQ: $1-5 $0 $1-5
構成3 $5 S3: $0 + Redshift: $25 $0 $30

構成1のコスト優位性:

  • R2のegress無料: BigQueryへのデータ転送でも課金されない
  • R2・BigQueryともに無料枠内: 小規模なら完全に$0も可能
  • Looker Studio無料: Google製品なので追加コストなし

セキュリティの観点

構成 データ保護層 IAM/アクセス制御 個人情報対応
構成1 3層(D1/R2/BQ) R2トークン + BQ IAM ◎
構成2 2層(D1/BQ) BQ IAM ○
構成3 3層(D1/S3/Redshift) S3 IAM + Redshift ○

構成1の3層データ保護:

1. D1: 生の個人情報(メール、氏名など)
   └─ アクセス: アプリケーションのみ

2. R2: 仮名化データ
   ├─ メール・氏名: AES-GCM暗号化
   └─ ユーザーID: SHA-256ハッシュ化
   └─ アクセス: データエンジニアのみ(読み書き権限)

3. BigQuery: 匿名化データ
   └─ ハッシュIDのみ(暗号化データは保存しない)
   └─ アクセス: データアナリスト全員(閲覧のみ)

セキュリティ上の利点:

  • 最小権限の原則: アナリストはBigQueryのみアクセス、R2の生データには触れない
  • データ漏洩リスク最小化: BigQueryから漏洩しても匿名化済み
  • 監査証跡: BigQueryのアクセスログで追跡可能

パフォーマンスの観点(グラフ表示速度)

ダッシュボードでグラフを表示する際の応答時間:

構成 応答時間 理由
構成1 ◎ BigQuery + Looker Studioネイティブ連携
構成2 ◎ 同上
構成3 ○ Redshift + Redash(JDBC接続)

小規模だとどの構成でも業務に支障をきたすほどの速度にはならないと思いますが、BigQuery + Looker Studioだとネイティブ連携しているので特にパフォーマンス面は優位です。

実装難易度の観点

ETL実装 + グラフ表示実装の総合評価:

構成 ETL実装 グラフ実装
構成1 ○(コード実装) ◎
構成2 ○(コード実装) ◎
構成3 ◎(GUI) △

Looker StudioにBigQueryのデータを追加するのは、BigQueryをデータソースに追加するだけでノーコードで済むので容易です。

総合評価

構成 コスト セキュリティ パフォーマンス 実装難易度 総合
構成1(採用) ◎ ◎ ◎ ○ ◎
構成2 ○ ○ ◎ ◎ ○
構成3 △ ○ ○ ○ △

総合的に評価して、構成1を採用しました。

パイプラインコード例(ポイント部分のみ)

1. D1からR2へのETL

ポイント:

  • Cron Triggers(scheduled): Workers の Cron Triggers を使い、毎日自動でバッチ処理を実行。手動実行が不要で運用負荷を軽減
  • 日付指定によるインクリメンタル抽出: 前日分のデータのみを抽出することで、処理対象を最小化。全量抽出に比べてD1への負荷とコストを削減
  • 個人情報の仮名化(hashPII): user_id をソルト付きでハッシュ化し、R2には個人を特定できないデータのみを保存。データ漏洩リスクを最小化
  • Hive形式のパーティショニング(year=/month=/day=): BigQuery外部テーブルやSparkと互換性のある標準的なディレクトリ構造。後からパーティション単位での再処理も容易
export default {
  async scheduled(event: ScheduledEvent, env: Env) {
    const yesterday = new Date();
    yesterday.setDate(yesterday.getDate() - 1);
    const dateStr = yesterday.toISOString().split('T')[0];
    
    // D1からデータ抽出
    const orders = await env.DB.prepare(`
      SELECT id, user_id, total_amount, created_at
      FROM orders WHERE DATE(created_at) = ?
    `).bind(dateStr).all();
    
    // 個人情報を仮名化
    const hashed = await Promise.all(
      orders.results.map(async (order: any) => ({
        order_id: order.id,
        user_id_hash: await hashPII(String(order.user_id), env.SALT),
        total_amount: order.total_amount,
        created_at: order.created_at
      }))
    );
    
    // R2に保存(日付パーティショニング)
    const [year, month, day] = dateStr.split('-');
    await env.R2_BUCKET.put(
      `d1/year=${year}/month=${month}/day=${day}/orders.json`,
      JSON.stringify({ orders: hashed })
    );
  }
};

2. R2からBigQueryへのロード

ポイント:

  • R2からの読み込み: R2のegress(データ転送)は無料のため、BigQueryへの転送コストがゼロ。S3やGCSからの転送と比較して大幅なコスト削減
  • Streaming Insert API: リアルタイムに近いデータ反映が可能。夜間バッチ後すぐにダッシュボードで確認できる
  • insertIdによる冪等性保証: 日付とインデックスを組み合わせた一意なIDにより、同じデータを再実行しても重複挿入を防止。障害時のリトライも安全
  • サービスアカウント認証(getGCPAccessToken): WorkersからGCPへの認証はJWTベースで実装。シークレットはwrangler secretで安全に管理
export default {
  async scheduled(event: ScheduledEvent, env: Env) {
    const yesterday = new Date();
    yesterday.setDate(yesterday.getDate() - 1);
    const dateStr = yesterday.toISOString().split('T')[0];
    
    // R2からデータ読み込み
    const [year, month, day] = dateStr.split('-');
    const object = await env.R2_BUCKET.get(
      `d1/year=${year}/month=${month}/day=${day}/orders.json`
    );
    const data = await object.json();
    
    // BigQueryにストリーミング挿入
    const token = await getGCPAccessToken(env);
    await fetch(
      `https://bigquery.googleapis.com/bigquery/v2/projects/${env.PROJECT}/datasets/${env.DATASET}/tables/orders/insertAll`,
      {
        method: 'POST',
        headers: { 'Authorization': `Bearer ${token}` },
        body: JSON.stringify({
          rows: data.orders.map((row, i) => ({ 
            insertId: `${dateStr}_${i}`, 
            json: row 
          }))
        })
      }
    );
  }
};

まとめ

Workers → R2 → BigQuery → Looker Studio構成は、低コスト・高セキュリティ・デバッグのしやすさを兼ね備えた選択肢です。
小規模サイト(MAU 5,000人程度)でも、月$0-1でデータ分析基盤を構築できます。

この記事が、Cloudflareで小規模アプリを開発していてデータ分析基盤の構成を検討している皆さんのお役に立てれば幸いです。

https://adventar.org/calendars/11671

関連リンク:
Cloudflare R2
BigQuery Streaming
Workers Cron

Sun* Developers

Discussion