Kế hoạch triển khai CI/CD

Tài liệu này mô tả hiện trạng, lợi ích, và lộ trình đưa CI/CD vào các dự án của Holatech. Phần kỹ thuật dựa trên repo template ci-templates đã hoàn thành ở phiên bản v2.1.0.


1. Vấn đề hiện tại

Việc đưa code lên môi trường chạy đang làm thủ công:

Ba điều đầu đều là hệ quả của điều thứ tư: quy trình nằm trong đầu người, không nằm trong file.


2. CI/CD là gì

Tự động hoá chuỗi: kiểm tra code → chạy test → đóng gói → triển khai. Mỗi khi có thay đổi đẩy lên GitLab, hệ thống tự chạy thay cho con người.

Quy trình được viết thành file .gitlab-ci.yml nằm trong chính repo — nghĩa là nó được review, được version, và ai cũng đọc được.


3. Lợi ích đạt được

Tiêu chí Trước Sau
Thời gian deploy Hàng giờ Vài phút, một lần bấm nút
Phát hiện lỗi Muộn (ở production) Sớm (khi push code)
Rủi ro sai sót Cao Thấp — các bước cố định, không ai gõ tay
Deploy bản lỗi Có thể xảy ra Tự động bị chặn ở cổng
Ai deploy được Vài người Cả team
Truy vết phiên bản Khó Trang Releases ghi rõ commit nào, image nào
Nơi lưu image Docker Hub / thủ công Registry của chính GitLab, cùng phân quyền với repo
Lùi bản khi sự cố Làm tay, dễ sai Một nút, nhập tag cũ

4. Môi trường triển khai

Môi trường Nằm ở đâu Kích hoạt bởi Pipeline làm gì
Local Máy dev Không chạy CI. Dev tự chạy lint/test trước khi push
Development Không có server Nhánh development + MR vào nó Cài package, prisma generate, eslint, type-check, test, quét bảo mật. Không build image, không deploy
Staging PC/server của công ty Nhánh staging (merge từ development) Mọi việc trên + build Docker image → quét CVE → deploy tự động
Production VPS của khách hàng Tag v1.2.3 Gắn tag phát hành lên đúng image đã chạy ở staging → deploy, bấm tay

Lưu ý về tên gọi. "Local" là máy dev, không có pipeline. "Development" trong tài liệu này là nhánh development — cổng kiểm tra code tự động, không phải một server. Đây là chỗ hay bị hiểu nhầm khi đọc lướt.

Nguyên tắc: build một lần, dùng lại nhiều lần

Production không build lại từ source. Nó gắn tag v1.2.3 lên đúng image đã chạy qua staging.

Lý do: build lại từ cùng một commit vẫn có thể ra kết quả khác (base image đã cập nhật, thư viện có version mới). Nếu build lại, thứ lên production không còn là thứ đã được kiểm tra.

Hệ quả với quy trình: tag phải đặt trên commit đã đi qua nhánh staging. Tag thẳng trên development sẽ bị pipeline chặn kèm thông báo giải thích.

Kho chứa image: GitLab Container Registry, không dùng Docker Hub

Mọi image của sản phẩm nằm trên GitLab Container Registry self-hosted — cùng hệ thống với repo, cùng phân quyền, không phải mua hay đăng ký dịch vụ ngoài.

Loại image Nguồn Cách xác thực
Image sản phẩm — CI đẩy lên Registry của GitLab Tự động, không cần khai báo gì
Image sản phẩm — server khách tải về Cùng registry đó Deploy token, chỉ quyền đọc
Image nền (node, alpine, trivy…) Docker Hub, qua Dependency Proxy của GitLab Runner tự lo

Docker Hub chỉ còn xuất hiện ở đúng một chỗ: nó là nguồn gốc của các image nền do bên thứ ba phát hành (Node, Alpine…). Ngay cả những image đó ta cũng không tải trực tiếp mà đi qua bộ đệm của GitLab, vì Docker Hub giới hạn 100 lượt tải mỗi 6 giờ theo địa chỉ IP — cả công ty dùng chung một IP nên sẽ đụng trần và mọi pipeline đứng hình cùng lúc.

Registry không để ở chế độ công khai. Server khách tải image bằng deploy token cấp riêng cho từng khách: chỉ quyền đọc, đặt được hạn dùng, và thu hồi được từng cái khi hết hợp đồng. Nếu để công khai thì image — vốn chứa toàn bộ mã nguồn đã biên dịch — ai cũng tải về được, và không có cách nào thu hồi.

Đổi lại, việc tự chạy registry kéo theo ba trách nhiệm vận hành, xem mục 7.

Hai đường đưa image lên máy khách

Mỗi khách một chính sách hạ tầng, template hỗ trợ cả hai:

  1. CI SSH thẳng vào server khách — khách cấp deploy key và mở SSH cho IP của runner. Deploy hoàn toàn tự động sau khi bấm duyệt.
  2. Khách tự chạy — pipeline sinh sẵn một gói deploy.sh gửi cho khách; khách chạy ./deploy.sh v1.2.3 trên máy họ. Không cần mở SSH, không cần cấp quyền gì trên hạ tầng khách.

Cả hai dùng chung một đoạn script, nên không có chuyện hai đường lệch nhau sau vài tháng.


5. Những gì GitLab làm được — kèm trạng thái thực tế

Ký hiệu: ✅ Có sẵn trong template, dùng được ngay · ⚙️ Cần cấu hình phía GitLab hoặc phía dự án · ⚠️ Chưa có, cần quyết định

Chất lượng code

Việc Trạng thái Ghi chú
Kiểm tra format và lỗi cú pháp mỗi lần push Job lint, type-check
Chạy test tự động Job test, kèm bảng "test nào đỏ" ngay trong MR
Kiểm tra dependency cài đúng lockfile Lệnh cài đều là "frozen lockfile" — lệch là đỏ ngay
Sinh Prisma Client đúng schema mới nhất Chạy sau mỗi lần cài, không dùng cache
Yêu cầu độ phủ tối thiểu, không đạt không cho merge ⚠️ Hiện chỉ hiển thị % coverage. Muốn chặn: đặt ngưỡng trong vitest.config/jest.config của dự án (job test tự đỏ). Quy tắc duyệt theo coverage của GitLab là tính năng trả phí

Bảo mật

Việc Trạng thái Ghi chú
Quét lỗ hổng trong code (SAST) ⚙️ Job chạy được trên bản Free; widget hiển thị trong MR là Ultimate. Bản Free đọc kết quả ở artifact gl-sast-report.json
Quét lỗ hổng trong thư viện phụ thuộc trivy:fs + audit
Quét lỗ hổng trong Docker image trivy:image, chặn deploy nếu có lỗ hổng HIGH/CRITICAL đã có bản vá
Chặn đẩy mật khẩu/khoá bí mật lên repo gitleaks + Secret Detection của GitLab
Danh sách thành phần của image (SBOM) CycloneDX, hiện ở tab Dependency List

Đóng gói & triển khai

Việc Trạng thái Ghi chú
Build Docker image, lưu vào kho chứa của GitLab Registry self-hosted, không dùng Docker Hub
Chặn bản lỗi trước khi deploy deploy:staging chờ trivy:image xong mới chạy
Kiểm tra container chạy được sau deploy Chờ healthcheck; container không lên là job đỏ và in log
Deploy staging tự động
Chờ phê duyệt trước khi lên production Job manual — phải có người bấm
Lùi về bản cũ khi sự cố deploy:rollback, nhập tag cũ
Chặn hai lần deploy đè nhau resource_group
Chạy thử image trước khi deploy (smoke test) ⚠️ Hiện chỉ kiểm tra sau khi deploy bằng healthcheck. Thêm bước chạy thử image ở CI là việc có thể làm sau

Quản trị & phiên bản

Việc Trạng thái Ghi chú
Bắt buộc pipeline xanh mới được merge ⚙️ Settings → Merge requests → "Pipelines must succeed". Có ở bản Free
Bảo vệ nhánh chính ⚙️ Settings → Repository → Protected branches, cho developmentstaging
Bắt buộc có người review ⚙️ Cần kiểm tra license: quy tắc bắt buộc số người duyệt là tính năng trả phí. Bản Free chặn được bằng Protected branch + "Pipelines must succeed"
Ép commit message theo chuẩn commitlint
Sinh changelog theo lịch sử commit Mỗi tag tạo một GitLab Release, notes sinh từ git log, gom theo loại commit
Ghi lại nguồn gốc mỗi bản build Release ghi rõ: commit nào, image nào, gói deploy nào đã gửi khách
Tự động đánh số phiên bản ⚠️ Hiện tag do người đặt tay. Tự động cần thêm công cụ (semantic-release); cân nhắc sau khi quy trình đã ổn

Điểm mấu chốt: bản build không đạt sẽ tự động bị chặn ở cổng, không cần ai canh gác thủ công.


6. Phương án triển khai theo giai đoạn

Ước lượng công sức là con số thô để lập kế hoạch, không phải cam kết.

Giai đoạn 0 — Hạ tầng (~1 tuần, admin GitLab)

Phải xong trước, vì mọi giai đoạn sau đều dựa vào.

Xong khi: một dự án thử tải được image từ máy ngoài mạng công ty.

Giai đoạn 1 — Thí điểm một dự án (~3–5 ngày)

Chọn một dự án nhỏ, đang chạy thật nhưng chưa quan trọng bằng các dự án khác.

Xong khi: mở một MR có lỗi lint và thấy nó bị chặn không cho merge.

Giai đoạn 2 — Thêm bảo mật (~1–2 ngày)

Xong khi: thử commit một chuỗi trông giống API key và thấy pipeline chặn lại.

Giai đoạn 3 — Tự động staging (~2–3 ngày)

