30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

target や config を理解して dbt model の出力先を制御できるようになる

dbt を使おうとすると、profile や target、config、property など、様々な概念が出てくる。
それらをあまり理解できていなくても、何となく動かすことはできるかもしれない。
しかし、これらの概念を理解していないと、意図した通りに動かしたり他者が記述した設定内容を理解したりするのは難しい。

この記事では、最初は理解が難しいこれらの概念について、「model の出力先の設定」を題材にして説明していく。
dbt runした際にどこにテーブルやビューが出力されるのか、設定を読み解いて理解できるようになることを目指す。

この記事の内容は dbt のバージョン1.8.5で動作確認している。
データウェアハウスは BigQuery を使用する。

概要

どのデータウェアハウスのどのテーブルにあるデータを使うのか、データの出力先はどこになるのか。
そういった設定には「profile による基本設定」と「config による個別設定」の 2 つがあり、個別設定によって基本設定を上書きすることができる。
この仕組みによって、特定の model についてだけ出力先を変える、といったことが可能になる。

まずは基本設定から見ていく。

基本設定

dbt では profile という概念によって、データウェアハウスとの接続に関する情報を扱う。
そしてその profile について記述するためのファイルが、profiles.yml。このファイルに、どのデータウェアハウスを使うのか、どのデータベースを使うのか、といったことを記述していく。

以下は、データウェアハウスとして BigQuery を使い、dbt-project-a-432612というプロジェクトのdestデータセットに model を出力する場合のprofiles.yml

my_profile_1: # profile の名前
  target: dev # デフォルトで使う target の名前
  outputs:
    dev: # target の名前
      type: bigquery # 接続先のデータウェアハウスの種類
      method: oauth # 認証に使う方法
      project: dbt-project-a-432612 # 使用するプロジェクト
      dataset: dest # 使用するデータセット

ひとつの profile は、ひとつ以上の target を持つ。データウェアハウスについての具体的な情報は target に書く。target はprofiles.yml<profile-name>.outputsに書いていく。
上記の例では、devという target を定義している。
詳細は後述するが、ひとつの profile のなかで複数の target を定義できるため、デフォルトで使う target を指定しておくことができる。profiles.yml<profile-name>.targetで指定する。
上記の例ではtarget: devがそれにあたる。

上記の例では target で 4 つの項目を設定している。
このうちmethodは認証に関するものであり、この記事の内容との関係は薄いので割愛する。
詳細は、公式ドキュメントの以下のページで確認できる。
https://docs.getdbt.com/docs/core/connect-data-platform/bigquery-setup

重要なのはprojectdatasetである。これで、接続先を指定している。
項目名はデータウェアハウスによって異なるので注意する。例えば Snowflake の場合はdatabaseschemaを使って設定を行う。
https://docs.getdbt.com/reference/dbt-jinja-functions/target

projectは、データの出力先として使われるだけでなく、source の参照先としても使われる。
例えば以下のように source を定義してfrom {{ source('src', 'user') }}とする場合、dbt-project-a-432612プロジェクトのsrcデータセットのuserテーブルが使われる。

version: 2

sources:
  - name: src # dataset を省略した場合は name が dataset として使われる
    tables:
      - name: user
        columns:
          - name: id
          - name: name

dbt-project-a-432612.src.userを用意し、それを参照しているfooという model を書いて$ dbt run -s fooを実行してみると……。
エラーになる。

$ dbt run -s foo
15:48:52  Running with dbt=1.8.5
15:48:52  Encountered an error:
Runtime Error
  No dbt_project.yml found at expected path <カレントディレクトリのパス>/dbt_project.yml
  Verify that each entry within packages.yml (and their transitive dependencies) contains a file named dbt_project.yml

dbt runを実行するためには、dbt_project.ymlを用意する必要がある。そして、dbt_project.ymlで、どの profile を使うか指定しなければならない。

