Product Management
Đăng nhập
ESC

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

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

Bài 53 — Polyglot CI — Multiple Languages

Đã đọc 0/14 mục · còn khoảng 13 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 từng làm việc ở một startup đang tăng trưởng nhanh, chắc chắn bạn đã chứng kiến cảnh này: hôm nay team dùng Node.js viết API, tháng sau team data khoe model Python đầu tiên, quý sau team platform quyết định viết một service cần hiệu năng cao bằng Go, rồi mobile lại chia thành iOS (Swift) và Android (Kotlin). Chỉ trong một năm, một codebase từng "thuần Node" đã trở thành một stack đa ngôn ngữ — hay như dân trong nghề gọi là polyglot (đa ngữ).

Vấn đề bắt đầu khi bạn phải build và deploy tất cả những thứ đó. Mỗi ngôn ngữ có công cụ build riêng: Node có npm/pnpm, Java có Maven/Gradle, Python có pip/poetry, Go có go build, iOS có xcodebuild. Mỗi ngôn ngữ có cách cache, cách chạy test, cách đóng gói khác nhau. Nếu mỗi team tự viết pipeline theo ý mình, bạn sẽ có một "vườn thú" CI: mười service, mười phong cách pipeline, không ai bảo trì nổi, và khi có sự cố bảo mật chung (ví dụ cần vá một CVE trong base image) thì phải sửa hai mươi chỗ khác nhau.

Bài này tập trung đúng vào bài toán đó: làm sao xây dựng một hệ thống CI phục vụ nhiều ngôn ngữ mà vẫn nhất quán, dễ bảo trì và cho phép mỗi team giữ được đặc thù kỹ thuật của mình. Đây không phải là chuyện "chọn ngôn ngữ nào tốt hơn", mà là chuyện kiến trúc pipeline — một kỹ năng cực kỳ giá trị khi bạn lên vai trò platform engineer hoặc tech lead. Chúng ta sẽ không đi sâu vào từng công cụ CI cụ thể (GitHub Actions, GitLab, Jenkins — các bài trước đã nói), cũng không đi vào caching hay artifact management chi tiết (bài 11, 12 lo phần đó). Ở đây, trọng tâm là chiến lược tổ chức CI đa ngôn ngữ.

Khái niệm cốt lõi

Polyglot CI là gì

Polyglot CI là cách tổ chức hệ thống tích hợp liên tục (CI) sao cho một tổ chức có thể build, test và đóng gói nhiều ngôn ngữ lập trình khác nhau theo một mô hình chung, thống nhất. Mục tiêu không phải là ép mọi ngôn ngữ vào một khuôn cứng nhắc, mà là tìm ra phần chung có thể chuẩn hoá và tách bạch nó khỏi phần riêng đặc thù từng ngôn ngữ.

Nguyên tắc trụ cột: Standardized base + language-specific layers

Đây là ý tưởng trung tâm của cả bài. Hãy hình dung pipeline của bạn như một chiếc bánh nhiều tầng:

  • Tầng nền chuẩn hoá (standardized base): những thứ mọi service đều cần, bất kể ngôn ngữ. Ví dụ: checkout code, xác thực (auth) vào registry, quét bí mật (secret scanning), gắn nhãn commit, quy ước đặt tên image, cách publish artifact, cách gửi thông báo kết quả, quy tắc bảo mật chung. Tầng này do team platform sở hữu và viết một lần.
  • Tầng đặc thù ngôn ngữ (language-specific layer): những thứ chỉ liên quan đến một ngôn ngữ. Ví dụ: Java cần mvn verify, Python cần pytest và ruff, Go cần go test ./... và golangci-lint, Node cần pnpm test. Tầng này được đóng gói thành các "template" hoặc "component" có thể tái sử dụng.
Sự tách bạch này giống hệt tư duy lập trình hướng đối tượng: có một lớp cha định nghĩa hành vi chung, các lớp con kế thừa và ghi đè phần riêng. Trong thế giới CI, "lớp cha" thường được hiện thực bằng reusable workflow (GitHub Actions), CI templates / includes (GitLab), hoặc shared library (Jenkins).

Ba mô hình tổ chức phổ biến

Khi đối mặt với stack đa ngôn ngữ, thực tế các công ty thường chọn một trong ba hướng:

1. Mô hình "một pipeline khổng lồ" (monolithic pipeline). Tất cả ngôn ngữ chung một file cấu hình dài, dùng if/else để rẽ nhánh theo ngôn ngữ. Đơn giản lúc đầu nhưng nhanh chóng trở thành ác mộng: file dài hàng nghìn dòng, sửa một chỗ vỡ chỗ khác. Chỉ nên dùng cho dự án rất nhỏ.

