Series Jenkins #6: Đưa Docker vào dây chuyền CI/CD – Đóng gói mọi thứ
Chào anh em! Ở Bài 5, chúng ta đã thiết lập thành công Webhook, giúp Jenkins tự động bắt tay vào việc ngay khi có dòng code mới được đẩy lên GitHub.
Nhưng hãy dừng lại một nhịp và suy nghĩ thực tế một chút. Giả sử anh em đang viết một API cho hệ thống Backend bằng Node.js hoặc Golang. Jenkins kéo code về xong rồi sao nữa? Nếu dùng cách truyền thống, anh em sẽ phải bắt Jenkins gõ lệnh cài đặt (npm install hoặc go mod download), sau đó chạy trực tiếp ứng dụng trên server chứa Jenkins.
Cách này sẽ dẫn đến một "tội ác" kinh điển trong làng IT: "Code chạy ngon trên máy em, nhưng lên server thì tạch!". Nguyên nhân là do xung đột môi trường (máy dev cài Node.js bản 20, server chạy bản 16, hoặc thiếu thư viện C++ ngầm định của hệ điều hành).
Để giải quyết triệt để vấn đề này, vị cứu tinh thứ hai của chúng ta xuất hiện: Docker.
Hôm nay, chúng ta sẽ kết hợp Jenkins và Docker để tạo ra một dây chuyền CI/CD chuẩn mực của các hệ thống Backend hiện đại.
1. Tại sao phải có Docker trong CI/CD?
Thay vì ném một đống code lên server và cầu nguyện nó chạy được, Docker cho phép chúng ta "đóng gói" toàn bộ mã nguồn, thư viện, và cả hệ điều hành nền (môi trường) vào một chiếc hộp gọi là Docker Container.
Quy trình chuẩn bây giờ sẽ là:
- Code: Dev gõ code và đẩy lên GitHub.
- Build: Jenkins nhận code, dùng lệnh của Docker để nhét code vào "hộp" (Build Docker Image).
- Ship & Run: Cái hộp đó được bê lên bất kỳ server nào (dù là Linux, Windows, máy ảo hay AWS) thì nó đều chạy giống hệt nhau, không bao giờ có chuyện sai lệch môi trường.
Hãy nhớ lại Bài 2, lúc viết file docker-compose.yml cài Jenkins, tôi đã dặn anh em phải map cái dòng volumes: /var/run/docker.sock:/var/run/docker.sock. Đó chính là bước "chừa đường lùi" để bây giờ thằng Jenkins ở bên trong container có thể gọi được lệnh Docker của máy chủ bên ngoài đấy! Kinh nghiệm thực chiến là ở chỗ đó.
2. Thực hành: Biến Jenkinsfile thành xưởng sản xuất Docker
Để Jenkins có thể đóng gói ứng dụng, dự án của anh em trên GitHub bắt buộc phải có một file mang tên Dockerfile.
Giả sử đây là một Dockerfile cực kỳ cơ bản cho dự án Node.js hoặc Golang của anh em:
# File: Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Bây giờ, chúng ta sẽ nâng cấp cái Jenkinsfile ở Bài 5 lên một tầm cao mới. Chúng ta không chỉ echo ra cho vui nữa, mà sẽ ra lệnh cho Jenkins chạy các lệnh Docker thực sự.
Anh em mở Jenkinsfile trong source code lên và sửa lại thành thế này:
pipeline {
agent any
// Định nghĩa các biến môi trường để dùng chung cho tiện
environment {
DOCKER_IMAGE = "my-backend-app"
// Lấy số thứ tự bản build của Jenkins làm Tag cho Image luôn (ví dụ: my-backend-app:v12)
IMAGE_TAG = "v${env.BUILD_NUMBER}"
}
stages {
stage('Kéo Code (Checkout)') {
steps {
echo "Đang kéo mã nguồn từ GitHub..."
git branch: 'main', url: 'https://github.com/Ten-Cua-Ban/Ten-Repo.git'
}
}
stage('Đóng gói (Build Docker Image)') {
steps {
echo "Bắt đầu đóng gói ứng dụng bằng Docker..."
// Chạy lệnh shell (sh) để build image
sh "docker build -t ${DOCKER_IMAGE}:${IMAGE_TAG} ."
sh "docker build -t ${DOCKER_IMAGE}:latest ."
}
}
stage('Chạy thử nghiệm (Run Container)') {
steps {
echo "Đang khởi động Container từ Image vừa build..."
// Xóa container cũ nếu đang chạy để tránh trùng lặp port
sh "docker rm -f my-running-app || true"
// Khởi chạy container mới
sh "docker run -d -p 3000:3000 --name my-running-app ${DOCKER_IMAGE}:latest"
}
}
}
}
3. Đọc hiểu đoạn code trên (Rất quan trọng)
Với tư cách là một kỹ sư, anh em không được phép copy-paste mà không hiểu hệ thống đang làm gì. Hãy nhìn vào 2 điểm mấu chốt sau:
- Block environment {}: Đây là nơi khai báo biến toàn cục. Thay vì phải gõ đi gõ lại tên image, ta gán nó vào biến. Cái biến env.BUILD_NUMBER là một tính năng cực hay của Jenkins. Mỗi lần Jenkins chạy, nó sẽ tăng số thứ tự lên (Job #1, Job #2...). Việc dùng nó làm Tag cho Docker Image (VD: my-backend-app:v5) giúp anh em quản lý version cực kỳ khoa học. Code lỗi ở bản v5? Anh em dễ dàng "quay xe" (rollback) về bản v4.
- Đoạn || true ở lệnh xóa container: docker rm -f my-running-app || true. Đây là thủ thuật (trick) cực kỳ phổ biến trong DevOps. Nếu container my-running-app chưa từng tồn tại (ví dụ lần chạy đầu tiên), lệnh xóa sẽ bị báo lỗi. Mà trong Jenkins, hễ có 1 dòng bash script báo lỗi là toàn bộ Pipeline sẽ bị đánh dấu đỏ (Failed) và dừng ngay lập tức. Cụm || true ép câu lệnh đó luôn trả về kết quả Thành công (Success) dù nó có xóa được container hay không.
4. Tận hưởng thành quả
Bây giờ, quy trình đã hoàn thiện. Anh em chỉ việc lưu code lại, gõ git push. Webhook sẽ đánh thức Jenkins. Jenkins kéo code về, đọc Dockerfile, hì hục build ra một cái Docker Image xịn xò, rồi tự động docker run nó lên.
Chỉ vài phút sau, anh em mở trình duyệt, gõ http://<IP-Của-Server>:3000 và ứng dụng Backend đã sừng sững ở đó, chạy mượt mà bên trong container!
Anh em đã đi được một chặng đường dài từ kẻ "deploy tay" bằng WinSCP đến việc dựng lên một quy trình đóng gói tự động chuẩn mực. Nhưng khoan vội ăn mừng, hệ thống này vẫn chỉ đang chạy trên một server duy nhất.
Thực tế đi làm, hệ thống của anh em có thể bao gồm hàng tá Microservices, kết nối với Database (như Vitess) hay Message Broker (như Kafka). Cách quản lý triển khai phức tạp này sẽ được bật mí trong Bài 7: Thực chiến: Triển khai tự động một dự án Backend. Chuẩn bị sẵn sàng nhé!
All rights reserved