name: 'my_dbt_project'
config-version: 2
version: '1.0.0'

profile: 'my_profile_1' # profiles.yml で定義した my_profile_1 を指定

nameversionに、この dbt プロジェクトの名前とバージョンを指定する。config-version2にする決まり。
そしてprofileキーに、使用する profile を指定する。

この状態で$ dbt run -s fooすると今度は上手くいき、dbt-project-a-432612.dest.fooというビューが作られる。

target の使い分け

ひとつの profile に対して、複数の target を定義できる。
以下の例では、先ほど定義したdevに加えて、prodという target を定義した。そしてprodではdbt-project-b-432615プロジェクトを使うと定義している。

my_profile_1: # profile の名前
  target: dev # デフォルトで使う target の名前
  outputs:
    dev: # target の名前
      type: bigquery
      method: oauth
      project: dbt-project-a-432612
      dataset: dest
    prod: # target の名前
      type: bigquery
      method: oauth
      project: dbt-project-b-432615
      dataset: dest

dbt runする際に--targetフラグを使うことで、使用する target を指定できる。
例えば$ dbt run -s foo --target prodを実行すると、dbt-project-b-432615.dest.fooビューが作られる。その際はもちろん、dbt-project-b-432615.src.userが source として使われる。
--targetフラグを使わなかった場合は、<profile-name>.targetで指定した target (上記の例だとdev)が使われる。
--targetフラグを使わず<profile-name>.targetも定義されていなかった場合はエラーになる。

$ dbt run -s foo
16:57:30  Running with dbt=1.8.5
16:57:30  target not specified in profile 'my_profile_1', using 'default'
16:57:30  Encountered an error:
Runtime Error
  The profile 'my_profile_1' does not have a target named 'default'. The valid target names for this profile are:
   - dev
   - prod

config

model 毎に、その出力先を設定することができる。
その仕組みを理解するためにはまず、config という概念を理解する必要がある。

model に対しては property を設定できる(model だけでなく seed や test などにも property を設定できるが、この記事では割愛する)。
そして property の中には、 config と呼ばれる特殊な property がある。

config は以下の 3 つの方法で設定できる。

  1. config()関数
  2. 個別の model について記述した.ymlファイル
  3. dbt_project.yml

上記の順番で優先度が高く、ひとつの config に対して複数の方法で設定されていた場合、優先度が高い方法で設定した内容が採用される。
詳細はこれから具体例を用いて説明していくので、config には複数の設定方法がある、優先順位が決まっている、ということをまずは覚えておけばよい。

model に設定できる config のうち、model の出力先に関係するのがdatabaseschema
BigQuery の場合はprojectdatasetという config も設定できる。それぞれ、databaseschemaと互換性がある。
BigQuery の用語(プロジェクトとデータセット)と合わせたほうが分かりやすいので、この記事ではprojectdatasetを使うことにする。

dbt_project.yml で config を設定する

まずはdbt_project.ymlproject config を使ってみる。

name: 'my_dbt_project'
config-version: 2
version: '1.0.0'

profile: 'my_profile_1'

models:
  my_dbt_project: # name で指定した project の名前を書く
    marts: # marts ディレクトリ以下にある model は……
      +project: dbt-project-x # dbt-project-x に出力される

model に対する config はmodels:の下に書いていく。まずは project の名前を書き、その下に具体的な設定を書いていく。
今回はmartsディレクトリ以下の model に対して config を設定したいので、まずはmartsと書く。そして、+<config名>: 設定内容を書く。上記の例ではproject config にdbt-project-xを設定している。

こうすると、martsディレクトリ以下にある model はdbt-project-xに出力されるようになる。

例えばmodels/の中身が以下のときに$ dbt run --target devすると、dbt-project-a-432612.dest.foodbt-project-x.dest.barが作られる。

models
├── foo.sql
├── marts
│   └── bar.sql
└── sources.yml

