dbt-project-evaluatorで独自のレイヤリングの制約をCI上で表現する

背景: 組織独自のレイヤリング構造の制約をCI上で表現したい

  • dbtを使ってデータのtransformを行なう場合、何かしらのレイヤリングを設計することが多いと思います
    • 例: データレイク => ステージング => dwh => マート
    • 例: staging => raw vault => business vault => fact & dim
  • レイヤリングを設計するということは設計したレイヤリングに沿わないデータリネージは基本的には許さない、ということを意味します
    • 例: マートからデータレイクを直接参照しない
  • 単純なリネージだけでなく、組織独自のリネージ上の制約を入れたい場合もあると思います
    • 例: adhocからadhocを参照させたくない
    • 例: business vaultからbusiness vaultを参照するのは許可するが、参照が多段になりbusiness vault内で例えば5段の参照関係がある、などは許可したくない
  • レビューの際に人間がこういった制約を毎回考慮するのは骨が折れます
    • 特に「チーム開発をしている場合」や「アナリティクスエンジニアリングには慣れているが、その組織での独自のレイヤリング構造のルールに慣れていない場合」などはこの制約を守り続けるのは大変です
    • SQLファイルをparseしてルールを守っているかを判断するスクリプトを書くこともできますが、運用がちょっと面倒ではあります
    • dbt test内で一緒に担保して欲しい
  • dbtが推奨するプロジェクト構成などの規約をテストするdbt-project-evaluatorというパッケージが存在しますが、自社のルールがこれにぴったり合わせるのが難しい場合もあると思います

課題: CI時にレイヤリングの情報をどう取得するか

  • elementaryを使うことで、dbtのモデルに対するメタデータやモデル間の情報は取得できるが、elementaryはon-run-endのタイミングで動く
  • そのため、Pull Requestを出してもらった際のCIが動くタイミングだと、新規で作られたdbtのモデルの情報がelementaryにまだ入っていないため、elementaryはこの課題には不向き

解決方法: dbt-project-evaluatorが内部で構築するテーブルを利用する

  • dbt-project-evaluatorが内部で構築するテーブルを利用すると、組織独自のレイヤリングの制約をSQLで簡単に書くことができます
  • dbt-project-evaluatorが内部で構築するテーブルの例1: int_all_dag_relationships
    • これはdbtのモデルの親子関係などdagの情報を集約したテーブルです
    • 直接親子関係がある場合だけでなく、A => B => Cというリネージがあった際のAとCのような間接的に影響があるような情報も入っています
      • A => B => Cのような情報はpath、2つ離れた関係であるという情報はdistanceという情報で入っています
      • また、参照元、参照先のモデルのidやファイル名、ディレクトリ名といった情報を取ることができます
        • 詳細は以下のスキーマを見てもらうと雰囲気が分かるかと思います
    • ref: Querying the DAG - dbt_project_evaluator
  • dbt-project-evaluatorが内部で構築するテーブルの例2: stg_nodes
    • int_all_dag_relationshipsがモデル間の情報を保持していたのに対し、stg_nodesはモデルのよりチッチな情報を保持します
    • ファイルの行数やyamlに埋め込まれているmetaの情報なども含まれます

これらのテーブルを使うと、組織独自の制約をSQLで簡単に表現することができます。例えば、「adhocからadhocを参照させたくない」という制約の場合は以下のように書けます。

select
    parent,
    child,
    distance,
from {{ ref("int_all_dag_relationships") }}
where
    parent_resource_type = "model"
    and parent_directory_path like "path/to/adhoc%"
    and child_directory_path like "path/to/adhoc%"

また、「同一のディレクトリ(レイヤ)内で多重の参照関係を許したくない」という場合の制約は以下のように書けます。

with
    ignore_depth_limit_nodes as (
        select unique_id
        from {{ ref("stg_nodes") }}
        where cast(json_extract_scalar(meta, '$.dbt_project_evaluator_ignore_depth_limit') as bool)
    )
select parent, child, distance,
from {{ ref("int_all_dag_relationships") }}
where
    parent_resource_type = "model"
    and parent_directory_path = child_directory_path
    and distance >= 3 -- 3はただの例です
    and child_id not in (select unique_id from ignore_depth_limit_nodes)

ここで、dbt_project_evaluator_ignore_depth_limitという謎の名前が出てきますが、これはyaml内で独自に設定する値になります。「新規で作るモデルはこの制約を満たしているものしか受け入れないようにしたい(傷口を広げたくない)が、既存のモデルはリファクタリングをしないと制約を満たせない。しかし、運用自体はしなければならない」という場合にdbt_project_evaluator_ignore_depth_limitが設定されているモデルはスキップさせる、というような柔軟な運用をすることができます。

version: 2
models:
  - name: my_child_model
    meta:
      dbt_project_evaluator_ignore_depth_limit: true

このテストはSingular data testsとして記述しておけばdbt test時に実行してくれるので、CI上のややこしい設定をせずSQLだけで制約を簡潔に済ますことができます。

補足: その他の設定

導入の際は以下も一緒に設定しました。

# BigQueryの場合、入れないと落ちてしまうので入れておく
# https://github.com/dbt-labs/dbt-project-evaluator/issues/421
dispatch:
  - macro_namespace: dbt
    search_order: ['dbt_project_evaluator', 'dbt']

# dbt-project-evaluatorで元から入っているテストで不要なものは無効にしておく
# https://dbt-labs.github.io/dbt-project-evaluator/latest/customization/customization/
models:
  dbt_project_evaluator:
    marts:
      dag:
        +enabled: false
      documentation:
        +enabled: false
      governance:
        +enabled: false
      performance:
        +enabled: false
      structure:
        +enabled: false
      tests:
        +enabled: false