dbt-osmosis を PR Check に入れて、やらかした2つのこと

複数の dbt プロジェクトを一つのリポジトリで管理するデータ基盤を扱うことがありました。そこにおいてモデルの description などのメタデータは、人によって記入がまばらで、基本的には埋められていない状態でした。

そこでまず、dbt の document coverage を関連する PR のたびに出力して、どれくらい埋まっているかが見えるようにしました。ただ、メタデータを手で書いて埋めるのはとても面倒です。なので、機械で埋められる箇所はできるだけ自動で埋めることにしました。その仕組みとして、GitHub Actions で PR Check のワークフローに dbt-osmosis を入れていました。

この仕組みを運用する中で、やらかしが2つありました。

dbt-osmosis による自動化

dbt-osmosis は、yaml の雛形生成や、ウェアハウスには実在するのに yaml に書かれていないカラムの追加など、メタデータまわりをいろいろ面倒見てくれるツールです。その中で中心的に使っているのは、上流モデルの description を下流モデルへ継承する機能です。上流モデルの description は自分の手で埋める必要がありますが、一度埋めれば、同じカラム名を持つ下流モデルには自動で同じ値が入ってくれるので、とても楽になります。

ただし、下流でカラム名が変わると伝播しません。なので実際には、ソースからテーブル単位の前処理をする層までを dbt-osmosis の継承に任せて、複数のテーブルを集計してマートを作るような層は自分で埋める、という線引きになっています。もっとも、マート層の開発はコーディングエージェントで行っていて description も AI が埋めてくれるので、そこはあまり困っていません。

やらかし①: PR Check の実行時間

1つめのやらかしは、PR Check のたびに全モデルを対象にして dbt を実行していたことです。モデルの数が多いうえに、1つ1つのモデルの規模も大きいので、PR Check に1時間ほどかかるようになっていました。自分でも嫌になりましたし、他のメンバーからも指摘されました。

対応として Slim CI を入れました。本番の manifest.json と比較して、変更のあったモデルだけを state:modified+ で対象にして実行する形です。

# PR Check のイメージ(抜粋)
- name: Download production manifest
  run: gsutil cp gs://<bucket>/manifest.json ./prod-state/manifest.json

- name: Build models
  run: |
    if [ -f ./prod-state/manifest.json ]; then
      # Slim CI: 変更のあったモデルだけを実行
      dbt build --select state:modified+ --state ./prod-state
    else
      # manifest が取得できないときはフルビルドに切り替え
      dbt build
    fi

なお、この形で動くのは、CI のビルド先に上流モデルのテーブルがすでに存在しているためです。PR ごとに空のスキーマへビルドするような構成では、state:modified+ で選ばれなかった上流への ref が解決できずに落ちるので、--defer を付けて本番側のテーブルへ振り替えることになります。

比較元になる manifest.json は GCS のバケットに置いて、main ブランチへのマージ時に更新する形にしています。

# main マージ時のワークフローで manifest.json を生成して更新
- name: Generate manifest
  run: dbt compile

- name: Upload production manifest
  run: gsutil cp target/manifest.json gs://<bucket>/manifest.json

これで PR Check は数分程度で終わるようになりました。

やらかし②: 無関係な差分の混入

2つめは、dbt-osmosis yaml refactor --auto-apply をプロジェクト全体を対象に実行し、その結果が PR ブランチへ直接 push されるようにしてしまったことです。これにより PR の本来の変更とはまったく関係のない yaml の差分が混ざり込み、場合によっては100行を超えることもありました。特にウェアハウスからの欠損カラム追加は、それ自体は非常に助かる反面、こうした関係ない差分をよく作っていました。レビューする側でも、自分の PR を見返すときでも同じで、レビューのしづらい状態になっていました。

対応は2つです。1つは、PR 時は変更されたモデルだけを対象にすること。git diff で検出した変更 SQL のみを dbt-osmosis に渡すようにしました。

# PR で変更された SQL だけを dbt-osmosis の対象にする
# (checkout は fetch-depth: 0 で origin/main まで取得しておく)
changed_models=$(git diff --name-only --diff-filter=d origin/main...HEAD -- 'models/**/*.sql' \
  | xargs -n1 basename | sed 's/\.sql$//')

if [ -n "$changed_models" ]; then
  dbt-osmosis yaml refactor --auto-apply $changed_models
fi

--diff-filter=d を付けているのは、削除された SQL を除くためです。これがないと、もう存在しないモデル名が dbt-osmosis に渡ってエラーになります。

もう1つは、プロジェクト全体を対象にした refactor を weekly 等低頻度のワークフローに切り出すことです。GitHub Actions の定期実行で main への PR が作られるので、それを週次で人がマージするようにしました。

# weekly でプロジェクト全体を refactor して PR を作る
on:
  schedule:
    - cron: "0 0 * * 1" # 毎週月曜

jobs:
  refactor:
    steps:
      # ...
      - run: dbt-osmosis yaml refactor --auto-apply
      - uses: peter-evans/create-pull-request@v6
        with:
          title: "chore: weekly dbt-osmosis refactor"
          branch: chore/dbt-osmosis-weekly

dbt-osmosis 自体は、面倒なメタデータの記入をかなり楽にしてくれるツールです。これから組み込もうとしている方が同じやらかしをしないための、参考になれば嬉しいです。