Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

本番相当の環境でdbt runを安全に検証する

はじめに

こんにちは。技術本部研究開発部 Architectグループの田辺です。本記事は Sansan Advent Calendar 2024 の 1日目です。弊チームでは新規dbtモデル開発時に dbt run を検証環境で検証してから本番環境へ反映させていました。検証環境にあるデータ量は少なく、ソースとなるプロダクトデータを模したものも検証用のものであるため、そこで期待通りに動作しても本番環境にリリースしてから期待通りでない動作になることがありました。例えば、エッジケースによるデータの不整合・ファンアウトの発生・実際のデータ量に対してはパフォーマンスの問題があるクエリなどが挙げられます。そのため、本番相当の環境でdbt runを安全に検証することに取り組みました。本記事ではその取り組みに関してご紹介します。

開発環境のご紹介

まず初めに、開発環境である全社横断データ分析基盤のアーキテクチャをご紹介します。

全社横断データ分析基盤のアーキテクチャ図

Amazon Aurora などから S3, Cloud Storage を経由して BigQuery で管理しているデータレイクへプロダクトデータが蓄積されます。その後、dbt ツールを用いてトランスフォーム処理を行い、エンドユーザーが利用する形へデータを加工します。

コードはGitHubリポジトリにて管理されており、mainブランチに統合されたものが本番環境で動作します。 そのため、統合前にステージングプロジェクトで動作確認を行ってからmainブランチへ取り込みます。 しかし、ステージングプロジェクトの持つデータは本番環境のそれと比較して非常に少量だったり、ほとんどなかったりします。 そのため、本番相当の環境でdbt runを安全に検証したい動機がありました。

dbt run テスト

当該取り組みを行うにあたって、考慮したポイントが3つあります。

  • dbt run によって生成されたデータは一定期間で削除する必要がある
    • 検証用データとなるため、一定期間で削除する必要がある
  • 開発の影響があるモデルだけテストしたい
    • すべてのモデルを dbt run させるには非常に時間を要する懸念があるため、開発差分のモデルだけ実行したい
  • GitHub Actions を使いたい
    • GUI, CLIから簡単に実行ができて、その実行結果をPRなどから引用したい

dbt run によって生成されたデータは一定期間で削除する必要がある

dbt run によって生成されたデータに削除漏れの懸念が起きないよう、検証用のデータセットを作成し、デフォルトの有効期限を3日にしてデータがすぐに消えるようにしました。dbt run の実行結果はこのデータセットに格納することで、3日で削除されるため安全です。

検証用データセット。

resource "google_bigquery_dataset" "ci_test_dbt_results" {
  dataset_id                  = "ci_test_dbt_results"
  location                    = "asia-northeast1"
  default_table_expiration_ms = 3 * 24 * 60 * 60 * 1000 # 3 days
}

dbt run 実行時に次のようなコマンドを与えます。

dbt run --models "$CHANGED_FILES" --target prod --vars 'mode: prod_ci_test'

generate_schema.sqlにおいて、varsの値を読み込んで、mode: prod_ci_testを与えた時のみ当該データセットに結果を格納します。

{% macro generate_schema_name(custom_schema_name, node) -%}
    {%- if var('mode', '') == 'prod_ci_test' -%}
        {% set custom_schema_name = 'ci_test_dbt_results' -%}
    {%- endif -%}
{%- endmacro %}

私達のdbtプロジェクトに置いては、モデル名を${スキーマ名}___${テーブル名(エイリアス名)}で定義しています。 本番環境では、モデル名を解析し、異なるスキーマへモデルがデプロイされるようマクロを組んでいます。

本検証においてはデプロイ先のスキーマは固定です。 モデル名(${スキーマ名}___${テーブル名(エイリアス名)})がそのままテーブル名となるようにしています。

{% macro generate_alias_name(custom_alias_name=none, node=none) -%}
    {%- if var('mode', '') == 'prod_ci_test' -%}
        {% set custom_alias_name = node.name.split('___')[0] + '___' + custom_alias_name -%}
    {%- endif -%}
{% endmacro %}

私たちのコードでは node.name.split('___')[0]がデータセット名に該当しcustom_alias_nameがモデル名に該当しました。 ここまでで、特定データセットに向けて dbt run を実行することは実現できたと思います。

開発の影響があるモデルだけテストしたい

