---
title: Kế hoạch triển khai CI/CD
url: https://share.jotbird.com/sleepy-soaring-mirage
updated_at: 2026-08-27T09:16:49.224196+00:00
---

# 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:

- **Tốn thời gian** — mỗi lần deploy chiếm hàng giờ của người có kinh nghiệm nhất.
- **Hay quên bước** — build, chạy migration, cập nhật tag image, restart container;
  bỏ sót một bước là một sự cố.
- **Lỗi phát hiện muộn** — nhiều lỗi chỉ lộ ra khi đã ở production, tức là khi
  khách đã nhìn thấy.
- **Phụ thuộc vài người** — người đó nghỉ phép thì cả nhóm không phát hành được.

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 `development` và `staging`                                                                        |
| **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.

- Registry của GitLab ra được internet với **chứng chỉ TLS thật** (Let's Encrypt).
  Server khách sẽ pull image từ đó. Chứng chỉ tự ký thì mỗi máy khách phải sửa
  cấu hình Docker — gánh nặng hỗ trợ vĩnh viễn.
- Bật **Dependency Proxy** cho group. Docker Hub giới hạn 100 lượt tải/6 giờ
  theo địa chỉ IP; runner của công ty dùng chung một IP nên sẽ đụng trần và cả
  nhóm đứng hình cùng lúc.
- Bật **metadata database** cho registry, đặt **cleanup policy** và lịch **garbage
  collection**. Đây là ba việc khác nhau, không phải một — xem mục 7.
- Chuẩn bị runner có Docker-in-Docker.

**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.

- Tạo hai nhánh `development` và `staging`, đặt Protected branch.
- Copy `examples/node-service.gitlab-ci.yml` thành `.gitlab-ci.yml`.
- Bật cổng kiểm tra code: `lint`, `type-check`, `test`, `build`.
- Bật "Pipelines must succeed" cho merge request.

**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)

- Bật preset `security-scan`: `trivy:fs`, `gitleaks`, `commitlint`, SAST,
  Secret Detection.
- Thống nhất cách xử lý khi quét ra lỗ hổng: sửa ngay, hay ghi nhận và hẹn.

**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)

- Bật preset `docker-publish`: build image, quét CVE, sinh SBOM.
- Chuẩn bị server công ty: `docker-compose.yml` dùng biến `${APP_IMAGE}`, file
  `.env`, và **healthcheck** trong compose.
- Đặt CI/CD Variables cho staging (xem mục 8).
- Bật `deploy:staging`.

**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)

- Bật `docker:release` (gắn tag phát hành lên image đã kiểm tra) và `release`
  (tạo mục ở trang Releases).
- Chọn đường deploy cho từng khách: CI SSH thẳng, hay gửi gói `deploy.sh`.
- Cấp **deploy token** riêng cho từng khách (chỉ quyền đọc registry, có hạn dùng,
  thu hồi được riêng lẻ).
- Bật `deploy:production` (bấm tay) và `deploy:rollback`.
- **Diễn tập lùi bản một lần** trước khi coi là xong.

**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.

- Mỗi dự án copy file ví dụ, sửa phần `variables`, đặt CI/CD Variables.
- Ghim `ref` vào tag của template — **không dùng `ref: main`**, vì một commit
  vào template có thể làm đỏ pipeline của mọi dự án cùng lúc.
- Viết lại tài liệu bàn giao cho khách (một trang, kèm gói `deploy.sh`).

---

## 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):

- `docker` + `docker compose v2`
- `docker-compose.yml` dùng `image: ${APP_IMAGE}` — **không** ghi cứng tag
- **`healthcheck`** trong compose. Không có nó thì bước chờ bị bỏ qua, và một
  container chết lúc khởi động vẫn cho ra pipeline màu xanh
- Đã `docker login` một lần vào registry bằng deploy token được cấp

---

## 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 **và** 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`:

- 3 preset cắm-là-chạy + thư viện job để dự án lớn tự lắp
- 2 file ví dụ hoàn chỉnh (service đơn, monorepo `be/` + `fe/`)
- Bộ kiểm tra tự động: `scripts/check.py` (tham chiếu chéo giữa các file) và
  `scripts/test-shell.sh` (**37 test** chạy thử phần shell nhúng trong template,
  gồm cả gói `deploy.sh` mà khách sẽ nhận)

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ệ.
