#AI应用交付的“最后一公里”:Docker + K8s + GitLab CI 自动化部署ML流水线
从训练到上线,只需一次
git push
一、为什么ML部署这么难?
如果说训练模型是数据科学的“从0到1”,那么把模型部署到生产环境、稳定地跑起来并提供服务,就是“从1到100”的鸿沟。我见过太多团队在Jupyter Notebook里跑出了漂亮的准确率,却在部署阶段陷入泥潭:环境不一致、依赖冲突、版本管理混乱、回滚困难……
核心痛点在于:
- 环境一致性:本地训练用的Python 3.9 + scikit-learn 1.2.3,生产环境却可能是Python 3.11 + scikit-learn 1.3.0,模型加载直接报错
- 交付流程碎片化:手动打包模型文件 → scp上传服务器 → 手动重启服务 → 发现端口冲突
- 难以回滚:新模型上线后效果反而变差,想切回旧版本却发现找不到上一次的镜像
- 缺乏可观测性:线上模型出问题了,不知道是推理延迟飙升还是内存泄漏
解决思路很简单:把模型当作应用来交付。用Docker封装环境,用Kubernetes编排部署,用GitLab CI串联整个流程。三个工具配合,实现“代码提交即部署”的自动化流水线。正如《Machine Learning Platform Engineering》中所说,Docker、Kubernetes和CI/CD是ML平台的基础设施,它们虽然不是ML专用的,但却是规模化运行ML系统的必需品。
二、整体架构:一次git push之后发生了什么?
先看一张宏观图,理解各个组件如何协作:
┌─────────────────────────────────────────────────────────────────┐
│ 开发者 git push → GitLab触发Pipeline │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Stage 1: Build & Test │
│ ├── 运行单元测试 / 模型验证 │
│ ├── 构建Docker镜像(包含模型 + FastAPI服务) │
│ └── 推送到私有镜像仓库(如Docker Hub / Harbor) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Stage 2: Deploy to K8s (Test环境) │
│ ├── 使用Helm更新values.yaml中的镜像Tag │
│ └── helm upgrade --install 部署到测试集群 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Stage 3: 人工审批 (Production) │
│ └── 测试环境验证通过 → 手动触发生产发布 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Stage 4: Deploy to K8s (Production环境) │
│ └── 使用生产环境values部署,滚动更新保证零停机 │
└─────────────────────────────────────────────────────────────────┘
这套架构在业界有成熟的参考实践。一个典型的MLOps项目会使用GitLab CI完成代码构建、Docker镜像打包,再通过Kubernetes YAML清单或Helm Chart完成自动化部署。整个过程的核心原则是:每次部署都基于同一个镜像,只是不同环境使用不同的配置。
三、Docker:给AI模型装上“移动集装箱”
Docker解决的是环境一致性问题。把模型文件、依赖库、服务代码全部打包进镜像,在任何有Docker的机器上都能跑出相同结果。
3.1 项目目录结构
ml-model-deploy/
├── model/
│ └── iris_model.pkl # 训练好的模型文件
├── app/
│ ├── __init__.py
│ └── main.py # FastAPI服务入口
├── requirements.txt # Python依赖
├── Dockerfile # 镜像构建文件
├── kubernetes/
│ ├── deployment.yaml # K8s Deployment
│ ├── service.yaml # K8s Service
│ └── ingress.yaml # K8s Ingress(可选)
├── helm-chart/ # Helm Chart(更优雅的方式)
│ ├── Chart.yaml
│ ├── values.yaml
│ └── templates/
│ ├── deployment.yaml
│ └── service.yaml
└── .gitlab-ci.yml # GitLab CI流水线定义
3.2 FastAPI模型服务代码
# app/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import joblib
import numpy as np
import os
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(title="Iris Classifier API", version="1.0.0")
# 加载模型(在容器启动时完成)
MODEL_PATH = os.getenv("MODEL_PATH", "/app/model/iris_model.pkl")
try:
model = joblib.load(MODEL_PATH)
logger.info(f"Model loaded successfully from {MODEL_PATH}")
except Exception as e:
logger.error(f"Failed to load model: {e}")
model = None
class IrisFeatures(BaseModel):
sepal_length: float
sepal_width: float
petal_length: float
petal_width: float
class PredictionResponse(BaseModel):
prediction: int
probabilities: list
model_version: str
@app.get("/health")
async def health_check():
"""K8s存活探针和就绪探针"""
return {"status": "healthy", "model_loaded": model is not None}
@app.post("/predict", response_model=PredictionResponse)
async def predict(features: IrisFeatures):
if model is None:
raise HTTPException(status_code=503, detail="Model not loaded")
data = np.array([[features.sepal_length, features.sepal_width,
features.petal_length, features.petal_width]])
try:
pred = model.predict(data)[0]
proba = model.predict_proba(data)[0].tolist()
return PredictionResponse(
prediction=int(pred),
probabilities=proba,
model_version=os.getenv("MODEL_VERSION", "v1")
)
except Exception as e:
logger.error(f"Prediction failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
3.3 Dockerfile:分阶段构建,缩小镜像体积
# Dockerfile
FROM python:3.11-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt -t /deps
FROM python:3.11-slim
WORKDIR /app
# 从builder阶段复制依赖
COPY --from=builder /deps /usr/local/lib/python3.11/site-packages
# 复制应用代码和模型
COPY app/ ./app/
COPY model/ ./model/
# 创建非root用户运行服务,安全加固
RUN useradd -m -u 1000 mluser && chown -R mluser:mluser /app
USER mluser
ENV MODEL_PATH=/app/model/iris_model.pkl
ENV MODEL_VERSION=v1
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
requirements.txt示例:
fastapi==0.104.1
uvicorn[standard]==0.24.0
joblib==1.3.2
numpy==1.26.2
scikit-learn==1.3.2
pydantic==2.5.0
3.4 本地测试Docker镜像
# 构建镜像
docker build -t my-registry/iris-classifier:v1.0 .
# 运行容器
docker run -d -p 8000:8000 --name iris-test my-registry/iris-classifier:v1.0
# 测试健康检查
curl http://localhost:8000/health
# 测试预测接口
curl -X POST http://localhost:8000/predict \
-H "Content-Type: application/json" \
-d '{"sepal_length":5.1,"sepal_width":3.5,"petal_length":1.4,"petal_width":0.2}'
四、Kubernetes:让模型服务永不掉线
Docker解决了“跑起来”的问题,Kubernetes解决的是“高可用地跑”的问题:自动重启、滚动更新、水平扩缩容、服务发现。
4.1 Deployment清单(定义Pod模板)
# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: iris-classifier
namespace: ml-prod
labels:
app: iris-classifier
spec:
replicas: 3 # 保持3个副本运行
strategy:
type: RollingUpdate # 滚动更新,零停机
rollingUpdate:
maxSurge: 1 # 更新时最多新增1个Pod
maxUnavailable: 0 # 更新时不允许Pod不可用
selector:
matchLabels:
app: iris-classifier
template:
metadata:
labels:
app: iris-classifier
annotations:
prometheus.io/scrape: "true"
prometheus.io/path: "/metrics"
prometheus.io/port: "8000"
spec:
containers:
- name: classifier
image: my-registry/iris-classifier:v1.0 # 会被CI动态替换
ports:
- containerPort: 8000
env:
- name: MODEL_VERSION
value: "v1.0"
- name: LOG_LEVEL
value: "INFO"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe: # 存活探针:容器挂了就重启
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe: # 就绪探针:流量只发给就绪的Pod
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
4.2 Service清单(暴露服务)
# kubernetes/service.yaml
apiVersion: v1
kind: Service
metadata:
name: iris-classifier-svc
namespace: ml-prod
spec:
selector:
app: iris-classifier
ports:
- port: 80
targetPort: 8000
type: ClusterIP # 集群内部访问,Ingress对外暴露
4.3 用Helm统一管理配置
直接维护YAML文件的问题是:测试环境和生产环境用不同的镜像Tag、不同的副本数,需要反复改文件。Helm Chart通过模板化解决这个问题。
helm-chart/values.yaml:
# 测试环境 values-test.yaml
replicaCount: 2
image:
repository: my-registry/iris-classifier
tag: v1.0
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
# 生产环境 values-prod.yaml
replicaCount: 5
image:
repository: my-registry/iris-classifier
tag: v1.0
pullPolicy: Always
service:
type: ClusterIP
port: 80
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
部署命令:
# 部署到测试环境
helm upgrade --install iris-classifier ./helm-chart \
-f helm-chart/values-test.yaml \
--set image.tag=v1.0
# 部署到生产环境
helm upgrade --install iris-classifier ./helm-chart \
-f helm-chart/values-prod.yaml \
--set image.tag=v1.0
五、GitLab CI:串起完整交付流水线
最后一块拼图是自动化。每次代码提交,GitLab CI自动完成:测试→构建镜像→推送到仓库→更新Kubernetes部署。
5.1 配置GitLab CI变量
在GitLab项目 Settings → CI/CD → Variables中配置:
| 变量名 | 用途 | 示例值 |
|---|---|---|
DOCKER_REGISTRY |
镜像仓库地址 | docker.io/myusername |
DOCKER_USERNAME |
仓库用户名 | myusername |
DOCKER_TOKEN |
仓库访问Token | dckr_pat_xxxxx |
K8S_NAMESPACE |
K8s命名空间 | ml-prod |
5.2 .gitlab-ci.yml完整定义
# .gitlab-ci.yml
stages:
- test
- build
- deploy-test
- deploy-prod
variables:
IMAGE_NAME: ${DOCKER_REGISTRY}/iris-classifier
IMAGE_TAG: ${CI_COMMIT_SHORT_SHA} # 用commit hash作为镜像Tag
# 缓存pip依赖,加速构建
cache:
paths:
- .venv/
# ============ Stage 1: 测试 ============
test:
stage: test
image: python:3.11-slim
script:
- pip install -r requirements.txt
- pip install pytest pytest-cov
- pytest tests/ --cov=app --cov-report=term
artifacts:
paths:
- .coverage
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
only:
- merge_requests
- main
# ============ Stage 2: 构建镜像 ============
build:
stage: build
image: docker:latest
services:
- docker:dind # Docker-in-Docker,用于构建镜像
before_script:
- docker login -u ${DOCKER_USERNAME} -p ${DOCKER_TOKEN} ${DOCKER_REGISTRY}
script:
# 构建并推送镜像
- docker build -t ${IMAGE_NAME}:${IMAGE_TAG} .
- docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest
- docker push ${IMAGE_NAME}:${IMAGE_TAG}
- docker push ${IMAGE_NAME}:latest
after_script:
- docker logout
only:
- main # 只有main分支才构建镜像
# ============ Stage 3: 部署到测试环境 ============
deploy-test:
stage: deploy-test
image: bitnami/kubectl:latest
before_script:
# 配置kubectl访问测试集群(通过GitLab变量传入kubeconfig)
- mkdir -p $HOME/.kube
- echo "$KUBECONFIG_TEST" | base64 -d > $HOME/.kube/config
script:
# 使用helm升级部署
- helm upgrade --install iris-classifier ./helm-chart/
--namespace ml-test
--set image.tag=${IMAGE_TAG}
--set replicaCount=2
-f ./helm-chart/values-test.yaml
# 等待部署完成
- kubectl -n ml-test rollout status deployment/iris-classifier --timeout=120s
environment:
name: test
url: https://test-iris.example.com
only:
- main
when: manual # 手动触发部署测试环境(可根据需求改为自动)
# ============ Stage 4: 部署到生产环境(需审批) ============
deploy-prod:
stage: deploy-prod
image: bitnami/kubectl:latest
before_script:
- mkdir -p $HOME/.kube
- echo "$KUBECONFIG_PROD" | base64 -d > $HOME/.kube/config
script:
- helm upgrade --install iris-classifier ./helm-chart/
--namespace ml-prod
--set image.tag=${IMAGE_TAG}
--set replicaCount=5
-f ./helm-chart/values-prod.yaml
- kubectl -n ml-prod rollout status deployment/iris-classifier --timeout=120s
environment:
name: production
url: https://iris.example.com
only:
- main
when: manual # 生产环境发布必须人工确认
5.3 流水线执行效果
当你完成上述配置后,每次git push到main分支:
- 测试阶段自动运行,如果测试失败,流水线提前终止,防止坏代码进入后续阶段
- 构建阶段用Git commit SHA给镜像打Tag(如
iris-classifier:a1b2c3d),保证每次提交对应唯一镜像 - 测试部署阶段将新镜像部署到测试环境,自动执行健康检查
- 生产部署需要手动点击"play"按钮触发,降低误操作风险
实际生产中,许多团队还会在构建阶段加入镜像安全扫描(如Trivy)和代码质量检查(如SonarQube)等环节,进一步提升交付质量。
六、进阶:从单模型到大规模模型管理
当模型数量从1个增长到100个时,这套模式仍然适用,但需要补充一些能力:
1. 模型版本管理:将训练好的模型文件存储在对象存储(如S3/MinIO)中,路径包含版本号(如s3://models/iris/v20260118/)。Kubeflow Pipeline在训练完成后自动将模型注册到Model Registry并打上版本标签。
2. 配置驱动的模型定义:用JSON/YAML文件描述每个模型的训练数据来源、流水线版本、超参数等信息,CI系统读取这些配置后触发对应的训练和部署任务。
3. GitOps持续同步:使用Argo CD这样的工具,它会持续监控Git仓库中部署配置的变化,自动将集群状态同步到Git中声明的状态。当你的Helm Chart配置(如镜像Tag)在Git中被更新时,Argo CD会在几分钟内完成实际的K8s部署,实现“Git是唯一真实来源”。
4. 模型监控与自动重训:部署Prometheus收集推理延迟、请求量等指标,配合Grafana构建监控大盘。当模型精度或数据分布发生漂移时,触发自动重训流水线。
七、写在最后
从“手动复制模型文件”到“一次git push全自动交付”,转变的核心不在于多写了一堆YAML文件,而在于将模型当作有生命周期的软件产物来对待。Docker解决了环境一致性的物理基础,Kubernetes提供了稳定运行的空间,CI/CD串联了从代码到服务的完整通道。
三个关键经验:
- 镜像即部署单元:每次构建产生一个不可变的镜像,Tag用commit hash而非
latest,保证可追溯 - 配置与代码分离:Helm values区分环境,同一镜像在不同环境使用不同配置
- 人工闸门保护生产:测试环境自动部署,生产环境需人工审批,多一道防线
这套实践我已经在多个AI项目中验证,无论你用的是scikit-learn、PyTorch还是TensorFlow,流程都是通用的。开始动手吧——从一个简单的模型服务开始,跑通整条流水线,再做扩展。
更多推荐




所有评论(0)