GitHub ActionsでdbtのCI/CDを実装する②手動テスト

# はじめに
前回の記事ではdbtのドキュメントを参考にGitHub Actionsを用いてCI/CDを実装しました。

https://qiita.com/hiro-wa/items/5f5fb0f7fa4a2b98c4c0

上記の記事で作成したワークフローでは、dbtのjobが実行されるのはpull requestがマージされた時のみです。
しかし開発中に「現在のブランチでdbt jobの動作確認をしたい」という場面があると思います。
dbt Cloud上でブランチを指定してjobを実行する方法は、私が調べた中では見つけることはできませんでした。（もし知ってる方がいましたらご教授ください。）

dbt Cloud APIを利用すればブランチを指定できるので、この記事ではブランチを指定してdbt jobを実行するワークフローをGitHub Actionsで実装していきます。
pushの度にdbt jobを実行するのは頻度が高くなりすぎるので、今回は好きなタイミングで手動実行できるようにしようと思います。

# 概要
ワークフローの中身は前回の記事とほとんど同じ構成で、手動実行するように変更したワークフローを実装していきます。

# 実装
`.github/workflows/`配下に`run_job_manually.yml`というファイルを作成し、以下を記述します。
```yml:run_job_manually.yml
name: run dbt Cloud job manually

# dbt jobを手動で実行
# ブランチとjob IDを指定する必要あり
on:
  workflow_dispatch:
    inputs:
      job_id:
        type: string
        description: 'Job ID'
        required: true

jobs:
  # jobを実行するためにdbt Cloud APIを呼び出す
  run_dbt_cloud_job:
    name: Run dbt Cloud Job
    runs-on: ubuntu-latest

    # 実行に必要な環境変数を設定
    env:
      DBT_ACCOUNT_ID: 00000 # enter your account id
      DBT_PROJECT_ID: 00000 # enter your project id
      DBT_PR_JOB_ID:  ${{ inputs.job_id }}
      DBT_API_KEY: ${{ secrets.DBT_API_KEY }}
      DBT_JOB_CAUSE: 'GitHub Pipeline CI Job Manually' 
      DBT_JOB_BRANCH: ${{ github.ref_name }}

    steps:
      - uses: "actions/checkout@v3"
      - uses: "actions/setup-python@v2"
        with:
          python-version: "3.9"
      - name: Run dbt Cloud job
        run: "python python/run_and_monitor_dbt_job.py"
```
前回の記事で作成したワークフローとの違いは以下です。
- ワークフローを手動実行するためにトリガーイベントを[workflow_dispatch](https://docs.github.com/ja/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch)にして、入力としてjob_idを求めている点
- 環境変数の`DBT_PR_JOB_ID`に対して、ワークフロー実行時に入力されたジョブIDを渡している点

アカウントIDとプロジェクトIDを入力してマージまで行い、この変更をmainブランチに反映させておきます。

# 動作確認
実装するワークフローとしては以上のみですが、ここでは正しくブランチとdbtのjob IDを渡してテストできるかを確認していきます。

## 利用するデータ

私の環境では接続先としてSnowflakeを用いていて、ソースとなる`LINEAR`テーブルには以下のデータが入っています。
- `x`カラム: 0~99の100個の整数
- `y`カラム: xを2倍して、それに0~49の中からランダムに選んだ整数を足した値

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319284/dbef18b5-110b-ef19-e861-7582e59228cf.png" width="80%">

イメージとしては`y = 2x`の線形関数にノイズが加わったものです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319284/f4de9dba-5b12-7f87-d7c3-de1e4b299414.png" width="80%">

データの生成処理にはSnowflakeのpython worksheetsを利用しました。
具体的なコードは以下の記事で紹介しています。

https://qiita.com/hiro-wa/items/d23f0a1ffaa005cdea3c

## 実施するテスト

今回は`y`カラムに対して、以下の2つのテストを行います。
- `not_null`: nullがないこと
- `not_negative`: 負数がないこと

`not_null`はdbtのデフォルトのテストで、`not_negative`の方は自分でカスタムテストを作成していきます。
`not_null`ではテストが成功して、`not_negative`ではテストが失敗するようにしたいので、あらかじめデータの1つを負数に置き換えておきます。
```SQL:Snowflake worksheet
update SOURCE_DB.PUBLIC.LINEAR set "y" = -10 where "x" = 50;
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319284/474e247d-77cc-54d7-eb3b-532c3ad9702a.png" width="80%">


`x=50`のデータだけ`y`が負数になっているのが分かります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319284/39549409-0f1e-4cf7-3f26-f1717be7db9f.png" width="80%">

## dbt実装

次にdbtでモデル等を実装します。
sourceとしてSnowflakeのテーブルを指定し、
```yml:src.yml
version: 2

sources:
  - name: source_db_public
    database: source_db
    schema: public
    tables:
      - name: linear
```

Snowflakeから単純にデータを取ってくるだけのモデルを作成します。
```sql:linear_model.yml
select
    "x" as x,
    "y" as y
from
    {{ source("source_db_public", "linear") }}
```

まずは`y`カラムに`not_null`テストのみを設定します。
```yml:properties.yml
version: 2

models:
  - name: linear_model
    description: "線型データ"
    columns:
      - name: y
        description: "欠損値チェック"
        tests:
          - not_null:
              config:
                store_failures: true
```

ここで一度mainブランチに反映させておきます。
動作確認のため、この時点で手動でワークフローを実行しdbt jobをテストしてみると`not_null`テストのみが実施されますが、`y`カラムに欠損値はないためテストは成功します。
![スクリーンショット 2023-04-25 0.01.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319284/77e4f98a-cd2e-477d-209f-be6ee2fa91b1.png)

次に`not_negative`テストを加えていきます。
`tests/generic/`配下に`not_negative.sql`を追加し、対象モデルから負数のデータを取得する（= 負数があると失敗する）テストを追加します。
```sql:not_negative.sql
{% test not_negative(model, column_name) %}

select * from {{ model }} where {{ column_name }} < 0

{% endtest %}
```

そのテストも実施対象に含めるためにpropertiesファイルに追記します。
```yml:properties.yml
version: 2

models:
  - name: linear_model
    description: "線型データ"
    columns:
      - name: y
        description: "欠損値・負数チェック" # 負数のチェックも含めることを追加
        tests:
          - not_null:
              config:
                store_failures: true
          # -- 以下を追加 --
          - not_negative:
              config:
                store_failures: true
```

この状態でコミット&pushします。

pull requestを作成する前にここでGitHubからワークフローを実行し、今回のコミットで変更された内容についてdbt jobが成功するかを確認しておきます。

リポジトリのActionsページ上で、作業していたブランチを選択しJob IDにはテストしたいジョブIDを指定してワークフローを実行します。
![スクリーンショット 2023-04-23 4.30.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319284/18673a46-77e9-8164-1043-313dd7da016f.png)

私がここで指定しているjobは、特定のモデル（今回だと`linear_model`）に対して`dbt run`と`dbt test`を実行するだけの単純なものです。

ワークフローを実行するとエラーになりました。
![スクリーンショット 2023-04-25 0.02.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319284/563e1986-148b-7887-84af-d1aa82574f52.png)

ログを参考にdbt jobの履歴を見てみると、今回のコミットで作成した`not_negative`テストで失敗していることが分かります。
![スクリーンショット 2023-04-25 0.03.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319284/40a71db3-ae03-da49-bc2d-937da103d56f.png)

今回は`store_failures: true`を指定しテストで検知されたデータをテーブルに格納しているので、そのテーブルを見てみると`x=50`のデータが検知されていることが分かります。
![スクリーンショット 2023-04-23 4.34.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319284/83261542-8d96-151b-7983-acdaf9441ef4.png)

このようにワークフローやdbt jobがエラーになることで、実装したテストか使用したデータに問題があることが分かります。
今回はデータにわざと間違った値を入れているのでデータに問題があることが原因ですが、実装したテストに問題がある場合はpull requestを作る前にコード修正の必要性に気づけます。

# まとめ
この記事では手動でdbt jobを実行するワークフローを作成しました。
今回作成したコードは以下で参照できます。

https://github.com/hiro-wa/dbt-snowflake

pull request作成前にjobを動かせるのはエビデンス作りにも役立つのかなと思います。

私自身まだdbt歴が浅く、より良い運用方法を模索中なので、「こんな運用方法あるよ！」という方はぜひご意見ください。