🍊

dbtコマンドを制限するマクロで事故を防ぐ

に公開

はじめに

dbt を日常的に利用していると、つい慣れでコマンドを打ってしまい、大規模なクエリ実行や本番データへの影響を与えてしまう事故を起こす可能性があります。
また、クエリコストや実行時間の影響が大きく、チーム全体の作業にも支障が出る可能性があります。

そこで、dbt の on-run-start でマクロを使い、実行時のパラメータや環境をチェックして「危険な実行を防ぐ」仕組みを作ります。

実行イメージ

検証環境

https://github.com/dbt-labs/jaffle_shop_duckdb

コマンドチェックの方法

コマンドチェックの方法として、--selectの必須化を設定します。
dbt rundbt build--select がない場合、全モデルが実行されてしまいます。
これを防ぐには、以下のマクロを作成します。

コマンドチェックマクロ

下記のマクロでコマンドチェックを行う。

macros/check_select_arg.sql
{% macro check_select_arg() %}
    {% if invocation_args_dict.which in ["run", "build", "snapshot"] %}
        {% if not invocation_args_dict.select %}
            {{
                exceptions.raise_compiler_error(
                    "The dbt "
                    ~ invocation_args_dict.which
                    ~ " command was executed without a --select argument. Please specify at least one model using --select."
                )
            }}
        {% endif %}
    {% endif %}
{% endmacro %}

コマンドチェックマクロの設定

dbt_project.ymlに設定することで、on-run-start に対応しているdbtコマンド実行前に、このマクロが呼び出されます。

  • 対応コマンド: dbt build, dbt compile, dbt docs generate, dbt run, dbt seed, dbt snapshot, dbt test
dbt_project.yml
on-run-start:
  - "{{ check_select_arg() }}"

実行結果(失敗)

dbt build--select が設定されていなかったため、エラーダウンします。

$ dbt build
03:21:08  Running with dbt=1.9.1
03:21:08  Registered adapter: duckdb=1.9.1
03:21:08  Unable to do partial parsing because a project config has changed
03:21:09  Encountered an error:
Compilation Error in operation jaffle_shop-on-run-start-0 (./dbt_project.yml)
  The dbt build command was executed without a --select argument. Please specify at least one model using --select.
  
  > in macro check_select_arg (macros/check_select_arg.sql)
  > called by operation jaffle_shop-on-run-start-0 (./dbt_project.yml)

実行結果(成功)

dbt build--select が設定されていたため、dbt buildコマンドが実行できました。

$ dbt build --select customers
03:22:43  Running with dbt=1.9.1
03:22:44  Registered adapter: duckdb=1.9.1
03:22:44  Unable to do partial parsing because a project config has changed
03:22:45  Found 5 models, 3 seeds, 1 operation, 20 data tests, 425 macros
03:22:45  
03:22:45  Concurrency: 24 threads (target='dev')
03:22:45  
03:22:45  1 of 1 START hook: jaffle_shop.on-run-start.0 .................................. [RUN]
03:22:45  1 of 1 OK hook: jaffle_shop.on-run-start.0 ..................................... [OK in 0.01s]
03:22:45  
03:22:45  1 of 4 START sql table model main.customers .................................... [RUN]
03:22:45  1 of 4 OK created sql table model main.customers ............................... [OK in 0.06s]
03:22:45  2 of 4 START test not_null_customers_customer_id ............................... [RUN]
03:22:45  3 of 4 START test relationships_orders_customer_id__customer_id__ref_customers_  [RUN]
03:22:45  4 of 4 START test unique_customers_customer_id ................................. [RUN]
03:22:45  2 of 4 PASS not_null_customers_customer_id ..................................... [PASS in 0.04s]
03:22:45  3 of 4 PASS relationships_orders_customer_id__customer_id__ref_customers_ ...... [PASS in 0.04s]
03:22:45  4 of 4 PASS unique_customers_customer_id ....................................... [PASS in 0.04s]
03:22:45  
03:22:45  Finished running 1 project hook, 1 table model, 3 data tests in 0 hours 0 minutes and 0.26 seconds (0.26s).
03:22:45  
03:22:45  Completed successfully
03:22:45  
03:22:45  Done. PASS=5 WARN=0 ERROR=0 SKIP=0 TOTAL=5

利用している仕組みの解説

on-run-start について

https://docs.getdbt.com/reference/project-configs/on-run-start-on-run-end
on-run-start は dbt の dbt_project.yml などで設定できるフックで、dbt コマンドが実行される直前に自動で呼ばれる処理を定義できます。
呼べる処理としては、SQLやマクロを設定できます。

設定例

dbt_project.yml
on-run-start:
  - "{{ check_select_arg() }}"
  - "{{ check_full_refresh() }}"
  - "{{ check_target_env() }}"

invocation_args_dict について

