feat: 完成 issue #8 ⑥ K8s/Helm 部署底座 + 昇腾适配层

This commit is contained in:
2026-08-04 22:04:18 +08:00
parent 691a812fcf
commit e620d4937e
20 changed files with 1013 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
apiVersion: v2
name: iaop
description: |
iAOP(云美工业AI优化平台)一键 Helm 部署底座(PRD 5.6「⑥ 部署底座」,
EPIC #8):内核(采集/数据总线/LLM 网关/RAG)+ 行业模板一键部署;
推理后端(NVIDIA 5090 GPU / 华为昇腾 NPU)可插拔,切换后端仅改 values。
type: application
version: 1.0.0
appVersion: "v1.0.0"
keywords:
- iAOP
- industrial-ai
- k8s
- helm
- inference
- ascend
+65
View File
@@ -0,0 +1,65 @@
# iAOP Helm Chart(一键部署底座)
对应 PRD 5.6「⑥ 部署底座」与 EPIC #8:
K8s/Helm/ArgoCD 部署底座;GPU/NPU 推理后端可插拔(5090 / 华为昇腾)。
**验收:一套 Chart 部署内核 + 模板;切换后端仅改 values,不动业务代码。**
## 目录结构
```
deploy/k8s/helm/iaop/
├── Chart.yaml Chart 元信息(name/version/appVersion)
├── values.yaml 全部配置点(镜像/后端/副本/资源/存储/灰度/域名)
├── templates/
│ ├── _helpers.tpl 标签/名称辅助模板
│ ├── backend-configmap.yaml 推理后端适配层配置(按 backend 分支渲染)
│ ├── deployment.yaml 推理服务 Deployment(nodeSelector 按后端分流)
│ ├── service.yaml ClusterIP 服务(:8000/v1)
│ ├── ingress.yaml 可选域名入口
│ ├── pvc.yaml 模型权重/日志持久化
│ └── NOTES.txt 部署后提示
└── README.md
```
## 一键部署
```bash
helm repo add iaop http://git.xayunmei.com/iaop/charts # 或本地路径
helm install iaop deploy/k8s/helm/iaop -n iaop --create-namespace
```
## 切换推理后端(仅改 values,业务代码零改动)
```bash
# 默认 gpu(NVIDIA 5090,vLLM/Triton):
helm install iaop deploy/k8s/helm/iaop -n iaop \
--set inference.backend=gpu \
--set inference.device=nvidia-5090
# 切换为华为昇腾 NPU(ACL/CANN/MindIE):
helm upgrade iaop deploy/k8s/helm/iaop -n iaop \
--set inference.backend=npu \
--set inference.device=ascend-910b \
--set inference.cannVersion=8.0
```
切换行为(由模板自动处理):
- `backend-configmap.yaml`:渲染对应后端适配层配置(npu 追加 `cann_version`);
- `deployment.yaml`:nodeSelector 自动切到 `ascend.com/npu=true`(GPU 为 `nvidia.com/gpu=true`),
并在 Pod 标签标注 `iaop.ai/inference-backend`;
- 后端实现差异全部收敛在 `core/inference-backend/` 适配层,业务代码零改动。
## 其他配置点(PRD 5.6)
| 配置点 | values 路径 | 说明 |
| --- | --- | --- |
| 资源配额 | `resources.requests/limits` | CPU/内存配额 |
| 推理后端选择 | `inference.backend` | `gpu` / `npu` |
| 灰度发布策略 | `rollingUpdate` | `maxUnavailable: 0, maxSurge: 1`(滚动) |
| 存储 | `storage.*` | 模型权重持久卷 |
| 域名入口 | `ingress.*` | 可选 Ingress |
## 说明
- Chart 未引入任何外部依赖(dependencies 为空),`helm template` 可离线渲染;
- 推理服务镜像对应 `core/inference-backend/` 适配层容器化(本文档发布配套镜像时更新 `image.tag`)。
+75
View File
@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
"""Helm Chart 结构 sanity 检查(无 helm CLI 环境下的离线基本验证)。
检查项:
1. Chart.yaml / values.yaml 均为合法 YAML;
2. templates/ 下所有模板文件存在且非空;
3. 模板中引用的 `.Values.xxx` 键均能在 values.yaml 中找到(防拼写错误);
4. 每个模板文件的 Go template 标记 `{{` / `}}` 数量配平。
用法:python _sanity_check.py
"""
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
TEMPLATES = os.path.join(HERE, "templates")
import yaml # noqa: E402
def load(path):
with open(path, "r", encoding="utf-8") as fh:
return yaml.safe_load(fh)
def values_paths(text):
"""提取模板文本中的 .Values.<a>.<b> 路径集合。"""
return set(re.findall(r"\.Values\.([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)", text))
def resolve(values, dotted):
node = values
for key in dotted.split("."):
if not isinstance(node, dict) or key not in node:
return False
node = node[key]
return True
def main():
failures = []
chart = load(os.path.join(HERE, "Chart.yaml"))
assert chart.get("apiVersion") == "v2", "Chart.yaml 应为 Helm v2 chart"
assert chart.get("name") == "iaop", "Chart.name 应为 iaop"
values = load(os.path.join(HERE, "values.yaml"))
assert values.get("inference", {}).get("backend") in ("gpu", "npu"), \
"values.inference.backend 应为 gpu|npu"
for name in sorted(os.listdir(TEMPLATES)):
path = os.path.join(TEMPLATES, name)
if not os.path.isfile(path):
continue
text = open(path, "r", encoding="utf-8").read()
if not text.strip():
failures.append(f"{name}: 模板文件为空")
continue
if text.count("{{") != text.count("}}"):
failures.append(f"{name}: Go template 标记 {{/}} 数量不配平")
for dotted in values_paths(text):
if not resolve(values, dotted):
failures.append(f"{name}: 引用了 values.yaml 中不存在的键 .Values.{dotted}")
if failures:
print("FAIL")
for f in failures:
print(" -", f)
sys.exit(1)
print(f"OK: Chart/values 合法,templates/{len(os.listdir(TEMPLATES))} 个模板校验通过")
if __name__ == "__main__":
main()
+13
View File
@@ -0,0 +1,13 @@
Thank you for installing {{ .Chart.Name }} {{ .Chart.Version }} (backend: {{ .Values.inference.backend }}).
iAOP 部署底座(PRD 5.6 / EPIC #8)已就绪,Release 名:{{ .Release.Name }}。
快速使用:
1. 查看部署状态:
kubectl get deploy,svc -l app.kubernetes.io/instance={{ .Release.Name }}
2. 健康巡检:
curl http://{{ include "iaop.fullname" . }}:{{ .Values.service.port }}/health
3. 切换推理后端(NVIDIA GPU ↔ 华为昇腾 NPU),仅改 values 后升级:
helm upgrade {{ .Release.Name }} . --set inference.backend=npu
4. 如需域名入口:
helm upgrade {{ .Release.Name }} . --set ingress.enabled=true --set ingress.host=iaop.example.com
@@ -0,0 +1,43 @@
{{/*
iAOP Helm Chart 辅助模板(_helpers.tpl)
*/}}
{{/*
展开 Chart 全名(release 名 + chart 名,截断到 63 字符)。
*/}}
{{- define "iaop.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Chart 名称标签。
*/}}
{{- define "iaop.name" -}}
{{- .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
通用标签。
*/}}
{{- define "iaop.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
app.kubernetes.io/name: {{ include "iaop.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{/*
选择器标签。
*/}}
{{- define "iaop.selectorLabels" -}}
app.kubernetes.io/name: {{ include "iaop.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{/*
推理后端镜像完整名(repository:tag)。
*/}}
{{- define "iaop.image" -}}
{{- printf "%s:%s" .Values.image.repository .Values.image.tag -}}
{{- end -}}
@@ -0,0 +1,36 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "iaop.fullname" . }}-backend
labels:
{{- include "iaop.labels" . | nindent 4 }}
data:
# 推理后端适配层配置(PRD 5.6:切换后端仅改 values.inference.backend)。
# 该配置与 core/inference-backend/config/backends.template.yaml 同构,
# 由推理服务容器在启动时读取。
backends.yaml: |
template: ti-cl4
version: 1.0.0
backend: {{ .Values.inference.backend }}
inference:
model: {{ .Values.inference.model | quote }}
endpoint: http://{{ include "iaop.fullname" . }}:{{ .Values.service.port }}/v1
timeout_seconds: {{ .Values.inference.timeoutSeconds }}
runtime: {{ .Values.inference.runtime | quote }}
device: {{ .Values.inference.device | quote }}
max_tokens: {{ .Values.inference.maxTokens }}
temperature: {{ .Values.inference.temperature }}
{{- if eq .Values.inference.backend "npu" }}
# 昇腾适配层专用字段(CANN 工具链版本;gpu 后端忽略)
cann_version: {{ .Values.inference.cannVersion | default "" | quote }}
{{- end }}
resources:
requests:
cpu: {{ .Values.resources.requests.cpu | quote }}
memory: {{ .Values.resources.requests.memory | quote }}
limits:
cpu: {{ .Values.resources.limits.cpu | quote }}
memory: {{ .Values.resources.limits.memory | quote }}
rollingUpdate:
maxUnavailable: {{ .Values.rollingUpdate.maxUnavailable }}
maxSurge: {{ .Values.rollingUpdate.maxSurge }}
@@ -0,0 +1,72 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "iaop.fullname" . }}
labels:
{{- include "iaop.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: {{ .Values.rollingUpdate.maxUnavailable }}
maxSurge: {{ .Values.rollingUpdate.maxSurge }}
selector:
matchLabels:
{{- include "iaop.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "iaop.selectorLabels" . | nindent 8 }}
iaop.ai/inference-backend: {{ .Values.inference.backend }}
spec:
{{- if eq .Values.inference.backend "gpu" }}
# NVIDIA GPU(5090)节点选择:推理节点打标 nvidia.com/gpu=true
nodeSelector:
nvidia.com/gpu: "true"
{{- else }}
# 华为昇腾 NPU 节点选择:推理节点打标 ascend.com/npu=true
nodeSelector:
ascend.com/npu: "true"
{{- end }}
containers:
- name: inference
image: "{{ include "iaop.image" . }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
env:
- name: IAOP_BACKEND_CONFIG
value: /etc/iaop/backends.yaml
volumeMounts:
- name: backend-config
mountPath: /etc/iaop
readOnly: true
{{- if .Values.storage.enabled }}
- name: model-store
mountPath: /models
{{- end }}
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
- name: backend-config
configMap:
name: {{ include "iaop.fullname" . }}-backend
{{- if .Values.storage.enabled }}
- name: model-store
persistentVolumeClaim:
claimName: {{ include "iaop.fullname" . }}-models
{{- end }}
@@ -0,0 +1,23 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "iaop.fullname" . }}
labels:
{{- include "iaop.labels" . | nindent 4 }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
rules:
- host: {{ .Values.ingress.host | quote }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ include "iaop.fullname" . }}
port:
name: http
{{- end }}
+17
View File
@@ -0,0 +1,17 @@
{{- if .Values.storage.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "iaop.fullname" . }}-models
labels:
{{- include "iaop.labels" . | nindent 4 }}
spec:
accessModes:
- ReadWriteOnce
{{- if .Values.storage.className }}
storageClassName: {{ .Values.storage.className }}
{{- end }}
resources:
requests:
storage: {{ .Values.storage.size }}
{{- end }}
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "iaop.fullname" . }}
labels:
{{- include "iaop.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "iaop.selectorLabels" . | nindent 4 }}
+63
View File
@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
# iAOP Helm Chart 默认 values(PRD 5.6「⑥ 部署底座」配置点)。
#
# 切换推理后端(NVIDIA 5090 GPU ↔ 华为昇腾 NPU)只需改:
# inference.backend: gpu | npu
# 其余部署(内核+模板、资源配额、灰度、存储、域名)无需改动。
# ---- 镜像 ----
image:
repository: iaop/inference
tag: v1.0.0
pullPolicy: IfNotPresent
# ---- 推理后端选择(PRD 5.6 配置点:切换后端仅改此处) ----
inference:
backend: gpu # gpu | npu
model: iaop-ti-cl4-v1
runtime: vllm # gpu: vllm|triton;npu: mindie|onnx-ascend
device: nvidia-5090 # gpu: nvidia-5090;npu: ascend-310p|ascend-910b
cannVersion: "" # npu: CANN 工具链版本(如 "8.0"),gpu 忽略
maxTokens: 1024
temperature: 0.1
timeoutSeconds: 60
# ---- 副本与滚动(灰度发布策略,PRD 5.6 配置点) ----
replicaCount: 2
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
# ---- 服务 ----
service:
type: ClusterIP
port: 8000
# ---- 域名入口 ----
ingress:
enabled: false
host: iaop.example.com
className: ""
# ---- 资源配额(PRD 5.6 配置点) ----
resources:
requests:
cpu: "2"
memory: 8Gi
limits:
cpu: "8"
memory: 32Gi
# ---- 存储(模型权重/日志持久化) ----
storage:
enabled: true
className: "" # 空 = 使用集群默认 StorageClass
size: 100Gi
# ---- 探针 ----
livenessProbe:
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
initialDelaySeconds: 10
periodSeconds: 5