2. Mô hình "mỗi ngôn ngữ một silo". Mỗi team tự do làm mọi thứ, không chia sẻ gì. Linh hoạt tối đa nhưng không có sự nhất quán, khó áp chính sách bảo mật chung, và kiến thức bị phân mảnh. Đây là trạng thái "vườn thú" mà ta muốn tránh.

3. Mô hình "nền chuẩn + template ngôn ngữ" (khuyến nghị). Team platform cung cấp một bộ template chuẩn hoá cho từng ngôn ngữ. Mỗi service chỉ cần "gọi" template phù hợp và truyền vài tham số. Đây là điểm cân bằng vàng giữa nhất quán và linh hoạt, và là thứ chúng ta sẽ thực hành trong bài.

Contract giữa nền và ngôn ngữ

Để mô hình thứ ba hoạt động, bạn cần định nghĩa một contract (giao ước) rõ ràng — tức là quy ước mà mọi template ngôn ngữ phải tuân thủ. Contract thường gồm:

  • Đầu vào chuẩn: service phải khai báo language, service_name, version.
  • Các "giai đoạn" chuẩn (stages): dù ngôn ngữ nào, pipeline đều đi qua các bước có tên chung như lint → test → build → scan → publish. Bên trong mỗi bước, lệnh cụ thể khác nhau, nhưng tên bước và thứ tự thì giống nhau. Điều này giúp dashboard, báo cáo, và metric có thể so sánh giữa các service.
  • Đầu ra chuẩn: kết quả test dạng JUnit XML, image được đẩy lên registry theo quy ước đặt tên registry/service:git-sha, báo cáo coverage theo định dạng chung.
Nhờ contract này, một service Go và một service Python nhìn từ bên ngoài "trông giống nhau" dù bên trong hoàn toàn khác biệt.

Tình huống thực tế

Ví dụ 1 — Tiki và bài toán 40 service, 5 ngôn ngữ

Hãy tưởng tượng một sàn thương mại điện tử lớn tại Việt Nam như Tiki (bối cảnh minh hoạ dựa trên đặc điểm chung của các sàn cỡ đó). Sau vài năm phát triển, họ có khoảng 40 microservice: phần lớn là Java (Spring Boot) cho nghiệp vụ đơn hàng, thanh toán; một cụm Python cho gợi ý sản phẩm và chống gian lận; vài service Go cho gateway và xử lý real-time; và Node cho các BFF (backend-for-frontend).

Ban đầu mỗi team viết pipeline riêng. Đến khi có một lỗ hổng nghiêm trọng trong thư viện log4j (bối cảnh gợi nhớ sự cố Log4Shell cuối 2021), team bảo mật yêu cầu: mọi image Java phải rebuild với base image đã vá trong 48 giờ. Kết quả là hỗn loạn — mỗi service dùng một Dockerfile khác nhau, một base image khác nhau, không ai biết tổng cộng bao nhiêu service bị ảnh hưởng.

Sau sự cố, họ chuyển sang mô hình nền chuẩn. Team platform tạo một reusable workflow ci-java.yml và một base image chung tiki/java-base:17. Giờ khi cần vá, họ chỉ sửa một base image, bump version, và tất cả service Java rebuild qua cùng một template. Bài học: sự nhất quán không chỉ để cho đẹp — nó là năng lực phản ứng sự cố. Khi mọi service Java đi qua cùng một cửa, bạn vá một lần là xong; khi mỗi service một kiểu, bạn phải đi gõ cửa từng team.

Ví dụ 2 — Startup fintech Singapore với stack Go + Python ML

Một startup fintech giả định ở Singapore, tạm gọi là PayFlow, có 12 kỹ sư. Backend giao dịch viết bằng Go (vì cần độ trễ thấp), còn team rủi ro dùng Python để train và serve model chấm điểm tín dụng. Hai thế giới này rất khác nhau: Go build nhanh (dưới 1 phút), còn pipeline Python phải cài numpy, scikit-learn, torch — mỗi lần cài mất 6–8 phút vì tải hàng trăm MB dependency.

Nếu ép cả hai vào một template chung, bạn sẽ hoặc làm chậm Go (bắt nó chờ cài môi trường Python) hoặc làm Python thiếu tối ưu. PayFlow giải bài này bằng cách giữ nền chuẩn ở tầng "vỏ ngoài" — cùng quy ước đặt tên image, cùng bước secret-scan, cùng cách publish — nhưng cho phép tầng ngôn ngữ tự quyết chiến lược môi trường. Template Go dùng base image nhẹ và cache module gọn nhẹ; template Python dùng một base image đã pre-install sẵn các thư viện ML nặng, được rebuild định kỳ hằng tuần thay vì mỗi lần chạy CI.

