Giới thiệu tổng quan về CI/CD trong Machine Learning

Trong môi trường phát triển Machine Learning (ML), việc đưa mô hình từ giai đoạn thử nghiệm nhanh chóng lên môi trường production đòi hỏi một quy trình tự động hoá chặt chẽ. CI/CD (Continuous Integration / Continuous Deployment) không chỉ giúp giảm thiểu lỗi do thao tác thủ công mà còn tạo điều kiện cho việc tái sử dụng, kiểm thử và theo dõi phiên bản mô hình một cách có hệ thống.

Bài viết sẽ hướng dẫn chi tiết cách xây dựng một pipeline CI/CD hoàn chỉnh cho dự án ML, bao gồm:

  • Docker hoá môi trường phát triển và inference.
  • Sử dụng GitHub Actions để tự động hoá các bước build, test, và deploy.
  • Tích hợp MLflow để quản lý mô hình, siêu tham số và artefact.

Toàn bộ quy trình sẽ được minh hoạ bằng một dự án dự đoán giá nhà (House Price Prediction) sử dụng Python, scikit‑learn và FastAPI.

Chuẩn bị môi trường và cấu trúc dự án

Cấu trúc thư mục đề xuất

project_root/
├─ app/
│  ├─ main.py            # FastAPI entrypoint
│  ├─ model.py           # Định nghĩa lớp ModelWrapper
│  └─ requirements.txt  # Dependencies cho runtime
├─ data/
│  └─ raw/
│     └─ housing.csv    # Dữ liệu gốc
├─ mlflow/
│  └─ mlruns/            # Thư mục lưu trữ run của MLflow
├─ tests/
│  └─ test_model.py     # Kiểm thử unit cho ModelWrapper
├─ Dockerfile
├─ docker-compose.yml
└─ .github/
   └─ workflows/
      └─ ci-cd.yml      # Workflow GitHub Actions

Cấu trúc này giúp tách biệt rõ ràng giữa code ứng dụng, dữ liệu, test và cấu hình CI/CD.

Dockerfile cho môi trường runtime

FROM python:3.11-slim

# Thiết lập biến môi trường
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