target としてdevを指定したので、profiles.ymlに記した内容に基づき、接続先はdbt-project-a-432612.destになる。
しかしmarts以下のディレクトリについてはdbt-project-xに出力するようにdbt_project.ymlに記したので、その設定が優先される。datasetについてはdbt_project.ymlで特に設定していないので、devで設定したdestがそのまま使われる。
そのため、marts/bar.sqlの実行結果がdbt-project-x.dest.barに書き込まれるのである。

次はdatasetも使ってみる。
dbt_project.yml+dataset: my_suffix_1を書き加える。

name: 'my_dbt_project'
config-version: 2
version: '1.0.0'

profile: 'my_profile_1'

models:
  my_dbt_project: # name で指定した project の名前を書く
    marts: # marts ディレクトリ以下にある model は……
      +project: dbt-project-x # dbt-project-x に出力される
      +dataset: my_suffix_1 # target.dataset に _my_suffix_1 という接尾辞をつけたデータセットに出力される

この状態で$ dbt run --target devすると、dbt-project-a-432612.dest.foodbt-project-x.dest_my_suffix_1.barが出力される。
my_suffix_1というデータセットに出力されるのではなく、target.dataset(今回の場合dest)とmy_suffix_1_でつないだデータセットに出力されるので、注意が必要。

model のprojectdataset config で設定しているのはあくまでも、「model の出力先の設定」である。「source としてどのデータベースを参照するか」には影響を与えない。
そのためのfoobarも、from {{ source('src', 'user') }}している場合はdbt-project-a-432612.src.userを参照するので注意する。

config を入れ子にする

以下のように config をネストさせていくことも可能。

name: 'my_dbt_project'
config-version: 2
version: '1.0.0'

profile: 'my_profile_1'

models:
  my_dbt_project: # name で指定した project の名前を書く
    marts: # marts ディレクトリ以下にある model は……
      +project: dbt-project-x # dbt-project-x に出力される
      +dataset: my_suffix_1 # target.dataset に _my_suffix_1 という接尾辞をつけたデータセットに出力される
      special: # marts/special ディレクトリ以下にある model は……
        +dataset: my_suffix_2 # target.dataset に _my_suffix_2 という接尾辞をつけたデータセットに出力される

このように書くと、marts/specialディレクトリ以下にある model はdest_my_suffix_2データセットに出力されるようになる。

marts/special/baz.sqlを用意して確認してみる。

models
├── foo.sql
├── marts
│   ├── bar.sql
│   └── special
│       └── baz.sql
└── sources.yml

この状態で$ dbt run --target devすると、以下のビューが作られる。

  • dbt-project-a-432612.dest.foo
  • dbt-project-x.dest_my_suffix_1.bar
  • dbt-project-x.dest_my_suffix_2.baz

specialで設定したのはdatasetのみなので、projectmartsで設定したdbt-project-xmarts/special/baz.sqlにも適用される。

個別の model について記述した .yml ファイルで config を設定する

model と同じディレクトリに.ymlファイルを用意し、そこに個別の model に対する property を記述することができる。
config も property の一部なので、projectdatasetもそのファイルに書くことができる。

例として、models/marts/special/qux.sqlという model を用意し、その model について記述したmodels/marts/special/qux.ymlを用意する。
models/marts/special/qux.ymlには以下の内容を書く。

version: 2

models:
  - name: qux
    config:
      project: dbt-project-y

quxprojectdbt-project-yにしている。
datasetは特に上書きしていないので、dbt_project.ymlの内容がそのまま使われる。qux.sqlmarts/special以下にあるので、今回の例だとmy_suffix_2になる。
その結果、models/marts/special/qux.sqlの内容はdbt-project-y.dest_my_suffix_2.quxに出力される。

このように、個別の model について記述した.ymlファイルで設定した内容は、dbt_project.ymlの内容よりも優先される。

config() 関数で config を設定する

