Hướng dẫn Setup Jenkins cho hệ thống Monolith - Bài 20: Quản lý Artifacts (Lưu trữ file JAR, WAR, hoặc ZIP sau khi build thành công)
Chúng ta đã đi đến bài học cuối cùng của Giai đoạn 2: Tự động hóa Kiểm thử và Phân tích Mã nguồn. Ở các bài trước, sau khi mã nguồn vượt qua hàng loạt chốt chặn khắt khe (Unit Test, Coverage, Trivy Security, SonarQube Quality Gate), chúng ta đã dùng lệnh archiveArtifacts để lưu gói cài đặt ngay trên Jenkins.
Tuy nhiên, trong môi trường vận hành thực tế với hệ thống Monolith (nơi mỗi gói build .tar.gz, .jar hay .war có thể nặng từ 50MB đến 300MB), việc quản lý Artifacts như thế nào để không làm sập ổ cứng Jenkins, đảm bảo tính toàn vẹn chống giả mạo và sẵn sàng phục vụ cho quá trình Deploy/Rollback ở Giai đoạn 3 là một nghệ thuật.
1. Bản chất của Artifact và nguyên tắc "Build Once, Deploy Anywhere"
Artifact là sản phẩm đóng gói hoàn chỉnh, sẵn sàng chạy được sau khi quá trình biên dịch và kiểm thử kết thúc:
-
Với Java Spring Boot Monolith: Tệp thực thi
.jarhoặc.war. -
Với Golang / C++ / C# Monolith: Tệp thực thi nhị phân (
binary) kèm thư mục cấu hình tĩnh. -
Với PHP Laravel / Node.js Monolith: Tệp nén
.tar.gzhoặc.zipchứa toàn bộ source code, thư mụcvendor/(đã loại bỏ gói dev) và các tài nguyên Frontend (public/build) đã biên dịch.
Nguyên tắc vàng trong CI/CD (Build Once, Deploy Anywhere): Bạn chỉ biên dịch và đóng gói mã nguồn đúng 1 lần duy nhất. Cùng một gói Artifact monolith-core-system-v1.2.0-b45-a1b2c3d.tar.gz đó sẽ được mang đi triển khai trên môi trường Staging để QC kiểm thử; khi QC nghiệm thu đạt, chính gói Artifact đó (được xác thực bằng mã băm MD5/SHA256) sẽ được đẩy thẳng lên Production mà tuyệt đối không chạy lại lệnh composer install hay mvn package từ đầu.
2. Cơ chế archiveArtifacts nội bộ của Jenkins và tính năng Fingerprinting
Trong Jenkins, hàm archiveArtifacts không chỉ đơn thuần là copy file từ thư mục tạm (workspace) về thư mục lưu trữ của Jenkins Master (/var/jenkins_home/jobs/<job-name>/builds/<build-number>/archive), mà còn cung cấp các tham số kiểm soát cực kỳ mạnh mẽ:
Groovy
archiveArtifacts(
artifacts: "${APP_NAME}-${env.BUILD_VERSION}.tar.gz, version.json, checksums.sha256",
excludes: "**/*.log, **/*.tmp",
fingerprint: true, // Bật tính năng điểm chỉ (MD5 Hash tracking)
onlyIfSuccessful: true, // Chỉ lưu trữ khi toàn bộ Pipeline thành công
allowEmptyArchive: false // Báo lỗi ngay nếu không tìm thấy file Artifact để lưu
)
Tại sao fingerprint: true lại sống còn đối với hệ thống Monolith?
Khi bạn bật fingerprint: true, Jenkins sẽ tự động tính toán mã băm MD5 cho tệp Artifact đó và lưu vào cơ sở dữ liệu dấu vân tay của Jenkins:
-
Giả sử bạn tách riêng Job 1: Build Monolith và Job 2: Deploy to Production.
-
Khi Job 2 lấy tệp Artifact từ Job 1 để đẩy lên máy chủ Production, nhờ có Fingerprint, Jenkins sẽ tự động vẽ sơ đồ liên kết giữa hai Job, cho phép bạn trả lời ngay câu hỏi: "Bản build #45 đang chạy trên Production được deploy vào ngày giờ nào, bởi ai?"
3. Tạo tệp kiểm tra tính toàn vẹn (SHA256 Checksum) đi kèm Artifact
Khi truyền tải một gói Monolith nặng hàng trăm MB qua mạng (SSH/SCP) từ Jenkins sang máy chủ Production ở Giai đoạn 3, nguy cơ rớt gói tin hoặc hỏng file (Corrupted Archive) là hoàn toàn có thể xảy ra.
Do đó, trước khi chạy archiveArtifacts, hãy luôn tạo một tệp checksums.sha256 đi kèm:
Bash
# Tạo mã băm SHA256 cho gói cài đặt Monolith
sha256sum ${APP_NAME}-${BUILD_VERSION}.tar.gz > ${APP_NAME}-${BUILD_VERSION}.tar.gz.sha256
# Nội dung file .sha256 sẽ có dạng:
# e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 monolith-core-system-v1.2.0.tar.gz
Khi đẩy lên Production Server, script triển khai chỉ cần chạy sha256sum -c *.sha256 để xác nhận tệp nguyên vẹn 100% trước khi giải nén.
4. Bài toán thực tế: Lưu trên Jenkins hay đẩy sang Kho lưu trữ chuyên dụng (Nexus / S3 / MinIO)?
Việc lưu trực tiếp Artifact bằng archiveArtifacts trên Jenkins rất tiện lợi nhưng có giới hạn về mặt kiến trúc:
| Tiêu chí so sánh | Lưu nội bộ trên Jenkins (archiveArtifacts) |
Kho lưu trữ ngoài (Nexus / Artifactory / AWS S3 / MinIO) |
|---|---|---|
| Độ phức tạp cài đặt | Cực kỳ đơn giản: Có sẵn ngay trong Jenkins, không cần cấu hình phức tạp. | Cần dựng thêm máy chủ lưu trữ chuyên dụng hoặc đăng ký dịch vụ Object Storage. |
| Áp lực ổ cứng Jenkins | Rất cao: Ví dụ 20 bản build x 200MB = 4GB cho mỗi dự án, rất dễ làm đầy ổ cứng của Jenkins Master. |
Bằng 0: Jenkins build xong sẽ đẩy file sang S3/Nexus rồi xóa trắng ngay trên ổ cứng Jenkins. |
| Tốc độ tải khi Deploy | Dễ bị nghẽn (Nếu 10 server Production cùng tải về lúc, giao diện Jenkins có thể bị đơ do nghẽn băng thông I/O). | Tối ưu hóa băng thông cao: Phân tải cực tốt cho hàng chục server đồng thời tải về mà không ảnh hưởng CI server. |
| Khuyến nghị sử dụng | Chỉ nên giữ lại 3 – 5 bản build gần nhất trên Jenkins để truy cập và kiểm tra nhanh. | Dùng để lưu trữ dài hạn (Long-term retention) các bản Release chính thức phục vụ Rollback bất cứ lúc nào. |
Cách cấu hình chính sách tự động xóa Artifact cũ (logRotator) để cứu ổ cứng Jenkins:
Ở đầu Jenkinsfile, trong khối options, bạn hãy tách biệt rõ số lượng Log lịch sử build muốn giữ và số lượng File Artifact nặng nề muốn giữ:
Groovy
options {
buildDiscarder(logRotator(
numToKeepStr: '30', // Giữ lại lịch sử log text của 30 lần build gần nhất (vì text rất nhẹ)
artifactNumToKeepStr: '5', // NHƯNG chỉ giữ lại file nén Artifact (.tar.gz/.jar) của 5 bản thành công mới nhất!
daysToKeepStr: '60',
artifactDaysToKeepStr: '14'
))
}
5. Cách chia sẻ Artifact giữa các Pipeline bằng Copy Artifact Plugin
Trong thực tế, bạn thường không gộp chung bước Build và bước Deploy Production vào cùng một lần chạy tự động, mà tách thành 2 Pipeline riêng:
-
monolith-ci-build: Tự động chạy mỗi khi có code mới để kiểm thử và tạo Artifact. -
monolith-cd-deploy: Khi cần lên Production, Tech Lead bấm chạy Job này, truyền vào số thứ tự bản build đã pass của Job 1 để lấy đúng file Artifact đó đem đi deploy.
Để Job 2 lấy được Artifact từ Job 1, bạn cài đặt plugin Copy Artifact và sử dụng đoạn lệnh sau ở Job 2:
Groovy
// Đoạn mã dùng ở Pipeline Deploy (Giai đoạn 3) để kéo Artifact từ Pipeline Build về
copyArtifacts(
projectName: 'monolith-production-pipeline',
selector: specific("${params.TARGET_BUILD_NUMBER}"), // Hoặc dùng lastSuccessful() để lấy bản mới nhất
filter: '*.tar.gz, *.sha256, version.json',
fingerprintArtifacts: true
)
6. Hoàn thiện Jenkinsfile tổng kết Giai đoạn 2 (Full CI & Artifact Management)
Dưới đây là tệp Jenkinsfile hoàn chỉnh nhất khép lại Giai đoạn 2: Tích hợp tối ưu nén đa luồng (pigz hoặc gzip), sinh mã kiểm tra toàn vẹn SHA256, lưu trữ trên Jenkins kèm tùy chọn đẩy lên kho lưu trữ ngoài (S3 / MinIO / Nexus):
Groovy
pipeline {
agent any
options {
// Giữ 30 bản log lịch sử, nhưng chỉ giữ tối đa 5 gói Artifact nặng trên ổ cứng Jenkins
buildDiscarder(logRotator(numToKeepStr: '30', artifactNumToKeepStr: '5'))
timeout(time: 30, unit: 'MINUTES')
timestamps()
}
environment {
APP_NAME = "monolith-core-system"
CACHE_ROOT = "/var/jenkins_home/.cache"
COMPOSER_CACHE_DIR = "${CACHE_ROOT}/composer"
NPM_CONFIG_CACHE = "${CACHE_ROOT}/npm"
SONAR_USER_HOME = "${CACHE_ROOT}/sonar"
}
stages {
stage('1. Fast Checkout & Versioning') {
steps {
checkout([
$class: 'GitSCM',
branches: scm.branches,
extensions: [
[$class: 'CloneOption', depth: 50, noTags: false, shallow: true, timeout: 10]
],
userRemoteConfigs: scm.userRemoteConfigs
])
script {
sh "mkdir -p ${COMPOSER_CACHE_DIR} ${NPM_CONFIG_CACHE} ${SONAR_USER_HOME}"
env.GIT_SHORT_HASH = sh(script: 'git rev-parse --short=7 HEAD', returnStdout: true).trim()
env.LATEST_SEMVER_TAG = sh(script: 'git describe --tags --abbrev=0 2>/dev/null || echo "v1.0.0"', returnStdout: true).trim()
env.CLEAN_BRANCH = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim().replaceAll('[^a-zA-Z0-9.-]', '-')
if (env.CLEAN_BRANCH == 'main' || env.CLEAN_BRANCH == 'master') {
env.BUILD_VERSION = "${env.LATEST_SEMVER_TAG}-b${env.BUILD_NUMBER}-${env.GIT_SHORT_HASH}"
} else {
env.BUILD_VERSION = "${env.LATEST_SEMVER_TAG}-${env.CLEAN_BRANCH}.${env.BUILD_NUMBER}-${env.GIT_SHORT_HASH}"
}
env.ARTIFACT_NAME = "${env.APP_NAME}-${env.BUILD_VERSION}.tar.gz"
currentBuild.displayName = "${env.BUILD_VERSION}"
}
}
}
stage('2. Cached Install Dependencies') {
steps {
sh 'composer install --no-interaction --prefer-dist'
}
}
stage('3. Run Unit Tests & Coverage') {
environment {
APP_ENV = "testing"
DB_CONNECTION = "sqlite"
DB_DATABASE = ":memory:"
XDEBUG_MODE = "coverage"
}
steps {
sh '''
mkdir -p reports
./vendor/bin/phpunit \
--log-junit reports/junit.xml \
--coverage-clover reports/coverage-clover.xml
'''
}
post {
always {
junit testResults: 'reports/junit.xml', allowEmptyResults: false
}
}
}
stage('4. Security & SonarQube Gate') {
steps {
script {
sh "trivy fs --cache-dir ${CACHE_ROOT}/trivy --scanners vuln,secret --severity CRITICAL --exit-code 1 ."
def scannerHome = tool 'SonarScanner-CLI'
withSonarQubeEnv('SonarQube-Main-Server') {
sh "${scannerHome}/bin/sonar-scanner -Dsonar.projectVersion=${env.BUILD_VERSION}"
}
}
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
stage('5. Build & Checksum Artifact') {
steps {
echo "=== Đóng gói Artifact và khởi tạo chữ ký toàn vẹn SHA256 ==="
sh '''
# 1. Tối ưu hóa thư viện cho môi trường Production
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
# 2. Khởi tạo tệp định danh phiên bản
cat <<EOF > version.json
{
"service": "${APP_NAME}",
"version": "${BUILD_VERSION}",
"commit": "${GIT_SHORT_HASH}",
"branch": "${CLEAN_BRANCH}",
"built_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
}
EOF
# 3. Nén gói mã nguồn (loại bỏ các thư mục không cần thiết trên Production)
tar --exclude='.git' \
--exclude='node_modules' \
--exclude='tests' \
--exclude='reports' \
--exclude="${ARTIFACT_NAME}*" \
-czf ${ARTIFACT_NAME} .
# 4. Sinh tệp kiểm tra toàn vẹn SHA256 và hiển thị dung lượng gói
sha256sum ${ARTIFACT_NAME} > ${ARTIFACT_NAME}.sha256
ls -lh ${ARTIFACT_NAME}*
'''
}
}
stage('6. Publish Artifact') {
steps {
echo "=== Lưu trữ Artifact và đánh dấu Fingerprint ==="
// Lưu trữ nội bộ trên Jenkins
archiveArtifacts(
artifacts: "${env.ARTIFACT_NAME}, ${env.ARTIFACT_NAME}.sha256, version.json",
fingerprint: true,
onlyIfSuccessful: true
)
/* TÙY CHỌN NÂNG CAO: Đẩy bản Release của nhánh main lên MinIO / AWS S3 / Nexus để lưu trữ dài hạn
script {
if (env.CLEAN_BRANCH == 'main' || env.CLEAN_BRANCH == 'master') {
sh "aws s3 cp ${env.ARTIFACT_NAME} s3://monolith-releases-bucket/${env.CLEAN_BRANCH}/${env.ARTIFACT_NAME}"
sh "aws s3 cp ${env.ARTIFACT_NAME}.sha256 s3://monolith-releases-bucket/${env.CLEAN_BRANCH}/${env.ARTIFACT_NAME}.sha256"
}
}
*/
}
}
}
post {
success {
script {
sendNotification('SUCCESS')
}
}
unstable {
script {
sendNotification('UNSTABLE')
}
}
failure {
script {
sendNotification('FAILURE')
}
}
always {
// Xóa file nén tạm trong workspace sau khi đã archive xong để giải phóng ngay dung lượng ổ cứng!
sh "rm -rf reports/ version.json .env ${env.ARTIFACT_NAME}*"
}
}
}
def sendNotification(String status) {
def gitAuthor = sh(script: "git log -1 --pretty=format:'%an'", returnStdout: true).trim()
def gitMessage = sh(script: "git log -1 --pretty=format:'%s'", returnStdout: true).trim().replaceAll('<', '<').replaceAll('>', '>')
def duration = currentBuild.durationString.replace(' and counting', '')
def statusIcon = (status == 'SUCCESS') ? "✅" : ((status == 'UNSTABLE') ? "⚠️" : "❌")
withCredentials([
string(credentialsId: 'telegram-bot-token', variable: 'TG_TOKEN'),
string(credentialsId: 'telegram-chat-id', variable: 'TG_CHAT_ID')
]) {
sh """
curl -s -X POST "https://api.telegram.org/bot\${TG_TOKEN}/sendMessage" \\
-d chat_id="\${TG_CHAT_ID}" \\
-d parse_mode="HTML" \\
-d disable_web_page_preview=true \\
--data-urlencode text="${statusIcon} <b>[${status}] ${env.APP_NAME}</b>
━━━━━━━━━━━━━━━
🏷 <b>Version:</b> <code>${env.BUILD_VERSION}</code>
🌿 <b>Branch:</b> <code>${env.CLEAN_BRANCH}</code>
👤 <b>Author:</b> ${gitAuthor}
💬 <b>Commit:</b> <i>${gitMessage}</i>
⏱ <b>Duration:</b> ${duration}
━━━━━━━━━━━━━━━
📦 <a href='${env.BUILD_URL}artifact/${env.ARTIFACT_NAME}'>Tải Artifact</a> | <a href='${env.BUILD_URL}console'>Console Log</a>" > /dev/null || true
"""
}
}
Điểm nhấn tinh tế ở khối
post { always }: Hãy chú ý câu lệnhrm -rf ... ${env.ARTIFACT_NAME}*ở dòng cuối cùng của Pipeline. Nhiều bạn quên xóa file.tar.gztrong thư mụcworkspacesau khi đã chạyarchiveArtifacts. Hậu quả là Jenkins lưu 2 bản copy của cùng một file nặng 200MB (1 bản trong thư mụcarchivecủa Build History, 1 bản nằm chết trongworkspace), làm tốn gấp đôi dung lượng ổ cứng một cách vô ích!
All rights reserved