Docker Swarm 迁移到 Kubernetes,最危险的做法是把 docker-compose.yml 逐字段“翻译”成 Deployment YAML,然后直接切流。两者都能运行容器,但关键网络、存储、健康检查、滚动和权限模型不一一对应。迁移失败通常不是 YAML 写错,而是原来被 Swarm 默认行为掩盖的依赖没有被显式建模。
本文以一个无状态 Web 服务为主线:源端由 Docker Swarm stack 部署,目标端由 Kubernetes 的 Deployment、Service 和 Ingress 运行,数据库仍作为外部受管依赖,不在本次迁移中搬库。持久化数据单独使用 StatefulSet 与 PVC,并先完成数据迁移方案。
文中的 <源Stack名>、<源服务名>、<命名空间>、<应用名>、<镜像地址>、<业务域名>、<健康检查路径>、<存储类名> 等尖括号内容均为占位符。<命名空间> 需要在迁移前由平台或 GitOps 流程创建并完成 RBAC、配额和网络基线。文中所有 kubectl 命令都显式携带 -n <命名空间>;请替换后执行。
迁移前的原则:先把现网事实做成清单
不要把 compose 文件当作唯一真相。一个 Swarm 服务的实际行为还受 stack 变量、secret、config、overlay 网络、节点标签、外置反向代理、卷驱动和历史手工操作影响。迁移前应形成可审计的“服务契约”,至少包括:
- 容器镜像、启动命令、环境变量来源、监听端口和进程用户。
- 副本数、可调度节点、CPU/内存限制、重启策略、发布策略。
- 对外入口、内部服务名、访问路径、TLS 终止位置和回源头。
- 需要持久化的数据路径、卷驱动、读写方式、备份和恢复演练记录。
- 健康检查命令、启动时间、真正可接流量的条件,以及依赖失效时的行为。
- 监控、日志、告警、权限、回滚和切流责任人。
先获取 Swarm 的只读事实。下面命令不会修改 stack;敏感环境变量、secret 名和内部地址不应直接复制到公开工单。
# 代码 1:确认源端 Swarm 状态、stack 服务与任务分布
docker version
docker info --format '{{.Swarm.LocalNodeState}} {{.Swarm.ControlAvailable}}'
docker stack services "<源Stack名>"
docker stack ps "<源Stack名>" --no-trunc
docker stack services 展示的是期望副本与当前副本的摘要;任务频繁重建、被调度到不同节点、或长期有 REJECTED 状态时,必须先解释原因。把一个本来就不稳定的服务迁走,不能证明 Kubernetes 有问题。
# 代码 2:导出源服务的配置、任务和网络引用
set -euo pipefail
stack_name="<源Stack名>"
service_name="<源服务名>"
evidence_dir="<证据目录>/swarm-$service_name-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$evidence_dir"
docker service inspect "$stack_name"_"$service_name" > "$evidence_dir/service-inspect.json"
docker service ps "$stack_name"_"$service_name" --no-trunc > "$evidence_dir/service-tasks.txt"
docker network ls > "$evidence_dir/network-list.txt"
docker volume ls > "$evidence_dir/volume-list.txt"
printf '%s\n' "$evidence_dir"
Stack 部署后的 service 名通常是 <源Stack名>_<源服务名> ,但不要假设所有环境都遵循此规则;应以 docker stack services 的输出为准。导出的 JSON 可能包含镜像仓库、环境变量或挂载路径,证据目录需要访问控制。
下面的脚本把源服务中最容易漏掉的字段抽出来,作为目标 YAML 审核清单。它依赖本机有 docker CLI 与 jq;若 jq 不存在,先安装经批准的软件包或把完整 JSON 提交给迁移评审,不要用脆弱的 grep 解析 JSON。
#!/usr/bin/env bash
# 代码 3:生成 Swarm 服务迁移核对清单
set -euo pipefail
service_ref="<源Stack名>_<源服务名>"
docker service inspect "$service_ref" \
| jq '.[0].Spec.TaskTemplate.ContainerSpec as $c
| {
image: $c.Image,
command: $c.Command,
args: $c.Args,
env: $c.Env,
mounts: $c.Mounts,
secrets: $c.Secrets,
configs: $c.Configs,
labels: $c.Labels,
service_labels: .[0].Spec.Labels,
networks: .[0].Spec.TaskTemplate.Networks,
endpoint: .[0].Endpoint.Spec,
placement: .[0].Spec.TaskTemplate.Placement,
resources: .[0].Spec.TaskTemplate.Resources,
restart_policy: .[0].Spec.TaskTemplate.RestartPolicy
}'
不要逐字映射字段;每项意图都要明确在 Kubernetes 中由什么对象实现。placement.constraints 可能变为 nodeSelector、nodeAffinity 或不再保留;发布端口则可能由 Service、Ingress 或负载均衡承担。
坑一:把 Swarm 的服务模型当成 Deployment 的逐行翻译
Swarm 的 service、task、replicas 与 Kubernetes 的 Deployment、ReplicaSet、Pod 看似相似,但调度和标签语义不同。Service 只路由到 selector 匹配、通常已经 Ready 的 Pod;Deployment selector 创建后不可随意修改。
不要在迁移时沿用无意义的临时标签。至少固定 app.kubernetes.io/name、app.kubernetes.io/instance、app.kubernetes.io/component,并让 Deployment template labels 与 selector 完全一致。
# 代码 4:目标应用的 Deployment 基础结构
apiVersion: apps/v1
kind: Deployment
metadata:
name: <应用名>
namespace: <命名空间>
labels:
app.kubernetes.io/name: <应用名>
app.kubernetes.io/instance: <环境标识>
app.kubernetes.io/component: web
spec:
replicas: <初始副本数>
selector:
matchLabels:
app.kubernetes.io/name: <应用名>
app.kubernetes.io/instance: <环境标识>
template:
metadata:
labels:
app.kubernetes.io/name: <应用名>
app.kubernetes.io/instance: <环境标识>
app.kubernetes.io/component: web
spec:
containers:
- name: web
image: <镜像地址>
imagePullPolicy: IfNotPresent
selector 与 template labels 不匹配会被 API 拒绝;更隐蔽的错误是 Service selector 少了环境标签,导致测试环境流量误打到同 namespace 的其他 Pod。不要在多个环境共享同一个 namespace 时只用 app=<应用名> 作为路由选择器。
资源请求决定调度时的保留量,limit 决定容器可用上界。Swarm 的 reservation/limit 不能机械照抄,因为 Kubernetes 节点可分配资源、sidecar、init container 与命名空间配额都可能不同。先根据源端真实峰值和容量预算设定,再用灰度数据修正。
# 代码 5:为同一容器补全端口、资源与安全上下文
resources:
requests:
cpu: "<CPU请求>"
memory: "<内存请求>"
limits:
cpu: "<CPU上限>"
memory: "<内存上限>"
ports:
- name: http
containerPort: <容器端口>
protocol: TCP
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
readOnlyRootFilesystem 只有在应用不再往镜像层写临时文件、PID、缓存或日志时才能启用。迁移前应让日志输出到 stdout/stderr,并为确有需要的临时目录挂载受控 emptyDir;否则 Pod 会启动但在首个写入点失败。
服务端 dry-run 只验证 API 与准入,不代表真实调度、镜像拉取或业务可用。
# 代码 6:校验目标清单并查看 Deployment 实际状态
kubectl apply --dry-run=server -n <命名空间> -f "<Deployment清单路径>"
kubectl apply -n <命名空间> -f "<Deployment清单路径>"
kubectl rollout status -n <命名空间> deployment/<应用名> --timeout=<等待时长>
kubectl get deployment,pod -n <命名空间> \
-l app.kubernetes.io/name=<应用名> -o wide
若 rollout status 超时,不要立刻 rollback 再重试。先查看 Pod 的 Pending、ImagePullBackOff、CrashLoopBackOff 或探针失败事件。每一种状态代表不同责任边界,反复 rollout 只会覆盖第一次失败的证据。
# 代码 7:通过 Pod 状态、事件与日志解释首次部署失败
kubectl get pod -n <命名空间> \
-l app.kubernetes.io/name=<应用名> -o wide
kubectl describe pod -n <命名空间> <Pod名称>
kubectl logs -n <命名空间> <Pod名称> --all-containers --tail=200
kubectl get event -n <命名空间> \
--sort-by=.lastTimestamp | tail -n 100
Pod 名称是临时对象,替换 <Pod名称> 前应通过前一条命令取得当前实例。若容器包含 sidecar,--all-containers 便于首轮排查;后续根因结论仍要明确是哪一个 container 的日志和退出码。
坑二:把 overlay 网络和发布端口误当成 Kubernetes Service 与 Ingress
Swarm overlay 网络天然给服务名提供内部可达路径,published port 又可能采用 routing mesh 或 host 发布模式。Kubernetes 中,Service 负责稳定虚拟 IP 和 Pod 选择,Ingress 或 Gateway 负责 HTTP 路由,负载均衡器、CNI、NetworkPolicy 负责各自层次的能力。把 Swarm 的 ports 直接填进 containerPort,外部流量并不会自动到达。
先建立 ClusterIP Service,让同 namespace 内服务通过稳定 DNS 访问。targetPort 应明确引用命名端口,避免容器端口变更后 Service 静默指向错误端口。
# 代码 8:仅提供集群内访问的 ClusterIP Service
apiVersion: v1
kind: Service
metadata:
name: <应用名>
namespace: <命名空间>
labels:
app.kubernetes.io/name: <应用名>
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: <应用名>
app.kubernetes.io/instance: <环境标识>
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
Service 的 selector 与 Deployment labels 必须一字不差;否则 Service 仍会创建,但 EndpointSlice 没有后端,入口层看到的可能是 503 或连接失败。创建后立即检查 EndpointSlice,不要只看 Service 有 ClusterIP。
# 代码 9:验证 Service 是否真的选择到了 Ready 后端
kubectl get service -n <命名空间> <应用名> -o wide
kubectl get endpointslice -n <命名空间> \
-l kubernetes.io/service-name=<应用名> -o yaml
kubectl get pod -n <命名空间> \
-l app.kubernetes.io/name=<应用名> \
-o custom-columns=NAME:.metadata.name,READY:.status.containerStatuses.ready,IP:.status.podIP
EndpointSlice 中 ready 条件为 true 才说明该 IP 可作为正常流量后端。若为空,按 selector、Pod readiness、命名空间与控制器事件逐项排查,不要通过手工创建 Endpoints 绕过问题。
外部 HTTP 路由由已部署并受平台运维的 Ingress Controller 或 Gateway 实现。下面使用标准 Ingress API;ingressClassName 必须是目标集群真实存在的 class。证书、WAF、限流与 source IP 保留策略不应在迁移窗口临时改变。
# 代码 10:将业务域名路由到 ClusterIP Service 的 Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: <应用名>
namespace: <命名空间>
spec:
ingressClassName: <IngressClass名称>
rules:
- host: <业务域名>
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: <应用名>
port:
name: http
不同 Ingress Controller 对 annotation 的支持不一致。迁移开始前确认 IngressClass、TLS、负载均衡地址、健康检查路径与访问日志位置。
网络默认允许还是默认拒绝,取决于 CNI 和 NetworkPolicy 基线。若目标 namespace 采用默认拒绝入站,应用必须显式允许来自入口控制器 namespace 或具有相应标签的 Pod;用 0.0.0.0/0 放通只是把网络模型退回裸奔。
# 代码 11:只允许指定入口组件访问 Web Pod 的 NetworkPolicy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-to-<应用名>
namespace: <命名空间>
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: <应用名>
app.kubernetes.io/instance: <环境标识>
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: <入口控制器命名空间>
ports:
- protocol: TCP
port: <容器端口>
该策略只在 CNI 实现 NetworkPolicy 时生效,且 namespaceSelector 标签必须真实存在。发布前核对入口 namespace、Pod 标签和 CNI 基线。
# 代码 12:从集群内临时调试容器验证 Service DNS 与健康端点
kubectl run -n <命名空间> network-check \
--image=curlimages/curl:<已批准版本> \
--restart=Never \
--rm -i -- \
curl --fail --silent --show-error \
"http://<应用名>.<命名空间>.svc.cluster.local<健康检查路径>"
该命令会创建并删除临时 Pod;先确认镜像、权限与审计要求。生产不允许临时 Pod 时,改用平台诊断工作负载或灰度 namespace。
坑三:把 Swarm local volume 当成 Kubernetes 持久化存储
Docker local volume 常与某台 Swarm 节点的磁盘绑定。服务在 Swarm 中恰好稳定落在该节点时看似正常,一旦迁移到 Kubernetes 或发生重调度,数据可能根本不在目标节点。把 hostPath 或临时 emptyDir 当成持久卷,只会把风险换一种形式保留。
先对每个源端 mount 明确分类:配置只读、可重建缓存、日志、用户上传、数据库数据、消息队列数据。数据库和其他有状态组件不应通过“复制一个卷目录”直接切换;需要停写、备份、校验、恢复、双写或逻辑复制等独立数据迁移方案。
# 代码 13:检查源端服务挂载与卷的实际驱动、范围
docker service inspect "<源Stack名>_<源服务名>" \
--format '{{json .Spec.TaskTemplate.ContainerSpec.Mounts}}' | jq .
docker volume inspect "<源卷名>"
docker volume ls --filter "name=<源卷名>"
volume inspect 中的 Driver、Mountpoint、Labels 和 Scope 是证据。Scope 为 local 时,卷只能在特定节点可见;即使 Scope 看起来是 global,也要确认外部存储驱动、权限、快照、备份和恢复流程是否满足目标集群要求。
若该服务只有可重建缓存,可使用 emptyDir 并明确它会在 Pod 重建后丢失。若是业务持久数据,选择受平台支持的 StorageClass,并在一次独立的数据演练中验证 PVC 绑定、挂载、备份恢复和故障域。
# 代码 14:为需要独占读写的应用数据申请 PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: <应用名>-data
namespace: <命名空间>
spec:
accessModes:
- ReadWriteOnce
storageClassName: <存储类名>
resources:
requests:
storage: <容量>
ReadWriteOnce 的语义由存储插件实现,不能简单理解成“任何时候只允许一个 Pod”。部署多副本前必须确认是否允许多节点挂载、是否需要 ReadWriteMany,或改为让应用无状态并将数据交给外部存储。
# 代码 15:在 Deployment 中明确挂载 PVC 与临时目录
spec:
template:
spec:
containers:
- name: web
volumeMounts:
- name: app-data
mountPath: <应用数据路径>
- name: tmp
mountPath: /tmp
volumes:
- name: app-data
persistentVolumeClaim:
claimName: <应用名>-data
- name: tmp
emptyDir: {}
这个片段应嵌入已有 Deployment 的正确层级,不能作为独立 YAML 直接 apply。应用挂载前,确认 UID/GID、fsGroup、init container 和存储权限模型一致;把宿主机目录 chmod 777 并不能修复 CSI 卷的权限问题。
# 代码 16:验证 PVC 绑定、Pod 挂载和容量事件
kubectl get pvc -n <命名空间> <应用名>-data -o wide
kubectl describe pvc -n <命名空间> <应用名>-data
kubectl get pod -n <命名空间> \
-l app.kubernetes.io/name=<应用名> -o wide
kubectl get event -n <命名空间> \
--sort-by=.lastTimestamp | rg -i 'volume|mount|attach|claim|provision' || true
PVC Bound 只证明控制平面完成绑定,不能证明数据与权限正确。应在隔离环境恢复备份并运行一致性检查。
对于确实需要稳定身份、稳定网络名与逐副本卷的组件,使用 StatefulSet,而不是把 replicas>1 的 Deployment 直接挂同一个 RWO PVC。下面只展示基本绑定结构,数据库参数、备份、反亲和、初始化和恢复策略必须由对应组件的官方运维方案负责。
# 代码 17:StatefulSet 的稳定身份与逐副本卷模板结构
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: <有状态应用名>
namespace: <命名空间>
spec:
serviceName: <无头Service名>
replicas: <副本数>
selector:
matchLabels:
app.kubernetes.io/name: <有状态应用名>
template:
metadata:
labels:
app.kubernetes.io/name: <有状态应用名>
spec:
containers:
- name: app
image: <镜像地址>
volumeMounts:
- name: data
mountPath: <应用数据路径>
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: <存储类名>
resources:
requests:
storage: <容量>
坑四:把 Swarm HEALTHCHECK 等同于 Kubernetes readiness 和 liveness
Swarm HEALTHCHECK 主要影响容器健康状态和调度观察;Kubernetes 则把“可以接流量”“进程失活需要重启”“启动较慢需要保护”拆成 readinessProbe、livenessProbe、startupProbe。只配置 liveness 而没有 readiness,Pod 可能在依赖未就绪时就被 Service 加入后端;把依赖数据库的深度检查设为 liveness,又可能让短暂下游故障引发全量重启。
先区分三个问题:
- startupProbe:应用是否还在正常启动窗口内,避免启动慢时被过早 liveness 杀死。
- readinessProbe:此副本现在是否适合接收业务流量。
- livenessProbe:进程是否进入无法自行恢复的状态,才需要 kubelet 重启。
# 代码 18:HTTP 应用的 startup、readiness、liveness 探针
startupProbe:
httpGet:
path: <启动检查路径>
port: http
periodSeconds: 5
failureThreshold: <最大启动检查次数>
readinessProbe:
httpGet:
path: <健康检查路径>
port: http
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet:
path: <存活检查路径>
port: http
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
三条路径可以相同,但语义必须经过应用团队确认。若健康检查依赖数据库,数据库短暂波动会让所有 Pod 同时 NotReady;这对保护流量可能合理,但不应再让 liveness 把每个 Pod 重启。检查端点必须无副作用,不得写数据、消耗大量资源或绕过认证后暴露敏感信息。
# 代码 19:查看探针事件、重启次数与当前 Ready 条件
kubectl get pod -n <命名空间> \
-l app.kubernetes.io/name=<应用名> \
-o custom-columns=NAME:.metadata.name,READY:.status.containerStatuses.ready,RESTARTS:.status.containerStatuses.restartCount,PHASE:.status.phase
kubectl describe pod -n <命名空间> <Pod名称>
kubectl logs -n <命名空间> <Pod名称> --previous --tail=200
--previous 只在容器发生过重启且日志仍可获取时有效。探针失败的结论应引用 describe 中的具体事件,例如连接拒绝、超时或 HTTP 状态码,而不是仅写“健康检查异常”。
坑五:把 Config、Secret、环境变量和镜像内配置混在一起
Swarm config 与 secret 的挂载方式、权限和更新行为不等于 Kubernetes ConfigMap 与 Secret。Kubernetes Secret 默认只是编码数据,不自动等于端到端加密;是否启用静态加密、外部密钥管理、工作负载身份和审计,取决于集群平台。绝不能把生产密码写进 Git 明文 YAML、镜像层、命令行历史或日志。
非敏感、可版本化配置可放 ConfigMap。下面示例展示应用配置文件挂载,不代表所有配置都应热更新;多数应用读取配置只发生在进程启动,需要通过受控发布让新配置生效。
# 代码 20:以文件形式挂载非敏感应用配置
apiVersion: v1
kind: ConfigMap
metadata:
name: <应用名>-config
namespace: <命名空间>
data:
app.yaml: |
server:
port: <容器端口>
logging:
level: <日志级别>
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: <应用名>
namespace: <命名空间>
spec:
template:
spec:
containers:
- name: web
volumeMounts:
- name: config
mountPath: /etc/<应用名>
readOnly: true
volumes:
- name: config
configMap:
name: <应用名>-config
这段 Deployment 片段同样需要合并到完整对象;对已存在的 Deployment,宜用受控 Helm、Kustomize 或 GitOps 变更,而不是手工多次 apply 造成字段漂移。ConfigMap 名变更并滚动发布,是比依赖卷更新时机更可预测的做法。
敏感值应从现有密钥管理或平台 Secret 同步机制导入。若必须使用 Kubernetes Secret 清单,stringData 方便生成但文件本身仍是敏感材料,必须从代码库、CI 日志和工单附件中隔离。
# 代码 21:Secret 作为环境变量来源的结构示例
apiVersion: v1
kind: Secret
metadata:
name: <应用名>-runtime
namespace: <命名空间>
type: Opaque
stringData:
DATABASE_URL: <由受控密钥系统注入的连接串>
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: <应用名>
namespace: <命名空间>
spec:
template:
spec:
containers:
- name: web
envFrom:
- secretRef:
name: <应用名>-runtime
环境变量可能被诊断输出、崩溃报告或进程工具暴露。高价值凭据宜采用平台支持的工作负载身份、外部 Secret 挂载或短期令牌;无论哪种方式,都需要轮换、双密钥切换和回滚预案。
# 代码 22:不展开 Secret 值地核对引用、ServiceAccount 与配置版本
kubectl get deployment -n <命名空间> <应用名> -o yaml \
| rg -n 'serviceAccountName|secretRef|configMap|image:|envFrom:' -C 2
kubectl get configmap -n <命名空间> <应用名>-config -o yaml
kubectl get secret -n <命名空间> <应用名>-runtime \
-o custom-columns=NAME:.metadata.name,TYPE:.type,CREATED:.metadata.creationTimestamp
命令故意不读取 secret.data。若需要验证凭据本身,只能在受控应用日志、数据库审计或经过授权的密钥系统中完成,不能通过把 Secret 解码贴到终端或工单里完成。
坑六:忽略切流、滚动、容量与回滚的组合效应
Swarm 的 update_config、restart_policy 和副本调度会在迁移时失去默认保障。Kubernetes 的 Deployment strategy、maxUnavailable、maxSurge、PodDisruptionBudget、HPA、反亲和和节点容量需要显式设计。没有 readiness、没有最小可用副本、没有入口灰度和没有快速回滚的“全量 DNS 切换”,不是迁移计划。
先把滚动策略写入 Deployment。maxUnavailable=0 只在有足够可调度容量、探针可信、存储不阻塞和资源配额允许时才可能保持可用;如果资源不足,rollout 会一直卡住,不能当作零风险配置。
# 代码 23:明确 Deployment 滚动发布策略
apiVersion: apps/v1
kind: Deployment
metadata:
name: <应用名>
namespace: <命名空间>
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
minReadySeconds: <最小就绪稳定秒数>
progressDeadlineSeconds: <发布超时秒数>
PodDisruptionBudget 只约束自愿中断,不能阻止崩溃、宕机、探针失败或资源不足。minAvailable 必须不高于稳定副本数,否则会阻断维护。
# 代码 24:为多副本 Web 服务保留最少可用 Pod
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: <应用名>
namespace: <命名空间>
spec:
minAvailable: <最少可用副本数>
selector:
matchLabels:
app.kubernetes.io/name: <应用名>
app.kubernetes.io/instance: <环境标识>
HPA 不是迁移当天的兜底开关。它需要可靠 metrics API、合理 requests 与业务指标,且不会修复下游容量不足。
# 代码 25:基于 CPU 利用率的 HPA 结构示例
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: <应用名>
namespace: <命名空间>
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: <应用名>
minReplicas: <最小副本数>
maxReplicas: <最大副本数>
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: <目标CPU利用率百分比>
迁移切流前,先把所有准备动作在灰度 namespace 或灰度域名完成。以下脚本只做 API 校验、部署与滚动等待,不修改 DNS、不删除 Swarm stack;它是切流前的一个检查点,不是完整迁移自动化。镜像仓库凭据、namespace、RBAC、存储类、IngressClass 和监控接入都应已就绪。
#!/usr/bin/env bash
# 代码 26:Kubernetes 灰度发布前置核验
set -euo pipefail
namespace="<命名空间>"
manifest_dir="<清单目录>"
app_name="<应用名>"
kubectl apply --dry-run=server -n "$namespace" -f "$manifest_dir"
kubectl apply -n "$namespace" -f "$manifest_dir"
kubectl rollout status -n "$namespace" "deployment/$app_name" --timeout=<等待时长>
kubectl get deployment,service,pod -n "$namespace" \
-l "app.kubernetes.io/name=$app_name" -o wide
kubectl get endpointslice -n "$namespace" \
-l "kubernetes.io/service-name=$app_name" -o yaml
如果灰度发布失败,停在当前阶段检查事件、日志、镜像、探针、网络策略和存储,不要切 DNS。真正切流应采用现有负载均衡或 DNS 的低风险机制,例如权重灰度、短 TTL 前置验证、可观测的分批流量,并保留 Swarm 原服务健康、未缩容、可立即回切。
发生业务回归时,Kubernetes 侧的回滚与入口回切要分开处理:先停止新增流量,再确认回滚目标版本可用,最后让入口恢复到已知健康的一侧。不要先删除 PVC、Deployment 或 namespace;这些动作可能造成数据丢失或让排障证据消失。
# 代码 27:只回滚 Deployment 版本并核验,不删除任何资源
namespace="<命名空间>"
app_name="<应用名>"
kubectl rollout history -n "$namespace" "deployment/$app_name"
kubectl rollout undo -n "$namespace" "deployment/$app_name" --to-revision=<已验证修订号>
kubectl rollout status -n "$namespace" "deployment/$app_name" --timeout=<等待时长>
kubectl get pod -n "$namespace" \
-l "app.kubernetes.io/name=$app_name" -o wide
rollout undo 只能回到 Kubernetes 仍保留的 Deployment revision,且不会回滚 ConfigMap、Secret、外部数据库、Ingress 控制器设置或 DNS。迁移计划应单独记录这些对象的版本和恢复方式;回滚成功的证据包括入口真实请求成功、后端 EndpointSlice Ready、错误率恢复以及 Swarm 或 K8s 中被选中的服务与预期一致。
成功标准不是“Pod Running”,而是路由、数据、配置、资源与回滚都已被可观测地迁移。把六个坑在切换前逐项关闭,迁移才不会变成线上补设计。更多生产环境迁移与运维实践,欢迎来 云栈社区 一起交流。