最後に、最も優先順位が高いconfig()関数を使った方法について述べる。

先ほど用意したmodels/marts/special/qux.sqlの内容を以下のようにする。

{{ config(
  project='dbt-project-z'
) }}
select name from {{ source('src', 'user') }}

そうすると、出力先はdbt-project-z.dest_my_suffix_2.quxになる。
このように、.ymlで定義した内容(今回の例だとproject: dbt-project-y)よりも、config()関数の内容(今回の例だとproject='dbt-project-z')が優先される。

generate_database_name と generate_schema_name

ここまで、model の出力先がどのように決まるのか見てきたが、その挙動は実は macro によって制御されている。
dbt には予めgenerate_database_nameという macro とgenerate_schema_nameという marco が用意されている。
そしてこれらの macro はdbt runなどを実行したときに暗黙的に呼び出され、そこに書かれているロジックによって macro の出力先が決まる。

そしてどちらの macro も、自分で定義して既存の振る舞いを上書きすることができる。

generate_database_name

generate_database_nameは、出力先の database を決めるための macro 。projectdatabaseと互換性があるため、databaseはそのままprojectに読み替えてしまって問題ない。
デフォルトの実装は以下のようになっている。

{% macro generate_database_name(custom_database_name=none, node=none) -%}

    {%- set default_database = target.database -%}
    {%- if custom_database_name is none -%}

        {{ default_database }}

    {%- else -%}

        {{ custom_database_name | trim }}

    {%- endif -%}

{%- endmacro %}

引数のcustom_database_nameは、 model に設定されたdatabase(もしくはproject) config 。
これが設定されない場合はdefault_database、つまりtarget.database(BigQuery の場合はtarget.project)が使われる。
custom_database_nameが設定されている場合は、それがそのまま(trimだけして)使われる。

このように、この記事でここまで説明してきた挙動は、この macro によって定義されているのである。

generate_schema_name

generate_schema_nameも同様に、出力先の schema を決定するロジックを定義している。datasetschemaと互換性があるため、schemaはそのままdatasetに読み替えてしまって問題ない。
デフォルトの実装は以下。

{% macro generate_schema_name(custom_schema_name, node) -%}

    {%- set default_schema = target.schema -%}
    {%- if custom_schema_name is none -%}

        {{ default_schema }}

    {%- else -%}

        {{ default_schema }}_{{ custom_schema_name | trim }}

    {%- endif -%}

{%- endmacro %}

独自定義によるオーバーライド

macros/generate_database_name.sqlあるいはmacros/generate_schema_name.sqlを定義することで、挙動を上書きすることができる。

例として、以下の内容でmacros/generate_database_name.sqlを作成して、出力先の database(もしくは project)を決めるロジックを自分で定義してみる。

{% macro generate_database_name(custom_database_name=none, node=none) -%}

    {{ 'dbt-project-y' }}

{%- endmacro %}

このように定義してdbt runすると、target や config でprojectをどのように設定していても、その設定は使われず、全ての model がdbt-project-yに出力されるようになる。

source の接続先

projectdatasetは source にも設定できる。
しかし source に対してはgenerate_database_namegenerate_schema_nameが実行されることはなく、projectdatasetに設定した値がそのまま使われる。
そのため、以下のように設定した状態でfrom {{ source('src', 'user') }}とすると、dbt-project-b-432615.src.userが参照される。

version: 2

sources:
  - name: src # dataset を省略した場合は name が dataset として使われる
    project: dbt-project-b-432615
    tables:
      - name: user
        columns:
          - name: id
          - name: name

target によって参照先を変えるということも可能。
以下のようにすると、devのときはdbt-project-a-432612を、prodのときはdbt-project-b-432615を参照するようになる。

version: 2