Kết quả: pipeline Go giữ nguyên tốc độ, pipeline Python giảm từ 8 phút xuống còn 2 phút cho phần setup vì đã có sẵn thư viện trong base image. Bài học: chuẩn hoá không có nghĩa là đồng nhất hoá mọi chi tiết. Bạn chuẩn hoá phần vỏ và giao ước, còn phần ruột thì để mỗi ngôn ngữ tối ưu theo bản chất của nó.

Ví dụ 3 — Team platform ôm quá nhiều dẫn đến nghẽn cổ chai

Một công ty gọi xe khu vực Đông Nam Á (bối cảnh giả định lấy cảm hứng từ các super-app) từng đi hơi quá đà theo hướng ngược lại. Team platform muốn kiểm soát tuyệt đối nên viết template cứng nhắc đến mức mọi tuỳ chỉnh nhỏ đều phải qua họ. Team Kotlin muốn thêm một bước phân tích tĩnh Detekt ư? Phải mở ticket, chờ team platform sửa template dùng chung, xét duyệt, phát hành. Một tuần trôi qua chỉ để thêm một dòng.

Kết quả là các team bắt đầu "lách": họ copy template ra, sửa riêng, và thế là sự nhất quán mà platform cố xây lại vỡ vụn từ bên trong. Họ phải thiết kế lại: template cung cấp các điểm mở rộng (extension points) — cho phép service truyền thêm bước tuỳ chỉnh qua tham số extra_steps, hoặc override một số biến. Bài học: một template polyglot tốt phải "chặt ở phần cần chặt, lỏng ở phần cần lỏng". Nếu bạn khoá quá chặt, các team sẽ tự thoát ra và bạn mất luôn quyền kiểm soát.

Hướng dẫn từng bước

Giả sử bạn dùng GitHub Actions (nguyên tắc áp dụng tương tự cho GitLab includes hay Jenkins shared library). Ta sẽ xây một hệ thống polyglot CI cho stack có Java, Python, Go và Node.

Bước 1 — Xác định "giai đoạn chuẩn". Ngồi cùng các team và thống nhất các stage chung: lint → test → build → scan → publish. Ghi rõ ý nghĩa từng stage, không quan tâm ngôn ngữ. Đây là xương sống contract của bạn.

Bước 2 — Tạo repo ci-templates riêng. Đây là nơi team platform sở hữu các reusable workflow. Ví dụ cấu trúc:

ci-templates/
  .github/workflows/
    ci-java.yml
    ci-python.yml
    ci-go.yml
    ci-node.yml
    _shared-publish.yml   # phần dùng chung mọi ngôn ngữ

Bước 3 — Viết một reusable workflow cho một ngôn ngữ. Ví dụ ci-go.yml:

on:
  workflow_call:
    inputs:
      service_name: { required: true, type: string }
      go_version:   { default: "1.22", type: string }

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with: { go-version: "${{ inputs.go_version }}" }
      - run: golangci-lint run          # stage: lint
      - run: go test ./... -race         # stage: test
      - run: go build -o app ./...       # stage: build
      - uses: ./.github/workflows/_shared-publish.yml  # phần chung

Điểm mấu chốt: các bước lint/test/build là đặc thù Go, còn bước publish gọi vào phần dùng chung.

Bước 4 — Tách phần chung ra _shared-publish.yml. Đưa vào đây: đăng nhập registry, quét bí mật, đặt tên image theo registry/${service}:${git_sha}, đẩy image, gửi thông báo. Mọi ngôn ngữ đều dùng lại phần này.

Bước 5 — Ở repo service, chỉ cần gọi template. File .github/workflows/ci.yml của một service Go trở nên cực ngắn:

jobs:
  build:
    uses: my-org/ci-templates/.github/workflows/ci-go.yml@v1
    with:
      service_name: order-gateway

Bước 6 — Mở điểm mở rộng. Thêm input extra_lint_args hoặc cho phép service khai báo thêm bước qua một file quy ước (ví dụ đọc ci.extra.sh nếu tồn tại). Đây chính là bài học từ ví dụ 3.

Bước 7 — Gắn version cho template. Dùng tag như @v1, @v2. Khi cần nâng cấp phá vỡ tương thích, bạn phát hành v2 và cho các team di chuyển dần, thay vì ép tất cả cùng lúc.

Bước 8 — Thêm base image dùng chung. Với các ngôn ngữ có dependency nặng (Python ML, Java), duy trì base image pre-built, rebuild theo lịch, để CI hằng ngày không phải cài lại từ đầu.

