AI Infra 训练营
总览
  • 总览
  • 完整安装
  • 核心 K8s
  • Cilium 网络
  • Longhorn 存储
  • 监控日志
  • CI / GitOps
  • 安全准入
  • CI/CD 实战(MySQL+Go+Vue)
  • HPA/Ingress/Hubble 实战
  • 面试速查 + 真实踩坑
  • Day 0 · 新手接管 Runbook
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
  • 心智模型
  • 看懂命令输出
  • 容器网络底层
  • K8s 网络深入
  • DNS 全套
  • 故障排查方法论
  • 心智模型
  • 容器挂载完整指南
  • K8s Volumes 大全
  • PV/PVC/CSI 深入
  • NFS 深入
  • 分布式存储概览
  • 故障排查 runbook
命令手册
HiHuo 主站
GitHub
总览
  • 总览
  • 完整安装
  • 核心 K8s
  • Cilium 网络
  • Longhorn 存储
  • 监控日志
  • CI / GitOps
  • 安全准入
  • CI/CD 实战(MySQL+Go+Vue)
  • HPA/Ingress/Hubble 实战
  • 面试速查 + 真实踩坑
  • Day 0 · 新手接管 Runbook
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
  • 心智模型
  • 看懂命令输出
  • 容器网络底层
  • K8s 网络深入
  • DNS 全套
  • 故障排查方法论
  • 心智模型
  • 容器挂载完整指南
  • K8s Volumes 大全
  • PV/PVC/CSI 深入
  • NFS 深入
  • 分布式存储概览
  • 故障排查 runbook
命令手册
HiHuo 主站
GitHub
  • 实操 Runbook

    • Runbook 总览:从零部署、查看、调试
    • 完整安装总 Runbook:5 台 Ubuntu 到可用平台
    • 核心 K8s Runbook:apiserver / etcd / kubelet / containerd / HAProxy
    • Cilium 网络 Runbook:安装、查看、调试
    • Longhorn 存储 Runbook:安装、查看、调试
    • 监控日志 Runbook:Prometheus / Grafana / Loki / Alertmanager
    • CI / GitOps Runbook:Harbor / Gitea / Jenkins / Kaniko / ArgoCD
    • 安全准入 Runbook:RBAC / PSA / Kyverno / ResourceQuota
    • 实战 Runbook:MySQL + Go + Vue 全链路 CI/CD 真实发布
    • 实战 Runbook:给应用加 HPA 自动扩缩 + Ingress 域名 + Hubble 流量观测
    • 面试速查:这套平台 + 高频问答 + 真实踩坑

实战 Runbook:MySQL + Go + Vue 全链路 CI/CD 真实发布

前面 05 装好了 Harbor / Gitea / Jenkins / ArgoCD 四件套,但"装好了"不等于"能用"。这篇用一个真实的三层应用(Vue 前端 + Go 后端 + MySQL)把整条链路接通,并跑一次真实发布和一次GitOps 滚动升级,证明这套环境能落地生产。

全程在本集群真机上跑通,下面的输出都是实测。


0. 目标链路