Xong khi: merge vào staging và thấy bản mới tự lên server công ty trong vài phút, không ai gõ lệnh.

Giai đoạn 4 — Cổng duyệt production (~3–5 ngày)

Xong khi: phát hành một version thật, và lùi lại được bản trước đó bằng một nút bấm.

Giai đoạn 5 — Nhân rộng (~1 ngày/dự án)

Sau khi thí điểm chạy ổn ít nhất hai tuần.


7. Việc phía hạ tầng — ba điều dễ bỏ sót

Bỏ Docker Hub và tự chạy registry nghĩa là nhận về ba trách nhiệm. Không làm thì chúng sẽ tự nhắc, vào lúc bất tiện nhất.

  1. Cleanup policy KHÔNG giải phóng ổ đĩa. Đây là chỗ dễ hiểu nhầm nhất trong tài liệu GitLab: policy chỉ xoá nhãn, phần dữ liệu bên dưới vẫn nằm nguyên. Muốn lấy lại dung lượng phải chạy garbage collection riêng, và bản thường cần một khoảng thời gian chỉ-đọc. Đặt policy rồi tưởng đã xong thì vài tháng nữa ổ đầy.

  2. Bật metadata database nếu muốn dọn dẹp không gián đoạn. Nó là điều kiện của garbage collection online, và của việc nhìn thấy dung lượng từng image. Nhập dữ liệu sau khi registry đã lớn sẽ tốn thời gian chỉ-đọc — làm sớm rẻ hơn.

  3. Uptime của GitLab giờ là ràng buộc của khách. GitLab sập hoặc đang nâng cấp thì khách không tải được image, không restart container được, không deploy được. Nếu có khách yêu cầu cam kết dịch vụ cao, cần tính phương án dự phòng.


8. Checklist cho mỗi dự án

Settings → CI/CD → Variables. Đánh dấu Masked cho giá trị bí mật, Protected cho biến chỉ dùng ở nhánh/tag được bảo vệ.

Biến Dùng cho
SSH_PRIVATE_KEY Khoá riêng của deploy key
SSH_KNOWN_HOSTS Kết quả ssh-keyscan <host>. Bỏ trống thì pipeline cảnh báo — chấp nhận ở staging, không ở production
STAGING_HOST / PRODUCTION_HOST Địa chỉ server đích
STAGING_URL / PRODUCTION_URL Hiện trên trang Environments
REGISTRY_DEPLOY_USER / REGISTRY_DEPLOY_TOKEN Deploy token để server pull image

Bẫy hay gặp nhất: biến đánh dấu Protected nhưng nhánh đang chạy chưa được bảo vệ → biến rỗng, job chết ở một chỗ khó hiểu. Pipeline kiểm tra trước và in ra nguyên nhân, nhưng biết trước vẫn đỡ mất thời gian hơn.

Phía server (staging và mỗi máy khách):


9. Rủi ro và điểm chưa xử lý

Rủi ro Mức Cách giảm
Migration cơ sở dữ liệu không tự lùi được Cao Xem mục quyết định bên dưới
Khách không cho mở SSH Trung bình Đã có sẵn đường gửi gói deploy.sh
GitLab sập → khách không deploy được Trung bình Cân nhắc mirror image cho khách quan trọng
Ổ đĩa registry đầy dần Trung bình Cleanup policy garbage collection, mục 7
Nhóm chưa quen Conventional Commits Thấp commitlint báo lỗi kèm gợi ý; quen sau vài MR
Template đổi làm vỡ dự án đang chạy Thấp Mọi dự án ghim ref vào tag; template có 37 test tự động

Cần quyết định: migration cơ sở dữ liệu

Đây là điểm hở duy nhất còn lại, và nó nằm ở chỗ đắt nhất.

Khi deploy hỏng, deploy:rollback đưa image về bản cũ — nhưng schema cơ sở dữ liệu thì đã đổi. Kết quả: code cũ chạy trên schema mới.

Ba hướng, cần chọn một:

Hướng Công sức Đánh đổi
Ép migration tương thích ngược (thêm cột ở bản này, xoá ở bản sau) 0 Rẻ nhất, nhưng là quy ước con người — CI không kiểm tra được
Backup DB trước khi migrate ~10 dòng Có đường lùi thật. Deploy chậm hơn theo kích thước DB, phải nghĩ chỗ lưu
Chặn ở CI — migration có DROP/ALTER ... DROP COLUMN thì bắt xác nhận tay ~20 dòng Bắt đúng loại thay đổi nguy hiểm, nhưng sẽ có báo nhầm

Khuyến nghị: làm hướng 1 ngay (viết thành quy ước), thêm hướng 2 ở Giai đoạn 4 trước khi có khách thật lên production.


10. Hiện trạng repo template

Đã hoàn thành, phiên bản v2.1.0:

Bước còn lại trước khi tag phát hành: chạy ci-lint với CI_LINT_TOKEN để GitLab tự xác nhận cấu hình hợp lệ.

JotBird Logo
Published with JotBird