私たちには feature ブランチで、作成or変更を加えたモデルをテストしたい動機がありました。 そのようなモデルを検知するスクリプトは下記になります。

set -exo pipefail

git fetch origin main

# Gitの差分からdbtモデルファイルを取得
CHANGED_FILES=$(git diff origin/main...HEAD --name-only --relative | grep 'models/.*\.sql$' | xargs -I {} echo '@{}' | tr '\n' ' ')

if [ -z "$CHANGED_FILES" ]; then
  echo "No dbt model files changed."
  exit 0
fi

# dbt runを実行し、結果を指定のBigQueryデータセットへ出力
echo "Start dbt run phase"
if ! dbt run --models "$CHANGED_FILES" --target prod --vars 'mode: prod_ci_test'; then
  echo "Error running dbt model: $CHANGED_FILES"
  exit 1
fi

echo "dbt run completed for changed models."
CHANGED_FILES=$(git diff origin/main...HEAD --name-only --relative | grep 'models/.*\.sql$' | xargs -I {} echo '@{}' | tr '\n' ' ')

mainブランチからの変更差分モデルを収集すること、それを dbt run の引数に与える形への成形しています。

dbt run --models "$CHANGED_FILES" --target prod --vars 'mode: prod_ci_test'

変更差分に対して dbt run を実行しています。

このスクリプトによって、変更差分がないときやdbt runによって発生したエラーを拾うことが実現できます。 ここまでで、シェルスクリプトによる実行が実現できました。

GitHub Actions を使いたい

これをGitHub Actions で動かせるようにします。

name: dbt_run_test

# git におけるmainと分岐したところから現在のコミットまでのdbtモデルに関する差分を特定データセットへdbt runするワークフロー。

on:
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}

jobs:
  dbt-run:
    name: Run dbt models
    runs-on: ubuntu-latest
    permissions:
      contents: write
      id-token: write

    defaults:
      run:
        shell: bash
        working-directory: 'workflows/dbt'
    steps:
      - name: Check out source repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0 # コミットのdiffを取るために取得

      - id: 'auth'
        name: 'Authenticate to Google Cloud'
        uses: 'google-github-actions/auth@v2'
        with:
          workload_identity_provider: '${{secrets.GCP_WORKLOAD_IDENTITY_PROVIDER_PROD }}'
          service_account: '${{ secrets.GCP_SERVICE_ACCOUNT_PROD }}'

      - name: 'Setup gcloud'
        uses: 'google-github-actions/setup-gcloud@v2'

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
      - name: Install dbt
        run: |
          python -m pip install --upgrade pip
          pip install dbt-core==1.8.2 dbt-bigquery==1.8.2

      - name: dbt deps # for dbt run
        run: |
          dbt deps

      - name: dbt run
        run: |
          ./../../tools/dbt_run_for_changed_models.sh

workflow_dispatch をオンにすることで、web UI から手動実行もできますし、次のようにCLI実行も可能です。

gh workflow run "dbt_run_test" --ref ブランチ名

当該ワークフローが複数同時に並行起動されても問題にならないよう、concurrencyを適切に設定することを推奨します。git diff が取れるように fetch-depthを適切に設定してください。このようなGitHub Actionsワークフローで検証スクリプトを動かす事が実現できます。

まとめ

本記事では dbt runの検証を本番相当の環境で安全に検証する取り組みを紹介しました。これまでは新規モデルを開発してもリリースするまで十分な検証が難しく、リリースして本番環境で動作してから問題が発生することもありました。本成果物によって本番相当の環境で安全に検証しつつ、PRなどでGitHub Actionsの証跡をポイントできるようになりました。 チームからの声として、当該ワークフローによる開発体験向上の声は多かったです。

Future Work として、今のアプローチでは差分モデルが多数になったり、そもそも実行の重いモデルを実行するときに長大な実行時間を要したりといった懸念があります。 そのため、十分な検証性を維持しつつ実行時間をリーズナブルにすることが課題です。

Sansanは、創業以来着実に成長を続けており、その成長に伴い扱うデータ量も飛躍的に増加しています。データ基盤の整備やデータの利活用がますます重要性を増しており、エンジニアにとって非常に刺激的でやりがいのある環境が整っています。このようなフィールドで活躍したいとお考えの方は、ぜひエントリーをご検討ください。

© Sansan, Inc.