https://docs.getdbt.com/reference/dbt-jinja-functions/flags
invocation_args_dictdbt コマンド実行時の引数情報をまとめた辞書です。

  • CLI で渡されたサブコマンド、フラグ、引数などが全て含まれています。
  • run_results.jsonargs と同等の情報を提供します。
  • マクロ内で利用して、実行環境や実行パラメータに応じた処理分岐が可能です。

取得できる主なキー

key 説明
which 実行されたサブコマンド (run, build, compile, snapshot など)
select --select で指定されたモデルやタグのタプル
invocation_command 実際に CLI で実行されたコマンド文字列
vars --vars で渡された変数
profiles_dir, project_dir プロジェクトやプロファイルのディレクトリ情報
その他 ログ設定、部分パースフラグ、デバッグフラグなど多数

[参考情報] 内容の確認方法

先ほど設定したマクロの中身を下記に変えると、invocation_args_dictの内容が確認できます。

macros/check_select_arg.sql
{% macro check_select_arg() %}
    {% do log(invocation_args_dict, info=True) %}
{% endmacro %}

1.下記のコマンドを実行

$ dbt build

1.実行結果

{
    "state_modified_compare_more_unrendered_values": False,
    "use_colors_file": True,
    "version_check": True,
    "print": True,
    "source_freshness_run_project_hooks": False,
    "cache_selected_only": False,
    "profiles_dir": "/〜/jaffle_shop_duckdb",
    "static_parser": True,
    "log_format": "default",
    "require_resource_names_without_spaces": False,
    "macro_debugging": False,
    "require_yaml_configuration_for_mf_time_spines": False,
    "log_format_file": "debug",
    "partial_parse": True,
    "require_nested_cumulative_type_params": False,
    "quiet": False,
    "introspect": True,
    "log_level": "info",
    "partial_parse_file_diff": True,
    "favor_state": False,
    "exclude": (),
    "include_saved_query": False,
    "strict_mode": False,
    "send_anonymous_usage_stats": True,
    "show_resource_report": False,
    "use_colors": True,
    "skip_nodes_if_on_run_start_fails": False,
    "log_file_max_bytes": 10485760,
    "select": (),
    "which": "build",
    "require_batched_execution_for_custom_microbatch_strategy": False,
    "export_saved_queries": False,
    "project_dir": "/〜/jaffle_shop_duckdb",
    "defer": False,
    "exclude_resource_types": (),
    "log_path": "/〜/jaffle_shop_duckdb/logs",
    "warn_error_options": {"include": [], "exclude": []},
    "state_modified_compare_vars": False,
    "write_json": True,
    "vars": {},
    "empty": False,
    "populate_cache": True,
    "require_explicit_package_overrides_for_builtin_materializations": True,
    "invocation_command": "dbt build",
    "indirect_selection": "eager",
    "printer_width": 80,
    "resource_types": (),
    "show": False,
    "log_level_file": "debug",
}

2.下記のコマンドを実行

$ dbt build --select customers

2.実行結果

{
    "write_json": True,
    "partial_parse_file_diff": True,
    "partial_parse": True,
    "project_dir": "/~/jaffle_shop_duckdb",
    "require_nested_cumulative_type_params": False,
    "log_level": "info",
    "show": False,
    "vars": {},
    "require_resource_names_without_spaces": False,
    "warn_error_options": {"include": [], "exclude": []},
    "defer": False,
    "skip_nodes_if_on_run_start_fails": False,
    "require_yaml_configuration_for_mf_time_spines": False,
    "static_parser": True,
    "quiet": False,
    "log_file_max_bytes": 10485760,
    "favor_state": False,
    "profiles_dir": "/~/jaffle_shop_duckdb",
    "include_saved_query": False,
    "log_level_file": "debug",
    "macro_debugging": False,
    "export_saved_queries": False,
    "indirect_selection": "eager",
    "empty": False,
    "cache_selected_only": False,
    "populate_cache": True,
    "exclude_resource_types": (),
    "send_anonymous_usage_stats": True,
    "select": ("customers",),
    "state_modified_compare_vars": False,
    "log_path": "/~/jaffle_shop_duckdb/logs",
    "introspect": True,
    "state_modified_compare_more_unrendered_values": False,
    "exclude": (),
    "resource_types": (),
    "show_resource_report": False,
    "printer_width": 80,
    "require_explicit_package_overrides_for_builtin_materializations": True,
    "source_freshness_run_project_hooks": False,
    "which": "build",
    "print": True,
    "invocation_command": "dbt build --select customers",
    "strict_mode": False,
    "require_batched_execution_for_custom_microbatch_strategy": False,
    "log_format_file": "debug",
    "use_colors_file": True,
    "use_colors": True,
    "log_format": "default",
    "version_check": True,
}

他の制限例

1. --full-refresh の制限

全件再処理は課金・時間コストが大きいため、通常禁止します。

{% macro check_full_refresh() %}
  {% if invocation_args_dict.full_refresh %}
    {{ exceptions.raise_compiler_error("Error: --full-refresh is not allowed.") }}
  {% endif %}
{% endmacro %}

