前言
前面几篇文章已经完成了 VLLMService Operator 的基础开发:先定义 VLLMService 这个自定义资源,再编写 Reconcile 逻辑,让 Operator 能够根据用户声明的 VLLMService 自动创建 Deployment。到这里,代码层面的逻辑已经基本打通,但 Operator 最终还是要放到 Kubernetes 集群里运行,所以这一篇开始进入真正的部署验证阶段。
本文的目标很明确:把前面写好的 VLLMService Operator 部署到 Kubernetes 集群中,然后创建一个 VLLMService 自定义资源,观察 Operator 是否能够自动创建 Deployment 和 Pod,最后通过 kubectl port-forward 访问 vLLM 的 OpenAI-compatible API,确认模型服务是否真正可用。
本文验证的链路如下:
VLLMService 自定义资源
-> VLLMService Operator
-> Deployment
-> Pod
-> vLLM OpenAI-compatible API
需要注意的是,本文只验证 Deployment 和 Pod 这一层,暂时不会增加 Service 和 HTTPRoute。Service 会放到第六篇继续实现,HTTPRoute 会放到后面的文章中再扩展。
一、当前实验环境
当前实验环境是一套单节点 Kubernetes 集群,节点名称为:
master-01
模型文件已经提前放在宿主机目录:
/data/models/Qwen2.5-1.5B-Instruct
本文使用的模型、镜像和端口如下:
模型名称:Qwen2.5-1.5B-Instruct
模型路径:/data/models/Qwen2.5-1.5B-Instruct
推理镜像:docker.m.daocloud.io/vllm/vllm-openai:latest
容器端口:8000
当前 Operator 项目路径为:
/root/projects/vllmservice-operator
在开始部署之前,项目里已经完成了以下内容:
1. 定义 VLLMService API 类型;
2. 生成 VLLMService CRD;
3. 编写 VLLMServiceReconciler;
4. 在 Reconcile 中创建或更新 Deployment;
5. 为 Deployment 设置 OwnerReference;
6. 在 PodTemplate 中配置镜像、模型路径、资源、PVC 挂载、schedulerName 和 nodeSelector。
二、构建并推送 Operator 镜像
修改完 controller.go 之后,先在项目目录下执行一次编译检查:
cd /root/projects/vllmservice-operator
make build
make build 的作用是本地编译 Operator 代码。如果 controller 代码中存在字段写错、类型不匹配、包未导入、变量未使用等问题,这一步就会直接报错。编译通过后,再构建 Operator 镜像:
make docker-build IMG=registry.cn-hangzhou.aliyuncs.com/docker-test-dai/vllmservice-operator:v0.1
这里构建的是 Operator 镜像,不是 vLLM 推理服务镜像。这个镜像里运行的是 Controller Manager,它的职责是监听 VLLMService 资源变化,并根据 VLLMService 的 spec 自动创建和维护 Deployment。构建完成后,将镜像推送到镜像仓库:
docker push registry.cn-hangzhou.aliyuncs.com/docker-test-dai/vllmservice-operator:v0.1
三、安装 VLLMService CRD
Operator 要监听 VLLMService 资源,前提是 Kubernetes API Server 已经认识这个自定义资源,所以需要先安装 CRD:
make install
安装完成后,查看 CRD 是否存在:
kubectl get crd | grep vllm
示例输出如下:
vllmservices.aiinfra.example.com 2026-06-23T08:28:16Z
看到 vllmservices.aiinfra.example.com 之后,说明 VLLMService 这个 CRD 已经成功安装到集群中。接着查看 CRD 的关键信息,确认后面写 CR YAML 时应该使用哪个 apiVersion 和 kind:
kubectl get crd vllmservices.aiinfra.example.com -o yaml
重点看下面几项:
spec:
group: aiinfra.example.com
names:
kind: VLLMService
versions:
- name: v1alpha1
served: true
storage: true
这里可以确定后续创建 VLLMService 时应该这样写:
apiVersion: aiinfra.example.com/v1alpha1
kind: VLLMService
其中 apiVersion 由 group + version 组成,也就是 aiinfra.example.com/v1alpha1;kind 则来自 CRD 中定义的 VLLMService。如果这两个字段写错,Kubernetes 就无法识别我们创建的自定义资源。
四、部署 Operator
CRD 安装完成后,就可以把 Operator 部署到 Kubernetes 集群中:
make deploy IMG=registry.cn-hangzhou.aliyuncs.com/docker-test-dai/vllmservice-operator:v0.1
这个命令会使用 config/default 下的 Kustomize 配置,把 Controller Manager、RBAC、ServiceAccount 等资源部署到集群中。部署完成后,先查看 Operator 命名空间是否创建成功:
kubectl get ns | grep vllm
示例输出:
vllmservice-operator-system Active 5m17s
然后查看 Operator Pod 是否正常运行:
kubectl -n vllmservice-operator-system get pod
示例输出:
NAME READY STATUS RESTARTS AGE
vllmservice-operator-controller-manager-7d9d9f488b-4hq5b 1/1 Running 0 5m19s
只要 Controller Manager Pod 处于 Running 状态,并且 READY 是 1/1,就说明 Operator 已经启动。如果 Pod 没有正常运行,可以先查看日志:
kubectl -n vllmservice-operator-system logs deploy/vllmservice-operator-controller-manager
五、准备模型存储
本文使用的是 hostPath 静态 PV,因为模型文件已经提前放在 master-01 节点的 /data/models 目录下。先创建测试命名空间:
kubectl create namespace ai-demo
然后编写 PV 和 PVC:
vim qwen-hostpath-pv-pvc.yaml
内容如下:
apiVersion: v1
kind: PersistentVolume
metadata:
name: qwen-model-pv
spec:
capacity:
storage: 50Gi
accessModes:
- ReadWriteOnce
storageClassName: manual
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /data/models
type: Directory
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- master-01
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: qwen-model-pvc
namespace: ai-demo
spec:
accessModes:
- ReadWriteOnce
storageClassName: manual
volumeName: qwen-model-pv
resources:
requests:
storage: 20Gi
应用这个 YAML后,查看 PV 和 PVC 状态:
kubectl get pv qwen-model-pv
kubectl -n ai-demo get pvc qwen-model-pvc
正常情况下,PVC 应该处于 Bound 状态。
六、创建 VLLMService 自定义资源
存储准备完成后,就可以创建 VLLMService 资源了。编写 YAML 文件:
vim qwen-vllmservice.yaml
内容如下:
apiVersion: aiinfra.example.com/v1alpha1
kind: VLLMService
metadata:
name: qwen-demo
namespace: ai-demo
spec:
image: docker.m.daocloud.io/vllm/vllm-openai:latest
modelPath: /data/models/Qwen2.5-1.5B-Instruct
modelName: qwen2.5-1.5b-instruct
replicas: 1
schedulerName: volcano
nodeSelector:
kubernetes.io/hostname: master-01
labels:
aiinfra.example.com/model: qwen2.5
aiinfra.example.com/runtime: vllm
aiinfra.example.com/team: infra
aiinfra.example.com/scheduler: volcano
port: 8000
resources:
requests:
cpu: "2"
memory: 8Gi
volcano.sh/vgpu-number: "1"
volcano.sh/vgpu-memory: "6144"
volcano.sh/vgpu-cores: "50"
limits:
cpu: "4"
memory: 16Gi
volcano.sh/vgpu-number: "1"
volcano.sh/vgpu-memory: "6144"
volcano.sh/vgpu-cores: "50"
storage:
pvcName: qwen-model-pvc
mountPath: /data/models
readOnly: true
应用这个yaml后,查看 VLLMService 是否创建成功:
kubectl -n ai-demo get vllmservice
这里需要明确一点:VLLMService 本身只是用户声明的期望状态,它并不会直接运行模型。真正运行模型的是 Operator 根据这个 VLLMService 自动创建出来的 Deployment 和 Pod。也就是说,用户提交的是一个更高层的抽象资源,Operator 负责把这个抽象资源转换成 Kubernetes 里真正能运行的工作负载。
七、验证 Deployment 和Pod是否自动创建
创建 VLLMService 后,Operator 的 Reconcile 逻辑会被触发。此时可以查看 Deployment:
kubectl -n ai-demo get deploy
正常情况下应该能看到类似输出:
NAME READY UP-TO-DATE AVAILABLE AGE
qwen-demo 1/1 1 1 6m
继续查看 Deployment 的详细信息:
kubectl -n ai-demo describe deploy qwen-demo
这里重点关注三类信息。
第一,Deployment 的 labels 是否符合预期。Deployment 本身应该带有 Operator 固定设置的 labels,也可以带有用户在 VLLMService 里声明的自定义 labels,例如:
app.kubernetes.io/name=vllmservice
app.kubernetes.io/instance=qwen-demo
app.kubernetes.io/managed-by=vllmservice-operator
aiinfra.example.com/model=qwen2.5
aiinfra.example.com/runtime=vllm
aiinfra.example.com/team=infra
aiinfra.example.com/scheduler=volcano
第二,Deployment 的 selector 是否稳定。推荐 selector 只使用下面两个固定标签:
app.kubernetes.io/name=vllmservice
app.kubernetes.io/instance=qwen-demo
Deployment 的 selector 创建后不能随便修改,所以不要把用户可能会变更的自定义 labels 放进去。如果把 team、runtime、model 这类用户自定义 labels 放到 selector 里,后续用户修改这些 labels 时,Operator 可能会尝试修改 Deployment selector,最终导致更新失败。
第三,Deployment 是否带有 OwnerReference。Operator 创建 Deployment 时应该把 VLLMService 设置成它的 owner,这样当用户删除 VLLMService 时,Kubernetes 的垃圾回收机制可以自动清理这个 Deployment。同时,如果在 SetupWithManager 里配置了 Owns(&appsv1.Deployment{}),当 Deployment 被人为修改时,也可以重新触发对应 VLLMService 的 Reconcile,从而体现 Operator 的自愈能力。
Deployment 创建完成后,继续查看 Pod 是否正常启动:
kubectl -n ai-demo get pod
示例输出:
NAME READY STATUS RESTARTS AGE
qwen-demo-78f5568f6b-rqghg 1/1 Running 0 6m57s
看到 Pod 处于 Running 状态,并且 READY 是 1/1,说明容器已经启动成功。接着可以查看 Pod 详细信息,确认调度、资源和存储挂载是否符合预期:
kubectl -n ai-demo describe pod qwen-demo-78f5568f6b-rqghg
重点检查以下内容:
1. Pod 是否调度到 master-01;
2. schedulerName 是否为 volcano;
3. nodeSelector 是否包含 kubernetes.io/hostname=master-01;
4. 容器镜像是否为 docker.m.daocloud.io/vllm/vllm-openai:latest;
5. 容器端口是否为 8000;
6. PVC 是否挂载到 /data/models;
7. requests 和 limits 中的 CPU、内存、vGPU 资源是否符合预期;
8. Events 中是否存在调度失败、挂载失败、镜像拉取失败等异常。
八、通过 port-forward 访问 vLLM API
目前第五篇还没有创建 Service,所以这里先直接对 Pod 做端口转发:
kubectl -n ai-demo port-forward pod/qwen-demo-78f5568f6b-rqghg 8888:8000
这条命令的含义是:把本机的 127.0.0.1:8888 转发到 Pod 的 8000 端口。vLLM 容器内部监听的是 8000 端口,所以本地访问 8888 就相当于访问 Pod 内的 vLLM 服务。
新开一个终端,访问 vLLM 的模型列表接口:
curl http://127.0.0.1:8888/v1/models
示例返回如下:
{
"object": "list",
"data": [
{
"id": "qwen2.5-1.5b-instruct",
"object": "model",
"created": 1782265769,
"owned_by": "vllm",
"root": "/data/models/Qwen2.5-1.5B-Instruct",
"parent": null,
"max_model_len": 4096,
"permission": [
{
"id": "modelperm-a6379dd7d50b344e",
"object": "model_permission",
"created": 1782265769,
"allow_create_engine": false,
"allow_sampling": true,
"allow_logprobs": true,
"allow_search_indices": false,
"allow_view": true,
"allow_fine_tuning": false,
"organization": "*",
"group": null,
"is_blocking": false
}
]
}
]
}
这里重点看三个字段:
id: qwen2.5-1.5b-instruct
owned_by: vllm
root: /data/models/Qwen2.5-1.5B-Instruct
这说明 vLLM 已经成功加载模型,并且 OpenAI-compatible API 可以正常访问。到这一步,VLLMService 到 Pod 再到 vLLM API 的链路已经打通。
九、这次验证到底证明了什么
通过这一轮部署和验证,可以确认当前 Operator 已经具备最基础的模型服务编排能力。具体来说,这次验证证明了以下几点:
1. VLLMService CRD 已经成功安装到 Kubernetes 集群;
2. Operator Controller Manager 能够正常启动;
3. 用户可以创建 VLLMService 自定义资源;
4. VLLMService 创建后能够触发 Reconcile;
5. Reconcile 能够根据 VLLMService 自动创建 Deployment;
6. Deployment 能够拉起模型服务 Pod;
7. Pod 能够挂载模型 PVC;
8. Pod 能够使用指定的 schedulerName 和 nodeSelector;
9. vLLM 容器能够正常启动并加载模型;
10. /v1/models 接口能够返回模型列表。
也就是说,本文打通的是下面这条链路:
用户声明 VLLMService
-> Operator 监听并调谐
-> 自动创建 Deployment
-> Deployment 创建 Pod
-> Pod 启动 vLLM
-> 通过 /v1/models 验证模型服务
这正是 Operator 的核心价值:用户不再直接编写底层 Deployment,而是声明一个更贴近业务语义的 VLLMService,由 Operator 负责把它转换成 Kubernetes 中真正运行的资源。
十、当前阶段的不足
虽然现在模型服务已经可以通过 port-forward 正常访问,但这种方式并不适合作为最终访问方式,因为当前访问依赖具体的 Pod 名称:
qwen-demo-78f5568f6b-rqghg
Pod 名称不是稳定入口。只要 Deployment 发生滚动更新、Pod 重建或者节点异常,Pod 名称和 Pod IP 都可能变化。因此,直接访问 Pod 只能用于开发和验证,不适合真正的服务暴露。
更合理的访问链路应该是:
VLLMService
-> Deployment
-> Pod
-> Service
Service 可以为一组 Pod 提供稳定的集群内访问入口。后续如果要接入 Gateway API,HTTPRoute 的后端也应该指向 Service,而不是直接指向 Pod。
本文只完成第一阶段验证,下一篇会继续给 VLLMService Operator 增加 Service 自动创建能力。
十一、本文总结
本文完成了 VLLMService Operator 的部署验证,整个过程包括:
1. 构建并推送 Operator 镜像;
2. 安装 VLLMService CRD;
3. 部署 VLLMService Operator;
4. 创建模型存储 PV 和 PVC;
5. 创建 VLLMService 自定义资源;
6. 验证 Operator 自动创建 Deployment;
7. 验证模型 Pod 正常运行;
8. 通过 port-forward 访问 vLLM 的 /v1/models 接口;
9. 确认模型服务能够正常返回。
到这里,VLLMService Operator 已经能够完成最基础的声明式编排:用户只需要提交一个 VLLMService,Operator 就可以自动创建 Deployment,并拉起 vLLM 模型服务 Pod。
不过这还只是第一步。当前访问方式仍然依赖 Pod port-forward,不够稳定,也不适合对外暴露。下一篇文章会继续扩展 Operator,让它在创建 Deployment 的同时自动创建 Service,为模型服务提供稳定的集群内访问入口。
本人水平有限,欢迎各位大佬批评指正。