sources:
  - name: src
    project: |
      {%- if  target.name == "dev" -%} dbt-project-a-432612
      {%- elif target.name == "prod" -%} dbt-project-b-432615
      {%- else -%} invalid_database
      {%- endif -%}
    tables:
      - name: user
        columns:
          - name: id
          - name: name

そして例えばprofiles.ymlを以下のようにしておくと、--target devdbt runしたときはdbt-project-a-432612.src.userを参照してdbt-project-xに model を出力し、--target prodのときはdbt-project-b-432615.src.userを参照してdbt-project-yに model を出力するようになる。
config で model の出力先を設定している場合は、もちろんそれが採用される。

my_profile_1: # profile の名前
  target: dev # デフォルトで使う target の名前
  outputs:
    dev: # target の名前
      type: bigquery
      method: oauth
      project: dbt-project-x
      dataset: dest
    prod: # target の名前
      type: bigquery
      method: oauth
      project: dbt-project-y
      dataset: dest

参考資料

Embulk に入門して Amazon RDS にあるデータを BigQuery に転送する

この記事では、Embulk を使ってデータ転送を行う方法について述べていく。
今回は題材として Amazon RDS から Google Cloud の BigQuery にデータを転送する。Embulk の実行はローカルマシンで行う。

使っている Embulk のバージョンは0.9.25
0.100.11だと異なる手順や設定が必要になると思われるので注意。

事前準備

まずは Amazon RDS のインスタンスを作成する。今回はデータベースの種類は MySQL にした。バージョンは8.0.35
ローカルマシンから Embulk を実行する都合上、パブリックアクセスを「あり」にしておく必要がある。そして、セキュリティグループを適切に設定し、ローカルマシンの IP アドレスからのアクセスを許可しておく必要もある。
作成後は$ docker run -it --rm mysql mysql -u admin -p -h <RDSインスタンスのエンドポイント>でインスタンスにログインし、以下のクエリを実行する。

CREATE DATABASE source_db;
USE source_db;
CREATE TABLE user (
  id INT NOT NULL AUTO_INCREMENT,
  name VARCHAR(191) NOT NULL,
  PRIMARY KEY (id)
);
INSERT INTO user (name) VALUES ('Alice'), ('Bob');

source_dbというデータベースを作り、そのなかにuserテーブルを作成、以下のデータを投入した。

mysql> SELECT * FROM user;
+----+-------+
| id | name  |
+----+-------+
|  1 | Alice |
|  2 | Bob   |
+----+-------+
2 rows in set (0.01 sec)

このデータを BigQuery に転送するのが今回の目的。

次に BigQuery 側の準備を行う。
任意の GCP プロジェクトの BigQuery にdestination_datasetというデータセットを用意しておく。テーブルは作らなくてよい。
RDS だけでなく BigQuery にアクセスする権限も必要なので、サービスアカウントキーを JSON 形式で発行しておく。

ローカルマシンで Embulk を実行できるようにする

今回は Docker を使って Embulk の実行環境を用意する。
Dockerfileinit_embulk_commands.sh、2 つのファイルを用意する。

まずはDockerfile

FROM openjdk:8-jdk

# embulk.jarをダウンロード
RUN mkdir -p /root/.embulk/bin && \
    curl -o /root/.embulk/bin/embulk.jar -L "https://dl.embulk.org/embulk-0.9.25.jar"

# 必要なgemをインストールするためのスクリプトを追加
COPY init_embulk_commands.sh /root/init_embulk_commands.sh
RUN chmod +x /root/init_embulk_commands.sh && /root/init_embulk_commands.sh

WORKDIR /workspace

# embulk実行のためのシェルコマンドを設定
RUN echo '#!/bin/bash\njava -classpath "/root/.embulk/bin/embulk.jar:/root/.embulk/lib/postgresql.jar" org.embulk.cli.Main "$@"' > /usr/local/bin/embulk && \
    chmod +x /usr/local/bin/embulk

CMD [ "embulk", "--version" ]

