Product Management
Đăng nhập
ESC

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

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

Bài 5 — Pipeline as Code — Khái niệm

Đã đọ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

Hãy tưởng tượng bạn vừa gia nhập một công ty fintech ở Quận 1, TP.HCM. Sếp giao cho bạn nhiệm vụ "làm cho pipeline build ổn định lại". Bạn hỏi: "Cấu hình pipeline hiện tại nằm ở đâu ạ?". Câu trả lời khiến bạn lạnh sống lưng: "Nằm trong Jenkins, anh Hùng cấu hình. Mà anh Hùng nghỉ việc tháng trước rồi."

Không ai biết chính xác pipeline đang làm gì. Không có tài liệu. Không có lịch sử thay đổi. Muốn xem cấu hình phải đăng nhập vào web UI của Jenkins, click qua từng job, đọc từng ô text đầy chú thích rối rắm. Muốn thử sửa thì phải sửa trực tiếp trên production — sửa sai là cả team đứng hình.

Đây chính xác là vấn đề mà Pipeline as Code (viết tắt là PaC — "pipeline dưới dạng mã nguồn") ra đời để giải quyết. Ở Bài 4 bạn đã phân biệt được CI, CD và Continuous Deployment. Bài này ta lùi lại một bước để trả lời câu hỏi nền tảng: pipeline nên được định nghĩa ở đâu và như thế nào? Đây là tư tưởng xuyên suốt toàn bộ khóa học — từ GitHub Actions (Bài 6, 7), GitLab CI (Bài 8), cho đến pipeline templates (Bài 38). Nếu bạn nắm chắc triết lý này ngay bây giờ, mọi công cụ sau này chỉ còn là cú pháp.

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

Pipeline as Code là gì?

Pipeline as Code nghĩa là: toàn bộ định nghĩa quy trình CI/CD của bạn được viết dưới dạng một hoặc nhiều file văn bản (thường là YAML, đôi khi là Groovy hoặc một DSL riêng), và những file này được lưu chung với mã nguồn của dự án trong hệ thống quản lý phiên bản (Git).

Nói cách khác, thay vì cấu hình pipeline bằng cách click chuột trên giao diện web, bạn viết nó ra thành file. File đó nằm ngay trong repository, ví dụ .github/workflows/ci.yml hoặc .gitlab-ci.yml hoặc Jenkinsfile. Pipeline trở thành một phần của codebase, đi cùng với code, được review cùng code, được version cùng code.

Đây là một nhánh cụ thể của tư tưởng lớn hơn tên là Everything as Code — vốn khởi nguồn từ Infrastructure as Code (hạ tầng dưới dạng mã). Triết lý chung: bất cứ thứ gì con người có thể cấu hình bằng tay thì đều nên được mô tả bằng mã, để máy tính đọc và tái tạo lại một cách nhất quán.

Vấn đề "pipeline trong UI" — di sản cần bỏ lại

Để hiểu vì sao PaC quan trọng, phải hiểu nó thay thế cái gì. Jenkins thời kỳ đầu (khoảng 2011–2016) và nhiều công cụ CI cùng thời đều theo mô hình cấu hình qua UI: bạn vào web, tạo một "job", điền vào các ô — repo ở đâu, chạy lệnh gì, deploy đi đâu — rồi bấm Save. Cấu hình đó được lưu vào một file XML nằm sâu trong server Jenkins mà không ai đụng tới trực tiếp.

Cách làm này sinh ra bốn nỗi đau lớn:

1. Không có version — không biết ai sửa gì, khi nào. Hôm qua pipeline chạy tốt, hôm nay hỏng. Ai đã đổi gì? Không ai biết, vì UI không lưu lịch sử. Bạn không thể git diff một cú click chuột.

2. Không review được. Một thay đổi lên pipeline production được thực hiện bằng cách người có quyền vào UI sửa trực tiếp. Không có cơ chế "để đồng nghiệp xem trước khi áp dụng". Sửa sai là hỏng ngay.

3. Không reproducible (không tái lập được). Server Jenkins cháy ổ cứng? Chúc may mắn dựng lại. Toàn bộ cấu hình nằm trong bụng con server đó. Muốn tạo môi trường staging giống hệt production ư? Bạn phải ngồi click lại từng job bằng tay, và chắc chắn sẽ sai vài chỗ.

4. Không có "một nguồn sự thật" (single source of truth). Code nằm trong Git, còn cách build code lại nằm trong đầu một con server khác. Hai thứ này lệch pha nhau, rất dễ dẫn tới tình huống "code của tôi build được trên máy tôi mà pipeline lại fail".

PaC giải quyết bằng cách nào?

