DataOps nâng cao 4 — CI/CD đầu-cuối cho analytics

13 thg 7, 2026 2 lượt xem
#ci-cd
#data-engineering
#dbt
#dataops
#github-actions

Từ "chạy dbt trên máy tôi" tới dây chuyền tự động

Bài DataOps 5 — CI/CD & tự động hoá đã dựng nguyên tắc chung. Bài này đi sâu hơn một bậc: thực chiến pipeline dbt + Git cụ thể, dán được vào repo NCB và chạy ngay. Nếu bạn cần nền tảng CI/CD phần mềm, xem DevOps CI/CD; còn ở đây ta tập trung vào tài sản analytics: model dbt, data test, và Airflow DAG.

Bối cảnh ngân hàng đặt ra ràng buộc rõ ràng: mọi thay đổi logic báo cáo — công thức phân loại nợ, quy tắc tính lãi, số liệu CAR — phải qua CI test và phê duyệt trước khi lên prod. Không ai được sửa tay trên prod. Mỗi thay đổi phải an toàn (không phá số liệu đang chạy) và truy vết được (biết ai đổi gì, khi nào, PR nào). CI/CD chính là cơ chế biến những yêu cầu tuân thủ đó thành cổng chặn tự động thay vì lời hứa của con người.

Git workflow: trunk-based cho analytics

Ta dùng trunk-based development: một nhánh chính main luôn ở trạng thái deploy được, mọi thay đổi đi qua feature branch ngắn hạn:

  1. Analyst tạo nhánh feat/npl-recalc từ main.
  2. Sửa model, commit, push.
  3. Mở Pull Request → CI tự chạy.
  4. Review: người thứ hai đọc diff, đối chiếu logic nghiệp vụ.
  5. CI xanh + đã approve → merge vào main (squash để lịch sử sạch).

Nhánh sống ngắn (giờ/ngày, không phải tuần) để tránh xung đột merge và "big-bang deploy". Đây là điểm khác với git-flow nhiều nhánh dài; nền tảng Git cho analytics xem thêm ở DataOps 3 — Git.

Ánh xạ môi trường

Mỗi tầng Git ứng với một môi trường warehouse riêng. Với dbt, khác biệt chỉ là target/schema, cùng một codebase:

Môi trườngAi dùngSchema đíchDữ liệuKhi nào build
devmỗi dev/branchdbt_<user> hoặc ci_pr_<n>sample/testkhi dev chạy tay, và trong CI
stagingteamanalytics_stagingbản sao ẩn danh gần prodkhi merge main
prodbáo cáo thậtanalyticsdữ liệu thậtsau khi approve

Điểm mấu chốt: dev có schema riêng cho từng người/từng PR (dbt_ha, ci_pr_412), nên hai người sửa cùng model không giẫm lên nhau, và CI của mỗi PR dựng bảng vào schema tạm rồi xoá sau — không đụng vào bảng thật.

Continuous Integration: cổng chặn trên mỗi PR

Mục tiêu CI: không một dòng SQL lỗi nào lọt vào main. Chuỗi bước điển hình, chạy tuần tự, fail bất kỳ bước nào là chặn merge:

#BướcLệnhBắt lỗi gì
1Checkoutactions/checkout
2Cài dbtpip install dbt-postgres
3Nạp packagedbt depsthiếu package, version lệch
4Lint SQLsqlfluff lint models/sai style, tên bảng lộn xộn
5Compiledbt compileref sai, macro hỏng
6Slim CI builddbt build --select state:modified+model đổi + downstream trên dữ liệu test
7Data test(nằm trong build)vi phạm unique/not_null/quan hệ
8dbt-checkpointpre-commit runthiếu test/mô tả theo quy ước
9Báo lên PRcheck statuschặn merge nếu fail

Slim CI — chỉ build phần đổi & downstream

