Vue 3 项目通常编译成静态资源后通过 Nginx 容器发布到 Kubernetes,灰度发布的难点在于流量可以切分,但前端页面引用的 JavaScript 和 CSS 资源必须与用户访问的版本严格对应。如果只调整 Ingress 权重而不处理构建产物版本和缓存策略,就会发生旧版 index.html 加载新版本入口脚本导致白屏。本文围绕 Kubernetes 的流量切分能力,结合 Vue 3 工程化构建,梳理一套可落地的灰度发布方案。

一、以版本化构建和镜像标记作为灰度前提
灰度发布的第一个前提是每次构建都能生成唯一可识别的产物。Vue 3 使用 Vite 或 Vue CLI 构建时,可以开启内容哈希命名,确保每个静态文件路径与内容强关联。例如在 Vite 配置中把 rollupOptions 的 assetFileNames 设置为带 hash 的格式,这样新版本修改代码后,生成的 app.js 文件名会变化,浏览器请求的不会命中旧缓存。这个机制是灰度发布安全性的基础,因为不同版本的资源可以同时存在于对象存储或 Nginx 容器中互不覆盖。
接下来需要把构建产物打包进不可变容器镜像。推荐使用多阶段构建,第一阶段用 node 镜像执行 npm install 和 npm run build,第二阶段用 nginx 镜像只拷贝 dist 目录。镜像标签必须和 Git 提交号或 CI 构建号绑定,不能只使用 latest,否则无法区分灰度版本和稳定版本。下面是一个 Dockerfile 示例:
# 第一阶段:构建 Vue 3 应用 FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . ARG VITE_API_BASE ENV VITE_API_BASE=$VITE_API_BASE RUN npm run build # 第二阶段:运行态镜像 FROM nginx:1.27-alpine COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]
在 CI 流水线中构建并推送镜像到镜像仓库,例如通过 docker build 传递 VITE_API_BASE 构建参数,再用 tag 形式 v1.2.0-commit-hash 推送。这样 Kubernetes 中的 Pod 只要拉取不同 tag 的镜像,就能明确运行哪个版本的 Vue 3 产物,为后续流量分配提供可区分的服务端点。
二、Kubernetes 流量切分的核心:Ingress Canary 注解
Kubernetes 原生 Service 的负载均衡默认是轮询模式,不会按版本权重分配流量。要实现灰度发布,常见的做法是使用 Ingress Nginx Controller 提供的 Canary 注解。它的原理是让同一个域名存在两个 Ingress,一个指向稳定版 Service,另一个金丝雀 Ingress 通过注解打开 canary 开关并设置权重,Ingress Controller 根据权重将部分请求转发到金丝雀 Service。
首先需要部署两个几乎相同的 Deployment 和 Service,区别仅在于镜像版本和标签。主版本继续服务默认流量,灰度版本只接受定向流量。下面是灰度 Deployment 和 Service 的简化清单:
apiVersion: apps/v1
kind: Deployment
metadata:
name: vue-app-canary
labels:
app: vue-app
version: canary
spec:
replicas: 2
selector:
matchLabels:
app: vue-app
version: canary
template:
metadata:
labels:
app: vue-app
version: canary
spec:
containers:
- name: vue-app
image: registry.ippipp.com/vue-app:1.3.0-9f3c2a
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
---
apiVersion: v1
kind: Service
metadata:
name: vue-app-canary
spec:
selector:
app: vue-app
version: canary
ports:
- port: 80
targetPort: 80
稳定版 Deployment 与 Service 的结构类似,只是镜像 tag 不同、version 标签为 stable。之后创建金丝雀 Ingress,通过注释 nginx.ingress.kubernetes.io/canary: "true" 和 canary-weight: "20" 将百分之二十的请求转发到金丝雀 Service。注意这里加不加引号在不同版本 Controller 中都能识别,但保持字符串最稳妥。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: vue-app-canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "20"
spec:
ingressClassName: nginx
rules:
- host: app.ippipp.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vue-app-canary
port:
number: 80
canary-weight 的取值是 0 到 100,表示将多大概率分配给金丝雀。它还支持基于 Header、Cookie 的流量切分,例如只让携带 x-canary: true 请求头的内部测试用户进入灰度版本,这样可以在小范围测试后再逐步提升权重。部分 Ingress Controller 还支持自定义匹配规则,例如通过 nginx.ingress.kubernetes.io/canary-by-header-value 指定固定值。这些注解需要主 Ingress 和金丝雀 Ingress 使用完全相同的 host 和 path,否则规则不会合并,流量切分不会生效。
三、灰度过程中的一致性保障:Nginx 配置与 Vue Router 模式
当流量按比例进入不同版本的 Pod 后,前端资源加载的一致性成为关键。典型问题是一个用户首次访问拿到新版 index.html,但浏览器在请求异步 chunk 时被负载到旧版 Pod,旧版没有这个 chunk 文件,从而触发 404 或白屏。虽然静态资源使用内容哈希后新老文件不存在覆盖问题,但请求到达哪台 Pod 取决于 Ingress 的均衡,不能保证后续资源请求一定回到同一个版本。
解决这个问题的常用方式是在 Nginx 中为 index.html 设置禁止缓存,而为带哈希的静态资源设置长缓存。当用户刷新页面时,会重新获取最新 index.html,再根据新的入口脚本和 chunk 列表加载对应资源。即便某个异步资源请求被错分到旧版 Pod,只要两个版本的静态资源不相互覆盖,旧版 Pod 依然能提供旧资源文件,但此时 index.html 已是新版,仍会引用新 chunk,错分到旧 Pod 会导致资源缺失。要真正避免这类错乱,推荐使用 Cookie 持久会话或让灰度版本只承接完整页面请求,静态资源由统一的 CDN 或对象存储提供,不走 Ingress 分流。
另外 Vue Router 如果使用 history 模式,Nginx 必须配置 try_files 将未匹配的路径回退到 index.html,否则刷新 /about 这类路由会返回 404。灰度版和稳定版的 Nginx 配置应当一致,下面是一个示例:
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
expires 0;
}
}
这段配置利用 Nginx 的 location 匹配将静态资源单独处理,避免其被 try_files 回退到 index.html。如果 Vue 3 项目还需要代理后端接口,可以在同一定位下增加 proxy_pass,但灰度期间建议后端接口也同步做版本切分,防止前端灰度版本调用到不兼容的新接口或旧接口。
四、工程化灰度流水线与回滚策略
灰度发布要落地到工程化流程,需要把构建、镜像推送、Kubernetes 部署串起来。常见的做法是 CI 系统在合并请求或手动触发时执行构建,生成带 Git commit 短哈希的镜像,然后通过 kubectl 或 Helm 更新金丝雀 Deployment 的镜像字段。之后根据灰度观察结果,逐步修改 Ingress 注解的 canary-weight 从 5% 提升到 20%、50%,最后把稳定版 Deployment 更新为新镜像并将权重调回 100%,同时删除金丝雀 Deployment。
下面是一个用 kubectl patch 调整金丝雀权重的示例脚本,可以集成到发布流水线中:
#!/usr/bin/env bash set -euo pipefail CANARY_INGRESS="vue-app-canary" NAMESPACE="production" WEIGHT="$1" if [[ -z "$WEIGHT" || "$WEIGHT" -lt 0 || "$WEIGHT" -gt 100 ]]; then echo "Usage: $0 <weight 0-100>" exit 1 fi kubectl annotate ingress "$CANARY_INGRESS" -n "$NAMESPACE" \ nginx.ingress.kubernetes.io/canary-weight="$WEIGHT" --overwrite echo "Canary weight updated to $WEIGHT%"
回滚策略同样重要。由于灰度版本通过独立的 Deployment 和 Service 运行,当出现错误率升高或用户反馈异常时,只需把 canary-weight 降为 0 或直接删除金丝雀 Ingress,流量就会全部回到稳定版。如果使用的是镜像 tag 与 Git 提交绑定,则回滚到上一个稳定镜像也只是一次 patch 操作。配合 Prometheus 监控和日志采集,可以观察不同版本 Pod 的 5xx 比例、接口错误率和前端资源加载失败率,帮助决定是否继续放量。
完整的灰度发布还需要考虑配置中心策略。前端在运行时需要的环境变量可以通过 Docker 构建参数在构建阶段传入,但这样做每个环境都需要重新构建镜像。另一种做法是在 Nginx 启动时由 entrypoint 脚本从环境变量动态生成 Js 配置文件,再由 Vue 3 应用加载。这样同一个镜像可以部署到测试、预发和生产,减少镜像构建次数和版本漂移。Kubernetes 的 ConfigMap 可以把环境配置注入 Pod,灰度版本也能使用不同的配置验证新后端或特性开关。
工程化灰度发布不是单个工具能解决的,它需要 Vue 3 构建产物策略、Docker 镜像版本管理、Ingress 流量切分和 Nginx 缓存配置协同工作。只有在每一层都做好版本隔离,才能让流量切分真正反映业务效果,减少发布风险。
Vue 3灰度发布Kubernetes 流量切分修改时间:2026-08-30 01:24:14