Product Management
Đăng nhập
ESC

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

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

Bài 38 — Pipeline Templates & Reusability

Đã đọc 0/15 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

Hãy tưởng tượng bạn là DevOps Engineer tại một công ty fintech ở TP.HCM. Ngày đầu tiên đi làm, sếp giao cho bạn một nhiệm vụ nghe có vẻ đơn giản: "Thêm bước quét bảo mật (security scan) vào pipeline của tất cả các service." Bạn mở kho code lên và... chết lặng. Có 47 microservice, và mỗi service có một file workflow riêng. Tệ hơn, chúng chẳng giống nhau chút nào. Service payment dùng Node 16, service notification dùng Node 18, service wallet thì copy-paste từ một dự án cũ nào đó với cả những bước không còn liên quan. Có service dùng npm ci, có service dùng yarn, và có service thì gọi thẳng npm install — điều mà đội bảo mật đã cấm từ năm ngoái.

Để thêm một bước quét bảo mật, bạn phải sửa tay 47 file, mỗi file một kiểu, và kiểm thử từng cái. Ba tuần trôi qua, và bạn vẫn chưa xong.

Đây chính là "cơn ác mộng bảo trì" (maintenance nightmare) mà bài học hôm nay sẽ giúp bạn tránh khỏi. Vấn đề gốc rễ không phải là bạn làm chậm — mà là kiến trúc pipeline của công ty đã sai ngay từ đầu: mỗi đội tự viết workflow của mình từ số không. Kết quả là drift — các pipeline lệch nhau dần theo thời gian, và không ai còn nắm được "chuẩn" thật sự là gì.

Giải pháp là Pipeline Templates & Reusability — tách phần logic chung ra thành các mẫu (template) dùng chung, để mỗi service chỉ cần khai báo "tôi muốn dùng mẫu này với vài tham số riêng", thay vì viết lại toàn bộ. Đây là kỹ năng phân biệt giữa một đội DevOps trưởng thành và một đội chỉ đang "chữa cháy" liên tục.

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

Tái sử dụng (reusability) là gì trong bối cảnh pipeline

Nguyên tắc lập trình DRY — Don't Repeat Yourself (Đừng lặp lại chính mình) — không chỉ áp dụng cho code ứng dụng, mà còn cho cả hạ tầng CI/CD. Khi logic "cách build một service Node.js" bị lặp lại ở 47 nơi, thì mỗi lần chuẩn thay đổi, bạn phải sửa 47 nơi. Reusability nghĩa là viết logic đó một lần duy nhất ở một nơi trung tâm, rồi mọi service tham chiếu đến nó.

Có ba cấp độ tái sử dụng, từ nhỏ đến lớn:

  • Step/Action level — tái sử dụng một bước nhỏ, ví dụ "đăng nhập vào Docker registry". Trong GitHub Actions, đây là composite action.
  • Job/Workflow level — tái sử dụng cả một quy trình con, ví dụ "build → test → push image". Trong GitHub Actions, đây là reusable workflow.
  • Template level — cả một pipeline khung, các đội chỉ điền tham số. Trong GitLab CI đây là include + extends, trong Jenkins là Shared Library.

Composite Action vs Reusable Workflow — đừng nhầm hai thứ này

Đây là điểm mà nhiều bạn học viên hay lẫn lộn, nên mình muốn làm rõ ngay:

  • Composite Action gói nhiều step thành một step lớn. Nó chạy bên trong một job đang tồn tại, chia sẻ cùng runner, cùng thư mục làm việc. Dùng khi bạn muốn tái sử dụng một chuỗi lệnh nhỏ (ví dụ: setup Node + cache + install).
  • Reusable Workflow gói nhiều job thành một workflow con. Nó được gọi như một job từ workflow cha, chạy trên runner riêng, có inputs, secrets và outputs rõ ràng. Dùng khi bạn muốn tái sử dụng cả một pipeline (build-test-deploy).
Cách nhớ đơn giản: composite action là "hàm nhỏ", reusable workflow là "module lớn".

Reusable Workflow trong GitHub Actions

Đây là công cụ chủ lực. Bạn tạo một file workflow đặc biệt với trigger workflow_call, khai báo các tham số đầu vào, rồi các repo khác gọi đến nó. Ví dụ, một reusable workflow build và kiểm thử Node.js:

# .github/workflows/node-ci.yml (đặt trong repo trung tâm: acme/ci-templates)
name: Reusable Node CI
on:
  workflow_call:
    inputs:
      node-version:
        required: false
        type: string
        default: '18'
      run-e2e:
        required: false
        type: boolean
        default: false
    secrets:
      NPM_TOKEN:
        required: true

