こんにちは!バクラク事業部 機械学習・データ部 データチームの@TrsNiumです。
弊社では、Google CloudのBigQuery上にデータ基盤を構築しています。BigQuery上に構築したデータ基盤は、dbt (Data Build Tool) を用いてELTします。ELTとは、Extract(抽出)、Load(ロード)、Transform(変換)の略で、データをソースから抽出し、BigQueryにロードし、その後にデータを変換するプロセスです。このプロセスにより、データ基盤利用者にとって利用しやすい形でデータをモデリングし提供しています。
しかし、本番環境でのデータ操作や変更は慎重に行う必要があります。なぜなら、本番環境のデータを参照しているBIダッシュボードや機械学習モデル、社内ツールなどに直接的な影響が出る可能性があるためです。これらのシステムは業務の意思決定や運用に欠かせないものであり、データの変更によって予期せぬトラブルや誤った判断が生じるリスクがあります。
さらに、弊社では、開発環境やステージング環境のデータ量は少なく、存在しているデータもSaaS間で整合性が取れたものではありませんでした。そのため、dbtの変更を本番環境にデプロイしてから初めてテストが失敗することに気づくという状況が頻繁に発生していました。この課題を解決するために、本番環境と同じデータを扱えるテスト環境を別途構築することが必要となりました。
この記事では、テスト環境の詳細とテスト環境へのデータのクローン/コピーについて紹介します。
dbtのテスト環境で確認していること
テスト環境では、以下のようなテストを実施しています
- dbt test: データのnullチェックや一意性(unique)の確認を行います。これにより、データの整合性を確保します。
- dbt run: 各種クエリが正しく実行可能であることを確認します。クエリがエラーなく実行されることを確認し、ビルドしたテーブルの品質を保証します。
これらのテストを適切に行うためには、本番環境と同様のデータが必要です。また、弊社ではBigQueryからSalesforceやGoogle SheetsなどへのリバースETLを行っており、データの一意性などを考慮する必要があります。しかし、開発環境やステージング環境には十分なデータが存在しないため、テストが不十分となることが課題となっていました。
テスト環境について
以下の図は、GitHub Actionsを利用して本番環境とテスト環境のデータ管理を行う仕組みを示しています。
1. データのクローン/コピー: GitHub Actions Workflowを使用して、テスト環境のデータは毎日定期的に本番環境からクローンまたはコピーされます (図の①)。
2. 利用者による変更のPush: 利用者は変更をGitHub上にPushします (図の②)。これにより、テスト環境でのdbtの実行やテストを行うGitHub Actions Workflowがトリガーされます。
3. GitHub Actions Workflow(テスト環境でのdbtの実行/test): Pushされた変更に基づいて、GitHub Actions Workflowがテスト環境でdbtの実行やテストを行います (図の③)。
アクセス制限
テスト環境にアクセスできるのは、CI/CDで使用するサービスアカウントとデータ基盤を管理する一部のユーザのみとしています。他のユーザーはCIなどを通じて間接的に利用することが可能です。これにより、セキュリティとデータの一貫性を確保しています。
- サービスアカウント: GitHub Actionsを通じてのみアクセスが許可されています。直接のアクセスは基本的にできません。
- データ基盤運用者: 一部の限られた個人にのみアクセス権限が付与されています。
Google Cloudプロジェクトレベルの分離
テスト環境は、Google Cloudプロジェクトレベルで本番環境と分けています。この分離により、テスト環境での変更が本番環境に影響を与えないようにしています。
データのコピー戦略
テスト環境のデータは、本番環境のデータを定期的にクローンまたはコピーすることで維持しています。このプロセスはGitHub Actions上で毎日行われます。テスト環境ではデータの更新(update)は行わず、常に本番環境と同じデータを使用するようにしています。これにより、テスト環境でのテスト結果が本番環境でも再現性を持つことを保証しています。
データのクローン/コピーについて
BigQueryのデータクローンとコピーについてですが、実テーブルに関してはテーブルのクローンを使用しています。クローンをすることで、テーブルのクローンコストなしでデータをコピーすることができます。クローンは実テーブルのみにしか行うことができないため、ViewやTable Functionなどは、SQLを発行しViewなどを作成するクエリを作成しています。これらのクエリ発行をするプロセスはPythonで書いており以下のようなスクリプトになっています。
なお、データのクローン/コピー後に全てのdbtモデルをビルドすることで、Table Functionをコピーする必要がなくなることもありますが、弊社ではクローン/コピー後にdbtをビルドしていないため、Table Functionをコピーする必要があります。
import logging from google.cloud import bigquery from typing import List, Callable def clone_table(source_table_ref, dest_table_ref): """ クローン元のテーブルをクローン先のテーブルに複製する関数です。 クローンを作成するSQLクエリを実行し、失敗した場合は再試行のためのキューに追加します。 """ query = f""" CREATE OR REPLACE TABLE `{dest_table_ref}` CLONE `{source_table_ref}` """ try: client = bigquery.Client() query_job = client.query(query) query_job.result() LOGGER.info(f'Successfully cloned table {source_table_ref} to {dest_table_ref}') except Exception: LOGGER.error(f'Failed to clone table {source_table_ref} to {dest_table_ref}', exc_info=True) def copy_view(source_table_ref, dest_table_ref, env): """ クローン元のビューをクローン先のビューに複製する関数です。 コピー元のビューではコピー元のGCPプロジェクトを参照するクエリになっているため、 それをクローン先のプロジェクトを参照するようにクエリを書き換えます。 クエリの書き換え後、ビューを作成するSQLクエリを実行し、失敗した場合は再試行のためのキューに追加します。 """ client = bigquery.Client() source_table = client.get_table(source_table_ref) view_query = source_table.view_query # コピー元のビューではコピー元のGCPプロジェクトを参照するクエリになっているため、 # それをクローン先のプロジェクトを参照するようにクエリを書き換えます。 for base_project_id, target_project_id in VIEW_QUERY_PROJECT_ID_MAPPING.items(): view_query = view_query.replace(base_project_id.format(env=env), target_project_id.format(env=env)) query = f""" CREATE OR REPLACE VIEW `{dest_table_ref}` AS {view_query} """ try: LOGGER.info(f'Copying view {source_table_ref} to {dest_table_ref}') query_job = client.query(query) query_job.result() LOGGER.info(f'Successfully copied view {source_table_ref} to {dest_table_ref}') except Exception: LOGGER.error(f'Failed to copy view {source_table_ref} to {dest_table_ref}', exc_info=True) def copy_table_function(source_function_ref, dest_function_ref, env): """ クローン元のテーブル関数をクローン先のテーブル関数に複製する関数です。 コピー元のテーブル関数ではコピー元のGCPプロジェクトを参照するクエリになっているため、 それをクローン先のプロジェクトを参照するようにクエリを書き換えます。 書き換えたクエリを用いて新たにテーブル関数を作成し、失敗した場合は再試行のためのキューに追加します。 """ client = bigquery.Client() try: if table_function_exists(client, dest_function_ref): client.delete_routine(dest_function_ref) LOGGER.info(f'Successfully deleted table function {dest_function_ref}') source_function = client.get_routine(source_function_ref) body = source_function.body # コピー元のテーブル関数ではコピー元のGCPプロジェクトを参照するクエリになっているため、 # それをクローン先のプロジェクトを参照するようにクエリを書き換えます。 for base_project_id, target_project_id in TABLE_FUNCTION_QUERY_PROJECT_ID_MAPPING.items(): body = body.replace(base_project_id.format(env=env), target_project_id.format(env=env)) routine = bigquery.Routine( dest_function_ref, type_=source_function.type_, language=source_function.language, body=source_function.body, arguments=source_function.arguments, ) client.create_routine(routine) LOGGER.info(f'Successfully copied table function {source_function_ref} to {dest_function_ref}') except Exception: LOGGER.error(f'Failed to copy table function {source_function_ref} to {dest_function_ref}', exc_info=True)
他にもデータセット、テーブル、Viewなどを一覧化し、全てのクローン/コピー操作を自動で実行する処理やリトライをする処理を組み合わせています。このスクリプトはGitHub Actions上で毎日実行され、本番環境の最新データをテスト環境に反映させる仕組みを確立しています。
クローンの利点
- コスト効率: クローンはデータの物理的なコピーを作成せず、既存のデータを参照するため、コストを抑えることができます。
- 高速: クローンの作成は非常に高速であり、大量のデータを短時間でテスト環境に反映することができます。
クローンの制限
- 実テーブルのみ対応: クローンは実テーブルに対してのみ行うことができます。ViewやTable Functionなどにはクローンを適用できません。
クローンの注意点
テーブルクローンを試みたけど諦めた方や、同じ仕組みを参考にして作ろうとする方には、データ量の増加に注意してください。 テーブルクローンは便利ですが、データ量が大きい場合にはコストの問題が発生する可能性があります。 テーブルクローン自体にはコストはかかりませんが、クローン元またはクローン先のテーブルでデータ変更が発生した場合、その変更したデータ量に対してコストがかかります。そのため、再クラスタリングや頻繁な更新が行われる場合、ストレージコストが予想以上に高くなることがあります。 データ量が大きい場合、パーティショニングなどを用いてデータを部分的にコピーする方法を検討してみてください。
詳しくはGoogle Cloudの公式ドキュメントを参照してください。
導入効果
GitHub Actionsを用いたテスト環境の導入により、以下のような効果が得られました
- 早期の問題発見: デプロイ前にテスト環境で問題を発見できるようになり、本番環境でのエラーを未然に防ぐことができました。
- 安心感の確保: テスト環境での問題発見が可能になったことで、データ基盤利用者は安心して新しいdbtモデルをデプロイできるようになりました。
- テスト結果の信頼性の確保: 本番環境と同じデータを使用することで、テスト結果の信頼性が向上しました。
まとめ
本番同様のデータを扱えるdbtテスト環境をBigQueryで構築することは、データ基盤の品質を確保し、開発効率を向上させるために非常に重要です。テスト環境を整備することで、データの正確性と一貫性を保ちながら、すばやい開発とデプロイを実現することができます。しかし、大量のデータを持つテーブルに対しては、今回紹介したアプローチの実現性を検討する必要があります。