k8s 灰度发布
· 1402 字 · 约 7 分钟
灰度发布(Canary Release)是一种降低发布风险的部署策略。通过让小部分用户先访问新版本,观察没问题后再逐步扩大范围,最终完成全量发布。
蓝绿部署(Blue-Green Deployment)是灰度发布的一种实现方式:同时部署两个版本(蓝色和绿色),通过 Service 的 selector 切换流量,实现零停机发布。
本文介绍如何使用 Helm 在 Kubernetes 中实现基于 Service 的蓝绿部署。
核心原理
蓝绿部署的核心思路:
- 双版本并存:同时运行蓝色(blue)和绿色(green)两个版本的 Deployment
- Service 选择器:通过 Service 的 selector 控制流量流向哪个版本
- 平滑切换:修改 Service 的 selector 即可切换版本,无需重启 Pod
- 快速回滚:如果新版本有问题,切换回旧版本即可
优势:
- 零停机发布:流量切换瞬间完成
- 快速回滚:切换 selector 即可回滚
- 验证充分:新版本可以先小流量验证
劣势:
- 资源占用:需要同时运行两个版本
- 复杂度增加:需要维护两套配置
Helm 模板结构
本示例的 Helm Chart 结构如下:
1
2
3
4
5
6
7
8
9
10
|
nginx/
├── Chart.yaml
├── values.yaml
└── templates/
├── _helpers.tpl # 模板辅助函数
├── deployment-blue.yaml # 蓝色版本 Deployment
├── deployment-green.yaml # 绿色版本 Deployment
├── service.yaml # Service(通过 selector 控制流量)
├── configmap-blue.yaml # 蓝色版本配置
└── configmap-green.yaml # 绿色版本配置
|
模板辅助函数
_helpers.tpl 定义了标签模板,用于区分蓝色和绿色版本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
# templates/_helpers.tpl
{{- define "nginx.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- define "nginx.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "nginx.greenLabels" -}}
{{ include "nginx.commonLabels" . }}
app.kubernetes.io/logverse-canary-type: green
{{- end }}
{{- define "nginx.blueLabels" -}}
{{ include "nginx.commonLabels" . }}
app.kubernetes.io/logverse-canary-type: blue
{{- end }}
{{- define "nginx.greenSelectorLabels" -}}
{{ include "nginx.commonSelectorLabels" . }}
app.kubernetes.io/logverse-canary-type: green
{{- end }}
{{- define "nginx.blueSelectorLabels" -}}
{{ include "nginx.commonSelectorLabels" . }}
app.kubernetes.io/logverse-canary-type: blue
{{- end }}
{{- define "nginx.commonLabels" -}}
helm.sh/chart: {{ include "nginx.chart" . }}
{{ include "nginx.commonSelectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "nginx.commonSelectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
|
关键点:
greenLabels 和 blueLabels:为 Pod 和 Deployment 添加版本标签
greenSelectorLabels 和 blueSelectorLabels:用于 Deployment 的 selector
canary-type 标签:区分蓝色和绿色版本
蓝色版本 Deployment
蓝色版本使用 app.kubernetes.io/logverse-canary-type: blue 标签:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
# templates/deployment-blue.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: {{ .Release.Namespace }}
name: {{ include "nginx.fullname" . }}-blue
labels:
{{- include "nginx.blueLabels" . | nindent 4 }}
spec:
strategy: # 更新策略
rollingUpdate: # 滚动更新配置
maxSurge: 1 # 一次滚动更新的最大副本数
maxUnavailable: 0 # 进行滚动更新时,最大不可用比例更新比例,表示在所有副本数中,最多可以有多少个不更新成功
type: RollingUpdate # 更新类型,采用滚动更新
selector:
matchLabels:
{{- include "nginx.blueSelectorLabels" . | nindent 6 }}
replicas: {{ .Values.blue.replicaCount }}
template:
metadata:
annotations:
# 配置文件改变时,自动重启
checksum/config: {{ include (print $.Template.BasePath "/configmap-blue.yaml") . | sha256sum }}
labels:
{{- include "nginx.blueLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
containers:
- name: {{ .Chart.Name }}-blue
image: "{{ .Values.blue.image.repository }}:{{ .Values.blue.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.blue.volumeMounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.blue.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
|
注意:
replicas 通过 values.yaml 控制,默认可以设置为 0(不接收流量)
selector 必须匹配 Pod 的 labels
绿色版本 Deployment
绿色版本与蓝色版本类似,只是标签不同:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
# templates/deployment-green.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: {{ .Release.Namespace }}
name: {{ include "nginx.fullname" . }}-green
labels:
{{- include "nginx.greenLabels" . | nindent 4 }}
spec:
strategy: # 更新策略
rollingUpdate: # 滚动更新配置
maxSurge: 1 # 一次滚动更新的最大副本数
maxUnavailable: 0 # 进行滚动更新时,最大不可用比例更新比例,表示在所有副本数中,最多可以有多少个不更新成功
type: RollingUpdate # 更新类型,采用滚动更新
selector:
matchLabels:
{{- include "nginx.greenSelectorLabels" . | nindent 6 }}
replicas: {{ .Values.green.replicaCount }}
template:
metadata:
annotations:
# 配置文件改变时,自动重启
checksum/config: {{ include (print $.Template.BasePath "/configmap-green.yaml") . | sha256sum }}
labels:
{{- include "nginx.greenLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
containers:
- name: {{ .Chart.Name }}-green
image: "{{ .Values.green.image.repository }}:{{ .Values.green.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.green.volumeMounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.green.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
|
Service 配置
Service 是流量切换的关键。它通过 selector 选择将流量导向蓝色还是绿色版本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# templates/service.yaml
apiVersion: v1
kind: Service
metadata:
namespace: {{ .Release.Namespace }}
name: {{ include "nginx.fullname" . }}
labels:
{{- include "nginx.commonLabels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "nginx.commonSelectorLabels" . | nindent 4 }}
|
关键点:Service 的 selector 只包含 commonSelectorLabels,不包含 canary-type。
这意味着:
- 如果要切换到绿色版本,需要修改 Service 的 selector,添加
app.kubernetes.io/logverse-canary-type: green
- 如果要切换到蓝色版本,添加
app.kubernetes.io/logverse-canary-type: blue
但是,本示例中 Service 没有包含 canary-type,这是为了简化演示。实际使用时,可以通过以下方式切换:
- 修改 values.yaml 中的
replicaCount(0 表示不接收流量)
- 或者修改 Service 的 selector
ConfigMap 配置
蓝色和绿色版本可以有不同的配置文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# templates/configmap-blue.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "nginx.fullname" . }}-blue
labels:
{{- include "nginx.blueLabels" . | nindent 4 }}
namespace: {{ .Release.Namespace }}
data:
{{- range $key, $value := .Values.blue.configmaps }}
{{ $key }}: |-
{{ $value | indent 4 }}
{{- end -}}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# templates/configmap-green.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "nginx.fullname" . }}-green
labels:
{{- include "nginx.greenLabels" . | nindent 4 }}
namespace: {{ .Release.Namespace }}
data:
{{- range $key, $value := .Values.green.configmaps }}
{{ $key }}: |-
{{ $value | indent 4 }}
{{- end -}}
|
Chart 元数据
1
2
3
4
5
6
7
|
# Chart.yaml
apiVersion: v2
name: nginx
description: A Helm chart for Kubernetes
type: application
version: 0.1.0
appVersion: "1.16.0"
|
配置值
values.yaml 定义了蓝色和绿色版本的配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
# values.yaml
green:
image:
repository: nginx
tag: 1.21
replicaCount: 5
configmaps:
index.html: |-
this is 1.21 green index.html
volumes:
- name: index
configMap:
name: test-nginx-green
items:
- key: index.html
path: index.html
volumeMounts:
- name: index
mountPath: /usr/share/nginx/html/index.html
subPath: index.html
readOnly: true
blue:
image:
repository: nginx
tag: 1.21
replicaCount: 0
configmaps:
index.html: |-
this is 1.21 blue index.html
volumes:
- name: index
configMap:
name: test-nginx-blue
items:
- key: index.html
path: index.html
volumeMounts:
- name: index
mountPath: /usr/share/nginx/html/index.html
subPath: index.html
readOnly: true
fullnameOverride: ""
podAnnotations: {}
podLabels: {}
service:
type: ClusterIP
port: 80
resources: {}
nodeSelector: {}
tolerations: []
affinity: {}
|
关键配置说明:
green.replicaCount: 5:绿色版本运行 5 个副本(当前版本)
blue.replicaCount: 0:蓝色版本运行 0 个副本(旧版本,不接收流量)
- 可以通过调整
replicaCount 控制流量分配
部署命令
使用 Helm 部署:
1
|
helm upgrade --install -f values.yaml --description "$description" --create-namespace -n dev test-nginx ./
|
参数说明:
--upgrade --install:如果不存在则安装,存在则升级
-f values.yaml:指定配置文件
--description:添加部署描述
--create-namespace:如果 namespace 不存在则创建
-n dev:部署到 dev 命名空间
test-nginx:Release 名称
实际使用流程
场景 1:首次部署
- 设置
green.replicaCount: 3,blue.replicaCount: 0
- 执行
helm upgrade --install
- Service 流量全部导向绿色版本
场景 2:发布新版本
- 修改
blue 配置为新版本镜像
- 设置
blue.replicaCount: 1(小流量验证)
- 执行
helm upgrade
- 观察新版本运行情况
场景 3:全量切换
- 确认新版本没问题
- 设置
blue.replicaCount: 3,green.replicaCount: 0
- 执行
helm upgrade
- 流量全部切换到蓝色版本
场景 4:快速回滚
如果新版本有问题:
- 设置
blue.replicaCount: 0,green.replicaCount: 3
- 执行
helm upgrade
- 流量立即切换回旧版本
注意事项
- 资源规划:同时运行两个版本需要足够的资源
- 数据库兼容性:确保新版本兼容旧版本的数据库 schema
- 配置管理:使用 ConfigMap 管理配置,避免硬编码
- 监控告警:部署后密切监控新版本指标
- 回滚演练:定期演练回滚流程,确保回滚可靠
总结
基于 Service 的蓝绿部署是一种简单有效的灰度发布方案:
- 优点:实现简单、切换快速、回滚方便
- 缺点:资源占用高、需要维护双倍配置
适用于对稳定性要求高、资源充足的生产环境。对于资源受限的场景,可以考虑使用 Ingress 或 Service Mesh 实现更细粒度的流量控制。
#K8s