Chạy lại toàn bộ warehouse mỗi PR thì chậm và tốn. Slim CI dùng cơ chế state-aware của dbt: so sánh với manifest.json của lần build production gần nhất để biết model nào đã đổi, rồi chỉ build những model đó cộng downstream.

# So state với manifest prod đã lưu artifact
dbt build --select state:modified+ --defer --state ./prod-artifacts

state:modified+ = "các model thay đổi và mọi thứ phía sau"; --defer = model không đổi thì trỏ thẳng tới bảng có sẵn ở prod thay vì dựng lại. Một PR sửa 1 model trong dự án 400 model có thể chỉ build 6 model — CI từ ~40 phút xuống ~2 phút. Chi tiết cơ chế state xem dbt 7 — deployment & CI.

Data test là bất biến chạy trong CI

Ngoài dbt test (unique, not_null, relationships), ta viết các truy vấn kiểm tra bất biến (invariant) — một SELECT trả về các dòng vi phạm; có dòng nào là CI fail. Ví dụ bất biến "không tài khoản VND nào được có số dư âm", chạy trên sandbox:

-- ▶ Chạy được
SELECT a.id AS account_id, a.account_no, a.balance, a.currency,
       c.full_name
FROM accounts a
JOIN customers c ON c.id = a.customer_id
JOIN transactions t ON t.account_id = a.id
JOIN employees e ON e.id = c.id
JOIN departments d ON d.id = e.department_id
WHERE a.currency = 'VND' AND a.balance < 0;

Trong dbt, câu này thường là một singular test đặt trong tests/: dbt bọc nó thành SELECT COUNT(*) ... và fail khi count > 0. CI chạy nó tự động trên mỗi PR nên không ai phải nhớ chạy tay. (Lưu ý: các phần join employees/departments ở trên chỉ để minh hoạ truy vấn nhiều bảng trên sandbox; test thật chỉ cần accounts + customers.)

Continuous Deployment: từ merge tới prod

Khi PR merge vào main, dây chuyền CD chạy theo trình tự an toàn, không nhảy thẳng vào prod:

merge main → deploy staging → smoke test → chờ approve → deploy prod → tag → theo dõi
  • Deploy staging (dbt build --target staging): dựng toàn bộ hoặc phần đổi vào analytics_staging với dữ liệu gần-prod.
  • Smoke test: vài truy vấn nhanh xác nhận số liệu trọng yếu hợp lý (tổng dư nợ không âm, số dòng bảng chính không tụt 50%).
  • Phê duyệt (approval gate): với dữ liệu ngân hàng đây là manual approval bắt buộc — một reviewer có thẩm quyền bấm duyệt trong GitHub Environment production trước khi bước prod chạy.
  • Deploy prod (dbt build --target prod): build vào analytics.
  • Tag commit (v2026.07.08) để cột số liệu prod với đúng một commit → truy vết được.

Blue-green & rollback cho warehouse

Để deploy không làm gãy báo cáo đang đọc, dùng blue-green: build phiên bản mới vào schema analytics_green, chạy smoke test, rồi swap (đổi tên schema / trỏ view) sang cho báo cáo. Nếu hỏng, rollback = trỏ lại schema analytics_blue — gần như tức thì, không phải chạy lại pipeline. Vì mỗi deploy gắn một tag, rollback code chỉ là git revert rồi re-deploy.

Deploy Airflow DAG như code

DAG cũng là code trong repo. CD không sửa DAG trên Airflow UI mà sync file .py từ Git vào thư mục dags/ của môi trường tương ứng (git-sync sidecar, hoặc CI đẩy lên bucket mà scheduler đọc). Nhờ vậy DAG đang chạy luôn khớp bản trong Git; chi tiết vận hành xem Airflow 7 — production ops.

Secret & connection theo môi trường

Không hardcode mật khẩu warehouse trong repo. Mỗi môi trường có bộ credential riêng, nạp lúc runtime. Cách hiện đại nhất là OIDC: GitHub Actions xin token ngắn hạn trực tiếp từ cloud (AWS/GCP) qua liên kết tin cậy, không cần lưu access key tĩnh nào trong repo.

permissions:
  id-token: write   # cho phép OIDC
  contents: read
# ...
- uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789:role/dbt-ci-prod
    aws-region: ap-southeast-1

Connection string / mật khẩu Postgres thì lưu ở GitHub Environment secrets tách theo staging/production, để job staging không bao giờ chạm được credential prod.

Sơ đồ pipeline CI/CD

Ví dụ GitHub Actions gần hoàn chỉnh

Hai workflow: ci.yml chạy trên PR, cd.yml chạy khi push main. YAML/bash dưới đây là minh hoạ để tham khảo (không phải SQL chạy trên sandbox).

# .github/workflows/ci.yml — chạy trên mỗi PR
name: dbt-ci
on:
  pull_request:
    branches: [main]

jobs:
  build-test:
    runs-on: ubuntu-latest
    environment: ci
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with: { python-version: "3.11" }

      - name: Cài dbt + sqlfluff
        run: pip install dbt-postgres==1.8.* sqlfluff==3.*

      - name: dbt deps
        run: dbt deps

      - name: Lint SQL
        run: sqlfluff lint models/ --dialect postgres

      - name: dbt compile
        run: dbt compile --target ci
        env:
          DBT_PG_PASS: ${{ secrets.CI_PG_PASS }}

      # Tải manifest của build prod gần nhất để so state
      - name: Lấy prod manifest
        run: aws s3 cp s3://ncb-dbt-artifacts/prod/manifest.json ./prod-artifacts/manifest.json

      - name: Slim CI build + test
        run: |
          dbt build --target ci \
            --select state:modified+ \
            --defer --state ./prod-artifacts
        env:
          DBT_PG_PASS: ${{ secrets.CI_PG_PASS }}

      - name: dbt-checkpoint (quy ước)
        run: pre-commit run --all-files
# .github/workflows/cd.yml — chạy khi merge vào main
name: dbt-cd
on:
  push:
    branches: [main]

jobs:
  staging:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.11" }
      - run: pip install dbt-postgres==1.8.*
      - run: dbt deps
      - name: Build staging
        run: dbt build --target staging
        env:
          DBT_PG_PASS: ${{ secrets.STAGING_PG_PASS }}
      - name: Smoke test
        run: dbt test --target staging --select tag:smoke

  production:
    needs: staging
    runs-on: ubuntu-latest
    environment: production   # yêu cầu manual approval
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/dbt-ci-prod
          aws-region: ap-southeast-1
      - uses: actions/setup-python@v5
        with: { python-version: "3.11" }
      - run: pip install dbt-postgres==1.8.*
      - run: dbt deps
      - name: Build production
        run: dbt build --target prod
      - name: Lưu manifest cho slim CI lần sau
        run: aws s3 cp target/manifest.json s3://ncb-dbt-artifacts/prod/manifest.json
      - name: Tag release
        run: |
          git tag "v$(date +%Y.%m.%d)-${GITHUB_SHA::7}"
          git push --tags

Cấu hình branch protection trên main: bắt buộc status check dbt-ci xanh + ít nhất 1 approval mới cho merge. Trên GitHub Environment production: bật Required reviewers để chặn bước prod cho tới khi có người duyệt.

Đo DORA cho analytics

Bốn chỉ số DORA áp được cho analytics, giúp đội tự đánh giá "chạy tốt hay không":

Chỉ sốÝ nghĩa cho analyticsNguồn đo
Deployment frequencybao lần lên prod / tuầnsố lần workflow cd thành công
Lead time for changestừ commit tới prod bao lâuthời gian commit → tag prod
Change failure rate% deploy gây lỗi số liệusố rollback / tổng deploy
Time to restoresửa xong bao lâu sau sự cố số liệumở → đóng incident

Với blue-green, time to restore rất thấp vì rollback chỉ là swap schema. Một đội analytics khoẻ mạnh thường deploy hằng ngày, lead time dưới nửa ngày, change failure rate một chữ số phần trăm.

Use case thực tế

Đổi công thức phân loại nợ xấu (NPL) cho báo cáo NHNN. Nghiệp vụ yêu cầu chỉnh ngưỡng ngày quá hạn của nhóm nợ nhóm 3 từ 90 sang 91 ngày, ảnh hưởng model fct_loan_classification và 4 model downstream (tỷ lệ NPL, trích lập dự phòng).

Trước đây quy trình là: mở SQL client sửa tay trên prod cuối tuần, không ai review, tuần sau phát hiện tỷ lệ NPL toàn hàng sai vì quên cập nhật một view — không biết deploy nào gây ra.

Với pipeline mới:

  1. Analyst tạo nhánh feat/npl-91d, sửa macro ngưỡng, mở PR.
  2. CI chạy 2 phút: sqlfluff xanh, dbt build --select state:modified+ build đúng 5 model đổi + downstream vào schema ci_pr_412, 37 data test pass (gồm bất biến "tổng dư nợ 5 nhóm = tổng danh mục").
  3. Trưởng nhóm rủi ro đọc diff, đối chiếu công văn NHNN, approve.
  4. Merge → staging build, smoke test so tỷ lệ NPL staging lệch < 0.3% so kỳ vọng.
  5. Giám đốc dữ liệu bấm duyệt trong Environment production.
  6. Deploy blue-green vào analytics_green, swap, tag v2026.07.08-a1b2c3d.

Kết quả: thay đổi lên prod trong cùng ngày, có PR + approval làm dấu vết kiểm toán, và khi kiểm toán nội bộ hỏi "ai đổi ngưỡng NPL, dựa vào đâu", câu trả lời là link PR #412 kèm review — đúng yêu cầu truy vết của ngân hàng.

Ghi nhớ

  • Trunk-based + PR ngắn hạn: main luôn deploy được; mọi thay đổi qua feature branch → PR → review → merge.
  • Ánh xạ môi trường bằng dbt target/schema: dev (schema riêng mỗi dev/PR) → staging → prod, cùng một codebase.
  • CI trên mỗi PR: checkout → dbt deps → sqlfluff → compile → slim CI state:modified+ → dbt test → dbt-checkpoint → chặn merge nếu fail.
  • Slim CI chỉ build model đổi + downstream nhờ so manifest.json; PR nhỏ build vài model thay vì cả warehouse.
  • CD đi tuần tự: merge → staging → smoke → manual approval → prod; blue-green để swap/rollback tức thì; tag mỗi release.
  • Không hardcode secret: OIDC token ngắn hạn + Environment secrets tách staging/prod; DAG deploy như code, không sửa trên UI.
  • Ngân hàng: mọi đổi logic báo cáo phải qua CI test + approval trước prod → an toàn và truy vết được.
  • Đo DORA (frequency, lead time, change failure rate, time to restore) để biết đội analytics vận hành có khoẻ không.

Bài viết liên quan

CSV/JSON/Parquet/Avro, lưu trữ theo hàng vs cột, nén, và OLTP vs OLAP.

13 thg 7, 2026 8

Data Engineering là gì, vai trò trong vòng đời dữ liệu, và bức tranh hệ sinh thái công cụ.

13 thg 7, 2026 5

Vì sao xử lý phân tán, mô hình Spark (RDD/DataFrame), lazy evaluation, shuffle và tối ưu.

13 thg 7, 2026 5

Đảm bảo chất lượng & minh bạch dữ liệu bằng dbt: data tests dựng sẵn (unique, not_null, relationships, accepted_values), singular test, unit test, và tài liệu tự sinh kèm lineage graph.

13 thg 7, 2026 5