はじめに
こんにちは、ML・データ部推薦基盤ブロックの栁澤(@i_125)です。私はZOZOのデータ基盤におけるデータガバナンス強化を実現するために、Analytics Engineerとして複数の部門を跨ぐプロジェクトチームに参加しています。本記事ではZOZOにおけるデータガバナンス上の課題と、その課題の解決策の1つとしてdbtを導入した話をご紹介します。
目次
背景
弊社のデータ基盤はGoogle CloudのBigQuery上に構築されています。データマート開発者はSQLファイルを用意しGitHubリポジトリにPull Requestを送ります。これによってCI/CDおよびCloud Composerのワークフローを通じてBigQuery上にテーブルを作成できます。このワークフローを実現しているCloud Composerの導入事例については以下の記事で紹介しているのでぜひご覧ください。
このような仕組みによってデータ利活用が促進され、アドホックな分析目的だけではなく、推薦システムやMarketing Automationなど様々な後続システムで利用されるようになりました。また各部署でダッシュボードによる可視化も積極的に行われ、現在は様々なBIツールから参照されています。主にLooker、Looker Studio、Power BIを利用しており、Looker(LookML)では一部データモデリングも行われています。
課題
データ利活用が進む中で、データ基盤におけるデータガバナンス上の課題が徐々に顕在化してきました。
データマートの乱立
前述の仕組みによってテーブル単体では一定のレビュープロセスを経たものがデータ基盤上に作成される一方で、以下の理由から徐々に似たような目的のデータマートが作成されるようになりました。
- 後続システム利用を想定した上で、体系的にモデリングされたデータが存在していない
- テーブルの数が多く似たようなテーブルの存在に気づかない
可視化・分析での利用を想定したデータマートとその利用方法を示したドキュメントは存在していましたが、後続システム利用を想定したデータマートは存在していませんでした。そのため後続システムの開発者は必要なデータマートをそれぞれ作成していました。
集計定義のばらつき
データマートの数に比例してクエリがDRYにならず、同じ指標のはずなのにテーブルによって集計定義が微妙に異なったり、集計定義を変更する際に変更漏れが発生したりしていました。
依存関係の洗い出しが困難
データマートの数の多さに加えて、各後続システム側で必要なワークフローを開発したことでより複雑な依存関係が生まれていました。それによってデータ連携やデータマート生成で障害が発生したり、データマート生成用のクエリを変更したりした場合に以下のような影響が発生していました。
- データマートを利用している後続システムが正常に稼働しない
- 正しい依存関係を全て洗い出すのに多大な工数を割いている
データモデリングツールの比較検討
これらの課題を解決するためには体系的なモデリングに則り品質担保されたデータの提供が必要です。この開発をスムーズに行うために、必要な要件を明らかにした上でツールを比較検討しました。
データ変換に関する要件
プロジェクトチームで議論した結果、データ変換に関する要件は以下となりました。
- 集計定義の集約
- 集計定義の変更履歴の追跡
- 実際に実行するクエリが分かる
- 依存関係や待機を考慮したワークフロー実行
- ワークフローのエラー検知
- ワークフローの部分的な自動リトライ
「集計定義の集約」「集計定義の変更履歴の追跡」という要件はデータモデリングを実践する上で一般的なものかと思います。一方、それ以外の要件はZOZOの事情に依るものです。
「実際に実行するクエリが分かる」については、データマートのモデリングを行うチームがクエリに書き慣れていた事もあり、実際に実行するクエリをイメージできる方がモデリングしやすかったためです。
残り3つの要件はデータマートの数の多さに関係しています。弊社のデータ基盤には20以上のソースシステムと1000以上のデータマートが存在しています。さらに後続システムでそのデータマートを参照しているので、データマート更新が遅延すると後続システムに影響します。そのため複雑な依存関係を解決しつつ効率的に更新するために、以下のような制御が必要です。
- 複数のソースシステムからの連携が完了次第、データマート更新を開始する
- データマートが依存しているソースシステムの連携が完了次第、データマート更新を開始する
- 失敗したデータマート更新のみ自動で指定回数リトライする
- 失敗したデータマートに依存しないデータマートは更新を続ける
ワークフローの実行イメージは以下のようになります。
データリネージュの可視化、データ品質監視、データ仕様のカタログ化など一見重要そうな要件が入っていないのは、既にDataplexの導入を検討していたためそちらでもカバーが可能と判断したためです。
データモデリングツールの選定
まずは集計定義の集約、集計定義の変更履歴の追跡という要件にフォーカスして以下の候補からデータモデリングツールを選定しました。
Dataformとdbtは一般的な選択肢です。また弊社ではこれまでも一部でLooker(LookML)を使ってディメンショナルモデリングを行っていたので候補に含めました。その際の導入事例については以下の記事で紹介しているのでぜひご覧ください。
要件を元に各ツールを比較すると以下のようになります。
要件 | Looker(LookML) | Dataform | dbt Cloud | dbt Core |
---|---|---|---|---|
集計定義の集約 | ○ | ○ | ○ | ○ |
集計定義の変更履歴の追跡 | ○ | ○ | ○ | ○ |
実際に実行するクエリが分かる | △ ※ | ○ | ○ | ○ |
依存関係や待機を考慮したワークフロー実行 | △ | △ | △ | △ |
ワークフローのエラー検知 | △ | △ | △ | △ |
ワークフローの部分的な自動リトライ | △ | △ | △ | △ |
※ ジョブ履歴から辿る必要がある。
既に利用していたLooker(LookML)を継続して利用することも考えられます。しかしLooker(LookML)は候補ツールの中で唯一、記法がYAMLベースとなります。そのため実際に実行するクエリが分かるかという点では他ツールに劣ります。
SQLブロックを使えば部分的にSQLベースの記法を実現できますが、ExploreやViewなどパーツ同士をJOINした結果をBigQueryに投げることもできます。そのため最終的にBigQuery上にどのようなクエリが投げられているのかは、その上層の設計に依ります。また、BigQueryのジョブ履歴を参照すれば実際に実行したクエリは分かります。しかしLooker(LookML)のUI上から簡単に遷移できなかったり、ジョブ履歴のクエリも改行等が挿入されていなかったりと人間にとって読みづらいなどの問題があります。
Looker(LookML)自体は、使いこなせばガバナンスを効かせやすいツールです。しかし今後データマートのモデリングを行うチームにとっては実際に実行するクエリをイメージできる記法が良いという意見が多く、他ツールが有力候補となりました。
問題は最後の3つの要件です。これらはどのデータモデリングツールでも単体では満たすことはできませんでした。そこで既に運用中のCloud Composerとの組み合わせで実現することにしました。
それではどのツールが最適でしょうか。実行制御を担うのはCloud Composer、集計定義や依存関係を管理するのはデータモデリングツールといった形で責務の分離をしたいです。しかしDataformには集計定義と依存関係だけをファイル出力する機能が存在しませんでした。またdbt Cloudはメリットとして実行環境や実行制御とセットになっている、IDEが付属している点が挙げられますが、今回はそれらを必要としなかったのと、デメリットとして利用料金がかかります。
そこでシンプルに集計定義と依存関係をファイル出力できるdbt Coreを採用しました。
レイヤリングによる責務の分離
実装方針
集計定義をDRYに集約するためレイヤリングによって責務を分離しました。メンテナンスができるだけ属人化しないように、dbt公式ドキュメントのbest-practicesをベースに以下の構成を取ることにしました。
- Sources:ソースシステムから連携された一次テーブル
- Staging:退避テーブル等同じスキーマ同士のUNIONおよび重複排除を実施
- Marts
- Core Marts:同じエンティティで、1対1または多対1の多重度のテーブル同士をJOINしたワイドテーブルを定義
- Intermediate:Core MartsとDomain Martsの中間層として複数のエンティティにまたがる集計ロジックを定義
- Domain Marts:各後続システムで必要なデータマートを定義
ベストプラクティスに則ると型変換やTimeZoneの統一などシンプルな変換処理はStagingで行うべきです。しかし弊社のデータ基盤ではStagingより前段のBigQuery格納時にこれらの処理を行っているため、Stagingでの実装は不要でした。また、今回はまず必要と思われる最低限のレイヤーを実装し、必要に応じて後からレイヤーを追加する等のリファクタリングをすることにしました。
今後の展望
データマートの数が非常に多いため、レイヤーによってはディメンショナルモデルを採用した方がよいかもしれません。またMartsの下層に位置するセマンティックレイヤーを導入するべきか、どのツールでどのように管理するのかについても決めていく必要があります。このあたりは今後の整理や運用する中で検討すべき事項としています。
dbtモデルを開発する上で工夫したこと
dbtモデルの開発効率化や品質担保のために以下の工夫をしました。
環境の分離
背景
Cloud Composerを使ったワークフローを開発するため、弊社には元々Dev/Stg/Prdの3環境が存在し、それぞれGoogle Cloudプロジェクトを分けていました。またdbtのモデル開発者とCloud Composerのワークフロー開発者は異なるロールなので、それぞれの環境やGitHubリポジトリを分けることで責務を明確にしたいと考えました。
工夫したこと
環境の分離については以下の方針としました。
- dbt開発者が自由にデプロイできるSandbox環境として、Google Cloudプロジェクトを新規作成
- dbt開発用のGitHubリポジトリを新規作成
dbt開発用のリポジトリでPull Requestをmain branchにマージすると、Cloud Composer開発用のリポジトリに対して自動でPull Requestが作成されます。Cloud Composer開発者は作成されたPull Requestをmain branchにマージすることで、dbtで定義した集計ロジックや依存関係をCloud Composerに適用します。
ダミーデータセットの生成
背景
dbtモデルをdbt run
で実体化したり、dbt test
でテストしたりする際に、Sources層として参照元のデータセットやテーブルが必要です。dbtのSource定義では本番環境のプロジェクトを直接参照できます。しかし弊社のデータ基盤ではガイドライン上、本番環境のデータを直接参照できません。そこで開発環境(Sandbox環境とDev環境)内に何らかの方法でデータセットやテーブルを生成する必要があります。
さらに弊社のデータ基盤の一部のテーブルでは会員の住所やメールアドレスといった秘密情報にあたるカラムにポリシータグを付与した上で保有しており、これをダミーデータに置き換える必要があります。ポリシータグの導入事例については以下の記事で紹介しているのでぜひご覧ください。
工夫したこと
開発環境(Sandbox環境とDev環境)内にビューを生成することで本番環境を間接的に参照するようにしました。その際にポリシータグ付きカラムはダミーデータに置換しました。また環境内で開発者同士の競合が起きないように、GitHubのbranchごとにデータセット単位でダミーデータを生成するdbt macrosを用意しました。
このdbt macrosはGitHub上でbranchを新規作成後、GitHub Actionsのworkflow_dispatch
イベントで手動実行します。BigQueryではビューの生成にはコストがかかりませんが、ダミーデータ生成が不要な対応の場合はCIの実行リソースが勿体無いので自動実行は避けました。
開発環境(Sandbox環境とDev環境)では参照するデータセットとデプロイ先となるデータセットをbranchごと・レイヤーごとに分岐させる必要があります。具体的には以下のように実装しました。
dbt_project.yml
ファイルでbranch名をプロジェクト変数として宣言します。
vars: branch_name: "development"
参照するデータセットはsources
ファイルでJinjaを用いて動的に定義しました。
version: 2 sources: - name: source_name database: | {%- if target.name == 'sandbox' -%} sandbox-project-name {%- elif target.name == 'dev' -%} dev-project-name {%- elif target.name == 'stg' -%} stg-project-name {%- elif target.name == 'prd' -%} prd-project-name {%- endif -%} schema: | {%- if target.name == 'sandbox' or target.name == 'dev' -%} dataset_name_{{ var('branch_name') }} {%- else -%} dataset_name {%- endif -%}
デプロイ先となるデータセットはdbt公式ドキュメントのAdvanced custom schema configurationを参考にハンドリングを実装しました。dbtで提供されているdbt macrosgenerate_schema_name
をオーバーライドしています。
{% macro generate_schema_name(custom_schema_name, node) -%} {%- set default_schema = target.schema -%} {%- if custom_schema_name is none -%} {{ default_schema }} {%- else -%} {%- if target.name == 'sandbox' or target.name == 'dev' -%} {{ default_schema }}_{{ custom_schema_name | trim }}_{{ var("branch_name") | trim }} {%- else -%} {{ default_schema }}_{{ custom_schema_name | trim }} {%- endif -%} {%- endif -%} {%- endmacro %}
さらにデプロイ先となるデータセットにレイヤー名を付与するため、dbt_project.yml
ファイルでディレクトリ構造に従ってカスタムスキーマを定義しました。この値は前述のdbt macrosgenerate_schema_name
に対してcustom_schema_name
として渡されます。
models: dbt_project_name: staging: +schema: stg marts: core: +schema: core domain: intermediate: +schema: int
dbtコマンドを実行する時は以下のように引数としてbranch名を渡します。
dbt run --vars "{'branch_name': 'issue_95'}" --target sandbox
これによってbranchごと・レイヤーごとにデータセットが生成されます。
SQLFluffを使ったフォーマット統一
可読性向上のためにSQLFluffを使ってフォーマットを統一するようにしました。main branchに対するPull Requestを作成すると、CIでSQLFluffを実行し、フォーマットに問題がある場合はエラーとなります。ちなみにSQLFluffを選択した理由はルールの細かなカスタマイズが可能なのと、もし将来的にdbt Cloudと併用する場合も同じルールの適用が可能なためです。
依存モデルを含むテスト
データマートを変更する際には、依存関係のあるダウンストリームのデータマートのテストも実行するようにしました。main branchに対するPull Request作成により、CIにてdbt test
を実行します。Warningレベルのログを含めたテスト結果がPull Request内にコメントで通知されます。
dbt Docsを使ったドキュメント生成
工夫したこと
各dbtモデルにはYAMLファイルにテーブルレベル、カラムレベルのdescriptionを記述できます。さらにdbt docs
コマンドによって、dbtモデルのdescriptionだけでなく、依存関係も可視化されたドキュメントサイトを自動生成できます。
今回Core Marts層およびIntermediate層の実装にあたって、バラついていた集計定義を統一するため利用者にヒアリングの上、調整をしたところもありました。そこで利用者が簡単に集計定義を確認できる様に以下の工夫をしました。
- persist_docsオプションを有効化することで、YAMLファイルで定義した内容をBigQueryのdescrptionにも反映
- main branchに対するPull Requestをマージすると、GitHub Pagesでホスティングしているdbt Docsサイトを更新
今後の展望
集計定義そのものはdescriptionに記述できても、その経緯や背景の詳細までは記述できません。現在そういった情報は社内ドキュメント管理ツールで管理しています。今後はdbtのドキュメント生成機能を拡張していくのか、それとも全く別のデータカタログツールを導入するのか、検討していきたいと考えています。
dbt導入に期待する効果
dbt導入はこれから実運用に入る段階のため期待する効果を以下に示します。
データマートの乱立が抑制される
後続システムでの利用も想定した上で体系的にモデリングしたデータと、dbt Docsによるドキュメントを提供することで、データマートの乱立が抑制されることを期待しています。ただし、もし今後データマートの整理を進める中でデータマート数をさらに抑制した方がよい場合は、レイヤーの追加や異なるモデリング手法などを検討します。
バラついていた集計定義の統一
Core Marts層またはIntermediate層で重要な指標や共通した指標を定義することで、下層ではそれらを参照すればよいため、集計定義を統一しやすい環境が整いました。今後は定期的に集計定義の見直しを行い、必要に応じてCore Marts層やIntermediate層を拡充していく予定です。
影響範囲の特定がスムーズになる
dbt Docsによって依存関係が可視化されるため、どのデータマートがどのデータマートを参照しているのかが明らかになります。これによって、データ連携やデータマート生成で障害が発生した場合やクエリを変更した場合の影響範囲の特定がスムーズになることを期待しています。
後続クエリの記述量が減る
これまで各データマートで似たようなJOINを繰り返し記述していましたが、Core Marts層またはIntermediate層を参照することで後続クエリの記述量が減ります。また後続クエリ側はJOIN数が減るのでクエリパフォーマンスの改善、ひいては実行時間や消費スロット削減等のコストメリットも見込めます。
品質担保されたデータマートを参照した開発が可能
各dbtモデルのテスト実装およびCI上での自動テストによって、後続のデータマート開発者は品質が担保されたデータマートを参照して開発できるようになります。
まとめ
本記事ではZOZOのデータ基盤でdbtを導入した話を紹介しました。ただdbtを導入しただけで全てが解決するわけではありませんが、dbtはサードパーティツールが充実していることもあり、ベストプラクティスを適用しやすくなったと感じます。既にデータ基盤を運用している組織でdbt導入を検討している方の手助けになれば幸いです。今後は社内のさまざまなプロダクトに対して品質が担保されたデータを提供できるよう、Marts層の拡充やモニタリングをはじめとする運用の最適化・自動化を進めていきたいと考えています。
ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。