# Cài đặt các gói hệ thống cần thiết
RUN apt-get update && apt-get install -y --no-install-recommends \
        build-essential \
        gcc && \
    rm -rf /var/lib/apt/lists/*

# Tạo thư mục làm việc
WORKDIR /app

# Sao chép file requirements và cài đặt dependencies
COPY app/requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

# Sao chép toàn bộ mã nguồn
COPY app/ ./

# Expose cổng API
EXPOSE 8000

# Lệnh khởi động FastAPI bằng Uvicorn
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Dockerfile trên tối ưu hoá kích thước image bằng cách sử dụng python:3.11-slim và loại bỏ cache sau khi cài đặt.

Thiết lập MLflow cho quản lý mô hình

MLflow cung cấp ba thành phần chính: Tracking, Projects và Models. Trong pipeline này chúng ta sẽ tập trung vào Tracking để lưu trữ siêu tham số, metric và artefact (model file).

Cài đặt và cấu hình MLflow

Thêm mlflow vào requirements.txt của thư mục app:

fastapi==0.110.0
uvicorn[standard]==0.27.0
scikit-learn==1.4.0
pandas==2.2.0
mlflow==2.9.2

Sau khi cài đặt, tạo một file mlflow_setup.py (không bắt buộc nhưng giúp tách biệt cấu hình):

import os
from mlflow import set_tracking_uri, set_experiment

# Đặt URI cho tracking server (có thể là local hoặc remote)
TRACKING_URI = os.getenv("MLFLOW_TRACKING_URI", "file:///app/mlflow")
set_tracking_uri(TRACKING_URI)

# Đặt tên experiment
set_experiment("house_price_prediction")

Trong môi trường Docker, biến môi trường MLFLOW_TRACKING_URI sẽ được thiết lập trong docker-compose.yml để lưu trữ artefact trong volume.

Đóng gói mô hình với MLflow

File model.py sẽ chịu trách nhiệm huấn luyện, lưu trữ và tải mô hình.

import os
import joblib
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
import mlflow
import mlflow.sklearn

# Đảm bảo MLflow đã được cấu hình
import mlflow_setup  # noqa: F401

class ModelWrapper:
    def __init__(self, model_path: str = "model.pkl"):
        self.model_path = model_path
        self.model = None
        if os.path.exists(self.model_path):
            self.model = joblib.load(self.model_path)

    def train(self, data_path: str):
        df = pd.read_csv(data_path)
        X = df.drop(columns=["price"])
        y = df["price"]
        X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

        # Bắt đầu một run mới
        with mlflow.start_run():
            rf = RandomForestRegressor(n_estimators=200, max_depth=10, random_state=42)
            rf.fit(X_train, y_train)
            score = rf.score(X_test, y_test)

            # Log siêu tham số và metric
            mlflow.log_param("n_estimators", 200)
            mlflow.log_param("max_depth", 10)
            mlflow.log_metric("r2", score)

            # Lưu mô hình dưới dạng artefact
            mlflow.sklearn.log_model(rf, "model")
            joblib.dump(rf, self.model_path)
            self.model = rf
            return score

    def predict(self, payload: dict):
        if self.model is None:
            raise RuntimeError("Model chưa được tải hoặc huấn luyện.")
        df = pd.DataFrame([payload])
        return self.model.predict(df)[0]

Nhờ việc log model vào MLflow, chúng ta có thể truy cập UI để so sánh các phiên bản, thậm chí rollback nếu cần.

Viết unit test cho ModelWrapper

Kiểm thử tự động là một phần không thể thiếu trong CI. Dưới đây là một test đơn giản kiểm tra quá trình huấn luyện và dự đoán.

import os
import tempfile
import pandas as pd
from model import ModelWrapper


def test_train_and_predict(tmp_path):
    # Tạo dữ liệu mẫu
    data = {
        "feature1": [1, 2, 3, 4, 5, 6, 7, 8],
        "feature2": [2, 3, 4, 5, 6, 7, 8, 9],
        "price": [100, 150, 200, 250, 300, 350, 400, 450]
    }
    df = pd.DataFrame(data)
    csv_path = tmp_path / "housing.csv"
    df.to_csv(csv_path, index=False)

    wrapper = ModelWrapper(model_path=str(tmp_path / "model.pkl"))
    r2 = wrapper.train(str(csv_path))
    assert r2 > 0.9

    # Kiểm tra dự đoán
    pred = wrapper.predict({"feature1": 9, "feature2": 10})
    assert isinstance(pred, float)

Test này sẽ được chạy trong môi trường CI mỗi khi có pull request mới.

Thiết lập GitHub Actions cho CI/CD

Workflow tổng quan

File .github/workflows/ci-cd.yml sẽ bao gồm các job sau:

  1. build: Xây dựng Docker image và push lên GitHub Container Registry (GCR).
  2. test: Chạy unit test trong container.
  3. deploy: Khi merge vào main, triển khai container lên môi trường production (có thể là VPS hoặc Kubernetes).

Nội dung file ci-cd.yml

name: CI/CD Pipeline for ML Project

on:
  push:
    branches: ["main", "dev"]
  pull_request:
    branches: ["main", "dev"]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository_owner }}/ml-app:${{ github.sha }}

  test:
    needs: build
    runs-on: ubuntu-latest
    container:
      image: ghcr.io/${{ github.repository_owner }}/ml-app:${{ github.sha }}
    steps:
      - name: Run unit tests
        run: |
          pip install pytest
          pytest tests/

  deploy:
    needs: test
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to Production Server
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.PROD_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            docker pull ghcr.io/${{ github.repository_owner }}/ml-app:${{ github.sha }}
            docker stop ml-app || true
            docker rm ml-app || true
            docker run -d \
              --name ml-app \
              -p 8000:8000 \
              -e MLFLOW_TRACKING_URI=file:///app/mlflow \
              ghcr.io/${{ github.repository_owner }}/ml-app:${{ github.sha }}

Workflow trên sử dụng GitHub Container Registry làm kho lưu trữ image, giúp giảm thời gian pull trên server production.

Triển khai môi trường Docker Compose cho development

Đối với các thành viên trong team, việc chạy toàn bộ stack (API + MLflow UI) bằng docker-compose là cách nhanh nhất.

version: "3.8"
services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - MLFLOW_TRACKING_URI=file:///mlflow
    volumes:
      - ./mlflow:/mlflow
    depends_on:
      - mlflow

  mlflow:
    image: ghcr.io/mlflow/mlflow:2.9.2
    ports:
      - "5000:5000"
    environment:
      - BACKEND_STORE_URI=sqlite:///mlflow/mlflow.db
      - ARTIFACT_ROOT=/mlflow/artifacts
    volumes:
      - ./mlflow:/mlflow

Sau khi chạy docker-compose up -d, API sẽ lắng nghe tại http://localhost:8000 và UI của MLflow tại http://localhost:5000. Nhờ việc mount volume, mọi artefact và database được lưu trữ trên host, giúp dễ dàng backup.

Best practices và các lưu ý quan trọng

1. Quản lý secret một cách an toàn

Không bao giờ hard‑code các thông tin nhạy cảm (API key, DB password) trong Dockerfile hoặc code. Sử dụng GitHub Secrets cho CI và docker run -e hoặc .env cho local development.

2. Kiểm tra reproducibility của môi trường

Đảm bảo requirements.txt được tạo bằng lệnh pip freeze > requirements.txt sau mỗi lần cập nhật dependency. Khi có thay đổi, chạy lại docker build để xác nhận không có lỗi.

3. Versioning mô hình

MLflow tự động tạo version cho mỗi run, nhưng bạn nên đặt tag semantic (v1.0.0, v1.1.0…) trên Git và đồng bộ với mlflow.set_tag("git_commit", "${{ github.sha }}") để có thể truy ngược nguồn.

4. Kiểm thử integration

Ngoài unit test, nên viết integration test để kiểm tra API thực tế trả về dự đoán đúng. Ví dụ, sử dụng httpx trong một test container.

5. Giám sát và logging

Thêm cấu hình logging chuẩn (JSON) trong FastAPI và cấu hình Prometheus exporter nếu triển khai trên Kubernetes. Điều này giúp phát hiện sớm các vấn đề về latency hoặc lỗi mô hình.

Kết luận

Việc xây dựng một pipeline CI/CD cho dự án Machine Learning không chỉ giúp tăng tốc độ đưa mô hình vào production mà còn nâng cao tính ổn định, khả năng mở rộng và khả năng tái sử dụng. Bằng cách kết hợp Docker, GitHub Actions và MLflow, chúng ta có thể đạt được một quy trình tự động hoá toàn diện, từ việc huấn luyện, kiểm thử đến triển khai. Đối với những ai muốn nâng cao kỹ năng và áp dụng thực tiễn vào dự án thực tế, Tham khảo khóa học "Vibe Coding Masterclass với Antigravity & Stitch" tại đây.