2. 本番環境での実行制限

target 名で判定し、本番直実行を禁止します。
もしくは、開発環境で実行ではないとエラーでもいいと思います。

{% macro check_target_env() %}
  {% if target.name == 'prod' %}
    {{
      exceptions.raise_compiler_error(
        "Error: This command cannot be executed directly in the prod environment."
      )
    }}
  {% endif %}
{% endmacro %}

[参考情報] target で取得できる情報

{% do log(target, info=True) %}
{
  'database': 'jaffle_shop',
  'schema': 'main',
  'path': 'jaffle_shop.duckdb',
  'config_options': None,
  'extensions': None, 
  'settings': {}, 
  'external_root': '.', 
  'use_credential_provider': None,
  'attach': None, 
  'filesystems': None,
  'remote': None, 
  'plugins': None,
  'disable_transactions': False, 
  'type': 'duckdb',
  'threads': 24, 
  'name': 'dev', 
  'target_name': 'dev',
  'profile_name': 'jaffle_shop'
}

[参考情報] flags で取得できる情報

CLIから渡されるすべての情報(サブコマンド、フラグ、引数)を取得するには、invocation_args_dictを使う必要があるが、一部であればflagsでも確認できます。

{% do log(flags, info=True) %}

下記のコマンドを実行

$ dbt build

実行結果

FAIL_FAST=False, 
CACHE_SELECTED_ONLY=False, 
VERSION_CHECK=True, 
PARTIAL_PARSE=True, 
NO_PRINT=None, 
WRITE_JSON=True, 
STATIC_PARSER=True, 
WARN_ERROR=None, 
INVOCATION_COMMAND='dbt build', 
SEND_ANONYMOUS_USAGE_STATS=True, 
DEBUG=False, 
INDIRECT_SELECTION='eager', 
WARN_ERROR_OPTIONS=WarnErrorOptions(
    include=[], 
    exclude=[]
), 
PROFILES_DIR='/~/jaffle_shop_duckdb', 
TARGET_PATH=None, 
USE_COLORS=True, 
LOG_PATH='/~/jaffle_shop_duckdb/logs', 
LOG_CACHE_EVENTS=False, 
USE_EXPERIMENTAL_PARSER=False, 
LOG_FORMAT='default', 
QUIET=False, 
INTROSPECT=True, 
PRINTER_WIDTH=80, 
EMPTY=False, 
FULL_REFRESH=False, 
STORE_FAILURES=False, 
WHICH='build'

[参考情報] VSCodeの拡張機能によるコマンド実行

https://marketplace.visualstudio.com/items?itemName=innoverio.vscode-dbt-power-user
dbt power user では、拡張機能によるコマンド実行を行うことができます。

1. 本来のログ出力

通常の CLI 実行では、invocation_command にサブコマンドが含まれる。

"invocation_command": "dbt run",
"which": "run"

2. VSCode 拡張機能(dbt power user)の実行時ログ

拡張機能のボタン操作で実行すると、invocation_command からサブコマンドが欠落する。

"invocation_command": "dbt ",
"which": "run"
  • Execute dbt SQL
  • Compiled SQL preview

など、実際には dbt run していないケースでも、whichrun が入り、かつ invocation_command"dbt " になる。

→ おそらく Python API 経由で dbt を呼び出しているため、サブコマンドが欠落している。詳細は未確認。

3. 発生している問題

  • check_select_arg マクロで dbt run 実行時に --select を必須化している。
  • しかし拡張機能の Execute dbt SQL などでも "dbt " + "which": "run" となり、マクロで 誤検知してエラーを出してしまう

4. 暫定対応案

invocation_command != "dbt " を条件に加えれば、拡張機能実行時は除外できる。

{% macro check_select_arg() %}
    {% if invocation_args_dict.which in [
        "run",
        "build",
        "snapshot",
    ] and invocation_args_dict.invocation_command != "dbt " %}
        {% if not invocation_args_dict.select %}
            {{
                exceptions.raise_compiler_error(
                    "The dbt "
                    ~ invocation_args_dict.which
                    ~ " command was executed without a --select argument. Please specify at least one model using --select."
                )
            }}
        {% endif %}
    {% endif %}
{% endmacro %}

おわりに

これらのマクロを dbt_project.ymlon-run-start に設定することで、コマンド実行前に自動チェックできます。

日常的な運用に組み込むことで、誤操作によるコスト増大や本番事故を未然に防げます。
特にチーム開発や本番・開発環境を行き来する運用では効果が高いです。

ぶっちゃけ、データウェアハウス側の権限設定で事故を防いだ方がいいので、この記事は暫定対応的に使える方法かなとも思っています。

参考

https://github.com/dbt-labs/jaffle_shop_duckdb
https://docs.getdbt.com/reference/project-configs/on-run-start-on-run-end
https://docs.getdbt.com/reference/dbt-jinja-functions/flags

Discussion