Lỗi thường gặp & mẹo

Lỗi 1: Ép mọi ngôn ngữ dùng chung một runner/môi trường. Go và Rust build cực nhanh, còn iOS bắt buộc chạy trên macOS runner (đắt hơn nhiều), Python ML cần nhiều RAM. Đừng dùng một cấu hình runner cho tất cả. Mẹo: cho template khai báo runs-on phù hợp từng ngôn ngữ.

Lỗi 2: Chuẩn hoá quá sâu, khoá cứng phần ruột. Như ví dụ 3, khi bạn không cho team tuỳ biến, họ sẽ copy template ra và bạn mất kiểm soát. Mẹo: luôn thiết kế extension point ngay từ đầu.

Lỗi 3: Không đặt tên artifact/image nhất quán. Nếu mỗi ngôn ngữ đặt tên image một kiểu, bạn không thể viết công cụ chung để dò tìm, promote hay rollback. Mẹo: quy ước đặt tên là phần bất khả xâm phạm của contract.

Lỗi 4: Quên phiên bản hoá template. Sửa template dùng chung mà không có version sẽ làm vỡ pipeline của hàng chục service cùng lúc lúc nửa đêm. Mẹo: luôn dùng tag version và thay đổi phá vỡ tương thích phải qua major version mới.

Lỗi 5: Coi thường sự khác biệt về công cụ bảo mật. Quét SCA cho Node (đọc package-lock.json) khác với Java (đọc pom.xml) hay Python (poetry.lock). Một công cụ scan chung không hiểu hết mọi ecosystem. Mẹo: cho tầng ngôn ngữ chọn scanner phù hợp, nhưng chuẩn hoá định dạng báo cáo đầu ra để tổng hợp chung.

Mẹo vàng: hãy đo "thời gian một service mới lên CI được". Với mô hình polyglot tốt, một team viết service Go mới chỉ cần thêm 5 dòng gọi template là có ngay pipeline đầy đủ. Nếu con số đó là "vài ngày", kiến trúc của bạn đang có vấn đề.

Bài tập thực hành

  • Lập bản đồ stack. Liệt kê các ngôn ngữ trong một dự án bạn từng làm (hoặc dự án giả định gồm Java + Python + Go + Node). Với mỗi ngôn ngữ, ghi ra: công cụ build, công cụ lint, cách chạy test, cách đóng gói.
  • Định nghĩa contract. Viết ra 5 stage chuẩn và mô tả đầu vào/đầu ra chuẩn cho mỗi service. Đây là "hiến pháp" cho hệ thống CI của bạn.
  • Viết một reusable workflow. Chọn một ngôn ngữ bạn quen nhất và viết một reusable workflow hoàn chỉnh (như ci-go.yml ở trên), có ít nhất một input tuỳ chỉnh và một điểm mở rộng.
  • Tách phần chung. Xác định 3 bước mà mọi ngôn ngữ đều cần và mô tả cách bạn tách chúng thành một workflow dùng chung.
  • Kịch bản sự cố. Giả sử có CVE nghiêm trọng trong base image Python. Với kiến trúc bạn thiết kế, hãy viết ra các bước để vá toàn bộ service Python. So sánh với trường hợp mỗi team dùng Dockerfile riêng — mất bao lâu?

Tóm tắt

Polyglot CI không phải là chuyện chọn ngôn ngữ, mà là chuyện kiến trúc pipeline cho một tổ chức có nhiều ngôn ngữ. Nguyên tắc trung tâm là nền chuẩn hoá + tầng đặc thù ngôn ngữ: bạn chuẩn hoá phần vỏ (giai đoạn chuẩn, quy ước đặt tên, bảo mật, publish) và để mỗi ngôn ngữ tự tối ưu phần ruột (lệnh build, cách cài môi trường, scanner phù hợp).

Ba bài học lớn từ thực tế: (1) sự nhất quán chính là năng lực phản ứng sự cố — khi mọi service cùng đi qua một template, bạn vá một lần là xong; (2) chuẩn hoá không có nghĩa đồng nhất hoá — hãy tôn trọng bản chất khác biệt của từng ngôn ngữ; (3) template phải "chặt ở phần cần chặt, lỏng ở phần cần lỏng", luôn để sẵn điểm mở rộng, kẻo các team tự thoát ra và bạn mất kiểm soát.

Khi làm đúng, phần thưởng rất cụ thể: một team viết service mới chỉ cần vài dòng gọi template là có ngay pipeline đầy đủ, và team platform có thể áp chính sách bảo mật lên toàn bộ tổ chức chỉ với một thay đổi. Đó chính là sức mạnh của polyglot CI được thiết kế tử tế.

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