CI/CD all in one với docker host (Phần 1)
🎯 Mục tiêu bài Lab
Bài lab này hướng dẫn chi tiết cách xây dựng luồng CI/CD “Cách 1: All-in-One” hoàn chỉnh, deploy ứng dụng (frontend + backend) lên Docker host. Lab này bao gồm cấu hình mạng MACVLAN bền vững, DNS tập trung, Reverse Proxy (NPM) với SSL, và quy trình build/deploy tự động có bước phê duyệt thủ công.
Sơ đồ Luồng hoạt động
👨💻 Developer push code mới lên GitLab (nhánh `nodejs`).
⬇️
📡 GitLab tự động gửi Webhook cho **Jenkins**.
⬇️
👷♂️ Jenkins nhận code, build **Docker images** (Fe & BE).
⬇️
📦 Jenkins push images lên GitLab Registry.
⬇️
⏸️Jenkins dừng lại, chờ phê duyệt từ CTO.
⬇️
🧑💼 CTO đăng nhập Jenkins và nhấn “Approve”.
⬇️
🚀 Jenkins chạy `kubectl apply` để deploy lên k8s. 📂 Cấu trúc Thư mục Chuẩn bị
/opt/devcom/
├── docker-compose.yml # (File 1: Xem Bước 4 bên dưới)
│
├── jenkins/
│ └── Dockerfile # (File 2: Xem Bước 3 bên dưới)
│
├── gitlab/ # (Docker sẽ tự tạo khi chạy lần đầu)
│ ├── config/ # Chứa file gitlab.rb quan trọng!
│ ├── data/
│ └── logs/
│
├── npm/ # (Docker sẽ tự tạo)
├── runner-config/ # (Docker sẽ tự tạo)
└── technitium-config/ # (Docker sẽ tự tạo)🤔 Tại sao lại chọn /opt/devcom/ ?
Quy ước phổ biến (Common Convention): Thư mục
/opt(optional) trong Linux thường dùng cho các ứng dụng “bên thứ ba” hoặc đóng gói. Đặt toàn bộ dự án Docker Compose vào đây giúp tách biệt (isolate) nó khỏi hệ điều hành chính.Tập trung & Dễ quản lý (Centralized & Manageable): Giữ tất cả các file cấu hình (IaC - Infrastructure as Code) và thư mục dữ liệu (volumes) tại cùng một nơi giúp bạn dễ dàng tìm kiếm, sao lưu, hoặc di chuyển khi cần.
Các lựa chọn “chuẩn” FHS khác (Advanced FHS Alternatives):
/srv/devcom/(dùng cho “service data”) hoặc tách bạch (config ở/etc, data ở/var/lib).
⚠️ Tránh dùng (Avoid)
Không nên tạo thư mục tùy tiện ở cấp gốc như
/projects/vì nó không theo chuẩn FHS và làm lộn xộn cấu trúc thư mục gốc của hệ điều hành.
Kết luận (Conclusion)
Sử dụng
/opt/devcom/là một lựa chọn tốt, cân bằng giữa tính đơn giản, quy ước phổ biến và dễ quản lý cho bài lab này.
Giai đoạn 0: Cài đặt Hạ tầng
Đây là giai đoạn quan trọng nhất (để cấu hình Network adapter ở dạng bridged(Autodetect), bao gồm cài đặt Docker, cấu hình mạng vật lý ảo (MACVLAN hoặc IPVLAN), và cấu hình mạng vĩnh viễn cho máy chủ host.
Bước 1: Cài đặt Docker Engine (Điều kiện tiên quyết)
Cài đặt các gói phụ thuộc:
sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg lsb-releaseThêm GPG Key chính thức của Docker:
sudo mkdir -m 0755 -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg-Thêm kho lưu trữ (Repository) của Docker:
echo “deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable” | sudo tee /etc/apt/sources.list.d/docker.list> /dev/nullCài đặt Docker Engine (Bản đầy đủ):
sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-compose-Kiểm tra dịch vụ Docker
sudo systemctl status dockerBạn sẽ thấy trạng thái
active (running). Giờ đây máy chủ Host đã sẵn sàng.Thêm User vào nhóm
docker:sudo usermod -aG docker ${USER}QUAN TRỌNG: Sau khi chạy lệnh này, bạn phải đăng xuất (log out) khỏi máy chủ và đăng nhập (log in) trở lại để thay đổi có hiệu lực.
Bước 2: Chuẩn bị Mạng Nền (IPVLAN hoặc MACVLAN)
Để khắc phục lỗi xung đột giữa
systemd-networkdvàcloud-init(nguyên nhân gây mất IP tĩnh), chúng ta cần vô hiệu hóacloud-inittrước khi cấu hình mạng.# 1. Tạo thư mục cấu hình cloud-init sudo mkdir -p /etc/cloud/cloud.cfg.d # 2. Tạo file cấu hình. echo “network: {config: disabled}” | sudo tee /etc/cloud/cloud.cfg.d/99-disable-networking.cfgGiải thích:
network: {config: disabled}báo chocloud-initbiết rằng nó không được phép quản lý hoặc tạo ra bất kỳ cấu hình mạng nào (/etc/netplan/*.yaml). Đây là bước bắt buộc để đảm bảo cấu hìnhsystemd-networkdcủa bạn không bao giờ bị ghi đè sau khi reboot.Trong linux thì có 2 cách quản lý network là NetworkManager và systemd-networkd. Cả 2 đều hoạt động ở phía background (dưới dạng deamon)
Còn với netplan, nó dùng file
yamlđể cấu hình, nó dùng ở phía trên, khi chạy nó sẽ apply cái cấu hình đó xuống bên dưới. Ở dưới NetworkManager hay systemd-networkd sẽ tự động sinh file tương ứng để phù hợp với cấu hình đó# Xóa các file Netplan cũ (50-cloud-init.yaml) sudo rm -f /etc/netplan/*.yaml # Đảm bảo NetworkManager đã disabled nếu systemd-networkd được sử dụng sudo systemctl stop NetworkManager || true sudo systemctl disable NetworkManager || true # Kích hoạt và khởi động lại systemd-networkd để bắt đầu quản lý mạng sudo systemctl enable systemd-networkd sudo systemctl restart systemd-networkd
Bước 2.1: Dùng IPVLAN
Sử dụng
systemd-networkdđể cấu hình Host IPVLAN.(Thayens160bằng tên card mạng vật lý của bạn)Tạo file
05-ens160.network(Biến card vật lý thành “cha”, dùngip ađể kiểm tra):sudo nano /etc/systemd/network/05-ens160.network[Match] Name=ens160 [Network] IPVLAN=ipvhost0*Giải thích:
[Match]: Định danh card mạng vật lý (parent).
Name=ens160: Tên card mạng vật lý đang được sử dụng (ví dụ: `ens160` hoặc `eth0`).
[Network]: Khối định nghĩa cách thức hoạt động của mạng.
IPVLAN=ipvhost0: Chỉ định rằng interface ảo có tên `ipvhost0` (được định nghĩa trong file `.netdev` tiếp theo) sẽ được gắn (attach) vào card mạng vật lý này. Card vật lý giờ đây chỉ là cầu nối (trunk).
Tạo file
10-ipvhost0.netdev(Định nghĩa interface ảo cho Host):sudo nano /etc/systemd/network/10-ipvhost0.netdev[NetDev] Name=ipvhost0 Kind=ipvlan [IPVLAN] Mode=L2*Giải thích:
[NetDev]: Khối định nghĩa một interface mạng mới.
Name=ipvhost0: Tên logic của interface ảo.
Kind=ipvlan: Loại interface là IPVLAN.
[IPVLAN]: Khối cấu hình riêng cho IPVLAN.
Mode=L2: Chế độ IPVLAN Layer 2. Đây là chế độ khuyến nghị cho VM, cho phép Host và Container giao tiếp trực tiếp trên cùng một IP subnet.
Tạo mạng Docker IPVLAN (Trước khi reboot):
docker network create -d ipvlan \ --subnet=192.168.1.0/24 \ --gateway=192.168.1.1 \ -o parent=ens160 \ -o ipvlan_mode=l2 \ VLAN110Khởi động lại (Reboot) để áp dụng toàn bộ cấu hình mạng Systemd:
sudo reboot
Bước 2.2: Đối với dùng MACVLAN (Cập nhật Systemd-networkd)
Cấu hình tương tự MACVLAN để Host có thể giao tiếp với Container ( MACVLAN Host cần một IP riêng biệt (eg:
.254) để giao tiếp với các Container )Tạo mạng
macvlan(Bắt buộc):docker network create -d macvlan \ --subnet=192.168.1.0/24 \ --gateway=192.168.1.1 \ -o parent=ens160 \ VLAN110Tạo file `.netdev` (Tạo interface ảo
macvlan-host):sudo nano /etc/systemd/network/20-macvlan-host.netdev[NetDev] Name=macvlan-host Kind=macvlan [MACVLAN] Device=ens160 Mode=bridge*Giải thích:
Kind=macvlan: Chỉ định loại interface là MACVLAN.
Device=ens160: Gắn MACVLAN vào card mạng vật lý.
Mode=bridge: Cho phép interface ảo này hoạt động như một cầu nối, giúp Host giao tiếp với các Container MACVLAN.
Tạo file
.network(Gán IP & Route chomacvlan-hostvà Fix ổn định):sudo nano /etc/systemd/network/25-macvlan-host.network[Match] Name=macvlan-host [Network] Address=192.168.1.254/24 [Link] ActivationPolicy=up [Route] Destination=192.168.1.31/32 Gateway=192.168.1.254 [Route] Destination=192.168.1.30/32 Gateway=192.168.1.254 [Route] Destination=192.168.1.32/32 Gateway=192.168.1.254 [Route] Destination=192.168.1.34/32 Gateway=192.168.1.254 [Route] Destination=192.168.1.33/32 Gateway=192.168.1.254 [Install] WantedBy=network-online.target*Giải thích:
Address=192.168.1.254/24: Gán IP tĩnh riêng cho interface MACVLAN Host để giao tiếp (vì Host không thể giao tiếp trực tiếp với MACVLAN Container).
[Route]: Khối này định nghĩa các tuyến đường (routes).
Destination=192.168.1.31/32: Tạo tuyến đường cục bộ chỉ ra rằng để tới IP của NPM (`.31`), Host phải dùng Gateway chính là IP của chính nó trên MACVLAN (`.254`). Đây là cách Host “nhìn thấy” các Container.
WantedBy=network-online.target: Đảm bảo khởi tạo ổn định sau khi reboot.
Kích hoạt và Áp dụng
systemd-networkd:# Khởi động lại service mạng (Sau khi reboot) sudo systemctl restart systemd-networkd
Bước 2.3: Cấu hình Docker Daemon (Trust Registry)
Cấu hình Docker Daemon trên Host OS để tin tưởng Registry nội bộ và biết sử dụng Technitium DNS (
192.168.1.30) để phân giải tên miền.(Áp dụng cho cả IPVLAN và MACVLAN.)Sửa file
/etc/docker/daemon.json:sudo nano /etc/docker/daemon.json{ “insecure-registries”: [”register.thongdev.site”], “dns”: [”192.168.1.30”, “8.8.8.8”] }*Giải thích:
“insecure-registries”: [”register.thongdev.site”]: Cho Docker biết rằng nó nên bỏ qua (skip) việc kiểm tra SSL/TLS tiêu chuẩn khi kết nối với Registry nội bộ này. Điều này giúp các container có thể push/pull image mà không bị lỗi xác thực SSL/TLS.
“dns”: [”192.168.1.30”, “8.8.8.8”]: Đảm bảo Docker daemon (và các container nếu không chỉ định DNS riêng) sử dụng Technitium DNS (
.30) để phân giải các tên miền nội bộ nhưgitlab.thongdev.site.
Khởi động lại Docker daemon:
sudo systemctl restart docker
Bước 2.4: Cấu hình DNS Server (Host OS)
XÓA HẲN file
resolv.confcũ đi rồi mới tạo lại. Việc sửa trực tiếp (overwrite) đôi khi không hoạt động do file này là symlink. Nhằm đảm bảo Host OS dùng Technitium DNS (192.168.1.30) để phân giải các tên miền cục bộ.Xóa và Tạo mới
resolv.conf:# Xóa file cũ (hoặc symlink cũ) sudo rm -f /etc/resolv.conf # Tạo file mới sudo nano /etc/resolv.confDán nội dung sau vào file mới:
nameserver 192.168.1.30 search thongdev.site options edns0 trust-adChốt cấu hình (Ngăn ghi đè):
sudo chattr +i /etc/resolv.conf(Lệnh này khóa file lại, đảm bảo không chương trình nào sửa đổi nó được nữa).
Kiểm tra DNS Resolution trên Host (Sau khi service DNS chạy):
nslookup gitlab.thongdev.siteServer: 192.168.1.30 Address: 192.168.1.30#53 Name: gitlab.thongdev.site Address: 192.168.1.31
NOTE: Gỡ lỗi và Kiểm tra trạng thái Networkd
Nếu bạn đã thực hiện các bước trên nhưng vẫn gặp vấn đề (ví dụ: mất IP, DNS không hoạt động), hãy sử dụng lệnh `journalctl` để kiểm tra log chi tiết của
systemd-networkd:Sử dụng các lệnh sau để xem log của dịch vụ mạng, giúp bạn xác định lỗi cấu hình hoặc lỗi thời gian khởi động (race condition):
# Xem log chi tiết của systemd-networkd (Từ đầu đến cuối) journalctl -u systemd-networkd --no-pagerLệnh này hiển thị toàn bộ log của
systemd-networkd. Bạn nên tìm các từ khóa nhưfail,error,conflict, hoặcnot foundđể biết chính xác lỗi nằm ở file cấu hình nào.
# Xem log chi tiết của systemd-networkd (Từ cuối log, mở rộng) journalctl -xeu systemd-networkdCờ
-x(mở rộng) và-e(từ cuối log) giúp bạn tập trung vào các sự kiện gần nhất và xem các chi tiết hỗ trợ gỡ lỗi (như mã lỗi và giải thích). Rất hữu ích khi kiểm tra sau khi vừa reboot.
Bước 3: Tạo file jenkins/Dockerfile
Tạo file tại
/opt/devcom/jenkins/Dockerfile( có bao gồm cả kubectl )# Start from the official Jenkins LTS image using JDK 17 FROM jenkins/jenkins:lts-jdk17 # Switch to root user to install packages USER root # Install prerequisites and Docker GPG key RUN apt-get update && apt-get install -y ca-certificates curl gnupg RUN install -m 0755 -d /etc/apt/keyrings RUN curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc RUN chmod a+r /etc/apt/keyrings/docker.asc # Add Docker repository RUN echo \ “deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \ $(. /etc/os-release && echo “$VERSION_CODENAME”) stable” | \ tee /etc/apt/sources.list.d/docker.list > /dev/null # Install Docker CLI RUN apt-get update && apt-get install -y docker-ce-cli # --- (CẬP NHẬT) CÀI ĐẶT KUBECTL --- RUN curl -LO “https://dl.k8s.io/release/$(curl -L -s #https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl” (với chip intel x86) # Chip apple silicol (arm) https://dl.k8s.io/release/stable.txt)/bin/linux/arm64/kubectl RUN install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl # --- HẾT CẬP NHẬT --- # Switch back to jenkins user USER jenkins