このなかでinit_embulk_commands.shをコピーしそれを実行している。
Dockerfileのコメントに書いたようにこれは、必要な gem をインストールするためのシェルスクリプト。
まずは以下の内容にする。

#!/bin/bash

# embulk.jarへのパス
EMBULK_JAR="/root/.embulk/bin/embulk.jar"

# 必要なgemをインストール
java -jar $EMBULK_JAR gem install embulk -v 0.9.25

この状態で$ docker build -t embulk-image .を実行して Docker image を作成する。

そしてその image から Docker container を作成・実行して Embulk のバージョンが表示されれば、Embulk の実行は成功。

$ docker run --rm  embulk-image
embulk 0.9.25

転送設定を記述し実行する

Embulk を動かすことに成功したので次は、転送元と転送先を設定し、実際に転送を行うようにする。

Embulk は多種多様なデータソースを対象としており、今回対象とした MySQL と BigQuery 以外にも、 Redshift や Snowflake なども対象にすることができる。
これを可能にしているのがプラグインシステムであり、対応するプラグインさえあれば、転送元や転送先を自由に設定できる。

今回の例では MySQL を転送元、BigQuery を転送先にするので、embulk-input-mysqlembulk-output-bigqueryというプラグインが必要になる。

init_embulk_commands.shを以下のように書き換えて、Docker image の作成時に必要なプラグインがインストールされるようにする。

#!/bin/bash

# embulk.jarへのパス
EMBULK_JAR="/root/.embulk/bin/embulk.jar"

# 必要なgemをインストール
java -jar $EMBULK_JAR gem install embulk -v 0.9.25
java -jar $EMBULK_JAR gem install embulk-input-mysql
java -jar $EMBULK_JAR gem install jwt -v 2.3.0
java -jar $EMBULK_JAR gem install multipart-post -v 2.1.1
java -jar $EMBULK_JAR gem install public_suffix -v 4.0.7
java -jar $EMBULK_JAR gem install mini_mime -v 1.0.2
java -jar $EMBULK_JAR gem install representable -v 3.0.4
java -jar $EMBULK_JAR gem install embulk-output-bigquery -v 0.6.4

使いたいプラグイン以外にも様々な gem をインストールしているが、それらは、embulk-output-bigqueryの依存関係を解決するために必要な gem。
必要な gem とそのバージョンは以下の記事を参考にした。
embulk-input-bigqueryのインストールでエラー - kikukawa's diary

そしてDockerfileCMD [ "embulk", "--version" ]CMD ["embulk", "run", "config.yml"]に書き換える。

config.ymlは Embulk の転送設定を記述するファイルであり、どのデータをどこに転送するのか記述していく。

今回は以下の内容のembulk/config.ymlを用意する。

in:
  type: mysql
  host: RDSインスタンスのエンドポイント
  port: 3306
  user: admin
  password: "rds password"
  database: source_db
  table: user
  select: "*"

out:
  type: bigquery
  mode: replace
  auth_method: json_key
  json_keyfile: "key.json"
  project_id: BigQueryのプロジェクト名
  dataset: destination_dataset
  table: user
  auto_create_table: true

inが転送元でoutが転送先。

out.json_keyfileにはサービスアカウントキーのパスを記述する。今回はkey.jsonにしたので、config.ymlと同じディレクトリ、つまりembulkディレクトリにkey.jsonとして置いておく。

つまり以下の構成になる。

.
├── Dockerfile
├── embulk
│   ├── config.yml
│   └── key.json
└── init_embulk_commands.sh

この状態で再び image を作り、そこから container を作成し実行する。container がconfig.ymlkey.jsonを利用できるように、-vオプションで Volume を作っている。

$ docker build -t embulk-image .
$ docker run --rm -v "$(pwd)/embulk:/workspace" embulk-image

そうするとdestination_datasetuserテーブルが作られ、RDS に入れておいたのと同じデータが入っている。