Khi pipeline trở thành file trong Git, cả bốn nỗi đau trên tan biến gần như tức thì:

  • Có version: mỗi thay đổi pipeline là một commit. git log .github/workflows/ cho bạn xem toàn bộ lịch sử: ai sửa, khi nào, vì sao (qua commit message). git blame chỉ ra chính xác dòng nào do ai viết.
  • Review được: thay đổi pipeline đi qua Pull Request giống hệt code thường. Đồng nghiệp comment, phê duyệt, rồi mới merge. Ta sẽ đào sâu quy trình này ở Bài 33.
  • Reproducible: clone repo về là có luôn pipeline. Dựng công cụ CI mới, trỏ vào repo, nó tự đọc file cấu hình và chạy. Không phụ thuộc vào trí nhớ hay một server cụ thể nào.
  • Single source of truth: code và cách build code nằm chung một chỗ, cùng một commit. Bạn checkout về commit của 3 tháng trước, thì cả code lẫn pipeline đều là phiên bản của 3 tháng trước — chúng luôn khớp nhau.

Declarative vs Imperative — hai phong cách viết pipeline

Một khái niệm cốt lõi bạn cần phân biệt sớm: pipeline có thể viết theo hai phong cách.

Declarative (khai báo): bạn mô tả cái bạn muốn, chứ không phải cách làm từng bước. Đa số YAML pipeline hiện đại (GitHub Actions, GitLab CI) theo phong cách này. Bạn khai báo: "có một job tên test, chạy trên Ubuntu, gồm các bước cài dependency rồi chạy npm test". Bạn không cần viết vòng lặp hay logic điều khiển phức tạp.

Imperative (mệnh lệnh): bạn viết ra từng bước logic như một chương trình thật sự — có biến, có vòng lặp, có điều kiện. Jenkins Scripted Pipeline (viết bằng Groovy) là ví dụ điển hình.

Xu hướng hiện đại nghiêng mạnh về declarative vì nó dễ đọc, dễ review, dễ chuẩn hóa. Nhưng khi logic phức tạp, đôi khi ta vẫn cần một chút "chất imperative". Hiểu được ranh giới này giúp bạn chọn đúng công cụ cho đúng bài toán.

Tình huống thực tế

Ví dụ 1 — Công ty giao hàng ở TP.HCM và "con server Jenkins ma"

Một startup logistics khoảng 40 kỹ sư (gọi là GiaoNhanh) chạy toàn bộ CI/CD trên một server Jenkins tự dựng, cấu hình hoàn toàn qua UI. Người dựng nó là một senior đã rời công ty. Sau nửa năm, họ có 60 job Jenkins, không ai dám đụng vào vì "sợ hỏng".

Một ngày, ổ SSD của server Jenkins chết. Không có backup cấu hình đúng nghĩa (chỉ có snapshot cũ 4 tháng). Team mất 9 ngày làm việc để dựng lại toàn bộ pipeline bằng cách... đoán và click lại từng job dựa trên trí nhớ tập thể. Trong 9 ngày đó, mọi bản deploy phải làm thủ công qua SSH.

Sau sự cố, họ chuyển sang GitLab CI với file .gitlab-ci.yml nằm trong từng repo. Sáu tháng sau, khi cần chuyển toàn bộ hạ tầng CI sang một cụm runner mới, việc "di cư" chỉ mất 2 tiếng — vì cấu hình pipeline nằm sẵn trong Git, chỉ cần trỏ runner mới vào là xong.

Bài học: Pipeline trong UI là một quả bom nổ chậm. Chi phí của việc "click cho nhanh" không hiện ra ngay, nó chờ đến ngày server chết hoặc người phụ trách nghỉ việc mới bùng lên.

Ví dụ 2 — Ngân hàng số và bài toán "ai đổi pipeline lúc 2 giờ sáng?"

Một ngân hàng số ở Đông Nam Á có yêu cầu tuân thủ (compliance) rất nghiêm: mọi thay đổi ảnh hưởng tới quy trình đưa code lên production đều phải truy vết được — ai làm, khi nào, ai duyệt. Ban đầu họ dùng một công cụ CI cấu hình qua UI, và khi kiểm toán viên hỏi "chứng minh cho tôi xem lịch sử thay đổi pipeline 12 tháng qua", team không trả lời được. UI chỉ cho biết cấu hình hiện tại, không có nhật ký.

Họ chuyển sang mô hình Pipeline as Code. Giờ đây mỗi thay đổi pipeline là một Pull Request bắt buộc có ít nhất 2 người duyệt, gắn với một commit có timestamp và tác giả rõ ràng. Khi kiểm toán, họ chỉ cần xuất git log của thư mục pipeline — bằng chứng đầy đủ, không thể chối cãi, không thể sửa lén.

