Docker で dbt プロジェクトを始める方法をいくつか試してみた

## この記事は何？

Docker を使った dbt 環境の構築方法には様々なものがあります（公式の dbt イメージを使う、 Python イメージに dbt をインストールする、 etc...）。

この記事ではその中からいくつかをピックアップして、それぞれでの dbt プロジェクトの開始方法を試してみました。

## 筆者の実行環境

- MacBook Pro (M1)
- Docker (v24.0.2)
- Docker Compose (v2.18.1)

## 今回構築した dbt 環境

- dbt-bigquery (v1.5.1)

## 今回検討した方法

今回検討した方法は次のとおりです:

| # |  Docker イメージ |  認証方法 |
| --- | --- | --- |
| 1-1 |  公式イメージ（ [`ghcr.io/dbt-labs/dbt-bigquery`](https://github.com/dbt-labs/dbt-bigquery/pkgs/container/dbt-bigquery/100031920?tag=1.5.1) ） |  OAuth 認証 |
| 1-2 |  公式イメージ（ [`ghcr.io/dbt-labs/dbt-bigquery`](https://github.com/dbt-labs/dbt-bigquery/pkgs/container/dbt-bigquery/100031920?tag=1.5.1) ） |  サービスアカウント認証 |
| 2-1 |  Python イメージ（ `python:3.9` ） |  OAuth 認証 |
| 2-2 |  Python イメージ（ `python:3.9` ） |  サービスアカウント認証 |

なお、どの方法でもやることはおおよそ下記の通りで同じです:

1. docker-compose.yml など各種ファイルを作成
1. Docker コンテナを起動して `dbt init`
1. 作成された dbt プロジェクトディレクトリ内に 1. で作成した各種ファイルを移動（ Git 管理しやすくするため）

:::note
**補足**

手順 2. と 3. はセットで行えるので、今回は `dbt_init.sh` というシェルスクリプトでまとめて行うようにしています。
:::

## 構築した環境の動作チェック方法

今回、「構築した dbt 環境がちゃんと動くか？」の確認として次のチェックを行いました:

- `dbt debug` の実行結果に `All checks passed!` と表示される
- `dbt docs serve` でドキュメントが確認できる


:::note
**補足**


記事内では省略していますが、 `dbt docs serve` 実行前に `dbt docs generate` の実行が必要です。
:::

## 方法 1-1 ｜公式イメージ＆ OAuth 認証

### STEP 1 ｜ファイル作成

ディレクトリ構造を次のようにします:

```
.
|-- .env
|-- dbt_init.sh
|-- docker-compose.yml
|-- Dockerfile
|-- requirements.txt
```

各ファイルには次の内容を記載します:

```.env
WORKING_DIR=/usr/src/app
```

```dbt_init.sh
#!/bin/bash

PROJECT_NAME=$1
PJ_LOCATION=$2

dbt init $PROJECT_NAME
cat ~/.dbt/profiles.yml > $PROJECT_NAME/profiles.yml
sed -i 's/location: US/location: '$PJ_LOCATION'/' $PROJECT_NAME/profiles.yml
mv .env dbt_init.sh docker-compose.yml Dockerfile requirements.txt $PROJECT_NAME/

cat $PROJECT_NAME/profiles.yml
```

:::note
**補足 1 ｜　`cat ~/.dbt/profiles.yml > $PROJECT_NAME/profiles.yml` の役割**

`dbt init` で設定した接続情報は `~/.dbt/profiles.yml` へ書き出されます。しかし、今回の Docker のマウント設定だと `~/.dbt/profiles.yml` は Docker コンテナを落としたタイミングで消えてしまいます。

dbt では　 `profiles.yml` の探索順としてまず作業ディレクトリ内を探し、そこに無かった場合に `~/.dbt/profiles.yml` を読みに行く仕様となっています [^1] 。そこで、　　`cat ~/.dbt/profiles.yml > $PROJECT_NAME/profiles.yml`　を行うことで、次回以降 Docker コンテナを起動した時に以前の接続情報を利用できるようにしています。

[^1]: [Connection profiles | dbt Developer Hub](https://docs.getdbt.com/docs/core/connect-data-platform/connection-profiles) を参照
:::

:::note
**補足 2 ｜　`sed -i 's/location: US/location: '$PJ_LOCATION'/' $PROJECT_NAME/profiles.yml` の役割**

dbt-bigquery では `dbt init` 時に選択できる location は `US` と `EU` しかありません [^2] 。そのため、この2つ以外の location を設定したい場合は `dbt init` 完了後に `profiles.yml` を直接編集する必要があります。この操作を `sed -i 's/location: US/location: '$PJ_LOCATION'/' $PROJECT_NAME/profiles.yml` で行っています。

[^2]: `dbt init` 時に訊かれる `profiles.yml` 設定項目は `profile_template.yml` で定義されています（ [→参照](https://docs.getdbt.com/reference/commands/init#profile_templateyml) ）。 dbt-bigquery では次のように定義されています（ [→参照](https://github.com/dbt-labs/dbt-bigquery/blob/main/dbt/include/bigquery/profile_template.yml) ）。 dbt-bigquery の `profile_template.yml` を見てみると、 location の選択肢が　`US` と `EU` しか無いことが確認できます。
:::

```docker-compose.yml
version: "3.8"

services:
  app:
    build: .
    image: dbt_env_official_image_oauth
    container_name: dbt_env_official_image_oauth
    env_file:
      - ./.env
    volumes:
      - ./:$WORKING_DIR
    working_dir: $WORKING_DIR
    ports:
      - 8080:8080
    entrypoint: /bin/bash
```

:::note
**補足｜ `entrypoint: /bin/bash` の役割**

公式イメージの　Entrypoint は `dbt` に設定されています [^3] 。このままだと `dbt_init.sh` の実行が困難なので、 Entrypoint を `/bin/bash` へ変更しています。

[^3]: [Install with Docker | dbt Developer Hub](https://docs.getdbt.com/docs/core/docker-install#running-a-dbt-docker-image-in-a-container) を参照
:::



```Dockerfile:Dockerfile
# ベースイメージ
FROM ghcr.io/dbt-labs/dbt-bigquery:1.5.1

# 必要なファイルをコンテナ内へコピー
COPY requirements.txt ${WORKING_DIR}

# 依存のインストール
RUN apt-get update && apt-get upgrade -y && apt-get install -y \
    gnupg \
    curl

# gcloud CLI のインストール
RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] http://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key --keyring /usr/share/keyrings/cloud.google.gpg  add - && apt-get update -y && apt-get install google-cloud-cli -y

# Python ライブラリのインストール
RUN pip install --upgrade pip && \
    pip install -r requirements.txt
```

:::note
**補足｜ Docker イメージへの　gcloud コマンドのインストール**

下記公式ドキュメントに実行ステップが記載されています:

- [gcloud CLI をインストールする  |  Google Cloud](https://cloud.google.com/sdk/docs/install?hl=ja)
:::

```requirements.txt
# 好きなライブラリを記述してください
```

### STEP 2 ｜ dbt プロジェクトを初期化

次のコマンドを実行します:

```
$ docker compose build
$ docker compose run --rm app \
  ./dbt_init.sh <YOUR_DBT_PJ_NAME> <YOUR_GCP_PJ_LOCATION>
```

コマンドが完了したら次のようなディレクトリ構造になっているはずです:

```
.
|-- <YOUR_DBT_PJ_NAME>/
    |-- 〜〜（ dbt 関連のディレクトリやファイルは省略）〜〜
    |-- .env
    |-- dbt_init.sh
    |-- docker-compose.yml
    |-- Dockerfile
    |-- requirements.txt
```

### チェック 1 ｜ `dbt debug`


STEP 2 が完了した状態で `cd <YOUR_DBT_PJ_NAME>/` した後、次のコマンドを実行します:

```
$ docker compose run --rm --service-ports app
[In container]# gcloud auth application-default login
[In container]# dbt debug
```

実行結果が　`All checks passed!` になれば OK です。

:::note warn
`gcloud auth application-default login` はコンテナを起動する度に実行する必要があります（理由は補足 2 を参照）。
:::

:::note
**補足 1 ｜ `--service-ports` フラグをつける理由**

> ２つめの違いとして、 docker-compose run コマンドはサービス設定ファイルで指定したポートを作成しません。これは、既に開いているポートとの衝突を避けるためです。サービス用のポートを作成し、ホスト側に割り当てるには、 --service-ports フラグを使います。
> 引用: https://docs.docker.jp/compose/reference/run.html
:::

:::note
**補足 2 ｜ `gcloud auth application-default login` をコンテナ起動の度に実行する必要がある理由**

`gcloud auth application-default login` は GCP の Application Default Credentials (ADC) を設定するコマンドです [^4] 。これにより、 Docker コンテナ内でも gcloud コマンドを使って dbt の OAuth 認証が通るようにしています。

ADC ではローカルに認証情報を保存します。保存先はコンテナ内の `$HOME/.config/gcloud/application_default_credentials.json` です [^5] 。しかし、今回その場所は Docker の Volume で指定したり等をしていないので、コンテナを落とすたびに削除されてしまいます。そのため、コンテナ起動のたびにコマンドの実行が必要となります。

[^4]: [アプリケーションのデフォルト認証情報を設定する  |  Google Cloud](https://cloud.google.com/docs/authentication/provide-credentials-adc?hl=ja) を参照
[^5]: [アプリケーションのデフォルト認証情報の仕組み  |  Google Cloud](https://cloud.google.com/docs/authentication/application-default-credentials?hl=ja) 参照
:::

### チェック 2 ｜ `dbt docs serve`


Docker コンテナ内に入った状態で次のコマンドを実行します:

```
[In container]# dbt docs serve
```

ブラウザからドキュメントが確認できれば OK です。



## 方法 1-2 ｜公式イメージ＆サービスアカウント認証

### STEP 1 ｜ファイル作成


ディレクトリ構造を次のようにします:

```
.
|-- .env
|-- dbt_init.sh
|-- docker-compose.yml
|-- <GCP_SA_KEYFILE_NAME>.json
|-- requirements.txt
```

各ファイルには次の内容を記載します:

```.env
WORKING_DIR=/usr/src/app
```

```dbt_init.sh
#!/bin/bash

PROJECT_NAME=$1
PJ_LOCATION=$2
KEYFILE_NAME=$3

dbt init $PROJECT_NAME
cat ~/.dbt/profiles.yml > $PROJECT_NAME/profiles.yml
sed -i '/keyfile:/c\      keyfile: '$WORKING_DIR'/'$KEYFILE_NAME'' $PROJECT_NAME/profiles.yml
sed -i 's/location: US/location: '$PJ_LOCATION'/' $PROJECT_NAME/profiles.yml
mv .env dbt_init.sh docker-compose.yml $KEYFILE_NAME requirements.txt $PROJECT_NAME/

cat $PROJECT_NAME/profiles.yml
```

```docker-compose.yml
version: "3.8"

services:
  app:
    image: ghcr.io/dbt-labs/dbt-bigquery:1.5.1
    container_name: dbt_env_official_image_sa
    env_file:
      - ./.env
    volumes:
      - ./:$WORKING_DIR
    working_dir: $WORKING_DIR
    ports:
      - 8080:8080
```

```requirements.txt
# 好きなライブラリを記述してください
```

### STEP 2 ｜ dbt プロジェクトを初期化

次のコマンドを実行します:

```
$ docker compose run --rm --entrypoint /bin/bash app \
  ./dbt_init.sh <YOUR_DBT_PJ_NAME> <YOUR_GCP_PJ_LOCATION> <GCP_SA_KEYFILE_NAME>.json
```

dbt の初期設定に関する質問ですが、 `keyfile:` の回答については任意の文字列で問題ありません （ `dbt_init.sh` 内の処理で自動的に設定されるため）。

コマンドが完了したら次のようなディレクトリ構造になっているはずです:

```
.
|-- <YOUR_DBT_PJ_NAME>/
    |-- 〜〜（ dbt 関連のディレクトリやファイルは省略）〜〜
    |-- .env
    |-- dbt_init.sh
    |-- docker-compose.yml
    |-- <GCP_SA_KEYFILE_NAME>.json
    |-- requirements.txt
```

### チェック 1 ｜ `dbt debug`

STEP 2 が完了した状態で `cd <YOUR_DBT_PJ_NAME>/` した後、次のコマンドを実行します:

```
$ docker compose run --rm app debug
```

実行結果が　`All checks passed!` になれば OK です。

### チェック 2 ｜ `dbt docs serve`

次のコマンドを実行します:

```
$ docker compose run --rm --service-ports app docs serve
```

ブラウザからドキュメントが確認できれば OK です。



## 方法 2-1 ｜ Python イメージ＆ OAuth 認証

### STEP 1 ｜ファイル作成

ディレクトリ構造を次のようにします:

```
.
|-- .env
|-- dbt_init.sh
|-- docker-compose.yml
|-- Dockerfile
|-- requirements.txt
```

```.env
WORKING_DIR=/usr/src/app
```

```dbt_init.sh
#!/bin/bash

PROJECT_NAME=$1
PJ_LOCATION=$2

dbt init $PROJECT_NAME
cat ~/.dbt/profiles.yml > $PROJECT_NAME/profiles.yml
sed -i 's/location: US/location: '$PJ_LOCATION'/' $PROJECT_NAME/profiles.yml
mv .env dbt_init.sh docker-compose.yml Dockerfile requirements.txt $PROJECT_NAME/

cat $PROJECT_NAME/profiles.yml
```

```docker-compose.yml
version: "3.8"

services:
  app:
    build: .
    image: dbt_env_python_image_oauth
    container_name: dbt_env_python_image_oauth
    env_file:
      - ./.env
    volumes:
      - ./:$WORKING_DIR
    working_dir: $WORKING_DIR
    tty: true
    ports:
      - 8080:8080
```

```Dockerfile:Dockerfile
# ベースイメージ
FROM python:3.9

# 必要なファイルをコンテナ内へコピー
COPY requirements.txt ${WORKING_DIR}

# 依存のインストール
RUN apt-get update && apt-get upgrade -y

# gcloud CLI のインストール
RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] http://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key --keyring /usr/share/keyrings/cloud.google.gpg  add - && apt-get update -y && apt-get install google-cloud-cli -y

# Python ライブラリのインストール
RUN pip install --upgrade pip && \
    pip install -r requirements.txt
```

```requirements.txt
dbt-bigquery==1.5.1
```

### STEP 2 ｜ dbt プロジェクトを初期化

次のコマンドを実行します:

```
$ docker compose build
$ docker compose up -d
$ docker exec -it <CONTAINER_ID> bash
[In container]# chmod 744 ./dbt_init.sh
[In container]# ./dbt_init.sh <YOUR_DBT_PJ_NAME> <YOUR_GCP_PJ_LOCATION>
```

:::note
**補足｜ `docker compose run` で `dbt_init.sh` を実行しない理由**

`docker compose run` で `dbt_init.sh` を実行しようとしたところ Permission Denined と言われてしまったので、コンテナ内に入って直接実行する方法を採っています。

```:該当エラーメッセージ
Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "./dbt_init.sh": permission denied: unknown
```
:::

コマンドが完了したら次のようなディレクトリ構造になっているはずです:

```
.
|-- <YOUR_DBT_PJ_NAME>/
    |-- 〜〜（ dbt 関連のディレクトリやファイルは省略）〜〜
    |-- .env
    |-- dbt_init.sh
    |-- docker-compose.yml
    |-- Dockerfile
    |-- requirements.txt
```

### チェック 1 ｜ `dbt debug`

STEP 2 が完了した状態で `cd <YOUR_DBT_PJ_NAME>/` した後、次のコマンドを実行します:

```
[In container]# gcloud auth application-default login
[In container]# dbt debug
```

実行結果が　`All checks passed!` になれば OK です。

:::note warn
`gcloud auth application-default login` はコンテナを起動するたび実行する必要があります（理由は先ほどと同じ）。
:::

### チェック 2 ｜ `dbt docs serve`

Docker コンテナ内で次のコマンドを実行します:

```
[In container]# dbt docs serve
```

ブラウザからドキュメントが確認できれば OK です。



## 方法 2-2 ｜ Python イメージ＆サービスアカウント認証

### STEP 1 ｜ファイル作成

ディレクトリ構造を次のようにします:

```
.
|-- .env
|-- dbt_init.sh
|-- docker-compose.yml
|-- Dockerfile
|-- <GCP_SA_KEYFILE_NAME>.json
|-- requirements.txt
```

各ファイルには次の内容を記載します:

```.env
WORKING_DIR=/usr/src/app
```

```dbt_init.sh
#!/bin/bash

PROJECT_NAME=$1
PJ_LOCATION=$2
KEYFILE_NAME=$3

dbt init $PROJECT_NAME
cat ~/.dbt/profiles.yml > $PROJECT_NAME/profiles.yml
sed -i '/keyfile:/c\      keyfile: '$WORKING_DIR'/'$KEYFILE_NAME'' $PROJECT_NAME/profiles.yml
sed -i 's/location: US/location: '$PJ_LOCATION'/' $PROJECT_NAME/profiles.yml
mv .env dbt_init.sh docker-compose.yml Dockerfile $KEYFILE_NAME requirements.txt $PROJECT_NAME/

cat $PROJECT_NAME/profiles.yml
```

```Dockerfile:Dockerfile
# ベースイメージ
FROM python:3.9

# 必要なファイルをコンテナ内へコピー
COPY requirements.txt ${WORKING_DIR}

# 依存のインストール
RUN apt-get update && apt-get upgrade -y

# Python ライブラリのインストール
RUN pip install --upgrade pip && \
    pip install -r requirements.txt
```

```docker-compose.yml
version: "3.8"

services:
  app:
    build: .
    image: dbt_env_python_image_sa
    container_name: dbt_env_python_image_sa
    env_file:
      - ./.env
    volumes:
      - ./:$WORKING_DIR
    working_dir: $WORKING_DIR
    tty: true
    ports:
      - 8080:8080
```

```requirements.txt
dbt-bigquery==1.5.1
```

### STEP 2 ｜ dbt プロジェクトを初期化


次のコマンドを実行する:

```
$ docker compose build
$ docker compose up -d
$ docker exec -it <CONTAINER_ID> bash
[In container]# chmod 744 ./dbt_init.sh
[In container]# ./dbt_init.sh <YOUR_DBT_PJ_NAME> <YOUR_GCP_PJ_LOCATION> <GCP_SA_KEYFILE_NAME>.json
[In container]# exit
$ docker stop <CONTAINER_ID>
```

dbt の初期設定に関する質問ですが、 `keyfile:` の回答については任意の文字列で問題ありません（ `dbt_init.sh` 内の処理で自動的に設定されるため）。

コマンドが完了したら次のようなディレクトリ構造になっているはずです:

```
.
|-- <YOUR_DBT_PJ_NAME>/
    |-- 〜〜（ dbt 関連のディレクトリやファイルは省略）〜〜
    |-- .env
    |-- dbt_init.sh
    |-- docker-compose.yml
    |-- Dockerfile
    |-- <GCP_SA_KEYFILE_NAME>.json
    |-- requirements.txt
```

:::note
**補足｜なぜコンテナを落とす必要があるのか？**

`docker-compose.yml` でのマウント設定:

```
- ./:$WORKING_DIR
```

上記手順の `docker exec -it <CONTAINER_ID> bash` 実行直後のコンテナ内の状態:

```
$WORKING_DIR
|-- docker-compose.yml
|-- <GCP_SA_KEYFILE_NAME>.json
|-- ...
```

`dbt_init.sh` 実行直後のコンテナ内の状態。 `docker-compose.yml` などが dbt プロジェクトディレクトリ内へ移動しています:

```
$WORKING_DIR
|-- <YOUR_DBT_PJ_NAME>/
    |-- docker-compose.yml
    |-- <GCP_SA_KEYFILE_NAME>.json
    |-- ...
```

次回以降、コンテナを起動した時の状態。 dbt プロジェクトディレクトリが作業ディレクトリとしてマウントされています:

```
$WORKING_DIR
|-- docker-compose.yml
|-- <GCP_SA_KEYFILE_NAME>.json
|-- ...
```

`dbt_init.sh` では `keyfile:` を `<GCP_SA_KEYFILE_NAME>.json` （＝次回以降、コンテナを起動した時のパス）で設定しています。そのため、 `dbt_init.sh` 実行直後の状態だとパスが合いません。そこで、一旦コンテナを落とすことでこの問題を解決しています。

なお、コンテナ内で `docker-compose.yml` を移動させたため `docker compose down` ではコンテナを落とせなくなっています。そのため、 `docker stop` でコンテナを止めています。

（この「 Python イメージ＆サービスアカウント認証」の方法では、 `dbt_init.sh` でやるより手動で操作したほうが良いのかもしれません…）
:::

### チェック 1 ｜ `dbt debug`

STEP 2 が完了した状態で `cd <YOUR_DBT_PJ_NAME>/` した後、次のコマンドを実行する:

```
$ docker compose up -d
$ docker exec -it <CONTAINER_ID> bash
[In container]# dbt debug
```

実行結果が　`All checks passed!` になれば OK です。

### チェック 2 ｜ `dbt docs serve`


Docker コンテナ内に入った状態で次のコマンドを実行します:

```
[In container]# dbt docs serve
```

ブラウザからドキュメントが確認できれば OK です。



## 検討してみた所感

- 今回試した中だと方法 1-2 「公式イメージ＆サービスアカウント認証」が一番シンプルかなぁ…
    - Docker イメージのビルドが不要だし…
    - Docker コンテナ起動の度に認証求められたりもしないし…
        - これに関しては OAuth 認証でもっと良い方法がありそうな気はしているが…
    - `docker compose run --rm [--service-ports] app <DBT_COMMAND>` とコマンドが長いのはやっててそのうち面倒に感じ始めそう…
        - Makefile を使えば多少改善できる？
            - 引数が可変になってくると難しそう…
- とはいえ、　 venv で済ませられるならそれが一番手っ取り早い気はする



## 結び

今回は Docker を使って dbt プロジェクトを始める方法をいくつか試してみました。

ミスやより良い方法などがございましたら、コメント等でご指摘いただければ幸いです :bow: