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

# 概要
この記事では[dbtのドキュメント](https://docs.getdbt.com/guides/orchestration/custom-cicd-pipelines/1-cicd-background)を参考に、GitHub Actionsを用いてdbtプロジェクトのCI/CDを実装していきます。
実装するワークフローは以下の2つです。
1. **コミットのpush時に自動実行されるワークフロー**
    処理内容：SQLFluffを用いてLintする
2. **pull requestがマージされた時に自動実行されるワークフロー**
    処理内容：dbt Cloud APIを呼び出してdbtのjobを実行する

※ この記事では既にdbtプロジェクトが存在し、dbtのjobも作成済みの状態を前提としています。

# 1. コミットのpush時に自動実行されるワークフロー

早速1つ目のCIワークフローから実装していきます。

## dbtプロジェクトへの処理追加

まずプロジェクトのルートディレクトリに`.github`フォルダを作成し、さらにその中に`workflows`フォルダを作成し、そこに`lint_on_push.yml`というファイルを作成します。
```
my_project
├── .github
│   ├── workflows
│   │   └── lint_on_push.yml
```

`lint_on_push.yml`ファイルの中身には以下を記述します。
```yml:lint_on_push.yml
name: lint dbt project on push

on:
  push:
    branches-ignore:
      - 'main'

jobs:
# this job runs SQLFluff with a specific set of rules
  # note the dialect is set to Snowflake, so make that specific to your setup
  # details on linter rules: https://docs.sqlfluff.com/en/stable/rules.html
  lint_project:
    name: Run SQLFluff linter
    runs-on: ubuntu-latest
  
    steps:
      - uses: "actions/checkout@v3"
      - uses: "actions/setup-python@v2"
        with:
          python-version: "3.9"
      - name: Install SQLFluff
        run: "pip install sqlfluff==0.13.1"
      - name: Lint project
        run: "sqlfluff lint models --dialect snowflake --rules L019,L020,L021,L022"

```

## ワークフロー実行

コードの説明の前にまず実行してみます。
上記の内容でファイルを作成したらコミットしてpushすることで、ワークフローが実行されます。
GitHub上でリポジトリのActionsタブを見てみると、ワークフローが実行され、Lintが成功していることが分かります。
![スクリーンショット 2023-04-23 21.23.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319284/dfcdb6ee-53a8-30ed-c1c5-2bb7f1fbd8cc.png)

## コード説明

このワークフローでは、mainブランチ以外にpushされた時に[SQLFluff](https://docs.sqlfluff.com/en/stable/)というLinterを用いてLintを行っています。
※ Lint: ざっくり言うとコードのフォーマットなど、書き方に問題がないかチェックする処理

`sqlfluff lint models`によってmodelsディレクトリ配下のSQLに対してLintを行います。
`--dialect`オプションでベースとなるSQLとしてSnowflakeのSQLを指定していて、`--rules`オプションでは以下4つのルールをチェックするように指定しています。

> L019: カラムなどを複数指定するときにカンマの位置が行の末尾になっていること
  L020: エイリアスでテーブル名を付けたり複数DBからテーブルを参照する時に、テーブル名が一意になっていること
  L021: あるカラムに対してGROUP BYとDISTINCTをどちらとも指定してしまっていないこと
  L022: CTEの閉じ括弧の後に空行があること

※ これらの4つのルールは例として指定しているだけなので、どのルールを設定するかはチーム毎に決める必要があります。

SQLFluffが他にどんなルールを指定できるかは、ドキュメントの[ルールに関するページ](https://docs.sqlfluff.com/en/stable/rules.html)を参照してください。

今回指定した4つのルールのいずれかを違反したSQLの記述があった場合はLintがエラーになります。
試しにCTEを使用している適当なモデルで、以下のようにCTEの閉じ括弧下の空行を削除してコミット&pushしてみます。
![スクリーンショット 2023-04-23 21.14.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319284/3a464acf-0bc2-c06b-4132-c1c66194f0d2.png)

ワークフローの結果を確認してみると、Lintの実行がエラーになり、L022のルール（CTEの閉じ括弧の後に空行があること）を守れていないことが分かります。
![スクリーンショット 2023-04-23 21.10.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319284/12742147-51c8-2a50-2319-f5cc68213816.png)

Lintが成功するようにコードを戻したら2つ目のワークフロー作成に移ります。


# 2. pull requestがマージされた時に自動実行されるワークフロー
次は、mainブランチにpushされた時（= pull requestがマージされた時）にdbt Cloud APIを呼び出してdbtのjobを実行するワークフローを実装していきます。

## dbt Cloud APIキーの取得
dbt Cloud APIを呼び出すためにAPIキーが必要なので、取得していきます。
まずdbt Cloudの画面で *Account Settings* ページにアクセスし、ページ下部にある *Service Tokens* の「New Token」ボタンを押します。
`CICD Token`のような名前を入力し、「+Add」を押して`Job Admin`の権限を選択し、saveします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319284/da575d97-3865-1f00-f5cd-9f87df3a3f73.png" width="60%">

そうするとTokenが作成されるので、コピーして安全な場所で保管しておきます。

## GitHubのsecretへの登録

次に、取得したTokenをGitHubリポジトリのsecretに保存していきます。
リポジトリの *Settings* の *Secrets and Variables* ドロップダウンにある *Actions* ページで、「New repository secret」ボタンを押します。
![スクリーンショット 2023-04-24 0.56.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319284/d368bf26-12b2-66f3-b848-f4ada85381c0.png)

*Name* には`DBT_API_KEY`、*Secrets* には作成したTokenを入力し、「Add secret」ボタンを押せば完了です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319284/79c0503d-e6dc-933c-7395-744f37487bcd.png" width="70%">

## dbtプロジェクトへの処理追加

次はdbtプロジェクトの作業に入ります。
プロジェクトのルートディレクトリに`python`フォルダを作成し、`run_and_monitor_dbt_job.py`というファイルを作成し、この[gist](https://gist.github.com/b-per/f4942acb8584638e3be363cb87769b48)の内容をコピペします。

このpythonファイルでは、渡されたjob IDなどの情報をもとにdbt Cloud APIを呼び出し、jobが成功したら正常終了、失敗したらエラーを投げるというような処理を行います。

では`.github/workflows`配下に`dbt_run_on_merge.yml`を作成します。
この記事で追加したファイルの最終的な構成は以下のようになります。
```
my_project
├── python
│   └── run_and_monitor_dbt_job.py
├── .github
│   ├── workflows
│   │   └── dbt_run_on_merge.yml
│   │   └── lint_on_push.yml
```

`dbt_run_on_merge.yml`には以下の内容を記述します。
```yml:dbt_run_on_merge.yml
name: run dbt Cloud job on push

# This filter says only run this job when there is a push to the main branch
# This works off the assumption that you've restrictred this branch to only all PRs to push to the deafult branch
# Update the name to match the name of your default branch
on:
  push:
    branches:
      - 'main'

jobs:

  # the job calls the dbt Cloud API to run a job
  run_dbt_cloud_job:
    name: Run dbt Cloud Job
    runs-on: ubuntu-latest

  # Set the environment variables needed for the run
    env:
      DBT_ACCOUNT_ID: 00000 # enter your account id
      DBT_PROJECT_ID: 00000 # enter your project id
      DBT_PR_JOB_ID:  00000 # enter your job id
      DBT_API_KEY: ${{ secrets.DBT_API_KEY }}
      DBT_JOB_CAUSE: 'GitHub Pipeline CI Job' 
      DBT_JOB_BRANCH: ${{ github.ref_name }}

    steps:
      - uses: "actions/checkout@v3"
      - uses: "actions/setup-python@v4"
        with:
          python-version: "3.9"
      - name: Run dbt Cloud job
        run: "python python/run_and_monitor_dbt_job.py"
```
`DBT_ACCOUNT_ID`, `DBT_PROJECT_ID`, `DBT_PR_JOB_ID`には自分の状況にあったIDをそれぞれ入力します。
それぞれのIDはdbt CloudのURLから取得できるので、ブラウザでdbt Cloudのjobのページを開きURLの数字を見ると、左から`アカウントID`・`プロジェクトID`・`ジョブID`に対応しています。

このワークフローでは、mainブランチにpushされた時（= pull requestがマージされた時）に各IDやTokenなどの情報を環境変数としてセットし、先ほど作成したdbt Cloud API呼び出し用のpythonファイルを実行します。

## ワークフロー実行

ではコミット&push後、pull requestを作ってマージまで行ってみます。

マージ後にActionsタブを確認すると、作成したワークフローが実行され成功しています。
![スクリーンショット 2023-04-24 0.03.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319284/be3c86c5-69a0-c265-5204-ed625b27a654.png)

詳細を見てみると、作成したpythonファイルに記述された通りにログが表示され、dbt jobが成功していることも分かります。
![スクリーンショット 2023-04-24 0.02.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319284/10572a72-4d30-bc07-a870-55200ebd6a84.png)

dbt Cloudの方で実行されたjobを見ると、GitHubのCI Jobから実行されたことや、トリガーになったコミットなども分かります。
![スクリーンショット 2023-04-24 2.38.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319284/03c3778c-6023-3bbd-9422-1037d2c9514f.png)


# まとめ
この記事ではdbtのドキュメントを参考に以下のCI/CDを実装しました。
- push時にLintを行う
- pull requestのマージ時にdbt Cloud jobを実行する