Bài học: Với các ngành chịu quản lý chặt (tài chính, y tế), PaC không còn là "nên có" mà là "bắt buộc". Nó biến việc tuân thủ từ một cơn ác mộng thành một câu lệnh git log. (Chủ đề compliance sẽ được đào sâu ở Bài 47.)

Ví dụ 3 — Chuỗi thương mại điện tử và tính "reproducible"

Một sàn thương mại điện tử vừa (khoảng 25 kỹ sư) muốn tạo môi trường staging giống hệt production để test trước ngày sale lớn 12/12. Hồi còn dùng Jenkins UI, mỗi lần dựng môi trường mới họ mất 3–4 ngày ngồi click và luôn có sai lệch — staging build bằng Node 16 trong khi production đã lên Node 18, dẫn tới lỗi chỉ xuất hiện đúng lúc chạy chính thức.

Sau khi chuyển pipeline vào file .github/workflows/, việc nhân bản môi trường trở nên tầm thường: sao chép file YAML, đổi vài biến môi trường, mọi phiên bản Node/toolchain đều được ghi rõ trong file nên không thể lệch. Trước đợt 12/12, họ dựng staging trong 15 phút thay vì vài ngày, và phát hiện được một lỗi cache trước khi nó kịp gây thiệt hại thật.

Bài học: Khi cách build được viết thành mã, môi trường trở nên "reproducible" — nhân bản chính xác đến từng con số phiên bản. Đây là điều bất khả thi với pipeline trong UI.

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

Đây là lối tư duy để áp dụng Pipeline as Code cho một dự án, dù bạn dùng công cụ nào:

Bước 1 — Xác định pipeline hiện tại đang "sống" ở đâu. Nếu dự án của bạn còn cấu hình build qua UI, hãy liệt kê ra: nó chạy những lệnh gì, theo thứ tự nào, trên môi trường nào, cần biến/bí mật gì. Đây là bước "khảo cổ" — chép lại tri thức đang nằm rải rác thành một danh sách rõ ràng.

Bước 2 — Chọn định dạng file phù hợp với công cụ. Mỗi công cụ có quy ước riêng về vị trí và tên file:

  • GitHub Actions: file YAML đặt trong thư mục .github/workflows/
  • GitLab CI: file .gitlab-ci.yml ở gốc repo
  • Jenkins hiện đại: file Jenkinsfile ở gốc repo (thay cho cấu hình UI cũ)
Điểm chung: file nằm trong repo, không nằm ngoài.

Bước 3 — Viết định nghĩa pipeline dưới dạng declarative. Bắt đầu nhỏ. Chỉ cần một job đơn giản: checkout code, cài dependency, chạy test. Ví dụ ý niệm (chưa đi sâu cú pháp — đó là Bài 6):

name: CI
on: push
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

Điểm mấu chốt cần thấy ở đây: chỉ nhìn file này, bất kỳ ai trong team cũng hiểu ngay pipeline làm gì — không cần đăng nhập vào đâu cả.

Bước 4 — Commit file này vào Git cùng với code. Đây là hành động biến "cấu hình" thành "code". Từ giờ, pipeline có lịch sử, có tác giả, có commit message.

Bước 5 — Đưa mọi thay đổi pipeline sau này qua Pull Request. Đừng bao giờ sửa pipeline production trực tiếp. Sửa trên nhánh, mở PR, để đồng nghiệp review, rồi merge. Chính công cụ CI sẽ tự phát hiện file mới và áp dụng.

Bước 6 — Xóa bỏ cấu hình UI cũ sau khi PaC đã chạy ổn. Đừng để tồn tại song song hai nguồn cấu hình — đó là công thức của rối loạn. Khi file trong Git đã hoạt động tốt, hãy vô hiệu hóa job UI cũ để chỉ còn một nguồn sự thật duy nhất.

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

Lỗi 1 — Vẫn giấu logic quan trọng trong UI hoặc trong biến môi trường của server. Nhiều người copy pipeline vào file nhưng lại để những bí quyết then chốt (ví dụ URL deploy, tham số đặc biệt) nằm trong cấu hình UI của runner. Kết quả: file YAML nhìn thì đẹp nhưng vẫn không reproducible. Mẹo: nguyên tắc vàng — nếu clone repo về mà pipeline không chạy được đúng, tức là bạn còn giấu tri thức ở đâu đó ngoài Git.

Lỗi 2 — Viết một file YAML khổng lồ 800 dòng. PaC không có nghĩa là nhồi mọi thứ vào một file. File quá dài thì cũng khó đọc và khó review y như UI vậy. Mẹo: tách nhỏ, tái sử dụng qua template (Bài 38 sẽ dạy kỹ). Bắt đầu đơn giản, mở rộng dần.

