Bài 4

Lab Airflow bằng Docker

Dựng một Airflow chạy được tại máy bằng Docker Compose chính thức của dự án (LocalExecutor + Postgres). Bài này hướng dẫn từng bước cài đặt, đặt một DAG ETL mẫu vào thư mục dags/, và đúc kết best practices khi viết DAG production.

Cài đặt với Docker Compose
Bộ compose chính thức của Airflow
# tạo thư mục lab và vào đó
mkdir airflow-lab && cd airflow-lab

# tải compose chính thức (chọn đúng version Airflow 2.x)
curl -LfO 'https://airflow.apache.org/docs/apache-airflow/stable/docker-compose.yaml'
# thư mục cho DAG, log, plugin
mkdir -p ./dags ./logs ./plugins ./config

# ghi UID hiện tại vào .env (Linux/macOS)
echo "AIRFLOW_UID=$(id -u)" > .env
docker compose up airflow-init
docker compose up -d
Thành phầnURL / Cách dùng
Airflow Web UIlocalhost:8080 (airflow / airflow)
Đặt DAGBỏ file .py vào thư mục ./dags — vài chục giây sau DAG hiện trên UI
Tắt labdocker compose down (thêm -v để xoá cả volume DB)
Cấu hình máy: Compose chính thức cần tối thiểu khoảng 4GB RAM cấp cho Docker (khuyến nghị 8GB). Nếu container airflow-init báo cảnh báo RAM/CPU, hãy tăng tài nguyên trong Docker Desktop trước khi up.
DAG ETL mẫu
dags/etl_csv_to_parquet.py

Một pipeline ETL tối giản: đọc CSV → transform → ghi kết quả (ở đây mô phỏng, in ra số dòng). Trong thực tế bước nặng nên là một SparkSubmitOperator trỏ vào cụm Spark của bạn.

from datetime import datetime, timedelta
from airflow.decorators import dag, task

default_args = {"owner": "data-team", "retries": 2,
                "retry_delay": timedelta(minutes=5)}

@dag(
    dag_id="etl_csv_to_parquet",
    default_args=default_args,
    schedule="0 6 * * *",        # 6h sáng mỗi ngày
    start_date=datetime(2026, 1, 1),
    catchup=False,
    tags=["etl", "lab"],
)
def etl_pipeline():

    @task()
    def extract():
        import csv
        path = "/opt/airflow/dags/data/sales.csv"
        with open(path) as f:
            rows = list(csv.DictReader(f))
        return {"path": path, "count": len(rows)}

    @task()
    def transform(meta):
        # mô phỏng làm sạch: loại dòng qty âm
        print(f"Transform {meta['count']} dòng từ {meta['path']}")
        return meta["count"]

    @task()
    def load(n):
        # mô phỏng ghi Parquet ra kho dữ liệu
        print(f"Ghi {n} dòng ra Parquet (partition theo ngày)")

    load(transform(extract()))

dag = etl_pipeline()

Phiên bản trigger Spark thay cho bước nặng (thay transform/load bằng một task submit Spark):

from airflow.providers.apache.spark.operators.spark_submit import SparkSubmitOperator

spark_job = SparkSubmitOperator(
    task_id="spark_transform",
    application="/opt/airflow/jobs/etl_job.py",
    conn_id="spark_default",        # khai báo trong Admin -> Connections
    name="etl_csv_to_parquet",
)
Đừng xử lý data lớn trong task Python: code trong PythonOperator/@task chạy ngay trong tiến trình của Scheduler/Worker. Nạp cả file vài GB vào pandas ở đây sẽ làm nghẽn Airflow. Hãy đẩy phần nặng sang Spark/SQL và để Airflow chỉ điều phối.
Best practices
Viết DAG bền vững
Idempotent: task chạy lại cùng một khoảng dữ liệu phải cho cùng kết quả. Ghi đè theo partition (overwrite) thay vì append mù, để retry/backfill không nhân đôi dữ liệu.
Atomic: mỗi task làm đúng một việc gọn. Ghi ra file tạm rồi đổi tên khi xong, tránh để lại dữ liệu nửa vời nếu task chết giữa chừng.
Dùng Connection/Variable, không hardcode: mọi host/mật khẩu/đường dẫn để trong Connections & Variables, không nhúng cứng vào file DAG đẩy lên Git.
retries hợp lý: đặt retries + retry_delay trong default_args để chịu được lỗi tạm thời; cấu hình cảnh báo (email/Slack) khi task fail thật.
Nhẹ ở cấp module: đừng đặt truy vấn DB/HTTP nặng ngoài hàm task — Scheduler parse file mỗi chu kỳ nên code đó sẽ chạy lặp đi lặp lại và làm chậm cả hệ thống.
Bài tập
Tự làm để khắc sâu
Gợi ý nối lab khác: có thể trỏ SparkSubmitOperator vào cụm Spark trong spark-lab, để Airflow đóng vai điều phối còn Spark lo xử lý dữ liệu — đúng kiến trúc thực tế.
← Bài trước
Bài 3: Operators & Sensors