Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 39 — CI/CD cho Microservices

Đã đọc 0/14 mục · còn khoảng 12 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

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.
Kỹ thuật cốt lõi cho monorepo là change detection (phát hiện thay đổi theo đường dẫn) hoặc dùng công cụ chuyên dụng như Nx, Turborepo, Bazel để xây "affected graph" — chỉ chạy pipeline cho service bị ảnh hưởng. (Chi tiết công cụ nằm ở bài về Monorepo CI/CD; ở đây bạn chỉ cần nhớ nguyên tắc: đừng build cái không đổi.)

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 payment trước, order (bản cũ) sẽ nhận response không đọc được → lỗi.
  • Nếu deploy order trước, order (bản mới) gọi payment (bản cũ) chưa có field mới → cũng lỗi.
Đây gọi là breaking contract change, và nó là nguyên nhân số một gây sự cố khi deploy microservices. Giải pháp không phải là "deploy cả hai cùng một giây" (điều gần như bất khả thi trong hệ thống phân tán), mà là backward-compatible change theo hai bước (expand–contract):

  • Expand: payment thê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ũ, payment mới xóa nó đi.
Để pipeline tự động ngăn breaking change lọt qua, ta dùng contract testing — tiêu biểu là công cụ Pact hoặc kiểm tra tương thích schema (schema compatibility check) cho Protobuf/Avro/OpenAPI ngay trong CI. Consumer (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.)
Triết lý nền tảng: contract test thay thế phần lớn nhu cầu chạy integration test rộng. Nếu contract được đảm bảo, mỗi service có thể được kiểm chứng độc lập.

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.
Kết quả sau 6 tuần: thời gian pipeline trung bình còn 11 phút, tỉ lệ pass lên 96%. Số sự cố production do breaking change giảm từ khoảng 3 vụ/tháng xuống gần như bằng 0.

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: wallet trả 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, wallet xóa field cũ.
Pipeline của 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ử payment cần đổi field amount (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.
Kim chỉ nam của cả bài: trong microservices, mỗi service phải deploy được độc lập. Mọi kỹ thuật ở trên — từ contract test, expand–contract, đến phân tầng test — đều phục vụ đúng một mục tiêu đó. Khi các service không còn phải "chờ nhau" để deploy, bạn mới thực sự thu được lợi ích của microservices, thay vì gánh chịu một "monolith phân tán" còn khổ hơn monolith ban đầu.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi