VLLMService Operator 开发第五篇:部署 Operator 并验证模型服务

前言

前面几篇文章已经完成了 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 时应该使用哪个 apiVersionkind

复制代码
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

其中 apiVersiongroup + version 组成,也就是 aiinfra.example.com/v1alpha1kind 则来自 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 状态,并且 READY1/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 放进去。如果把 teamruntimemodel 这类用户自定义 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 状态,并且 READY1/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,为模型服务提供稳定的集群内访问入口。

本人水平有限,欢迎各位大佬批评指正。