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の管理とテストは、一時環境でのテスト実行パターンを活用することで以下を実現できました:
- Git管理: すべてのUDF変更をバージョン管理
- 自動テスト: 一時環境でのデプロイ→テスト→cleanup
- CI/CD統合: mainブランチへのpush時に自動デプロイ
特にcleanup処理の徹底(trap cleanup EXIT)が、実運用において重要なポイントでした。
同様の課題を抱えているチームの参考になれば幸いです。
参考リンク
さいごに
グループ研究開発本部 AI研究開発室では、データサイエンティスト/機械学習エンジニアを募集しています。ビッグデータの解析業務などAI研究開発室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。皆さんのご応募をお待ちしています。
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD


