Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn vừa nhận việc tại một startup fintech ở Quận 1, TP.HCM. Ngày đầu tiên, bạn push một đoạn code nhỏ để sửa nút "Thanh toán". Vài phút sau, Slack của team nổ tung: bug xuất hiện trên production, khách hàng không đặt được đơn, và sếp gọi điện hỏi "chuyện gì đang xảy ra?". Điều tồi tệ là đoạn code của bạn thực ra rất đơn giản — nó chỉ vô tình chứa một biến chưa khai báo mà một công cụ lint bình thường sẽ bắt được trong 3 giây.
Đây chính xác là lý do các build stage (các giai đoạn trong quy trình build) ra đời. Một pipeline CI/CD không phải là một cỗ máy "chạy build rồi deploy" đơn giản. Nó là một chuỗi các cửa kiểm soát (gate) được sắp xếp có chủ đích, mỗi cửa có nhiệm vụ chặn một loại lỗi khác nhau trước khi nó tới tay người dùng. Bốn giai đoạn kinh điển mà bạn sẽ gặp ở gần như mọi công ty là: Lint → Test → Build → Scan.
Trong bài này, chúng ta không bàn về khái niệm CI/CD tổng quát (đã có ở các bài trước), cũng không đi sâu vào từng công cụ chuyên biệt như SonarQube hay SAST/DAST (những bài sau sẽ nói kỹ). Thay vào đó, mục tiêu của tôi là giúp bạn hiểu kiến trúc của một build pipeline chuẩn: mỗi stage làm gì, tại sao thứ tự này lại quan trọng, và làm sao thiết kế để pipeline vừa nhanh vừa an toàn. Nắm được điều này, bạn sẽ đọc hiểu bất kỳ file .gitlab-ci.yml hay GitHub Actions workflow nào, và tự tay dựng được một pipeline đúng chuẩn công nghiệp.
Khái niệm cốt lõi
Một build pipeline điển hình gồm các stage nối tiếp nhau. Kết quả của stage trước quyết định stage sau có được chạy hay không. Đây là nguyên tắc fail fast (thất bại sớm): nếu code lỗi ở bước rẻ và nhanh, ta dừng ngay, không phí tài nguyên chạy các bước đắt đỏ phía sau.
Stage 0 — Checkout & Restore Cache
Trước khi làm bất cứ điều gì, runner (máy chạy pipeline) phải lấy code về. Đây là bước checkout: clone repository tại đúng commit đã trigger pipeline. Kèm theo đó, ta thường restore cache — khôi phục các thư viện đã tải trước đó (như node_modules, thư mục .m2 của Maven, hay Go module cache) để không phải tải lại từ đầu mỗi lần.
Đây là bước "chuẩn bị sân khấu". Nó không kiểm tra chất lượng gì cả, nhưng làm đúng ở đây giúp toàn bộ pipeline nhanh hơn đáng kể. (Chi tiết về caching sẽ có ở Bài 11.)
Stage 1 — Lint: Bắt lỗi phong cách và lỗi tĩnh
Lint là bước phân tích code mà không cần chạy nó. Công cụ lint đọc source code và cảnh báo về: lỗi cú pháp, biến khai báo mà không dùng, code không nhất quán về format, các anti-pattern nguy hiểm (như so sánh == thay vì === trong JavaScript), thậm chí gợi ý bảo mật cơ bản.
Ví dụ theo từng ngôn ngữ:
- JavaScript/TypeScript: ESLint, Prettier
- Python: Pylint, Flake8, Ruff (nhanh hơn nhiều)
- Go: golangci-lint (gói nhiều linter trong một)
- Java: Checkstyle, SpotBugs
Stage 2 — Test: Kiểm chứng hành vi
Nếu lint hỏi "code có viết đúng không?" thì test hỏi "code có chạy đúng không?". Đây là nơi ta chạy unit test (kiểm thử từng hàm/module nhỏ), và có thể cả integration test (kiểm thử sự phối hợp giữa các thành phần).
Ở giai đoạn này bạn thường thấy thêm hai khái niệm:
- Code coverage (độ phủ): bao nhiêu phần trăm dòng code được test chạm tới. Nhiều team đặt ngưỡng tối thiểu, ví dụ 70%.
- Test report: kết quả test được xuất ra dạng chuẩn (JUnit XML) để CI hiển thị đẹp trên giao diện.
Stage 3 — Build: Tạo ra artifact
Build là bước biến source code thành sản phẩm chạy được — gọi là artifact. Tùy công nghệ, artifact có thể là:
- File
.jarhoặc.war(Java) - Thư mục
dist/đã bundle và minify (frontend React/Vue) - Một container image (Docker)
- Một binary đã compile (Go, Rust)
Artifact được tạo ra ở đây phải bất biến (immutable) — build một lần, dùng lại y hệt qua các môi trường. (Chủ đề Artifact Management ở Bài 12 sẽ nói kỹ.)
Stage 4 — Scan: Quét bảo mật và lỗ hổng
Sau khi có artifact, ta scan (quét) nó để tìm lỗ hổng. Có nhiều loại scan:
- SCA (Software Composition Analysis): quét các thư viện phụ thuộc (dependency) xem có CVE — lỗ hổng đã biết — hay không. Ví dụ:
npm audit, Trivy, Snyk. - SAST (Static Application Security Testing): quét chính source code của bạn tìm mẫu code nguy hiểm.
- Container scan: quét image Docker tìm package hệ điều hành lỗi thời.
Tại sao thứ tự lại quan trọng đến vậy?
Hãy nhìn tổng thể theo thời gian và chi phí:
| Stage | Thời gian điển hình | Bắt loại lỗi |
|---|
| Lint | 5–20 giây | Style, lỗi tĩnh |
|---|---|---|
| Test | 1–10 phút | Lỗi logic, hành vi |
| Build | 2–15 phút | Lỗi biên dịch, đóng gói |
| Scan | 30 giây–5 phút | Lỗ hổng bảo mật |
Tình huống thực tế
Ví dụ 1: Tiki và bài học "fail fast"
Giả định một team backend tại Tiki (sàn thương mại điện tử lớn của Việt Nam) ban đầu cấu hình pipeline theo thứ tự sai: họ build Docker image trước, rồi mới chạy test. Mỗi lần build image mất khoảng 8 phút vì service viết bằng Java, phải compile toàn bộ và đóng gói.
Vấn đề lộ ra vào một chiều thứ Sáu cao điểm: developer push liên tục 30 commit để kịp sprint. Rất nhiều commit fail ở test, nhưng vì test đứng sau build, mỗi lần fail vẫn ngốn trọn 8 phút build vô ích. CI runner bị nghẽn, hàng đợi (queue) kéo dài tới 40 phút. Cả team ngồi chờ.
Diễn giải: Nhóm DevOps đảo lại thứ tự thành Lint → Test → Build. Kết quả: những commit lỗi bị chặn ngay ở phút thứ 2, không bao giờ chạm tới bước build 8 phút. Thời gian queue trung bình giảm từ 40 phút xuống dưới 10 phút.
Bài học: Thứ tự stage không phải chuyện thẩm mỹ. Đặt bước rẻ và nhanh (lint, test) trước bước đắt (build) tiết kiệm tài nguyên và thời gian của cả team một cách khổng lồ, đặc biệt khi lưu lượng commit cao.
Ví dụ 2: Startup fintech và stage Scan cứu một bàn thua
Một startup ví điện tử ở Singapore (ta gọi là "PayNow Lite") có pipeline gồm Lint → Test → Build nhưng bỏ qua stage Scan để "chạy cho nhanh". Trong ba tháng, mọi thứ ổn.
Rồi lỗ hổng Log4Shell (CVE-2021-44228 trong thư viện Log4j) bùng nổ toàn cầu. Vì không có stage SCA scan, team không hề biết một microservice thanh toán của họ đang dùng phiên bản Log4j dính lỗ hổng nghiêm trọng — cho phép kẻ tấn công thực thi code từ xa. Họ chỉ phát hiện khi đội bảo mật bên ngoài cảnh báo, sau ba ngày âm thầm bị dò quét.
Diễn giải: Sau sự cố, họ thêm stage Scan dùng Trivy chạy sau build, cấu hình để fail pipeline nếu phát hiện CVE mức Critical. Từ đó, mọi image có dependency lỗ hổng đều bị chặn tự động, không thể deploy.
Bài học: Stage Scan không phải "tùy chọn cho vui". Với sản phẩm động tới tiền và dữ liệu người dùng, một stage scan tự động là lá chắn cuối cùng trước khi lỗ hổng ra production. Con số thực tế: theo báo cáo ngành, hơn 80% lỗ hổng trong ứng dụng đến từ dependency của bên thứ ba, không phải code do chính team viết.
Ví dụ 3: Team frontend và cạm bẫy "flaky test" chặn build
Một team frontend tại một công ty edtech ở Hà Nội đặt ngưỡng: pipeline fail nếu bất kỳ test nào fail. Nghe rất đúng. Nhưng họ có vài flaky test (test lúc pass lúc fail do phụ thuộc timing) trong bộ integration test. Kết quả: khoảng 15% pipeline fail dù code hoàn toàn đúng. Developer bắt đầu có thói quen nguy hiểm — cứ thấy đỏ là bấm "re-run" mà không đọc log.
Diễn giải: Vấn đề không nằm ở kiến trúc stage mà ở chất lượng test bên trong stage Test. Team tách flaky test ra một job riêng (cho phép chạy lại tự động, không chặn merge), đồng thời lập backlog sửa dứt điểm chúng. Niềm tin vào pipeline được khôi phục.
Bài học: Một stage chỉ hữu ích khi tín hiệu của nó đáng tin. Nếu pipeline "đỏ giả" quá thường xuyên, người ta sẽ học cách phớt lờ nó — và khi đó, cửa kiểm soát mất hết giá trị. Chất lượng nội dung trong mỗi stage quan trọng ngang với việc sắp xếp stage.
Hướng dẫn từng bước
Hãy cùng dựng một pipeline 4 stage cho một dự án Node.js. Tôi dùng cú pháp minh họa gần với GitHub Actions (các bài sau sẽ đi sâu công cụ cụ thể).
Bước 1 — Định nghĩa các stage rõ ràng. Chia pipeline thành các job tách biệt, mỗi job một nhiệm vụ:
jobs:
lint: # Stage 1
test: # Stage 2 — chạy sau khi lint pass
build: # Stage 3 — chạy sau khi test pass
scan: # Stage 4 — chạy sau khi build xong
Bước 2 — Cấu hình Checkout và Cache. Mỗi job bắt đầu bằng việc lấy code và khôi phục cache:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # tự động restore cache node_modules
- run: npm ci # cài dependency từ lockfile
Bước 3 — Stage Lint. Chạy linter, để nó chặn pipeline nếu có lỗi:
lint:
steps:
- run: npm run lint # ví dụ: eslint . --max-warnings=0
Lưu ý --max-warnings=0: coi cả cảnh báo là lỗi, giữ code base sạch tuyệt đối.
Bước 4 — Stage Test. Chỉ chạy khi lint pass. Xuất report và coverage:
test:
needs: lint # phụ thuộc: chỉ chạy sau khi lint xong
steps:
- run: npm test -- --coverage
Bước 5 — Stage Build. Chỉ chạy khi test pass. Tạo artifact:
build:
needs: test
steps:
- run: npm run build
- run: docker build -t myapp:${{ github.sha }} .
Chú ý tag image bằng commit SHA — mỗi build có một artifact định danh duy nhất, bất biến.
Bước 6 — Stage Scan. Quét image vừa build:
scan:
needs: build
steps:
- run: trivy image --severity CRITICAL,HIGH --exit-code 1 myapp:${{ github.sha }}
--exit-code 1 nghĩa là: nếu tìm thấy lỗ hổng High/Critical, trả về mã lỗi và pipeline fail.
Bước 7 — Định nghĩa gate rõ ràng. Chuỗi needs (lint → test → build → scan) chính là các cửa kiểm soát. Chỉ khi tất cả xanh, commit mới được coi là "đủ điều kiện". Nếu đây là nhánh chính, bạn có thể nối tiếp bước deploy phía sau.
Bước 8 — Tối ưu song song hóa nơi có thể. Lint và một số test không phụ thuộc nhau có thể chạy song song để tiết kiệm thời gian. Nhưng luôn giữ nguyên tắc: những gì đắt (build, scan) chỉ chạy sau khi những gì rẻ (lint, test) đã xanh.
Lỗi thường gặp & mẹo
Lỗi 1: Đảo ngược thứ tự stage. Build hoặc deploy trước khi test là sai lầm phổ biến nhất. Nó phá vỡ nguyên tắc fail fast và đốt tài nguyên vô ích. Mẹo: luôn tự hỏi "bước này rẻ hay đắt?" và xếp rẻ lên trước.
Lỗi 2: Lint chỉ cảnh báo mà không chặn. Nhiều team để lint chạy nhưng không cho nó fail pipeline. Kết quả là warning tích tụ hàng nghìn cái, chẳng ai đọc. Mẹo: cấu hình lint fail pipeline ngay từ đầu dự án, khi code base còn sạch — về sau rất khó dọn.
Lỗi 3: Bỏ qua stage Scan vì "cho nhanh". Như ví dụ PayNow Lite, đây là quả bom nổ chậm. Mẹo: thêm ít nhất một SCA scan (như npm audit hoặc Trivy) — chi phí gần như bằng không nhưng giá trị bảo vệ rất lớn.
Lỗi 4: Coi flaky test là "chuyện bình thường". Test chập chờn làm xói mòn niềm tin vào toàn bộ pipeline. Mẹo: cô lập flaky test ra job riêng và ưu tiên sửa dứt điểm; đừng để developer quen tay "re-run".
Lỗi 5: Mỗi stage cài lại dependency từ đầu. Không dùng cache khiến pipeline chậm gấp nhiều lần. Mẹo: luôn restore cache ở bước checkout (chi tiết ở Bài 11).
Lỗi 6: Ngưỡng coverage quá cao ngay từ đầu. Bắt 90% coverage trên một code base cũ chỉ khiến team viết test đối phó. Mẹo: đặt ngưỡng hợp lý (60–70%) và tăng dần.
Mẹo vàng: Hãy coi mỗi stage là một "hợp đồng": nếu stage này xanh, tôi cam kết code không có loại lỗi mà stage này chịu trách nhiệm. Bốn stage cộng lại tạo thành một chuỗi cam kết đủ mạnh để bạn tự tin deploy.
Bài tập thực hành
- Vẽ sơ đồ pipeline của chính bạn. Lấy một dự án bất kỳ (cá nhân hoặc ở công ty) và vẽ ra 4 stage Lint → Test → Build → Scan. Với mỗi stage, ghi rõ: dùng công cụ gì, mất bao lâu, và nó chặn loại lỗi nào.
- Cố ý phá để hiểu gate. Trên một repo thử nghiệm, tạo lần lượt: (a) một lỗi lint (biến chưa dùng), (b) một unit test fail, (c) một dependency có lỗ hổng đã biết. Quan sát pipeline dừng ở đúng stage nào. Đây là cách tốt nhất để "cảm" được các cửa kiểm soát.
- So sánh thứ tự. Cấu hình một pipeline với thứ tự đúng (lint→test→build) và một với thứ tự sai (build→test). Push 5 commit lỗi và đo tổng thời gian chạy của hai phiên bản. Ghi lại chênh lệch — bạn sẽ thấy fail fast tiết kiệm bao nhiêu.
- Thêm một stage Scan. Nếu pipeline hiện tại của bạn chưa có scan, thêm Trivy hoặc
npm auditvà cấu hình để fail khi có CVE Critical. Chạy thử và ghi lại kết quả.
Tóm tắt
- Một build pipeline chuẩn gồm bốn stage kinh điển: Lint → Test → Build → Scan, đứng trước là bước Checkout & Restore Cache.
- Lint bắt lỗi tĩnh và phong cách (rẻ, nhanh nhất). Test kiểm chứng hành vi. Build tạo artifact bất biến. Scan tìm lỗ hổng bảo mật.
- Nguyên tắc xương sống là fail fast: xếp bước rẻ và nhanh trước bước đắt, để lỗi bị chặn sớm nhất, tiết kiệm tài nguyên và cho developer feedback nhanh.
- Mỗi stage là một cửa kiểm soát (gate): stage sau chỉ chạy khi stage trước xanh, tạo thành chuỗi cam kết chất lượng.
- Thứ tự stage đúng và chất lượng nội dung bên trong mỗi stage (test không flaky, lint có chặn, scan có ngưỡng) đều quan trọng như nhau. Một pipeline "đỏ giả" hay bỏ sót scan sẽ mất hết giá trị.