Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn vừa chạy xong một pipeline CI hoàn hảo: code được lint sạch, test xanh toàn bộ, và bước build tạo ra một file .jar, một thư mục dist/, hoặc một Docker image. Nhưng rồi… bạn làm gì với cái sản phẩm build đó? Nếu câu trả lời là "build lại từ đầu ở môi trường production", thì bạn đang mắc một trong những lỗi nghiêm trọng nhất của CI/CD hiện đại.
Artifact — hay còn gọi là "hiện vật build" — chính là kết quả đóng gói của quá trình build: một Docker image sẵn sàng chạy, một package npm để chia sẻ, một file .war để deploy lên server Java. Và Artifact Management là toàn bộ nghệ thuật lưu trữ, đánh phiên bản, phân phối và bảo mật những sản phẩm đó.
Vì sao bài này quan trọng đến vậy? Bởi vì nguyên tắc vàng của CI/CD là "build once, deploy many" — build một lần, deploy nhiều lần. Bạn build artifact đúng một lần trong pipeline, rồi cùng chính artifact ấy đi qua staging, UAT, và production mà không bao giờ build lại. Nếu mỗi môi trường build lại từ source, bạn không có gì đảm bảo rằng thứ chạy trên production giống hệt thứ bạn đã test. Đây là căn nguyên của vô số sự cố kiểu "trên máy tôi chạy được mà".
Trong bài này chúng ta sẽ tập trung vào hai loại artifact phổ biến nhất mà gần như dev nào cũng gặp: Container image (Docker) và JS package (npm), cùng với một cái nhìn về Maven cho hệ sinh thái Java. Bạn sẽ hiểu registry hoạt động thế nào, cách versioning cho đúng, và làm sao để không biến kho artifact thành một bãi rác tốn tiền.
Khái niệm cốt lõi
Artifact là gì và registry là gì
Artifact là sản phẩm bất biến (immutable) của quá trình build. "Bất biến" nghĩa là một khi đã tạo ra và gắn phiên bản, nó không bao giờ thay đổi. Version 1.4.2 hôm nay phải giống hệt version 1.4.2 sáu tháng sau.
Registry (hoặc repository) là nơi lưu trữ artifact — giống như một "kho hàng" có địa chỉ, có xác thực, có phiên bản. Mỗi loại artifact có kiểu registry riêng:
- Container image → lưu ở container registry: Docker Hub, GHCR (GitHub Container Registry), ECR (Amazon Elastic Container Registry), ACR (Azure Container Registry), GCR/Artifact Registry của Google.
- JS package (npm) → lưu ở npm registry: npmjs.org (public), GitHub Packages, hoặc registry riêng như JFrog Artifactory, Verdaccio.
- Java package (Maven/Gradle) → lưu ở Maven repository: Maven Central, Nexus, Artifactory.
Container image — cách đánh địa chỉ và tag
Một Docker image có định danh dạng:
<registry>/<namespace>/<repository>:<tag>
Ví dụ: ghcr.io/vietnamcos/payment-service:1.4.2 hay 123456789.dkr.ecr.ap-southeast-1.amazonaws.com/checkout:sha-a1b2c3d.
Phần tag là nơi thể hiện chiến lược versioning. Có ba nhóm tag bạn nên hiểu:
- Tag theo semantic version:
1.4.2,1.4,1— dễ đọc, tốt cho việc release chính thức. - Tag theo Git commit SHA:
sha-a1b2c3d— bất biến tuyệt đối, truy vết được chính xác commit nào tạo ra image. Rất được ưa chuộng trong pipeline hiện đại. - Tag
latest: tiện nhưng nguy hiểm.latestlà con dao hai lưỡi — vì nó thay đổi theo thời gian, bạn không bao giờ chắclatesthôm nay là image nào. Trong production, đừng bao giờ deploy bằng taglatest.
sha256:... — đây là hash nội dung image, bất biến 100%. Nếu bạn muốn đảm bảo tuyệt đối, hãy pin theo digest chứ không chỉ tag, vì tag có thể bị ghi đè.JS package — public, private và scoped
Với npm, một package có name và version trong file package.json. Version tuân theo SemVer (Semantic Versioning): MAJOR.MINOR.PATCH, ví dụ 2.3.1.
- MAJOR tăng khi có breaking change (phá vỡ tương thích).
- MINOR tăng khi thêm tính năng mà vẫn tương thích ngược.
- PATCH tăng khi vá lỗi nhỏ.
@vietnamcos/ui-kit — dấu @ và tên scope giúp gom nhóm và trỏ về registry riêng.Một khác biệt quan trọng so với Docker: package npm về nguyên tắc là immutable sau khi publish — bạn không được publish đè một version đã tồn tại. Đây là lý do vì sao sự cố left-pad năm 2016 (khi tác giả unpublish một package tí hon khiến cả nửa internet build lỗi) đã khiến npm siết chặt quy tắc unpublish.
Vì sao cần registry riêng thay vì chỉ dùng public
Nhiều bạn hỏi: "Sao không cứ dùng Docker Hub và npmjs.org cho khỏe?". Có mấy lý do rất thực tế:
- Bảo mật & riêng tư: Code nội bộ, image chứa cấu hình nhạy cảm không thể để public.
- Rate limit: Docker Hub giới hạn số lượt pull cho tài khoản miễn phí. Một cluster Kubernetes lớn có thể "đốt" hết hạn mức chỉ trong vài phút, làm deploy đứng hình.
- Tốc độ & chi phí băng thông: Registry đặt gần hạ tầng của bạn (ví dụ ECR trong cùng region AWS
ap-southeast-1ở Singapore) sẽ pull nhanh và rẻ hơn nhiều so với kéo từ Mỹ về. - Kiểm soát vòng đời: Registry riêng cho phép đặt policy tự xóa image cũ, quét bảo mật, và quản lý quyền chi tiết.
Tình huống thực tế
Ví dụ 1 — Tiki và bài toán rate limit của Docker Hub
Một công ty thương mại điện tử lớn tại Việt Nam (ta gọi là "sàn TMĐT X" theo mô hình giống Tiki) vận hành khoảng 80 microservice trên Kubernetes. Ban đầu, mọi base image như node:18-alpine, nginx, postgres đều được pull trực tiếp từ Docker Hub.
Vào một buổi sáng cao điểm sale, họ scale cluster từ 40 lên 120 node để chịu tải. Mỗi node mới khởi động lại pull hàng chục image. Kết quả: Docker Hub trả về lỗi 429 Too Many Requests — quá hạn mức pull ẩn danh (100 lượt / 6 giờ / IP). Hàng loạt pod kẹt ở trạng thái ImagePullBackOff, dịch vụ scale không kịp, và trải nghiệm mua sắm giờ vàng bị ảnh hưởng.
Cách họ xử lý: Dựng một pull-through cache registry (dùng Amazon ECR ở region Singapore làm proxy cache cho Docker Hub) và trỏ toàn bộ base image về đó. Đồng thời đẩy tất cả image tự build lên ECR riêng. Sau đó, các lượt pull đi trong nội bộ AWS, không còn chạm rate limit, tốc độ pull giảm từ 40 giây xuống dưới 8 giây mỗi image.
Bài học: Đừng để pipeline production phụ thuộc vào registry public. Hãy có một registry riêng hoặc ít nhất một lớp cache, đặc biệt khi bạn scale động.
Ví dụ 2 — Đội frontend chia sẻ UI kit qua GitHub Packages
Một startup fintech ở TP.HCM có 4 team frontend, mỗi team làm một sản phẩm riêng (app khách hàng, dashboard nội bộ, trang landing, cổng đối tác). Cả bốn đều cần dùng chung một bộ component: button, input, bảng dữ liệu theo đúng design system.
Ban đầu họ copy-paste code component giữa các repo. Hệ quả: mỗi khi sửa màu brand, phải sửa ở 4 nơi, và luôn có nơi bị quên, dẫn đến giao diện lệch nhau.
Giải pháp: Tách bộ component thành một scoped package @fintechvn/ui-kit, publish lên GitHub Packages (registry npm riêng tư miễn phí đi kèm GitHub org). Pipeline CI của repo ui-kit mỗi khi merge vào main sẽ tự động bump version theo SemVer và npm publish. Bốn team còn lại chỉ cần khai báo trong .npmrc để trỏ scope @fintechvn về GitHub Packages, rồi npm install @fintechvn/ui-kit@^2 như một dependency bình thường.
Kết quả: sửa design một lần, publish version 2.1.0, các team pull về khi sẵn sàng. Nhờ SemVer, họ kiểm soát được: bản vá lỗi (2.0.x) tự động nhận, còn breaking change (3.0.0) thì chủ động nâng cấp khi có thời gian.
Bài học: Artifact không chỉ để deploy — nó còn là cách chia sẻ code tái sử dụng giữa các team một cách có phiên bản, có kiểm soát.
Ví dụ 3 — Ngân hàng và yêu cầu truy vết bằng commit SHA
Một ngân hàng số ở Đông Nam Á chịu sự giám sát của cơ quan quản lý. Khi có sự cố production, họ bắt buộc phải trả lời được câu hỏi: "Chính xác đoạn code nào đang chạy khi sự cố xảy ra?".
Trước đây họ tag image bằng SemVer như 2.4.0. Nhưng giữa các lần release, họ hotfix và build đè, đôi khi tag 2.4.0 bị trỏ tới nhiều commit khác nhau. Khi audit, không ai chắc chắn commit nào tương ứng.
Giải pháp: Chuyển sang chiến lược tag kép — mỗi image build ra được gắn hai tag: một tag SemVer để con người đọc (2.4.0) và một tag immutable theo Git SHA (sha-9f3c1e0). Deploy production luôn tham chiếu theo SHA hoặc digest. Registry được bật chế độ immutable tags (ECR hỗ trợ) để không ai push đè được. Mọi bản ghi deploy đều lưu digest sha256:....
Nhờ vậy, mỗi khi cần điều tra, họ dò từ digest đang chạy ngược về đúng một commit duy nhất trong Git — truy vết hoàn hảo, thỏa mãn yêu cầu tuân thủ.
Bài học: Trong môi trường cần audit, tag bất biến theo commit SHA và immutable registry là bắt buộc, không phải tùy chọn.
Hướng dẫn từng bước
Dưới đây là quy trình chuẩn để build và publish artifact trong pipeline. Tôi minh họa bằng GitHub Actions, nhưng nguyên tắc áp dụng cho mọi CI.
Bước 1 — Build một Docker image và push lên GHCR:
name: build-and-push
on:
push:
branches: [main]
jobs:
docker:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # cần quyền này để push lên GHCR
steps:
- uses: actions/checkout@v4
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
ghcr.io/vietnamcos/payment-service:sha-${{ github.sha }}
ghcr.io/vietnamcos/payment-service:latest
Chú ý: ta gắn cả tag theo github.sha (bất biến, để truy vết) lẫn latest (tiện cho môi trường dev). Production sẽ deploy bằng tag SHA.
Bước 2 — Publish một npm package lên GitHub Packages:
Trong repo package, tạo file .npmrc:
@vietnamcos:registry=https://npm.pkg.github.com
Rồi trong workflow:
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://npm.pkg.github.com
- run: npm ci
- run: npm run build
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Trước khi publish, hãy chắc version trong package.json đã được bump — nếu trùng version cũ, npm sẽ từ chối.
Bước 3 — Tiêu thụ (consume) artifact ở nơi khác:
Ở repo khác, tạo .npmrc cùng nội dung trỏ scope, đăng nhập bằng token, rồi:
npm install @vietnamcos/ui-kit@^2.0.0
Với Docker, ở bước deploy bạn chỉ cần:
docker pull ghcr.io/vietnamcos/payment-service:sha-9f3c1e0
Bước 4 — Đặt policy dọn dẹp:
Cấu hình registry tự xóa image cũ. Ví dụ với ECR, đặt lifecycle policy: "giữ 10 image mới nhất theo mỗi tag prefix, xóa các image untagged quá 14 ngày". Điều này quan trọng vì mỗi image có thể nặng hàng trăm MB, tích tụ nhanh chóng thành hóa đơn lưu trữ đáng kể.
Lỗi thường gặp & mẹo
Lỗi 1 — Deploy bằng tag latest. Đây là lỗi kinh điển. latest thay đổi theo thời gian nên bạn mất khả năng biết chính xác đang chạy gì, và rollback trở nên bất khả thi vì "quay lại latest" chẳng có nghĩa gì. Mẹo: luôn deploy bằng tag bất biến (SHA hoặc digest).
Lỗi 2 — Build lại artifact ở mỗi môi trường. Nếu staging build một image, production build một image khác từ cùng source, chúng vẫn có thể khác nhau (do dependency cập nhật, thời điểm build khác). Mẹo: build một lần, promote cùng một artifact qua các môi trường bằng cách chỉ thay đổi cấu hình, không thay đổi image.
Lỗi 3 — Không dọn kho, để artifact chất đống. Một team có thể tạo ra hàng trăm image mỗi tuần từ các PR. Sau vài tháng, registry phình lên hàng trăm GB. Mẹo: bật lifecycle/retention policy ngay từ đầu, phân biệt rõ artifact "release" (giữ lâu) và artifact "tạm/PR" (xóa sớm).
Lỗi 4 — Hardcode credential registry trong code hoặc log. Token push lên registry mà lộ ra là thảm họa. Mẹo: luôn dùng secret của CI (như secrets.GITHUB_TOKEN), và ưu tiên xác thực ngắn hạn qua OIDC thay vì token tĩnh khi có thể.
Lỗi 5 — Publish npm với version trùng hoặc quên bump. Pipeline sẽ fail hoặc âm thầm không publish. Mẹo: tự động hóa việc bump version (dùng công cụ như changesets hay semantic-release) dựa trên commit message, để con người không phải nhớ.
Mẹo bonus — Đặt tên có ý nghĩa và nhất quán. Một quy ước tag rõ ràng (sha-<commit> cho mọi build, v<semver> cho release) giúp cả team đọc registry như đọc sách, không phải đoán mò.
Bài tập thực hành
- Thiết lập container registry cá nhân: Tạo một repo GitHub, viết một
Dockerfileđơn giản (ví dụ một web server tĩnh với nginx), rồi viết workflow GitHub Actions build và push image lên GHCR với hai tag: một theogithub.shavà một làlatest. Kiểm tra image xuất hiện ở tab Packages của repo.
- Publish một npm package nội bộ: Tạo một package nhỏ tên
@<tên-bạn>/hello-utilsvới một hàm tiện ích. Cấu hình.npmrctrỏ về GitHub Packages, publish version1.0.0. Sau đó sửa hàm, bump lên1.0.1và publish lại. Ở một repo thứ hai, install và dùng thử package đó.
- Thực hành versioning: Cho ba tình huống thay đổi sau, hãy quyết định version mới từ
2.3.1theo SemVer: (a) vá một lỗi hiển thị, (b) thêm một prop mới không phá vỡ gì, (c) đổi tên một API public. Giải thích lựa chọn của bạn.
- Thiết kế lifecycle policy: Viết bằng lời (hoặc JSON nếu bạn dùng ECR) một chính sách dọn dẹp cho registry của một team tạo ~50 image PR mỗi tuần: giữ artifact release bao lâu, xóa artifact untagged sau bao nhiêu ngày, giữ tối đa bao nhiêu image theo mỗi loại.
Tóm tắt
Artifact là sản phẩm bất biến của quá trình build, và Artifact Management là cách bạn lưu trữ, đánh phiên bản, phân phối và bảo mật chúng. Nguyên tắc cốt lõi xuyên suốt là "build once, deploy many" — build một lần rồi promote cùng một artifact qua mọi môi trường.
Ba loại artifact chính bạn cần nắm: container image (đẩy lên GHCR, ECR, ACR), npm package (npmjs.org, GitHub Packages, Artifactory), và Maven cho hệ Java. Mỗi loại có registry riêng, cơ chế xác thực riêng, nhưng chung mô hình push/pull.
Về versioning: dùng Git SHA hoặc digest cho tính bất biến và truy vết, dùng SemVer cho khả năng đọc và quản lý tương thích, và tránh xa latest trong production. Đừng quên hai việc dễ bị bỏ qua nhưng gây đau nhất: dựng registry riêng để không dính rate limit của registry public, và đặt policy dọn dẹp để kho artifact không phình ra thành hóa đơn khổng lồ.
Khi bạn quản lý artifact tốt, bạn có được ba điều quý giá: khả năng truy vết (biết chính xác gì đang chạy), khả năng rollback (quay lại một version cụ thể trong tích tắc), và niềm tin rằng thứ đã test chính là thứ đang chạy. Đó là nền tảng để các bài tiếp theo về deployment và promotion strategy trở nên vững chắc.