jobs:
  build-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
      - run: npm ci
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
      - run: npm run lint
      - run: npm test
      - if: ${{ inputs.run-e2e }}
        run: npm run test:e2e

Và mỗi service chỉ cần một file cực ngắn để gọi nó:

# .github/workflows/ci.yml (trong repo service payment)
name: CI
on: [push, pull_request]
jobs:
  ci:
    uses: acme/ci-templates/.github/workflows/node-ci.yml@v1
    with:
      node-version: '18'
      run-e2e: true
    secrets:
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Từ 80 dòng lặp lại, giờ mỗi service chỉ còn 10 dòng. Và khi cần thêm bước security scan, bạn chỉ sửa một file node-ci.yml, tất cả 47 service tự động được cập nhật.

Versioning — chìa khóa để template không phá vỡ mọi thứ

Bạn thấy dòng @v1 ở trên không? Đó là điểm mấu chốt. Nếu bạn để các service trỏ vào @main, thì mỗi khi bạn sửa template, tất cả pipeline sẽ đổi ngay lập tức — kể cả khi thay đổi đó gây lỗi (breaking change). Cách làm chuyên nghiệp là gắn tag phiên bản theo semantic versioning cho template: v1, v1.2.0, v2. Đội dùng template chủ động nâng cấp khi họ sẵn sàng, không bị ép buộc.

Nguyên tắc thiết kế template tốt

Một template tốt cần cân bằng giữa tính chuẩn hóa (mọi người làm giống nhau) và tính linh hoạt (mỗi đội vẫn có chỗ tùy biến). Nếu template quá cứng nhắc, các đội sẽ bỏ nó và tự viết lại. Nếu quá lỏng lẻo, nó chẳng khác gì không có template. Bí quyết là:

  • Đưa những gì bắt buộc phải giống nhau (bảo mật, chuẩn build) vào phần cứng của template.
  • Đưa những gì khác nhau theo service (phiên bản runtime, có chạy e2e không) ra thành inputs có giá trị mặc định hợp lý.

Tình huống thực tế

Tình huống 1: Tiki và bài toán 47 microservice

Giả sử một sàn thương mại điện tử lớn tại Việt Nam (bối cảnh tương tự Tiki) có 47 microservice trên GitHub. Ban đầu mỗi đội tự do viết workflow. Sau một sự cố bảo mật — một service vô tình build image với thư viện có lỗ hổng nghiêm trọng vì thiếu bước quét SCA — ban lãnh đạo yêu cầu chuẩn hóa toàn bộ.

Đội Platform tạo một repo trung tâm tiki/ci-templates, trong đó có ba reusable workflow: node-service.yml, java-service.yml, và go-service.yml. Mỗi workflow đã tích hợp sẵn: lint, test, quét bảo mật với Trivy, build image, và đẩy lên registry nội bộ với tag chuẩn. Việc di chuyển (migration) diễn ra trong 6 tuần: mỗi đội thay file workflow dài của mình bằng đúng 12 dòng gọi template.

Kết quả: Lần sau, khi công ty cần nâng phiên bản Trivy và thêm chính sách "chặn build nếu có lỗ hổng CVSS ≥ 7", đội Platform chỉ sửa template một lần, phát hành v2, và gửi thông báo. Thời gian triển khai từ "3 tuần sửa tay 47 file" giảm còn "1 giờ sửa 1 file + thông báo nâng cấp".

Bài học: Chi phí lớn nhất của việc không có template không nằm ở lần viết đầu tiên, mà ở mọi lần thay đổi về sau. Template chuyển chi phí bảo trì từ O(số service) về O(1).

Tình huống 2: Startup và cái bẫy "template quá thông minh"

Một startup logistics ở Singapore (khoảng 15 kỹ sư) học theo bài này và tạo template. Nhưng họ mắc lỗi ngược lại: họ nhồi quá nhiều logic điều kiện vào một reusable workflow duy nhất. File template phình lên hơn 400 dòng với hàng chục input kiểu if-this-then-that: deploy-to, use-canary, skip-tests, custom-build-command, override-dockerfile...

Kết quả là chẳng ai hiểu nổi template hoạt động thế nào. Khi một service cần hành vi hơi khác, kỹ sư lại thêm một input mới, khiến template ngày càng rối. Sau 4 tháng, template trở thành một "God workflow" khó bảo trì hơn cả 15 file riêng lẻ ban đầu.

