Mở đầu — vì sao bài này quan trọng
Nếu bạn đã đi qua các bài trước về GitHub Actions, GitLab CI, build stages hay các chiến lược deploy như rolling update và canary, thì bạn đang có trong tay đầy đủ "gạch xây". Nhưng khi bạn bước vào một hệ thống microservices thật sự — nơi có 20, 50, thậm chí 200 service chạy song song — mọi thứ bạn học được ở quy mô một ứng dụng (monolith) đột nhiên vỡ vụn. Một pipeline đẹp đẽ cho một service không tự động nhân bản thành một hệ thống CI/CD lành mạnh cho 50 service.
Đây chính là lý do bài này quan trọng. Ở Việt Nam, phần lớn các công ty công nghệ tăng trưởng nhanh — từ các sàn thương mại điện tử, ví điện tử, đến các startup fintech — đều đi theo con đường tách monolith thành microservices trong 3–4 năm đầu. Và khi đó, "nút thắt cổ chai" không còn nằm ở việc viết code, mà nằm ở việc làm sao để build, test và deploy hàng chục service một cách nhất quán, an toàn và không giẫm chân nhau.
Bài học này tập trung riêng vào những vấn đề chỉ xuất hiện ở kiến trúc microservices: sự bùng nổ số lượng pipeline, thay đổi hợp đồng (contract) giữa các service phải deploy phối hợp, và câu hỏi kinh điển — "tôi phải test bao nhiêu service để chắc chắn thay đổi của mình không làm sập hệ thống?".
Khái niệm cốt lõi
Vấn đề N service × N pipeline
Với monolith, bạn có một repository, một pipeline, một artifact, một lần deploy. Với microservices, bạn có N service. Nếu mỗi service tự viết pipeline riêng theo cách riêng, bạn sẽ có N pipeline khác nhau về cấu trúc, về cách đặt tên stage, về phiên bản công cụ scan, về cách quản lý secret.
Hậu quả rất thực tế: khi bạn cần nâng cấp phiên bản Node từ 18 lên 20 trên toàn hệ thống, bạn phải sửa 50 file YAML. Khi có lỗ hổng bảo mật trong một base image, bạn phải rà 50 nơi. Đây là lý do khái niệm pipeline template (đã đề cập ở bài về reusability) trở thành sống còn ở quy mô microservices — nhưng ở đây ta nhìn nó dưới góc độ vận hành toàn đội, không phải kỹ thuật viết template.
Nguyên tắc vàng: service tự chủ về code, nhưng pipeline nên được chuẩn hóa tập trung. Mỗi team sở hữu service của mình (ownership), nhưng khung pipeline nên đến từ một "golden template" chung do team Platform/DevOps duy trì.
Repo strategy: monorepo hay polyrepo?
Ở microservices, quyết định đặt code ở đâu ảnh hưởng trực tiếp đến CI/CD:
- Polyrepo (mỗi service một repo): pipeline độc lập rõ ràng, mỗi push chỉ trigger đúng service đó. Nhưng thay đổi xuyên service (ví dụ sửa một API dùng chung) phải tạo nhiều pull request ở nhiều repo, khó phối hợp.
- Monorepo (tất cả trong một repo): dễ chia sẻ code chung, dễ thay đổi đồng bộ, nhưng pipeline phải đủ thông minh để chỉ build service nào thực sự thay đổi — nếu không, mỗi commit nhỏ sẽ kích hoạt build toàn bộ 50 service, đốt tiền và thời gian vô ích.
Contract change và coordinated deploy
Đây là phần khó nhất và đặc trưng nhất của microservices. Giả sử service order gọi API của service payment. Nếu bạn đổi kiểu dữ liệu của một field trong response của payment, thì:
- Nếu deploy
paymenttrước,order(bản cũ) sẽ nhận response không đọc được → lỗi. - Nếu deploy
ordertrước,order(bản mới) gọipayment(bản cũ) chưa có field mới → cũng lỗi.
- Expand:
paymentthêm field mới nhưng vẫn giữ field cũ. Deploy độc lập, không ảnh hưởng ai. orderđược cập nhật để dùng field mới. Deploy độc lập.- Contract: Sau khi chắc chắn không còn ai dùng field cũ,
paymentmới xóa nó đi.
order) publish "kỳ vọng" của mình; pipeline của provider (payment) chạy kiểm tra xem thay đổi có phá vỡ kỳ vọng đó không, trước khi cho merge.Integration testing — test bao nhiêu service là đủ?
Câu hỏi trong ghi chú gốc: "integration testing — bao nhiêu service?". Câu trả lời của mentor: càng ít càng tốt, nhưng đúng chỗ.
Nếu mỗi pipeline của một service phải khởi động cả 50 service khác để test tích hợp, thì mỗi build sẽ mất 40 phút, tốn tài nguyên khổng lồ, và test sẽ "flaky" (chập chờn) vì phụ thuộc quá nhiều thứ. Đây là cái bẫy phổ biến.
Chiến lược đúng theo tầng:
- Trong CI của từng service: chủ yếu là unit test và contract test. Không khởi động service khác thật; thay vào đó dùng mock/stub dựa trên contract đã thống nhất. Nhanh, ổn định, chạy trên mọi commit.
- Test tích hợp hẹp: chỉ dựng service của bạn + các dependency trực tiếp (thường 1–3 service), dùng Testcontainers hoặc docker-compose để dựng database/message queue thật.
- Test end-to-end toàn hệ thống: chạy trên một môi trường staging chung, theo lịch (nightly) hoặc trước khi promote, không chạy trên mọi commit. (Chi tiết chiến lược E2E ở bài riêng.)
Tình huống thực tế
Ví dụ 1 — Sàn thương mại điện tử "ShopViet" và cơn ác mộng 60 phút
ShopViet (tên giả định, mô phỏng một sàn TMĐT tầm trung ở TP.HCM) tách monolith thành 34 microservices trong 2 năm. Ban đầu mỗi team tự viết pipeline. Đến khi hệ thống chạy trên GitHub Actions, họ phát hiện: mỗi lần một developer sửa một dòng ở service catalog, pipeline lại chạy toàn bộ test tích hợp gọi qua cart, order, payment, inventory. Kết quả: build trung bình 58 phút, và tỉ lệ pass chỉ khoảng 70% do test tích hợp flaky.
Team Platform vào cuộc với ba thay đổi:
- Chuyển sang golden pipeline template dùng reusable workflow — mọi service kế thừa một workflow chung, giảm 34 file YAML riêng còn một template + 34 file cấu hình ngắn 15 dòng.
- Áp dụng contract testing bằng Pact: mỗi service publish contract lên Pact Broker. Test tích hợp cross-service trong CI được thay bằng contract verification.
- Chỉ giữ một luồng E2E chạy nightly trên staging.
Bài học rút ra: vấn đề không phải là "test ít đi", mà là "test đúng tầng". Contract test bắt lỗi tương thích sớm và rẻ, giải phóng pipeline khỏi gánh nặng integration test rộng.
Ví dụ 2 — Ví điện tử "PayNow" và cú deploy phối hợp suýt gây sự cố
PayNow (giả định, mô phỏng một ví điện tử Đông Nam Á) cần đổi định dạng mã giao dịch từ số nguyên sang chuỗi UUID — một thay đổi contract giữa service wallet và 6 service tiêu dùng nó. Một kỹ sư mới định deploy wallet với field kiểu mới trước, tin rằng "deploy service gốc trước là đúng".
Trong buổi review, tech lead chặn lại và áp dụng expand–contract:
- Tuần 1:
wallettrả về cả hai field:txn_id(số cũ) vàtxn_ref(UUID mới). Deploy độc lập, không ai bị ảnh hưởng. - Tuần 2–3: lần lượt 6 service consumer chuyển sang dùng
txn_ref, mỗi service deploy riêng, có canary theo dõi. - Tuần 4: sau khi log xác nhận không còn ai đọc
txn_id,walletxóa field cũ.
wallet có bước schema compatibility check tự động: khi kỹ sư thử xóa txn_id ở tuần 1, CI fail ngay với thông báo "breaking change: field txn_id bị 6 consumer phụ thuộc".Bài học rút ra: trong microservices, không có khái niệm "deploy đúng thứ tự để tránh lỗi" cho breaking change — chỉ có "làm cho thay đổi trở thành non-breaking rồi deploy độc lập từng service". Và pipeline phải là hàng rào tự động, không dựa vào việc con người nhớ quy tắc.
Ví dụ 3 — Startup fintech Hà Nội và bài học về ownership pipeline
Một startup fintech ở Hà Nội có 12 service nhưng chỉ 2 kỹ sư DevOps. Họ mắc lỗi ngược lại với ShopViet: tập trung hóa quá mức. Mọi thay đổi pipeline của bất kỳ service nào đều phải qua team DevOps duyệt. Kết quả: DevOps trở thành nút thắt cổ chai, developer chờ 2–3 ngày chỉ để thêm một biến môi trường.
Họ tái cân bằng theo mô hình "paved road": team Platform cung cấp template và các "guardrail" (rào chắn) bắt buộc — quét bảo mật, quản lý secret, chuẩn tag image. Nhưng phần cấu hình riêng của service (biến môi trường, test riêng) thì team sản phẩm tự chủ hoàn toàn trong khuôn khổ template.
Bài học rút ra: microservices CI/CD là bài toán cân bằng giữa tự chủ (autonomy) và chuẩn hóa (standardization). Tập trung quá thì nghẽn, phân tán quá thì hỗn loạn. "Paved road" — con đường lát sẵn nhưng không bắt buộc đi — là điểm cân bằng tốt.
Hướng dẫn từng bước
Đây là lộ trình thực tế để thiết lập CI/CD cho một hệ thống microservices từ đầu hoặc để cải tổ hệ thống đang lộn xộn:
- Chọn repo strategy có ý thức. Quyết định monorepo hay polyrepo dựa trên quy mô team và mức độ chia sẻ code, không theo trào lưu. Ghi lại lý do.
- Xây golden pipeline template. Tạo một reusable workflow/template chuẩn gồm các stage: lint → unit test → contract test → build image → scan → push. Mọi service kế thừa nó thay vì tự viết.
- Thiết lập change detection. Với monorepo, cấu hình pipeline chỉ chạy cho service có file thay đổi (dùng path filter hoặc affected graph). Với polyrepo, điều này miễn phí.
- Đưa contract testing vào CI. Chọn Pact (cho REST/messaging) hoặc schema check (cho gRPC/Protobuf, OpenAPI). Consumer publish kỳ vọng, provider verify trước khi merge. Đây là bước có ROI cao nhất.
- Phân tầng test rõ ràng. Quy ước: unit + contract chạy mọi commit; integration hẹp (service + dependency trực tiếp qua Testcontainers) chạy trên PR; E2E toàn hệ thống chạy nightly trên staging.
- Chuẩn hóa tag image và promotion. Mỗi service build ra image với tag bất biến (immutable, thường theo commit SHA). Cùng một image được promote qua các môi trường, không build lại. (Chi tiết ở bài Promotion Strategy.)
- Deploy độc lập, không "big bang". Mỗi service có pipeline deploy riêng. Với breaking change, áp dụng expand–contract để tránh phải deploy đồng thời.
- Thiết lập guardrail bắt buộc + tự chủ có kiểm soát. Team Platform sở hữu security scan, secret management, chuẩn tag. Team sản phẩm tự chủ cấu hình riêng trong khuôn khổ.
Lỗi thường gặp & mẹo
- Chạy full integration test trên mọi commit. Đây là lỗi kinh điển làm pipeline chậm và flaky. Mẹo: thay bằng contract test, dồn E2E vào lịch nightly.
- Copy-paste pipeline giữa các service. Mỗi service một bản sao YAML dẫn đến "drift" — mỗi nơi một phiên bản. Mẹo: dùng template/reusable workflow ngay từ đầu, dù chỉ có 3 service.
- Xử lý breaking change bằng cách "deploy cùng lúc". Trong hệ phân tán, "cùng lúc" không tồn tại thật; luôn có khoảng thời gian hai phiên bản cùng chạy. Mẹo: luôn làm thay đổi backward-compatible (expand–contract).
- Quên rằng contract có hai chiều. Nhiều team chỉ test provider, quên rằng consumer cũng có kỳ vọng. Mẹo: consumer-driven contract (Pact) đảm bảo cả hai phía.
- Để pipeline phụ thuộc thứ tự deploy thủ công. Nếu "phải nhớ deploy A trước B" thì sớm muộn ai đó sẽ quên. Mẹo: mã hóa quy tắc thành check tự động trong CI, để máy chặn thay vì người nhớ.
- Build lại image ở mỗi môi trường. Image ở staging khác image ở production → mất nguyên tắc "test cái bạn deploy". Mẹo: build một lần, promote cùng một artifact.
- Bỏ quên môi trường staging chung. Nếu không có nơi nào cả 50 service cùng chạy, bạn không bao giờ phát hiện lỗi tương tác thật. Mẹo: duy trì một staging đồng bộ, chạy smoke test sau mỗi promote.
Bài tập thực hành
- Vẽ dependency graph. Chọn (hoặc tưởng tượng) một hệ thống 5 service:
user,order,payment,inventory,notification. Vẽ ai gọi ai. Xác định những cặp service nào có "contract" cần bảo vệ.
- Thiết kế test theo tầng. Với service
order, liệt kê cụ thể: những gì test bằng unit, những gì test bằng contract, những gì cần integration hẹp, và những gì thuộc E2E. Ghi rõ cái nào chạy trên mỗi commit, cái nào chạy nightly.
- Kịch bản expand–contract. Giả sử
paymentcần đổi fieldamount(số) thành object{value, currency}. Viết ra 3 bước deploy (expand, migrate consumer, contract) và nêu rõ ở bước nào pipeline nên fail nếu ai đó cố xóa field cũ quá sớm.
- Phác thảo golden template. Viết ra (dạng danh sách stage) một pipeline template chung cho mọi service, đánh dấu stage nào là guardrail bắt buộc (không được tắt) và stage nào team sản phẩm được tùy chỉnh.
- Ước lượng chi phí. Giả sử hệ thống 40 service, mỗi build 10 phút. So sánh chi phí runner giữa hai kịch bản: (a) mọi commit build tất cả, (b) chỉ build service thay đổi (giả định trung bình 1 commit chạm 2 service). Con số này giúp bạn thấy giá trị của change detection.
Tóm tắt
CI/CD cho microservices không phải là "làm 50 lần cái pipeline cho một service", mà là một bài toán khác về chất. Ba thách thức cốt lõi cần nhớ:
- Bùng nổ pipeline (N service × N pipeline): giải bằng chuẩn hóa qua golden template, kết hợp change detection để không build cái không đổi, và cân bằng giữa tự chủ với guardrail bắt buộc theo mô hình "paved road".
- Contract change và coordinated deploy: không bao giờ dựa vào "deploy đúng thứ tự"; luôn biến thay đổi thành backward-compatible qua expand–contract, và để pipeline tự động chặn breaking change bằng contract testing.
- Integration testing bao nhiêu service: càng ít càng tốt, đúng tầng — unit + contract cho mọi commit, integration hẹp cho PR, E2E toàn hệ thống cho nightly. Contract test là đòn bẩy giá trị nhất, thay thế phần lớn nhu cầu integration test rộng.