Lỗi 3 — Nhầm lẫn "as Code" với "phải viết bằng ngôn ngữ lập trình phức tạp". Nhiều bạn mới nghĩ PaC nghĩa là phải giỏi Groovy hay scripting. Không phải. Phần lớn PaC hiện đại là YAML declarative — bạn khai báo chứ không lập trình. Mẹo: ưu tiên declarative; chỉ dùng imperative/scripting khi logic thật sự phức tạp và declarative không diễn đạt nổi.

Lỗi 4 — Không test pipeline như test code. Pipeline là code, mà code thì có bug. Sửa YAML rồi push thẳng lên nhánh chính là rất dễ làm hỏng build của cả team. Mẹo: luôn thử nghiệm thay đổi pipeline trên nhánh riêng trước, xem nó chạy xanh (pass) rồi mới merge.

Lỗi 5 — Nhét secret trực tiếp vào file YAML. Vì file nằm trong Git, viết mật khẩu/token thẳng vào đó nghĩa là lộ bí mật cho bất kỳ ai đọc được repo, và Git giữ nó mãi mãi trong lịch sử. Đây là lỗi bảo mật nghiêm trọng. Mẹo: không bao giờ hardcode secret; dùng cơ chế secret riêng của công cụ (Bài 7 và Bài 16 chuyên sâu về chủ đề này).

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

  • Khảo cổ pipeline. Chọn một dự án bạn đang tham gia (hoặc một repo mã nguồn mở bất kỳ). Tìm file định nghĩa pipeline của nó — .github/workflows/, .gitlab-ci.yml, hoặc Jenkinsfile. Đọc và viết ra bằng tiếng Việt: pipeline này chạy những bước nào, theo thứ tự ra sao? Nếu dự án không có file như vậy, hãy suy nghĩ: hiện tại code được build và deploy bằng cách nào, và tri thức đó đang "sống" ở đâu?
  • Chứng minh giá trị của version. Vào một repo mã nguồn mở lớn trên GitHub, mở file workflow, và bấm vào "History" (hoặc chạy git log trên file đó). Quan sát lịch sử thay đổi. Ghi lại 3 điều bạn học được từ lịch sử này mà bạn không thể biết được nếu pipeline nằm trong UI.
  • Phân loại phong cách. Tìm hai ví dụ pipeline: một viết declarative (YAML) và một viết imperative (ví dụ Jenkins Scripted Pipeline bằng Groovy). Chỉ ra sự khác biệt trong cách chúng diễn đạt cùng một ý tưởng "chạy test rồi deploy".
  • Tư duy phản biện. Viết một đoạn ngắn (150 từ) trả lời: nếu công ty bạn vẫn đang dùng pipeline cấu hình qua UI, bạn sẽ thuyết phục sếp chuyển sang Pipeline as Code bằng những lập luận nào? Dùng ít nhất một trong ba tình huống thực tế ở bài này làm dẫn chứng.

Tóm tắt

Pipeline as Code là tư tưởng nền tảng của CI/CD hiện đại: định nghĩa toàn bộ quy trình build/test/deploy dưới dạng file văn bản, lưu chung với code trong Git.

Những điều cần khắc cốt ghi tâm:

  • Pipeline cấu hình qua UI (kiểu Jenkins cổ điển) sinh ra bốn nỗi đau: không có version, không review được, không reproducible, không có single source of truth.
  • PaC hóa giải cả bốn: mỗi thay đổi là một commit có lịch sử, đi qua Pull Request để review, clone repo là tái lập được, và code với cách build luôn nằm chung một chỗ, một commit.
  • Phần lớn PaC hiện đại theo phong cách declarative (khai báo bằng YAML) — bạn mô tả cái bạn muốn, không phải lập trình từng bước. Imperative chỉ dùng khi logic thật phức tạp.
  • Nguyên tắc kiểm tra vàng: nếu clone repo về mà pipeline không tự chạy đúng, tức là bạn còn giấu tri thức ngoài Git.
  • Tuyệt đối không hardcode secret vào file pipeline — vì Git giữ nó mãi mãi.
Nắm vững triết lý này, bạn đã có "hệ quy chiếu" để bước vào các bài tiếp theo: Bài 6 và 7 sẽ dạy bạn viết PaC cụ thể với GitHub Actions, Bài 8 với GitLab CI, và Bài 9 sẽ cho bạn thấy chính Jenkins cũng đã tiến hóa từ UI sang Jenkinsfile như thế nào. Mọi công cụ đều khác nhau ở cú pháp, nhưng chung một tư tưởng: pipeline là code.

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