Bài học: Đừng cố gói mọi trường hợp vào một template. Thà có ba template rõ ràng, đơn giản (một cho web frontend, một cho API backend, một cho worker) còn hơn một template "vạn năng" mà không ai hiểu. Nếu số lượng input vượt quá khoảng 6-8 cái, đó là dấu hiệu bạn nên tách template.

Tình huống 3: Ngân hàng với Jenkins Shared Library

Một ngân hàng tại Hà Nội dùng Jenkins (không phải GitHub Actions) cho hơn 30 ứng dụng nội bộ. Họ áp dụng cùng nguyên lý reusability nhưng với công cụ khác: Jenkins Shared Library. Đội Platform viết các hàm dùng chung trong thư mục vars/, ví dụ buildAndDeploy.groovy. Mỗi ứng dụng chỉ cần một Jenkinsfile ngắn:

@Library('acme-pipeline-lib@v3') _
buildAndDeploy(
    lang: 'java',
    deployEnv: 'staging'
)

Toàn bộ logic phức tạp — kiểm tra compliance, ký số artifact, ghi log audit cho yêu cầu của Ngân hàng Nhà nước — nằm trong thư viện, được đội bảo mật kiểm soát. Các đội ứng dụng không thể (và không cần) sửa những bước bắt buộc này.

Bài học: Reusability không phải đặc quyền của GitHub Actions. GitLab có include/extends, Jenkins có Shared Library, Azure DevOps có template YAML. Nguyên lý giống hệt nhau: tách logic chung, tham chiếu qua phiên bản, để một đội trung tâm sở hữu chuẩn.

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

Đây là quy trình mình khuyên bạn áp dụng khi muốn đưa template vào một tổ chức đang có nhiều pipeline lộn xộn.

Bước 1 — Khảo sát và tìm mẫu chung. Đọc qua 5-10 pipeline hiện có. Ghi lại phần nào lặp lại (checkout, setup runtime, test, build image) và phần nào thực sự khác nhau giữa các service. Phần lặp lại chính là "ứng viên" cho template; phần khác nhau chính là các inputs tương lai.

Bước 2 — Tạo repo template trung tâm. Tạo một repo riêng, ví dụ your-org/ci-templates. Đặt reusable workflow vào .github/workflows/. Repo này nên có README rõ ràng và ví dụ sử dụng cho từng template.

Bước 3 — Viết template đầu tiên cho một loại service phổ biến nhất. Đừng cố làm hết mọi ngôn ngữ ngay. Chọn loại service nhiều nhất (ví dụ Node.js), viết template cho nó với các input tối thiểu và giá trị mặc định hợp lý.

Bước 4 — Gắn tag phiên bản. Tạo tag v1. Đây là hợp đồng giữa bạn và các đội dùng template. Từ giờ, mọi thay đổi phá vỡ tương thích phải lên v2.

Bước 5 — Di chuyển một service thí điểm. Chọn một service, thay workflow của nó bằng lời gọi template. Chạy thử, so sánh kết quả với pipeline cũ để đảm bảo hành vi giống nhau. Đây là bước kiểm chứng quan trọng trước khi nhân rộng.

Bước 6 — Nhân rộng và viết tài liệu. Sau khi thí điểm ổn, di chuyển dần các service còn lại. Viết tài liệu ngắn gọn: "Muốn setup CI cho service Node mới? Copy 10 dòng này." Tài liệu tốt giúp template được đón nhận thay vì bị né tránh.

Bước 7 — Thiết lập quy trình quản trị. Xác định ai được quyền sửa template (thường là đội Platform), và quy trình thông báo khi phát hành phiên bản mới. Không có bước này, template sẽ lại drift theo cách khác.

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

Lỗi 1 — Trỏ vào @main thay vì tag phiên bản. Đây là lỗi phổ biến nhất. Khi bạn sửa template, mọi pipeline đổi ngay lập tức, và một lỗi nhỏ có thể làm hỏng CI của cả tổ chức cùng lúc. Mẹo: Luôn dùng tag (@v1) hoặc commit SHA cho môi trường sản xuất. Chỉ dùng @main khi đang phát triển thử nghiệm.

Lỗi 2 — Quên rằng secrets không tự động kế thừa. Trong GitHub Actions, reusable workflow không tự thấy secrets của repo cha. Bạn phải truyền tường minh qua khối secrets:, hoặc dùng secrets: inherit nếu muốn truyền tất cả. Nhiều bạn debug hàng giờ chỉ vì quên điều này.

