Series Jenkins #6: Đưa Docker vào dây chuyền CI/CD – Đóng gói mọi thứ
1. Tư duy "Đóng gói" với Docker
Thay vì ném một cục source code lên server và phó mặc cho hệ điều hành ở đó tự xử lý, Docker mang đến một tư duy hoàn toàn mới: Đóng gói cả môi trường.
Anh em có thể hình dung thế này:
- Dockerfile: Là bản vẽ thiết kế. Nó quy định: "Tôi cần hệ điều hành Alpine Linux nhỏ gọn, cài sẵn Node.js 18, copy code vào thư mục /app và chạy lệnh npm start".
- Docker Image: Jenkins sẽ đọc bản thiết kế đó và đúc ra một chiếc "hộp container" đóng kín (Image).
- Docker Container: Khi mang chiếc hộp này lên server thật và bật nó lên, nó sẽ chạy y hệt như lúc ở trên máy dev, bất chấp server ngoài đời đang chạy Windows, Ubuntu hay CentOS.
2. Chuẩn bị "Vũ khí": Bí kíp Docker out of Docker (DooD)
Để Jenkins có thể gõ được các lệnh như docker build hay docker run, bản thân Jenkins phải được cấp quyền nói chuyện với Docker.
Nếu anh em nhớ lại cấu hình file docker-compose.yml lúc cài Jenkins ở Bài 2, tôi đã cố tình chèn một dòng rất quan trọng:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
Dòng này cho phép Jenkins (đang nằm trong container) "thò tay" ra ngoài và sử dụng chung lõi Docker (Docker engine) với máy chủ vật lý. Nhờ vậy, Jenkins có sức mạnh đóng gói mọi project của anh em.
3. Nâng cấp Jenkinsfile: Đưa Docker vào đường ống
Giả sử trong source code dự án Backend của anh em đã có sẵn một file Dockerfile xịn xò. Bây giờ, chúng ta sẽ viết lại Jenkinsfile để bác quản gia Jenkins biết cách dùng Docker.
Anh em hãy thay thế Jenkinsfile cũ bằng đoạn mã sau:
pipeline {
agent any
// 1. Khai báo các biến môi trường dùng chung
environment {
// Đặt tên cho Image của dự án
APP_NAME = "backend-api-service"
// Dùng luôn số thứ tự của bản build làm Tag (VD: v12, v13...)
IMAGE_TAG = "v${env.BUILD_NUMBER}"
}
stages {
stage('Kéo Code (Checkout)') {
steps {
echo "Đang kéo mã nguồn mới nhất từ GitHub..."
git branch: 'main', url: 'https://github.com/YourUsername/Your-Repo.git'
}
}
stage('Đóng gói (Build Docker Image)') {
steps {
echo "Bắt đầu nhét code vào hộp Docker..."
// Build image với 2 tag: một tag theo version, một tag latest
sh "docker build -t ${APP_NAME}:${IMAGE_TAG} ."
sh "docker build -t ${APP_NAME}:latest ."
}
}
stage('Dọn dẹp môi trường cũ (Clean Up)') {
steps {
echo "Xóa container cũ đang chạy để lấy chỗ cho container mới..."
// Dùng || true để ép Jenkins không báo lỗi nếu container chưa từng tồn tại
sh "docker rm -f running-backend-app || true"
}
}
stage('Khởi chạy ứng dụng (Run Container)') {
steps {
echo "Đang khởi động Container mới..."
// Chạy container ngầm (-d) và map port 8080 ra ngoài
sh "docker run -d --name running-backend-app -p 8080:8080 ${APP_NAME}:latest"
}
}
}
}
4. Đọc vị quy trình (Dành cho Kỹ sư thực chiến)
Là một người làm kỹ thuật, anh em đừng chỉ copy/paste. Hãy nhìn sâu vào cách Pipeline này hoạt động:
- Quản lý phiên bản tự động (Versioning): Bằng cách dùng biến ${env.BUILD_NUMBER}, mỗi lần Jenkins chạy, nó sẽ tự sinh ra một chiếc hộp có mã số riêng (v1, v2, v3...). Điều này cực kỳ quan trọng đối với các hệ thống yêu cầu tính sẵn sàng cao. Nếu bản v20 vừa tung ra bị lỗi vỡ API, anh em chỉ cần gõ 1 lệnh docker run ... v19 là hệ thống lùi về bản cũ (rollback) mượt mà trong 3 giây.
- Thủ thuật || true: Lệnh docker rm -f sẽ báo lỗi màu đỏ loét nếu nó không tìm thấy container nào để xóa (ví dụ ở lần chạy đầu tiên). Mà Jenkins hễ thấy đỏ là sẽ đánh "Failed" và bắt dừng toàn bộ quy trình ngay lập tức. Cụm || true ở cuối lệnh là một trick của dân DevOps để nhắc Jenkins: "Kệ nó, lỗi xóa không được thì cứ coi như thành công và đi tiếp đi".
5. Tận hưởng dây chuyền hiện đại
Bây giờ anh em lưu Jenkinsfile, commit và push lên GitHub.
Ngồi nhâm nhi tách cà phê, nhìn sang màn hình Jenkins, anh em sẽ thấy các khối Stage View xanh mướt lần lượt sáng lên. Bác quản gia kéo code, cặm cụi đóng gói Docker Image, dẹp bỏ container cũ và bật container mới lên hoàn toàn tự động.
Môi trường dev, staging hay production giờ đây không còn là rào cản nữa. Bất kể là Node.js, Golang, hay PHP, chỉ cần nằm trong Docker, nó sẽ chạy chính xác 100% như những gì anh em đã lập trình.
Chúng ta đã thành công tạo ra bản build hoàn chỉnh và chạy nó ngay trên máy chứa Jenkins. Tuy nhiên, trong thực tế, không ai host ứng dụng Production chung nhà với ông Jenkins cả. Ở Bài 7: Thực chiến – Triển khai tự động dự án Backend, tôi sẽ chia sẻ cách đẩy Image này lên một kho lưu trữ tập trung (Registry), rồi ra lệnh cho các Server Production khác kéo về và chạy mượt mà không làm gián đoạn người dùng (Zero-downtime).
All rights reserved