开发者 git push 源码 (demo-app)
  → Jenkins 拉代码 (Pipeline from SCM)
  → Kaniko 在 Pod 内构建 backend + frontend 镜像
  → 推到 Harbor (10.0.24.28:30002/bootcamp/*)
  → Jenkins 改 部署repo (demo-deploy) 的镜像 tag 并 push 回 Gitea   ← CI 与 CD 的交接点
  → ArgoCD watch demo-deploy,自动 sync
  → K8s 滚动更新 demo 命名空间的 Pod
应用运行时:Vue(nginx) --/api/--> Go backend --> MySQL(Longhorn PVC)

两个 repo 分开是 GitOps 的关键:源码 repo 触发 CI,部署 repo 描述期望状态、被 ArgoCD watch。镜像 tag 的每次变更都在 Git 里可审计、可回滚。


1. 应用项目结构

源码仓库 demo-app:

demo-app/
├── backend/
│   ├── go.mod                 # 依赖 go-sql-driver/mysql
│   └── main.go                # /api/items (GET/POST) + /api/info + /healthz,启动建表
├── frontend/
│   ├── package.json           # vue3 + vite
│   ├── vite.config.js
│   ├── index.html
│   ├── src/main.js
│   ├── src/App.vue            # 调 /api/items 增删查
│   └── nginx.conf             # 静态 + 反代 /api/ 到 backend:8080
├── Dockerfile.backend         # golang 多阶段 → alpine
├── Dockerfile.frontend        # node 构建 → nginx
└── Jenkinsfile                # kaniko 构建两镜像 + 更新部署repo

后端连库(环境变量注入,密码来自 K8s Secret)+ 启动自动建表的核心:

dsn := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?parseTime=true",
    env("DB_USER","root"), env("DB_PASS","bootcamp"),
    env("DB_HOST","mysql"), env("DB_NAME","demo"))
// 重试 40 次等 MySQL 起来(Pod 启动竞速,见踩坑 3)
db.Exec(`CREATE TABLE IF NOT EXISTS items (id INT AUTO_INCREMENT PRIMARY KEY, ...)`)

MySQL 8.0 认证插件坑:mysql:8.0 默认 caching_sha2_password。go-sql-driver/mysql v1.8.1 原生支持它(通过 RSA 公钥交换,无需 TLS),不用加 --default-authentication-plugin 之类的启动参数。注意 --mysql-native-password=ON 是 MySQL 8.4+ 的写法,在 8.0 上会让 mysqld 启动失败。

前端 nginx 反代,让前后端同源、免 CORS:

location /api/    { proxy_pass http://backend:8080/api/; }
location /healthz { proxy_pass http://backend:8080/healthz; }
location /        { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; }

部署仓库 demo-deploy:

demo-deploy/manifests/
├── 00-namespace.yaml       # demo
├── 01-mysql-secret.yaml    # root 密码
├── 02-mysql.yaml           # PVC(longhorn 2Gi) + Deployment(Recreate) + Service
├── 03-backend.yaml         # Deployment×2 + Service,image tag 由 CI 改写
└── 04-frontend.yaml        # Deployment×2 + Service(NodePort 31000)

MySQL 用 strategy: Recreate:单副本挂 RWO PVC,默认 RollingUpdate 会让新旧 Pod 抢同一个卷触发 Multi-Attach。


2. Jenkinsfile:Kaniko 构建 + GitOps 回写

关键点:①一个 agent Pod 里放 两个 kaniko 容器(各构建一个镜像,kaniko 不适合一个容器跑两次)+ 一个 git 容器;②harbor-auth Secret 以 config.json 挂进 /kaniko/.docker;③最后 clone 部署 repo、sed 改 tag、push 回去。

pipeline {
  agent { kubernetes { yaml '''
apiVersion: v1
kind: Pod
spec:
  containers:
  - { name: kaniko-backend,  image: gcr.io/kaniko-project/executor:debug, command: ["/busybox/cat"], tty: true,
      volumeMounts: [{ name: docker-config, mountPath: /kaniko/.docker }] }
  - { name: kaniko-frontend, image: gcr.io/kaniko-project/executor:debug, command: ["/busybox/cat"], tty: true,
      volumeMounts: [{ name: docker-config, mountPath: /kaniko/.docker }] }
  - { name: git, image: alpine/git:latest, command: ["cat"], tty: true }
  volumes:
  - { name: docker-config, secret: { secretName: harbor-auth, items: [{ key: .dockerconfigjson, path: config.json }] } }
''' } }
  environment { REG = '10.0.24.28:30002/bootcamp'; TAG = "${BUILD_NUMBER}" }
  stages {
    stage('Build backend') { steps { container('kaniko-backend') {
      sh '/kaniko/executor --context dir://${WORKSPACE} --dockerfile Dockerfile.backend --destination ${REG}/backend:${TAG} --insecure --skip-tls-verify --cache=false' } } }
    stage('Build frontend') { steps { container('kaniko-frontend') {
      sh '/kaniko/executor --context dir://${WORKSPACE} --dockerfile Dockerfile.frontend --destination ${REG}/frontend:${TAG} --insecure --skip-tls-verify --cache=false' } } }
    stage('Update deploy repo (GitOps)') { steps { container('git') { sh '''
      git clone http://bootcamp:bootcamp@gitea-http.gitea.svc.cluster.local:3000/bootcamp/demo-deploy.git deploy
      cd deploy
      sed -i "s|image: .*/backend:.*|image: ${REG}/backend:${TAG}|"   manifests/03-backend.yaml
      sed -i "s|image: .*/frontend:.*|image: ${REG}/frontend:${TAG}|" manifests/04-frontend.yaml
      git config user.email ci@local; git config user.name jenkins
      git commit -am "ci: release build ${TAG}" || echo "no change"
      git push origin main
    ''' } } }
  }
}

--insecure --skip-tls-verify:Harbor 是学习环境 HTTP,kaniko push 要加这两个;同时各节点 containerd 也要信任该 HTTP registry(05 节第 3 步的 certs.d)。生产应给 Harbor 配 HTTPS 后去掉。


3. 准备:Harbor 项目 + Gitea 仓库

# Harbor 建公开项目 bootcamp(公开 → 节点 containerd 拉镜像免 imagePullSecret)
curl -u admin:bootcamp -X POST http://10.0.24.31:30002/api/v2.0/projects \
  -H "Content-Type: application/json" -d '{"project_name":"bootcamp","public":true}'

# Gitea 建两个 repo
for r in demo-app demo-deploy; do
  curl -u bootcamp:bootcamp -X POST http://10.0.24.31:30022/api/v1/user/repos \
    -H "Content-Type: application/json" \
    -d "{\"name\":\"$r\",\"private\":false,\"default_branch\":\"main\",\"auto_init\":false}"
done

# 推代码(git 内嵌 http 凭据)
cd demo-app    && git init -b main && git add -A && git commit -m init && \
  git remote add origin http://bootcamp:bootcamp@10.0.24.31:30022/bootcamp/demo-app.git && git push -u origin main
cd demo-deploy && git init -b main && git add -A && git commit -m init && \
  git remote add origin http://bootcamp:bootcamp@10.0.24.31:30022/bootcamp/demo-deploy.git && git push -u origin main

harbor-auth 推送凭据 Secret(05 节第 7 步已建在 jenkins ns,Kaniko 用它 push):

kubectl create secret docker-registry harbor-auth -n jenkins \
  --docker-server=10.0.24.28:30002 --docker-username=admin --docker-password=bootcamp

4. 创建 Jenkins 流水线 job 并触发

Jenkins 有 CSRF 保护,API 操作要带 crumb + cookie。job 用 "Pipeline script from SCM" 指向 demo-app 的 Jenkinsfile:

J=http://10.0.24.31:30808; AUTH="admin:bootcamp"
# config.xml: flow-definition + CpsScmFlowDefinition + GitSCM(url=...demo-app.git, */main, scriptPath=Jenkinsfile)
CRUMB=$(curl -s -c /tmp/jc -u "$AUTH" "$J/crumbIssuer/api/json" | jq -r .crumb)
curl -s -b /tmp/jc -u "$AUTH" -H "Jenkins-Crumb:$CRUMB" -H "Content-Type:application/xml" \
  --data-binary @config.xml "$J/createItem?name=demo-app"
curl -s -b /tmp/jc -u "$AUTH" -H "Jenkins-Crumb:$CRUMB" -X POST "$J/job/demo-app/build"   # 触发构建

demo-app 是公开 repo,Jenkins controller 在集群内可匿名 clone http://gitea-http.gitea.svc.cluster.local:3000/...,无需配 git 凭据。

构建过程真实输出(控制台关键行):

Pod [Pending][ContainersNotReady]  ...                      # agent pod 拉 kaniko/git 镜像
+ /kaniko/executor ... --destination 10.0.24.28:30002/bootcamp/backend:2 ...
INFO Building stage 'golang:1.22-alpine'                    # go mod tidy + build
INFO Building stage 'alpine:3.20'
+ /kaniko/executor ... --destination .../frontend:2 ...
INFO Building stage 'node:20-alpine'                        # npm install + vite build
INFO Building stage 'nginx:alpine'
+ git clone ... demo-deploy.git && sed -i ... && git push   # 回写部署 repo
Finished: SUCCESS

构建完成后核对:

# Harbor 有镜像
curl -s -u admin:bootcamp http://10.0.24.31:30002/api/v2.0/projects/bootcamp/repositories | grep -o '"name":"[^"]*"'
# "bootcamp/frontend"  "bootcamp/backend"

# 部署 repo 的 image tag 已被 CI 改写
curl -s -u bootcamp:bootcamp http://10.0.24.31:30022/bootcamp/demo-deploy/raw/branch/main/manifests/03-backend.yaml | grep image:
#   image: 10.0.24.28:30002/bootcamp/backend:2

5. ArgoCD Application 接管部署

# repo 连接(HTTP 公开 repo,insecure=true 允许 http)
apiVersion: v1
kind: Secret
metadata:
  name: demo-deploy-repo
  namespace: argocd
  labels: { argocd.argoproj.io/secret-type: repository }
stringData:
  type: git
  url: http://gitea-http.gitea.svc.cluster.local:3000/bootcamp/demo-deploy.git
  insecure: "true"
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata: { name: demo, namespace: argocd }
spec:
  project: default
  source:
    repoURL: http://gitea-http.gitea.svc.cluster.local:3000/bootcamp/demo-deploy.git
    targetRevision: main
    path: manifests
  destination: { server: https://kubernetes.default.svc, namespace: demo }
  syncPolicy:
    automated: { prune: true, selfHeal: true }
    syncOptions: [ CreateNamespace=true ]

顺序很重要:先跑一次 Jenkins 构建(把部署 repo 的 tag 从占位 :0 改成真实 :2),再创建 ArgoCD Application。否则 ArgoCD 会先去拉 :0 镜像 → ImagePullBackOff。

应用后 ArgoCD 自动 sync,真实收敛过程:

[10s]  sync=Synced health=Progressing  pods=[0/1 0/1 0/1 1/1 0/1]   # mysql 先起
[120s] sync=Synced health=Progressing  pods=[0/1 0/1 1/1 1/1 1/1]   # frontend 起,backend 等 mysql
[230s] sync=Synced health=Healthy      pods=[1/1 1/1 1/1 1/1 1/1]   # backend 连上库,全绿

$ kubectl get pods,svc,pvc -n demo
pod/backend-…   1/1 Running 1 (...)   # restarts=1:等 mysql 初始化时重试耗尽过一次
pod/frontend-…  1/1 Running
pod/mysql-…     1/1 Running
service/frontend NodePort 80:31000/TCP
pvc/mysql-data  Bound  pvc-…  2Gi  RWO  longhorn

6. 端到端验证:前端 → 后端 → MySQL

F=http://10.0.24.31:31000
curl -s $F/ | grep -o '<title>[^<]*</title>'          # <title>Demo CI/CD</title>
curl -s $F/api/info                                    # {"app":"demo-backend","version":"1.0.0"}
curl -s -X POST $F/api/items -H 'Content-Type: application/json' -d '{"name":"第一条-CICD跑通"}'   # {"id":1,...}
curl -s $F/api/items                                   # 返回写入的条目(经 nginx→backend→MySQL)

实测:前端 200、/api/info 返回版本、POST 三条全 201、GET 读回三条带时间戳。前端 nginx 反代 → Go 后端 → MySQL 全链路通。

数据持久化验证(Longhorn 的价值)

删掉 MySQL Pod 模拟节点故障/重调度,数据应不丢:

kubectl delete pod -n demo -l app=mysql              # 新 Pod 会重挂同一个 Longhorn 卷
kubectl wait -n demo --for=condition=Ready pod -l app=mysql --timeout=180s
curl -s $F/api/items                                  # 还是那三条,id/时间戳不变 ✓

实测 MySQL Pod 在另一节点重建后,三条数据原样还在 —— 数据真的存在 Longhorn 多副本卷上。


7. GitOps 滚动升级(真实发布闭环)

改一行源码版本,走完整流水线,看 ArgoCD 自动把线上滚到新版:

# 1) 改源码并 push
sed -i 's/const version = "1.0.0"/const version = "2.0.0"/' backend/main.go
git commit -am "feat: 后端 2.0.0" && git push origin main
# 2) 触发 Jenkins 构建 #3 → backend:3/frontend:3 → 部署 repo tag 改成 :3
# 3) ArgoCD 自动检测并滚动升级

真实滚动过程:

[30s] deploy镜像=...backend:2  线上版本="1.0.0"     # 还没同步
[40s] deploy镜像=...backend:3  线上版本="1.0.0"     # ArgoCD 同步了 manifest,Pod 正在滚
[50s] deploy镜像=...backend:3  线上版本="2.0.0"     # 新 Pod 就绪,版本切换
curl -s $F/api/info     # {"app":"demo-backend","version":"2.0.0"}  ← 升级成功
curl -s $F/api/items    # 三条数据仍在 ← 跨版本升级数据不丢

这就是完整闭环:git push 源码 → CI 构建镜像 → 改部署 repo → ArgoCD 自动滚动升级 → 数据持久。滚动更新天然零停机(旧 Pod 在新 Pod 就绪前继续服务)。


8. 踩坑记录(实测)

坑现象原因 / 解法
部署 repo 文件名对不上构建到最后报 sed: manifests/backend.yaml: No such file or directory,镜像其实已推成功Jenkinsfile sed 的文件名要和部署 repo 实际文件名(带序号 03-backend.yaml)一致
MySQL 8.0 认证参数加了 --mysql-native-password=ON 后 mysqld 起不来那是 8.4+ 的写法;8.0 直接不加,靠 go-sql-driver 1.8 支持 caching_sha2_password
Pod 启动竞速backend restarts=1、日志 connection refused :3306backend 比 MySQL 先起;靠应用内重试(40×3s)+ readinessProbe 自愈,别删 Pod
ArgoCD 拉到占位镜像先建 App 后跑 CI → ImagePullBackOff :0先跑一次 CI 把 tag 改成真实值,再建 ArgoCD Application
kaniko 推 HTTP Harborpush 失败--insecure --skip-tls-verify + 节点 containerd certs.d 信任该 HTTP registry
一个 kaniko 容器构建两镜像第二次构建异常kaniko 不适合一个容器跑两次;agent Pod 里放两个 kaniko 容器各构建一个

9. 入口速查

用途地址账号
应用前端http://<节点>:31000—
Gitea(源码/部署 repo)http://<节点>:30022bootcamp/bootcamp
Jenkins(看构建)http://<节点>:30808admin/bootcamp
Harbor(看镜像)http://<节点>:30002admin/bootcamp
ArgoCD(看同步/拓扑)http://<节点>:30080admin / kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d

到这里,从一行代码改动到生产环境滚动上线、数据持久不丢的完整链路就全部验证通过了。

在 GitHub 上编辑此页
Prev
安全准入 Runbook:RBAC / PSA / Kyverno / ResourceQuota
Next
实战 Runbook:给应用加 HPA 自动扩缩 + Ingress 域名 + Hubble 流量观测