2025.12.26

BigQuery UDFのカオスをDataformでテスタブルにした話

どうも。S.Y.です。
野良コードが点在しがちなBigqueryのUDFを、いい感じに一元管理する方法をご紹介します。

TL;DR

  • 複数プロジェクトに散在していたBigQuery UDFをDataformリポジトリで一元管理
  • 一時環境パターンでUDFの自動テスト基盤を構築
  • CI/CDパイプラインで安全なデプロイメントを実現

背景: 管理されていないUDFの実態

私たちはBigQueryでのデータ分析に多数のUDF(ユーザー定義関数)を活用していました。バージョン比較、コード変換、各種計算処理など、24個のUDFが複数のBigQueryプロジェクトに散在している状態でした。

課題

  • テストできない: 変更の正しさを検証する手段がない
  • レビューできない: BigQueryコンソールから直接更新されるため、変更内容の追跡が不可能
  • 履歴が追えない: Gitのような変更管理の仕組みがない

一言で表現すると、各自が好き勝手に更新できる無法地帯でした。

解決方針: DataformでUDFを管理する

Dataformとは

Dataformは、BigQueryのデータパイプラインを管理するためのツールです。SQLベースでテーブル、ビュー、そしてUDF(ユーザー定義関数)を定義でき、Git管理とテストフレームワークが統合されています。

Dataformの主な特徴:

  • コードベース管理: すべてのBigQueryリソース(テーブル、ビュー、UDF)をコードとして管理
  • テストフレームワーク: dataform testコマンドで単体テストを実行可能
  • デプロイメント: dataform runコマンドでBigQueryに一括デプロイ
  • 依存関係管理: リソース間の依存関係を自動解決

なぜDataformを選んだのか

課題を解決するため、以下の要件を満たすツールとしてDataformを選択しました:

  • Git管理が自然に統合できる
  • BigQueryとネイティブに連携できる
  • テストフレームワークが公式提供されている
  • UDFだけでなく、将来的にテーブル定義も統合管理できる拡張性

最終的な構成

├── definitions/
│   ├── namespace_a/       # 名前空間A (11 UDFs)
│   │   ├── function_1.sqlx
│   │   └── ...
│   └── namespace_b/       # 名前空間B (13 UDFs)
│       ├── function_2.sqlx
│       └── ...
├── test_templates/        # テストケース定義
│   ├── namespace_a.js
│   └── namespace_b.js
├── includes/
│   └── unit_test_utils.js # Google公式テストフレームワーク
├── test_runner.sh         # テスト実行スクリプト
└── cloudbuild.yaml        # CI/CDパイプライン

技術的チャレンジ: BigQuery UDFのテスタビリティ

問題: UDFはデプロイ済みでないとテストできない

Dataformのテストフレームワークには制約があります。テスト対象のUDFが既にBigQueryにデプロイされている必要があるのです。

UDF定義 → BigQueryにデプロイ → テスト実行

これでは「デプロイ前にテストする」というCI/CDの基本原則が崩れます。本番環境に影響を与えずにテストするには、別のアプローチが必要でした。

解決策: 一時環境でのテスト実行

一時的なテスト専用データセットを作成し、そこにUDFをデプロイしてテストするというパターンを採用しました。

基本的なフロー

#!/bin/bash
set -e

# 1. 一時的なテストデータセット名を生成
TEST_DATASET="fn_test_${TIMESTAMP}_${GIT_HASH}"

# 2. BigQueryにテストデータセットを作成
bq mk --dataset "${TEST_DATASET}"

# 3. UDF定義ファイルを一時的に書き換え
cp -r definitions definitions_backup
find definitions -name "*.sqlx" -exec sed -i \
  's/dataset: "production_dataset"/dataset: "'${TEST_DATASET}'"/g' {} \;

# 4. テストデータセットにUDFをデプロイ
dataform run .

# 5. テストケースをコピーして実行
cp test_templates/*.js definitions/
dataform test .

# 6. cleanup処理

Cleanup処理の設計

テストが成功しても失敗しても、必ず環境をクリーンアップする必要があります。ローカルファイルが一時的な状態で残ると、次のCI/CD実行時に問題が起きるため、徹底したcleanup処理を実装しました。

cleanup() {
  # 1. テストデータセットを削除
  bq rm -r -f "${TEST_DATASET}" 2>/dev/null || true

  # 2. Dataform認証ファイルを削除
  rm -f .df-credentials.json

  # 3. 元のUDF定義ファイルを復元
  if [[ -d definitions_backup ]]; then
    rm -rf definitions
    mv definitions_backup definitions
  fi

  # 4. workflow_settings.yamlを復元
  if [[ -f workflow_settings.yaml.backup ]]; then
    mv workflow_settings.yaml.backup workflow_settings.yaml
  fi

  # 5. 一時ファイルを削除
  rm -rf definitions_source
  rm -f definitions/*/*.tmp
}

# エラー時も確実にcleanupを実行
trap cleanup EXIT

trap cleanup EXITの重要性

この1行により、スクリプトがどのように終了しても(成功、失敗、Ctrl+C中断など)、必ずcleanup処理が実行されます。これがないと:

  • テストデータセットがBigQueryに残り続ける → コスト増加
  • ローカルファイルが書き換わった状態で残る → 次のデプロイで本番環境を破壊

なぜBackup/Restoreが必要なのか

当初、「最後にgit checkoutすればいいのでは?」と考えましたが、これでは不十分です:

  • .df-credentials.jsonはgitignoreされている
  • workflow_settings.yamlも動的に書き換えている
  • 一時ファイル(.tmp)はgit管理外

そのため、明示的なbackup/restore処理を実装しました。

スクリプトの責務分離

