前言
刚好!最近公司业务需求,需要搭建一套 CVAT (Computer Vision Annotation Tool) 计算机视觉标注系统,开源标注工具。
虽然官方提供了 Helm Chart,但在面对内网环境、老版本 K8s (v1.21)、Ceph 存储以及特定节点调度等现实需求时,官方文档过于 "简洁" 了。
本文完整记录了从 Chart 下载到最终成功登录的全过程,重点分析了部署过程中遇到的 OPA 服务连接拒绝 、浏览器安全策略报错 等"深坑"及其解决方案。
- 官方 GitHub : cvat-ai/cvat
- Helm 版本: v3.19
- CVAT Chart 版本: 2.56.0
- Kubernetes 版本: 1.21.10 (注:官方要求 >=1.23,本文包含强行兼容方案)
一、 背景与硬性需求
在开始之前,明确一下我们的部署环境限制,这也是后续修改配置的依据:
- 节点调度限制:
- CVAT 必须运行在名为
gpusrv14的特定节点上。 - 难点 :该节点被打上了污点(Taint)
dedicated=ceph:NoSchedule,普通 Pod 无法调度上去。我们需要配置容忍度(Tolerations)。
- 持久化存储:
- 必须对接公司现有的 Ceph 集群(StorageClass:
rook-cephfs),且容量要求大。
- 内网环境:
- 服务器无法直接连接公网,依赖 HTTP Proxy。
二、 部署准备与 Chart 处理
1. 下载 Chart
首先拉取官方 Chart 包并解压:
bash
helm pull oci://registry-1.docker.io/cvat/cvat --version 2.55.0
tar -xzf cvat-2.55.0.tgz
cd cvat
2. 解决 K8s 版本兼容性问题(K8s 1.21 版本)
官方 Chart 的 Chart.yaml 里硬性规定了 kubeVersion: '>= 1.23.0-0'。如果你像我一样还在用 K8s 1.21,直接 install 安装会报错。
解决方案 :
修改 Chart.yaml,强行降低版本要求。虽然有风险,但实测 CVAT 2.56 在 1.21 上核心 API 依然兼容。
yaml
# Chart.yaml
description: A Helm chart for Kubernetes
kubeVersion: '>= 1.21.0-0' # 修改此处
3. 初始化配置文件
为了保持原文件整洁,我们复制一份配置进行覆盖:
bash
cp values.yaml values.override2.yaml
# 或(一样)
helm show values . > values.override2.yaml
三、 核心配置修改(values.yaml)
1. 存储配置:对接 Ceph
默认的 PVC 只有 20Gi 且不指定 StorageClass。我们需要修改 cvat 和 kvrocks (Redis) 两处的存储配置。
修改 values.override2.yaml:
yaml
defaultStorage:
enabled: true
storageClassName: rook-cephfs # 重点:指定你的 Ceph SC 名称
accessModes:
- ReadWriteMany # 建议多读写模式,方便扩展
size: 1000Gi # 生产环境,给大点
2. 调度配置:亲和性与污点容忍(重点)
这是最复杂的一步。我们要把 Pod 钉死在 gpusrv14 节点上,并且要能"忍受"它的污点。
步骤 A:给节点打标签
bash
kubectl label node gpusrv14.aa.com -n gpu cvat-node=true
步骤 B:检查节点污点
bash
kubectl describe node/gpusrv14.aa.com -n gpu | grep Taints
# 输出: dedicated=ceph:NoSchedule
# 这意味着如果没有对应的 toleration,Pod 无法调度上来。
步骤 C:修改 Helm 配置
找到 cvat.backend 部分。CVAT Chart 设计比较好的一点是,backend 下的配置会被 worker 等子组件继承,所以不需要每个微服务都改一遍。
yaml
cvat:
backend:
# 1. 强行指定节点(双重保险)
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- gpusrv14.aa.com
# 2. 简单的节点选择器
nodeSelector:
cvat-node: true
# 3. 污点容忍(必须配置,否则 Pending)
tolerations:
- key: "group" # 多余的
operator: "Equal"
value: "gpu"
effect: "NoSchedule"
- key: "dedicated" # 对应 dedicated=ceph:NoSchedule
operator: "Equal"
value: "ceph"
effect: "NoSchedule"
四、 启动安装与踩坑实录
执行安装命令:
bash
helm install gpu-cvat . -n gpu-cvat --create-namespace -f values.yaml -f values.override2.yaml
安装命令下去后,并没有想象中的"一键成功",而是迎来了两个深坑。
坑一:OPA 服务无限重启 (Connection refused)
现象 :
gpu-cvat-opa Pod 状态异常,查看日志发现健康检查失败:
requests.exceptions.ConnectionError: HTTPConnectionPool(host='opa', port=8181): ... Connection refused
原因分析 :
OPA (Open Policy Agent) 是 CVAT 的权限策略引擎。
- 默认情况下,OPA 启动时只监听
localhost(127.0.0.1)。 - K8s 的 Liveness Probe(探针)是通过 Pod IP 访问的,属于"外部"访问。
- 因为监听地址限制,探针连接被拒绝,K8s 判定服务不健康,导致无限重启。
解决方案(临时暴力有效) :
尝试通过 Helm --set 传参失败(可能是 Chart 里的参数映射有问题),直接修改 Deployment 最快。
bash
kubectl edit deployment gpu-cvat-opa -n gpu-cvat
找到 args 部分,手动添加 --addr=0.0.0.0:8181,强制监听所有网络接口:
yaml
containers:
- args:
- run
- --server
- --addr=0.0.0.0:8181 # 【关键修改】让它监听所有IP
- --bundle
修改保存后,Pod 重启,状态瞬间变为 Running。
其中,尝试过 upgrade 这些服务,但不生效:
bash
helm upgrade ... --set cvat.opa.extraArgs="{--addr=0.0.0.0:8181}" --reuse-values
可能某些版本的 CVAT Helm Chart 中,extraArgs 的传递方式或者 OPA 的默认启动命令(Entrypoint)优先级更高,导致修改根本没有生效。
永久解决方案(未验证) :
手动编辑的配置会在下一次 helm upgrade 时丢失。为了让配置持久化,需要将参数写入 values.override2.yaml。
在配置文件中找到 cvat.opa 部分,加入 extraArgs:
yaml
cvat:
opa:
# 强制让 OPA 监听所有接口,解决健康检查不通的问题
extraArgs:
- "--addr=0.0.0.0:8181"
这样下次执行 Helm 更新时,这个参数就会自动带上,不用再手动改 Deployment 了。
坑二:浏览器白屏与 405 报错 (HTTP vs HTTPS)
现象 :
配置好 Nginx HTTP 转发后,访问页面出现白屏或报错(F12):
- API 请求报
405 Not Allowed或401 Unauthorized。 - 控制台报错:
The Cross-Origin-Opener-Policy header has been ignored... - JS 报错:
ReferenceError: structuredClone is not defined。
原因分析 :
这是现代浏览器的安全机制。CVAT 是一个重型前端应用,使用了 SharedArrayBuffer 等特性来实现多线程处理。
浏览器规定:这些高级特性必须在"安全上下文" (Secure Context) 下才能运行。
所谓安全上下文,要么是 localhost,要么必须是 HTTPS。如果是 HTTP,这些 API 直接被禁用,导致代码崩盘。
解决方案(配置 Nginx SSL + 安全头) :
我们必须配置 HTTPS,并且在 Nginx 中显式添加 COOP/COEP 响应头。
Nginx 配置参考:
nginx
server {
listen 443 ssl;
server_name gpu-cvat.aa.com; # 统一使用一个域名
ssl_certificate /etc/nginx/conf.d/sans_crt2/server.crt;
ssl_certificate_key /etc/nginx/conf.d/sans_crt2/server.key;
# 1. 后端 API 转发
location ~ ^/(api|auth|admin|django-static)/ {
proxy_pass http://gpu-cvat-backend-service.gpu-cvat.svc.cluster.local:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 2. 前端页面转发
location / {
proxy_pass http://gpu-cvat-frontend-service.gpu-cvat.svc.cluster.local:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 必须加上这两个 Header,否则 CVAT 内部 JS 会报错
add_header Cross-Origin-Opener-Policy 'same-origin';
add_header Cross-Origin-Embedder-Policy 'require-corp';
}
}
关于 Nginx 转发规则,虽然官方文档未明说后端路径,但通过 F12 开发者工具观察网络请求,并参考 Chart 源码中 templates/ingress.yaml 的分流定义,可以明确 /api、/auth、/admin 及 /django-static 这类涉及到数据交互和 Django 后台的请求必须精准转发至后端 Service,而其余流量则指向前端。
五、 创建管理员
部署成功且页面能打开后,发现------没有账号登录 。
CVAT 默认不创建任何账号,需要手动进入容器执行 Django 命令来创建超级管理员(教程下一步)。
一键创建命令:
bash
# 你部署 CVAT 的命名空间
export HELM_RELEASE_NAMESPACE="gpu-cvat"
# 你给这个 Helm 实例起的名称
export HELM_RELEASE_NAME="gpu-cvat"
BACKEND_POD_NAME=$(kubectl get pod --namespace $HELM_RELEASE_NAMESPACE -l tier=backend,app.kubernetes.io/instance=$HELM_RELEASE_NAME,component=server -o jsonpath='{.items[0].metadata.name}')
kubectl exec -it --namespace $HELM_RELEASE_NAMESPACE $BACKEND_POD_NAME -c cvat-backend -- python manage.py createsuperuser
按照提示输入用户名、邮箱和密码即可。
创建好后,就可以使用这个账号登录系统。
六、 存储验证
前面在 values.yaml 中配置了 storageClassName: rook-cephfs 后,并且已经部署完成了,现在验证一下是不是用了 ceph 的存储。
1. 查看集群内 PVC 状态
通过 kubectl get pv 命令可以看到,CVAT 的后端数据(backend-data)和 kvrocks 已经成功绑定了我们预设的 1000Gi 大容量存储,且状态均为 Bound。
bash
# 查看 CVAT 相关的持久化卷
kubectl get pv | grep gpu-cvat
注意 :可以看到
gpu-cvat-backend-data的模式是 RWX (ReadWriteMany),这对于多副本部署至关重要。
2. 追踪底层的 Ceph Subvolume
为了确认数据确实落在了 CephFS 上,我们可以随机挑选一个 PV 进行 describe,查看它在 Ceph 内部的 subvolumePath:
bash
kubectl describe pv/pvc-ec561f53-c54b-47d7-9ddb-6e01850046df | grep subvolumePath
# 输出结果:
# subvolumePath=/volumes/csi/csi-vol-cee516.../de776093...
3. Ceph 后台验证
最后,登录 Ceph Dashboard 或直接在 Ceph 节点查看文件系统,可以看到该路径已经自动创建并开始记录数据。
总结
这次 Helm 部署过程暴露了 CVAT Chart 的几个痛点:
- 微服务拆分过细 :虽然架构先进,但运维成本高。如果不是
backend配置支持继承,单独配置每个 Pod 的调度策略会非常痛苦。 - 默认配置不合理 :OPA 默认只监听
localhost导致健康检查失败,这是一个非常低级的默认配置坑,迫使我们必须手动介入。 - 文档缺失 :官方文档未强调 HTTPS 对于前端功能的强制性,导致很容易在 HTTP 环境下浪费时间排查 JS 报错。
- 内网友好度低 :内网环境下,Grafana 插件下载、依赖包拉取都需要自己在
values.yaml里注入 Proxy 环境变量。
感觉这个 Helm-Chart 做得不够完善,并且加上公司内网原因,导致没法一键部署。