dbtコマンドを制限するマクロで事故を防ぐ
はじめに
dbt を日常的に利用していると、つい慣れでコマンドを打ってしまい、大規模なクエリ実行や本番データへの影響を与えてしまう事故を起こす可能性があります。
また、クエリコストや実行時間の影響が大きく、チーム全体の作業にも支障が出る可能性があります。
そこで、dbt の on-run-start でマクロを使い、実行時のパラメータや環境をチェックして「危険な実行を防ぐ」仕組みを作ります。
実行イメージ
検証環境
コマンドチェックの方法
コマンドチェックの方法として、--selectの必須化を設定します。
dbt run や dbt build で --select がない場合、全モデルが実行されてしまいます。
これを防ぐには、以下のマクロを作成します。
コマンドチェックマクロ
下記のマクロでコマンドチェックを行う。
{% 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
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 について
on-run-start は dbt の dbt_project.yml などで設定できるフックで、dbt コマンドが実行される直前に自動で呼ばれる処理を定義できます。
呼べる処理としては、SQLやマクロを設定できます。
設定例
on-run-start:
- "{{ check_select_arg() }}"
- "{{ check_full_refresh() }}"
- "{{ check_target_env() }}"
invocation_args_dict について
invocation_args_dict は dbt コマンド実行時の引数情報をまとめた辞書です。
- CLI で渡されたサブコマンド、フラグ、引数などが全て含まれています。
-
run_results.jsonのargsと同等の情報を提供します。 - マクロ内で利用して、実行環境や実行パラメータに応じた処理分岐が可能です。
取得できる主なキー
| key | 説明 |
|---|---|
which |
実行されたサブコマンド (run, build, compile, snapshot など) |
select |
--select で指定されたモデルやタグのタプル |
invocation_command |
実際に CLI で実行されたコマンド文字列 |
vars |
--vars で渡された変数 |
profiles_dir, project_dir
|
プロジェクトやプロファイルのディレクトリ情報 |
| その他 | ログ設定、部分パースフラグ、デバッグフラグなど多数 |
[参考情報] 内容の確認方法
先ほど設定したマクロの中身を下記に変えると、invocation_args_dictの内容が確認できます。
{% 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の拡張機能によるコマンド実行
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 SQLCompiled SQL preview
など、実際には dbt run していないケースでも、which に run が入り、かつ 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.yml の on-run-start に設定することで、コマンド実行前に自動チェックできます。
日常的な運用に組み込むことで、誤操作によるコスト増大や本番事故を未然に防げます。
特にチーム開発や本番・開発環境を行き来する運用では効果が高いです。
ぶっちゃけ、データウェアハウス側の権限設定で事故を防いだ方がいいので、この記事は暫定対応的に使える方法かなとも思っています。
参考
Discussion