CI/CD cho dữ liệu: kiểm thử trước khi gộp, triển khai sau khi gộp

Xem thử

Một analytics engineer của GDev Mart sửa công thức net_amount trong dbt, chạy thử trên máy mình thấy ổn rồi đẩy thẳng lên nhánh main. Đêm đó pipeline chạy với mô hình mới, tầng gold sai 8% doanh thu, và sáng hôm sau bảng điều khiển của giám đốc hiển thị số lệch. Không ai sai về ý định; vấn đề là không có cổng chặn nào giữa "tôi nghĩ là đúng" và "đã chạy ở production".

Hãy dự đoán: bạn sẽ đặt những kiểm tra tự động nào, ở những thời điểm nào, để thay đổi như trên bị chặn trước khi chạm production? Liệt kê ít nhất ba kiểm tra và nói mỗi kiểm tra bắt loại lỗi nào.

Sau bài này, bạn có thể

  • Sắp xếp các kiểm tra CI từ rẻ tới đắt và nói mỗi tầng bắt loại lỗi nào.
  • Viết lệnh slim CI dbt build --select state:modified+ --defer --state ... và giải thích từng thành phần.
  • Đọc và viết workflow GitHub Actions cho CI (schema riêng cho mỗi pull request) và CD (environment: production).
  • Giải thích vì sao phải bật branch protection để CI thật sự chặn được việc gộp.

Cần biết trước: bài "Git trong dự án dữ liệu: nhánh, pull request và những thứ không được commit" và bài "Test, tài liệu và lineage: làm dữ liệu đáng tin"; để làm lab cần repository GitHub chứa một dự án dbt.

Mô hình tư duy 1: CI là cổng chặn trước khi gộp, CD là đường ray sau khi gộp

  • CI (Continuous Integration): mỗi lần mở pull request, hệ thống tự chạy một bộ kiểm tra; chỉ khi tất cả đạt mới được gộp.
  • CD (Continuous Delivery/Deployment): sau khi gộp vào main, hệ thống tự triển khai thay đổi lên môi trường đích theo quy trình đã định.

DataOps áp dụng tư duy này cho dữ liệu, nên ngoài kiểm tra mã còn có kiểm tra dữ liệu và mô hình. Một bộ CI cho dự án dbt của GDev Mart gồm các tầng từ rẻ đến đắt:

TầngKiểm traBắt lỗi nàoChi phí
1Định dạng và lint (ruff, sqlfluff)Phong cách, cú pháp SQL, lỗi vặtVài giây
2Biên dịch dbt (dbt parse, dbt compile)Tham chiếu sai, Jinja lỗiVài giây
3Kiểm thử đơn vị cho hàm Python (pytest)Logic biến đổiVài giây
4Build mô hình thay đổi trên dữ liệu mẫu cùng test (dbt build --select state:modified+)Công thức sai, test dữ liệu vỡVài phút
5So sánh dữ liệu trước và sau thay đổiLệch số bất ngờVài phút

Nguyên tắc: rẻ chạy trước, đắt chạy sau, và dừng ngay khi có tầng thất bại để tiết kiệm thời gian.

Đang tải sơ đồ…

Cách đọc hình: các kiểm tra xếp từ rẻ tới đắt; lỗi ở tầng nào thì dừng ở tầng đó. Chỉ khi toàn bộ xanh mới cho gộp; cấu hình branch protection trên GitHub để bắt buộc điều này, không dựa vào thiện chí của từng người.

Đang tải Animation…

Slim CI: chỉ build phần thay đổi

Build toàn bộ dự án dbt cho mỗi pull request tốn thời gian và tiền. Slim CI chỉ build mô hình đã thay đổi và những mô hình phụ thuộc vào nó, bằng cách so với manifest của lần chạy production gần nhất:

bash
dbt build --select state:modified+ --defer --state prod_artifacts --target ci

Giải từng phần: state:modified+ chọn mô hình đã sửa và mọi mô hình xuôi dòng; --state prod_artifacts trỏ tới thư mục chứa manifest.json của production để biết cái gì đã đổi; --defer nghĩa là với mô hình không build trong lần này, dùng bảng production thay vì bắt build lại thượng nguồn; --target ci ghi vào schema riêng để không đụng dữ liệu thật.

Mỗi pull request nên có schema riêng (ví dụ ci_pr_482) và được xóa khi gộp hoặc đóng bằng một workflow riêng chạy khi pull_request có kiểu closed (bạn sẽ viết nó trong lab), để hai người chạy CI cùng lúc không giẫm lên nhau. Profile ci đọc tên schema từ biến CI_SCHEMA qua env_var.

Workflow GitHub Actions mẫu

yaml
name: dbt-ci
on:
  pull_request:
    paths: ["dbt/**", "tests/**"]

