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,secretsvàoutputsrõ ràng. Dùng khi bạn muốn tái sử dụng cả một pipeline (build-test-deploy).
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
inputscó 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
inputscho một reusable workflow.
- Viết reusable workflow: Tạo một file
reusable-ci.ymlvới triggerworkflow_call, nhận inputnode-version(mặc định18) vàrun-lint(boolean, mặc địnhtrue). 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.ymlgọi template ở bài 2, đồng thời truyền một secretNPM_TOKENxuống template. Kiểm tra xem bạn có dùng đúng cú phápsecrets: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.1hayv2)? 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ậninputs,secrets,outputs, và được gọi qua cú phápuses: 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.