ᕕ( ᐛ )ᕗ Jimyag's Blog

k8s 灰度发布

· 1402 字 · 约 7 分钟

灰度发布(Canary Release)是一种降低发布风险的部署策略。通过让小部分用户先访问新版本,观察没问题后再逐步扩大范围,最终完成全量发布。

蓝绿部署(Blue-Green Deployment)是灰度发布的一种实现方式:同时部署两个版本(蓝色和绿色),通过 Service 的 selector 切换流量,实现零停机发布。

本文介绍如何使用 Helm 在 Kubernetes 中实现基于 Service 的蓝绿部署。

核心原理

蓝绿部署的核心思路:

  1. 双版本并存:同时运行蓝色(blue)和绿色(green)两个版本的 Deployment
  2. Service 选择器:通过 Service 的 selector 控制流量流向哪个版本
  3. 平滑切换:修改 Service 的 selector 即可切换版本,无需重启 Pod
  4. 快速回滚:如果新版本有问题,切换回旧版本即可

优势:

  • 零停机发布:流量切换瞬间完成
  • 快速回滚:切换 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 }}

关键点:

  • greenLabelsblueLabels:为 Pod 和 Deployment 添加版本标签
  • greenSelectorLabelsblueSelectorLabels:用于 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,这是为了简化演示。实际使用时,可以通过以下方式切换:

  1. 修改 values.yaml 中的 replicaCount(0 表示不接收流量)
  2. 或者修改 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:首次部署

  1. 设置 green.replicaCount: 3blue.replicaCount: 0
  2. 执行 helm upgrade --install
  3. Service 流量全部导向绿色版本

场景 2:发布新版本

  1. 修改 blue 配置为新版本镜像
  2. 设置 blue.replicaCount: 1(小流量验证)
  3. 执行 helm upgrade
  4. 观察新版本运行情况

场景 3:全量切换

  1. 确认新版本没问题
  2. 设置 blue.replicaCount: 3green.replicaCount: 0
  3. 执行 helm upgrade
  4. 流量全部切换到蓝色版本

场景 4:快速回滚

如果新版本有问题:

  1. 设置 blue.replicaCount: 0green.replicaCount: 3
  2. 执行 helm upgrade
  3. 流量立即切换回旧版本

注意事项

  1. 资源规划:同时运行两个版本需要足够的资源
  2. 数据库兼容性:确保新版本兼容旧版本的数据库 schema
  3. 配置管理:使用 ConfigMap 管理配置,避免硬编码
  4. 监控告警:部署后密切监控新版本指标
  5. 回滚演练:定期演练回滚流程,确保回滚可靠

总结

基于 Service 的蓝绿部署是一种简单有效的灰度发布方案:

  • 优点:实现简单、切换快速、回滚方便
  • 缺点:资源占用高、需要维护双倍配置

适用于对稳定性要求高、资源充足的生产环境。对于资源受限的场景,可以考虑使用 Ingress 或 Service Mesh 实现更细粒度的流量控制。

#K8s