jobs:
  checks:
    runs-on: ubuntu-latest
    env:
      DBT_PROFILES_DIR: ./dbt
      DBT_PASSWORD: ${{ secrets.CI_WAREHOUSE_PASSWORD }}
      CI_SCHEMA: ci_pr_${{ github.event.number }}
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v7
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: ruff check . && sqlfluff lint dbt/models
      - run: pytest -q
      - run: dbt deps --project-dir dbt
      - run: dbt parse --project-dir dbt
      - uses: dawidd6/action-download-artifact@v27
        with:
          workflow: dbt-deploy.yml
          name: prod-manifest
          path: prod_artifacts
          branch: main
      - run: dbt build --project-dir dbt --target ci --select state:modified+ --defer --state prod_artifacts

Chú ý bốn điểm. on.pull_request.paths chỉ kích hoạt khi thay đổi đụng đúng thư mục liên quan (nhưng nếu workflow này là status check bắt buộc của branch protection, pull request không đụng các thư mục đó sẽ kẹt ở trạng thái Pending và không gộp được, nên workflow bắt buộc thường bỏ paths); mật khẩu lấy từ secrets của GitHub, không nằm trong mã; biến CI_SCHEMA cô lập từng pull request; và các bước xếp từ rẻ tới đắt, bước nào thất bại thì workflow dừng.

CD: đưa thay đổi lên production có kiểm soát

Sau khi gộp vào main, một workflow thứ hai triển khai:

yaml
name: dbt-deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v7
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: dbt build --project-dir dbt --target prod
        env:
          DBT_PASSWORD: ${{ secrets.PROD_WAREHOUSE_PASSWORD }}
      - run: dbt docs generate --project-dir dbt --target prod
      - uses: actions/upload-artifact@v7
        with:
          name: prod-manifest
          path: dbt/target/manifest.json

environment: production cho phép gắn phê duyệt thủ công và bí mật riêng cho production trong cài đặt GitHub: ai cũng mở được pull request nhưng chỉ người được chỉ định mới duyệt bước triển khai.

Đang tải sơ đồ…

Cách đọc hình: hai pha tách biệt. Trước khi gộp, CI chỉ ghi vào schema tạm. Sau khi gộp, CD mới ghi vào production, dưới environment: production có thể yêu cầu phê duyệt. Production không bao giờ bị chạm bởi mã chưa qua kiểm tra.

Dữ liệu vẫn cần kiểm thử ngoài mã

Mã đúng chưa chắc kết quả đúng. Hai kiểm tra đáng thêm:

  • So sánh trước và sau: chạy hai phiên bản trên cùng dữ liệu mẫu và so tổng doanh thu, số dòng; chênh trên ngưỡng (ví dụ 0,5%) thì chặn và hiển thị chênh lệch để người duyệt giải thích. Thư viện dbt-audit-helper có macro hỗ trợ.
  • Dữ liệu mẫu cố định: bộ dữ liệu nhỏ (vài trăm dòng) kèm kết quả kỳ vọng được commit cùng mã, để mọi lần chạy CI đều quyết định (deterministic) và độc lập với nguồn thật.

Những hiểu lầm thường gặp

  • "Chạy được trên máy tôi là đủ." Máy bạn khác môi trường CI và production (phiên bản, dữ liệu, quyền); CI là bằng chứng lặp lại được.
  • "CI chạy toàn bộ dự án mỗi lần cho chắc." Tốn kém và chậm; slim CI với state:modified+ giữ độ bao phủ mà giảm thời gian.
  • "Bí mật ghi trong tệp workflow cho tiện." Tệp nằm trong lịch sử Git; luôn dùng secrets.
  • "CD tự động nghĩa là không cần phê duyệt." Với production có thể đặt bước duyệt thủ công qua environment; tự động hóa không đồng nghĩa mất kiểm soát.

Tự kiểm tra (có đáp án)

Câu 1. Vì sao xếp kiểm tra từ rẻ tới đắt? Đáp án: lỗi vặt bị bắt sau vài giây mà không phải chờ build đắt; khi một tầng thất bại thì dừng ngay, tiết kiệm thời gian và tiền.

Câu 2. state:modified+ kết hợp --defer giải quyết vấn đề gì? Đáp án: chỉ build mô hình đã đổi và phần xuôi dòng, còn mô hình thượng nguồn dùng bảng production thay vì build lại, nên CI nhanh và rẻ.

Câu 3. Vì sao mỗi pull request nên có schema riêng? Đáp án: để hai lần chạy CI đồng thời không ghi đè lên nhau, và production không bị chạm; schema bị xóa khi gộp hoặc đóng.

Nếu chỉ nhớ 3 điều

  1. CI chặn thay đổi lỗi trước khi gộp; CD triển khai sau khi gộp; bật branch protection để bắt buộc.
  2. Kiểm tra xếp rẻ trước, đắt sau; dùng slim CI (state:modified+, --defer) và schema riêng cho mỗi pull request.
  3. Bí mật nằm trong secrets; production dùng environment có phê duyệt, và thêm so sánh dữ liệu trước sau để bắt lỗi logic.

Nguồn tham khảo