test_runner.shから呼び出されるtest_udfs_with_temporary_deploy.shは、UDF操作に特化:

deploy_udfs() {
  # テストファイルを一時削除(デプロイ時に不要)
  rm -f definitions/*/test_cases.js

  # UDFをデプロイ
  dataform run . --timeout=10m
}

test_udfs() {
  # テンプレートからテストファイルを復元
  cp test_templates/namespace_a.js definitions/namespace_a/test_cases.js
  cp test_templates/namespace_b.js definitions/namespace_b/test_cases.js

  # テスト実行
  dataform test .
}

設計思想:

  • test_runner.sh: 環境管理(認証、データセット、cleanup)
  • test_udfs_with_temporary_deploy.sh: UDF操作(デプロイ、テスト)

CI環境での並列実行対応

複数のcommitが同時にpushされた場合、テストが並列実行される可能性があります。データセット名の衝突を防ぐため、タイムスタンプとGitハッシュを組み合わせています:

SHORT_SHA=$(date +%s)_$(git rev-parse --short HEAD)
TEST_DATASET="fn_test_${SHORT_SHA}"

これにより、各CI実行が独立したテストデータセットを持ち、互いに干渉しません。

CI/CD統合: Cloud Buildパイプライン

mainブランチへのpush時に自動実行される3ステップパイプラインを構築しました。

steps:
  # Step 1: 環境検証
  - name: 'gcr.io/cloud-builders/gcloud'
    id: 'verify-auth'
    entrypoint: 'bash'
    args:
      - '-c'
      - |
        set -e
        set -o pipefail
        gcloud auth list
        bq ls --project_id=$PROJECT_ID --max_results=5

  # Step 2: UDFテスト実行 (Quality Gate)
  - name: 'gcr.io/cloud-builders/gcloud'
    id: 'run-tests'
    entrypoint: 'bash'
    args:
      - '-c'
      - |
        set -e
        set -o pipefail
        npm install -g @dataform/cli
        ./test_runner.sh

  # Step 3: 本番デプロイ
  - name: 'gcr.io/cloud-builders/gcloud'
    id: 'deploy-udfs'
    entrypoint: 'bash'
    args:
      - '-c'
      - |
        set -e
        set -o pipefail
        npm install -g @dataform/cli
        dataform run . --timeout=10m
    waitFor: ['run-tests']

エラーハンドリングの強化

Cloud Build環境での信頼性を確保するため、以下の設定を徹底:

set -e            # コマンド失敗時に即座に終了
set -o pipefail   # パイプライン中の失敗も検出

これらがないと、例えばnpm installが失敗しても次のステップが実行され、わかりにくいエラーメッセージになってしまいます。

テストケースの実装例

Googleの公式テストフレームワークを使用してテストケースを定義しています。

JavaScript実装UDFのテスト

// test_templates/namespace_b.js
const { generate_udf_test } = unit_test_utils;

generate_udf_test("compare_version", [
  // 基本的な比較
  {
    inputs: ["'1.0.0'", "'1.0.1'"],
    expected_output: "-1"  // 1.0.0 < 1.0.1 }, { inputs: ["'2.0.0'", "'1.9.9'"], expected_output: "1" // 2.0.0 > 1.9.9
  },
  // 桁数の違い
  {
    inputs: ["'1.0'", "'1.0.0'"],
    expected_output: "-1"  // 1.0 < 1.0.0
  },
  // 先頭ゼロの扱い
  {
    inputs: ["'1.01.0'", "'1.1.0'"],
    expected_output: "0"   // 1.01.0 == 1.1.0
  }
]);

SQL実装UDFのテスト

// test_templates/namespace_a.js
generate_udf_test("normalize_version", [
  // 正常な変換
  {
    inputs: ["'1.2.3'"],
    expected_output: "10203"
  },
  {
    inputs: ["'10.15.7'"],
    expected_output: "101507"
  },
  // 無効なパターン
  {
    inputs: ["'1.2'"],
    expected_output: "NULL"
  },
  {
    inputs: ["'abc.def.ghi'"],
    expected_output: "NULL"
  }
]);

テスト実行例

$ ./test_runner.sh

=== BigQuery UDF Test Runner ===
Project ID: your-project
Test Dataset: fn_test_1735142400_a1b2c3d
Location: US
================================

Creating temporary test dataset...
✅ Dataset created

Deploying UDFs to test dataset...
✅ UDF deployment completed

Running UDF tests...
✅ All tests passed

Cleaning up...
✅ Cleanup completed

🎉 All UDF tests completed successfully!

得られた成果

定量的効果

  • デプロイ前の自動テスト実施率: 100%
  • コードレビューカバレッジ: 100%
  • 変更履歴の可視化: Git履歴で完全追跡可能

定性的効果

  • 安心してリファクタリング可能: テストがあるので変更の影響範囲を確認できる
  • プルリクエストベースのレビュー文化: チーム全体で品質を担保
  • 新メンバーのオンボーディング容易化: リポジトリを見ればUDFの全体像が把握できる

まとめ

BigQuery UDFの管理とテストは、一時環境でのテスト実行パターンを活用することで以下を実現できました:

  1. Git管理: すべてのUDF変更をバージョン管理
  2. 自動テスト: 一時環境でのデプロイ→テスト→cleanup
  3. CI/CD統合: mainブランチへのpush時に自動デプロイ

特にcleanup処理の徹底(trap cleanup EXIT)が、実運用において重要なポイントでした。

同様の課題を抱えているチームの参考になれば幸いです。


参考リンク

さいごに

グループ研究開発本部 AI研究開発室では、データサイエンティスト/機械学習エンジニアを募集しています。ビッグデータの解析業務などAI研究開発室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。皆さんのご応募をお待ちしています。

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事