dbt 는 SQL 로 쓴 변환을 의존 관계에 맞춰 순서대로 실행하고, 그 결과를 테스트하고 문서로 만드는 도구다. 데이터를 직접 옮기지 않고 목적지 엔진에 SQL 을 보내기만 하므로, 여기서 목적지는 Dremio 가 된다. 실행 시점을 정하고 실패를 다시 돌리는 일은 Airflow 가 맡는다. 세 조각의 역할이 겹치지 않게 나누는 것이 이 조합의 전부다.
dbt 의 model 은 머신러닝 모델과 아무 상관이 없다. models/ 디렉터리에 놓인 .sql 파일 하나가 모델 하나이고, 그 파일에 적힌 SELECT 문의 결과가 뷰나 테이블로 만들어진다. 파일 이름이 곧 만들어지는 객체 이름이다.
dbt 에서 Dremio 에 붙으려면 dbt-dremio 어댑터를 설치하고 profiles.yml 을 적는다. 기본 위치는 ~/.dbt/profiles.yml 이다. Invalid value for '--profiles-dir': Path '/root/.dbt' does not exist 는 그 디렉터리가 없다는 뜻이므로 먼저 만든다.
dremio_project:
target: dev
outputs:
dev:
type: dremio
software_host: dremio.example.com
port: 9047
user: "{{ env_var('DREMIO_USER') }}"
password: "{{ env_var('DREMIO_PASSWORD') }}"
use_ssl: false
object_storage_source: "$scratch"
object_storage_path: "no_schema"
dremio_space: "analytics"
dremio_space_folder: "staging"
threads: 4
dremio_space 와 dremio_space_folder 가 모델이 만들어질 자리다. 이 값을 비워 두면 개인 이름이 붙은 홈 스페이스에 만들어지고, 다른 사용자에게 보이지 않아 "쿼리는 돌았는데 데이터셋이 없다" 는 상황이 된다. 모델마다 다른 자리에 두려면 모델 파일 안에서 덮어쓴다.
{{ config(
dremio_space='analytics',
dremio_space_folder='mart'
) }}
SELECT * FROM {{ source('legacy', 'orders') }}
두 파일은 대상이 다르다. 이름이 비슷해 헷갈리기 쉽다.
| 파일 | 대상 | 테스트 실행 시점 |
|---|---|---|
schema.yml |
dbt 가 만들어 낼 모델. 컬럼 설명과 제약 테스트를 적는다 | dbt test · dbt build |
source.yml |
dbt 가 읽어 올 원천 데이터셋. source() 로 참조할 이름을 선언한다 |
dbt test --select source:* · dbt source freshness |
models 아래 항목에 name 이 없으면 a list element for 'models' does not have a name attribute 오류가 난다. 리스트 요소마다 name 이 필수다.
version: 2
sources:
- name: legacy
database: oracle_source
schema: SALES
tables:
- name: orders
- name: customers
models:
- name: order_daily
description: '일자별 주문 집계'
columns:
- name: order_date
tests:
- not_null
Airflow 에서 dbt 를 돌리는 방법은 둘이다. BashOperator 로 dbt run 을 통째로 실행하면 태스크가 하나로 뭉쳐 어느 모델에서 실패했는지 화면에서 보이지 않는다. astronomer-cosmos 는 manifest.json 을 읽어 모델 하나를 태스크 하나로 펼쳐 준다.
DbtDag 는 DAG 자체를 만들어 주는 것이고 DbtTaskGroup 은 이미 있는 DAG 안에 태스크 묶음을 끼워 넣는 것이다. unsupported operand type(s) for >>: 'DAG' and 'DbtDag' 는 DAG 를 다른 DAG 에 이어 붙이려 한 것이므로, 기존 DAG 안에 넣을 생각이었다면 DbtTaskGroup 을 써야 한다.
from cosmos import DbtTaskGroup, ProjectConfig, ProfileConfig, RenderConfig
dbt_tg = DbtTaskGroup(
group_id="dbt_transform",
project_config=ProjectConfig("/opt/airflow/dbt/dremio_project"),
profile_config=ProfileConfig(
profile_name="dremio_project",
target_name="dev",
profiles_yml_filepath="/opt/airflow/dbt/profiles.yml",
),
render_config=RenderConfig(select=["path:models/mart"]),
)
start >> dbt_tg >> end
RenderConfig(select=...) 로 특정 경로의 모델만 펼칠 수 있다. dbt 의 --select 문법을 그대로 쓴다.
여러 모델을 한꺼번에 던지면 Dremio 실행기가 밀린다. 조절할 자리는 두 군데다.
profiles.yml 의 threads 를 낮춘다. dbt 가 동시에 보내는 쿼리 수의 상한이다.operator_args 로 넘긴다.dbt_tg = DbtTaskGroup(
group_id="dbt_transform",
project_config=...,
profile_config=...,
operator_args={"pool": "dremio_pool"},
)
풀은 Airflow UI 의 Admin → Pools 에서 슬롯 수를 정해 두어야 한다. 둘 중 하나만 걸면 다른 쪽에서 새어 나가므로 threads 와 풀 슬롯을 함께 맞춘다.
Dremio 리플렉션은 dbt 모델을 만든다고 따라 생기지 않는다. 다만 Dremio 는 리플렉션 생성·갱신을 SQL DDL 로 제공하므로, dbt 의 post-hook 에 넣어 모델 빌드 직후에 이어 붙일 수 있다.
{{ config(
post_hook=[
'ALTER TABLE {{ this }} CREATE RAW REFLECTION "{{ this.identifier }}_raw" USING DISPLAY (order_date, amount)'
]
) }}
이미 같은 이름의 리플렉션이 있으면 오류가 나므로, 반복 실행을 전제로 한다면 생성과 갱신을 나눠 REST API 로 다루는 편이 다루기 쉽다. 어느 쪽이든 리플렉션이 dbt 의 의존 그래프에 들어오지는 않는다.
pip install dbt 는 껍데기 패키지라서 어댑터가 함께 오지 않는다. dbt-core 와 dbt-dremio 를 같이 설치한다. pip uninstall dbt 를 해도 명령이 남아 있는 이유도 이것이다.ref() 로 의존을 표현할 수 없는 모델들은 dbt 가 순서를 알 수 없다. 이때는 depends_on 을 주석으로 명시해 그래프에 넣는다 — -- depends_on: {{ ref('order_daily') }}.NullPointerException: FlightClient not instantiated 는 Arrow Flight 연결이 만들어지지 않은 상태에서 쿼리를 보냈다는 뜻이다. 호스트·포트와 TLS 설정을 먼저 본다./mnt/c/... 경로를 Airflow 컨테이너에 마운트하면 경로가 두 겹이 된다. docker-compose.yml 의 volumes 에 적은 호스트 경로는 도커 데몬이 보는 경로 기준이다.