Lỗi 3 — Template quá cứng, không có đường thoát. Nếu template không cho phép bất kỳ tùy biến nào, các đội có nhu cầu đặc biệt sẽ bỏ template và tự viết lại — và bạn quay về vạch xuất phát. Mẹo: Thêm một input kiểu extra-steps hoặc cho phép truyền lệnh build tùy chỉnh có kiểm soát, để có "van xả áp" hợp lý.

Lỗi 4 — Không giới hạn độ sâu lồng nhau. Reusable workflow có thể gọi reusable workflow khác, nhưng GitHub giới hạn độ sâu (hiện tại là 4 cấp). Lồng quá sâu khiến việc gỡ lỗi trở nên khủng khiếp. Mẹo: Giữ cấu trúc phẳng, tối đa 2 cấp.

Lỗi 5 — Không nghĩ đến khả năng gỡ lỗi. Khi CI hỏng, kỹ sư của đội service mở log ra và thấy toàn bước "lạ" mà họ không viết. Mẹo: Đặt tên bước rõ ràng trong template, và ghi log thân thiện. Template tốt là template mà người dùng nó vẫn hiểu được khi có sự cố.

Mẹo vàng — Bắt đầu nhỏ. Đừng cố chuẩn hóa mọi thứ trong một lần. Tách ra một bước dùng chung (ví dụ chỉ "login registry") thành composite action trước, cảm nhận lợi ích, rồi mở rộng dần lên reusable workflow đầy đủ.

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

  • Nhận diện lặp lại: Lấy 3 file workflow bất kỳ (của bạn hoặc từ một dự án mã nguồn mở). Liệt kê những phần giống nhau và những phần khác nhau. Từ danh sách "khác nhau", đề xuất các inputs cho một reusable workflow.
  • Viết reusable workflow: Tạo một file reusable-ci.yml với trigger workflow_call, nhận input node-version (mặc định 18) và run-lint (boolean, mặc định true). Bên trong: checkout, setup Node, npm ci, và chạy lint có điều kiện.
  • Gọi và truyền secret: Viết một workflow ci.yml gọi template ở bài 2, đồng thời truyền một secret NPM_TOKEN xuống template. Kiểm tra xem bạn có dùng đúng cú pháp secrets: không.
  • Thiết kế versioning: Giả sử bạn cần thêm một bước quét bảo mật bắt buộc vào template — điều này sẽ làm fail một số service đang có lỗ hổng. Đây là thay đổi phá vỡ tương thích hay không? Bạn sẽ đặt nó vào phiên bản nào (v1.1 hay v2)? Giải thích lý do.
  • Tình huống mở: Công ty bạn có 8 service Node, 5 service Python, và 3 service Go. Bạn nên tạo 1 template "vạn năng" hay 3 template riêng? Viết một đoạn ngắn lập luận cho lựa chọn của bạn, dựa trên nguyên tắc cân bằng chuẩn hóa và linh hoạt đã học.

Tóm tắt

Khi một tổ chức có nhiều service tự viết pipeline riêng, hậu quả tất yếu là drift và cơn ác mộng bảo trì — mỗi thay đổi chuẩn phải nhân lên hàng chục lần. Giải pháp là tái sử dụng pipeline: viết logic chung một lần, để các service tham chiếu đến.

Những điểm cốt lõi cần nhớ:

  • Ba cấp độ tái sử dụng: composite action (bước nhỏ), reusable workflow (pipeline con), và template khung. Chọn đúng cấp cho đúng nhu cầu.
  • Reusable workflow trong GitHub Actions dùng trigger workflow_call, nhận inputs, secrets, outputs, và được gọi qua cú pháp uses: org/repo/.github/workflows/x.yml@version.
  • Versioning là bắt buộc: luôn trỏ vào tag phiên bản, không phải @main, để các đội chủ động nâng cấp và tránh bị breaking change bất ngờ.
  • Cân bằng chuẩn hóa và linh hoạt: đưa phần bắt buộc vào cứng, phần khác biệt ra thành input có mặc định. Tránh cả template quá cứng lẫn "God workflow" quá phức tạp.
  • Nguyên lý này áp dụng ở mọi công cụ: GitLab (include/extends), Jenkins (Shared Library), Azure DevOps (template) — tư duy giống nhau.
Template biến chi phí bảo trì từ O(số service) về O(1). Đó là bước chuyển từ một đội DevOps "chữa cháy" sang một đội DevOps thực sự trưởng thành. Ở bài tiếp theo, chúng ta sẽ áp dụng chính tư duy tái sử dụng này vào bối cảnh cụ thể hơn là CI/CD cho microservices.

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