
灰度发布（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 结构如下：

```
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` 定义了标签模板，用于区分蓝色和绿色版本：

```yaml
# 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` 标签：

```yaml
# 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

绿色版本与蓝色版本类似，只是标签不同：

```yaml
# 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 选择将流量导向蓝色还是绿色版本：

```yaml
# 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 配置

蓝色和绿色版本可以有不同的配置文件：

```yaml
# 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 -}}
```

```yaml
# 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 元数据

```yaml
# Chart.yaml
apiVersion: v2
name: nginx
description: A Helm chart for Kubernetes
type: application
version: 0.1.0
appVersion: "1.16.0"
```

## 配置值

values.yaml 定义了蓝色和绿色版本的配置：

```yaml
# 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 部署：

```bash
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: 3`，`blue.replicaCount: 0`
2. 执行 `helm upgrade --install`
3. Service 流量全部导向绿色版本

### 场景 2：发布新版本

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

### 场景 3：全量切换

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

### 场景 4：快速回滚

如果新版本有问题：

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

## 注意事项

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

## 总结

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

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

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

