0%

使用Helm安装、 升级、 回滚Kubernetes应用

前言

在我之前做的项目里,我们对Microk8s微服务的更新是通过自制tar包的方式做的, tar包存储了镜像和YAML文件。 每次升级时,我们需要先删除所有的YAML资源,然后重新创建新的资源。 这种方式存在以下问题:

  • 服务中断:由于需要先删除旧资源再创建新资源,这会导致短暂的服务中断,影响用户体验
  • 复杂的回滚逻辑:如果升级失败,回滚到之前的版本变得非常复杂,需要手动恢复旧的YAML文件,并且容易出错

了解到Helm可以有效解决以上问题, Helm是Kubernetes 的包管理工具,方便用户快速发现、 共享和使用Kubernetes构建的应用。 以下举例演示如何使用Helm实现安装、升级、回滚操作

环境准备

创建一个Flask应用

创建一个简单的 Flask 应用,并准备两个版本:

  • 1.0版本, 输出 “Hello, foo!”
  • 2.0版本, 输出 “Hello, foo v2!”

文件结构

1
2
3
4
flask-app/
├── app.py
├── requirements.txt
└── Dockerfile

app.py

1
2
3
4
5
6
7
8
9
10
from flask import Flask

app = Flask(__name__)

@app.route('/', methods=['GET'])
def root():
return "hello foo"

if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)

requirements.txt

1
Flask

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
FROM rockylinux:9.3

RUN dnf install -y python3 python3-pip

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY app.py .

CMD ["python3", "app.py"]

构建并测试镜像

构建镜像

1
docker build -t mydockerregistry.com:5000/flask-app:1.0 .

本地运行容器测试

1
2
3
docker run -d -p 8000:8000 mydockerregistry.com:5000/flask-app:1.0
curl localhost:8000/
hello foo

测试OK后, 推送镜像到repo

1
docker push mydockerregistry.com:5000/flask-app:1.0

配置Kubernetes环境

创建secret存储私有镜像仓库认证信息

为了从私有镜像仓库拉取镜像,我们需要在Kubernetes集群中创建一个Secret,用于存储认证信息。 这一步是必要的,因为Kubernetes默认无法直接访问需要认证的私有镜像仓库
执行以下命令创建Secret

1
2
3
4
5
kubectl create secret docker-registry my-registry-secret \
--docker-server=mydockerregistry.com:5000 \
--docker-username=dockeruser \
--docker-password=123456 \
--namespace flask-app

说明:

  • 该 Secret 将被 Helm Chart 中的 imagePullSecrets 引用,确保 Kubernetes 能够从私有镜像仓库拉取镜像。

配置 Containerd 忽略证书验证(仅限测试环境)

在每个节点上,编辑 /etc/containerd/config.toml 文件,添加以下配置以忽略私有镜像仓库的证书验证

1
2
3
4
[plugins."io.containerd.grpc.v1.cri".registry]
[plugins."io.containerd.grpc.v1.cri".registry.configs]
[plugins."io.containerd.grpc.v1.cri".registry.configs."mydockerregistry.com:5000".tls]
insecure_skip_verify = true

重启 containerd 服务以应用更改

1
systemctl restart containerd

创建Helm Chart

Helm Chart 是 Helm 的核心组件,它允许我们以模板化的方式定义 Kubernetes 资源。下面我们创建一个名为 flask-app-chart 的 Chart。

1
helm create flask-app-chart

生成的文件结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
flask-app-chart/
├── charts
├── Chart.yaml
├── templates
│   ├── deployment.yaml
│   ├── _helpers.tpl
│   ├── hpa.yaml
│   ├── ingress.yaml
│   ├── NOTES.txt
│   ├── serviceaccount.yaml
│   ├── service.yaml
└── values.yaml

修改Chart文件
逐个修改以下文件, 或者直接使用: https://github.com/PCJ600/helm-chart-demo/tree/main/charts/v1.0/flask-app-chart

Chart.yaml

1
2
3
4
5
6
apiVersion: v2
name: flask-app-chart
description: A Helm chart for deploying a simple Flask app
type: application
version: 1.0.0 # Chart 版本
appVersion: "1.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
# 全局名称覆盖
fullnameOverride: ""
nameOverride: ""

# 副本数
replicaCount: 6

# 镜像配置
image:
repository: "mydockerregistry.com:5000/flask-app"
tag: "1.0"
pullPolicy: "IfNotPresent"
pullSecrets:
- my-registry-secret

# 资源限制
resources:
limits:
cpu: "500m"
memory: "512Mi"
requests:
cpu: "250m"
memory: "256Mi"

# 环境变量
env:
ENV_VAR_1: "value1"
ENV_VAR_2: "value2"

# Ingress 配置
ingress:
enabled: true
host: "flask-app.example.com"
path: "/foo(/|$)(.*)"
pathType: "ImplementationSpecific"

# Service 配置
service:
port: 8000

serviceAccount:
create: false
name: ""

autoscaling:
enabled: false

templates/deployment.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
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "flask-app-chart.fullname" . }}
labels:
app: {{ include "flask-app-chart.name" . }}
chart: {{ .Chart.Name }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
replicas: {{ .Values.replicaCount | default 1 }}
selector:
matchLabels:
app: {{ include "flask-app-chart.name" . }}
template:
metadata:
labels:
app: {{ include "flask-app-chart.name" . }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default "latest" }}"
imagePullPolicy: {{ .Values.image.pullPolicy | default "IfNotPresent" }}
ports:
- containerPort: 8000
protocol: TCP
resources:
{{- toYaml .Values.resources | nindent 12 }}
env:
{{- if .Values.env }}
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- end }}
imagePullSecrets:
{{- if .Values.image.pullSecrets }}
{{- range .Values.image.pullSecrets }}
- name: {{ . }}
{{- end }}
{{- else }}
- name: my-registry-secret
{{- end }}

注: 配置了imagePullSecrets, 支持从私有仓库拉取镜像

template/service.yaml

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: {{ include "flask-app-chart.fullname" . }}
namespace: {{ .Release.Namespace }}
spec:
selector:
app: {{ include "flask-app-chart.name" . }}
ports:
- port: {{ .Values.service.port | default 8000 }}
targetPort: {{ .Values.service.port | default 8000 }}
type: ClusterIP

template/ingress.yaml
定义ingress模板, 支持集群外部访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "flask-app-chart.fullname" . }}-ingress
namespace: {{ .Release.Namespace }}
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2 # 将 /foo/(.*) 重写为 /$1
spec:
ingressClassName: nginx # 指定 IngressClass 名称
rules:
- host: {{ .Values.ingress.host | quote }}
http:
paths:
- path: {{ .Values.ingress.path | default "/foo(/|$)(.*)" }}
pathType: {{ .Values.ingress.pathType | default "ImplementationSpecific" }}
backend:
service:
name: {{ include "flask-app-chart.fullname" . }}
port:
number: {{ .Values.service.port | default 8000 }}

templates/_helpers.tpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{{/*
Create a default fully qualified app name.
We truncate at 63 characters because some Kubernetes name fields are limited to this (by the DNS naming spec).
*/}}
{{- define "flask-app-chart.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Chart.Name .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}

{{/*
Create a default name for the chart.
*/}}
{{- define "flask-app-chart.name" -}}
{{- default .Chart.Name .Values.nameOverride -}}
{{- end -}}

本地测试部署
在正式发布 Chart 到仓库之前,可以先在本地进行测试部署

1
2
kubectl create ns flask-app
helm install flask-app ./flask-app-chart --namespace flask-app

验证OK

1
2
# curl flask-app.example.com/foo
hello foo

测试完成后,可以卸载Chart, 后面准备发布Chart到仓库

1
helm -n flask-app uninstall flask-app

发布Chart到仓库

检查Chart的正确性

1
2
3
4
helm lint flask-app-chart
# helm lint flask-app-chart/
==> Linting flask-app-chart/
[INFO] Chart.yaml: icon is recommended

打包Chart

1
2
# helm package flask-app-chart
Successfully packaged chart and saved it to: /path/to/flask-app-chart-1.0.0.tgz

上传 Chart 到私有 Helm 仓库

1
curl -u "helmuser:123456" --data-binary "@flask-app-chart-1.0.0.tgz" https://myhelmrepo.com/api/charts -k

如果需要删除旧版本的 Chart,使用以下命令

1
curl -X DELETE "https://myhelmrepo.com/api/charts/flask-app-chart/0.1.0" -u "helmuser:123456"

通过Helm Repo安装Chart

添加私有 Helm 仓库

1
2
3
4
helm repo add my-private-repo https://myhelmrepo.com \
--ca-file ~/.helm/certs/myhelmrepo.com.crt \
--username helmuser \
--password 123456

更新仓库缓存并查看可用的 Charts

1
2
3
4
helm repo update
helm search repo -l flask
NAME CHART VERSION APP VERSION DESCRIPTION
my-private-repo/flask-app-chart 1.0.0 1.0 A Helm chart for deploying a simple Flask app

安装Chart到指定命名空间flask-app

1
helm install flask-app my-private-repo/flask-app-chart --namespace flask-app --version 1.0.0

验证安装是否成功

1
2
# curl flask-app.example.com/foo
hello foo

再发布2.0版本的Chart, 用于测试升级和回滚功能

改下app.py, 返回’hello foo v2’

1
2
3
4
5
6
7
8
9
10
from flask import Flask

app = Flask(__name__)

@app.route('/', methods=['GET'])
def root():
return "hello foo v2"

if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)

构建并推送新版本镜像

1
2
docker build -t mydockerregistry.com:5000/flask-app:2.0 .
docker push mydockerregistry.com:5000/flask-app:2.0

更新Chart配置, 修改Chart.yaml和values.yaml

Chart.yaml
更新版本号为2.0.0, 并设置appVersion为2.0

1
2
3
4
5
6
7
# cat Chart.yaml
apiVersion: v2
name: flask-app-chart
description: A Helm chart for deploying a simple Flask app
type: application
version: 2.0.0
appVersion: "2.0"

改下values.yaml, 将镜像版本从1.0更新为2.0

1
2
3
image:
repository: "mydockerregistry.com:5000/flask-app"
tag: "2.0"

打包并上传新的Chart

1
2
helm package flask-app-chart
curl -u "helmuser:123456" --data-binary "@flask-app-chart-2.0.0.tgz" https://myhelmrepo.com/api/charts -k

使用Helm升级到新版本

使用helm upgrade将应用从1.0版本升级到2.0版本

1
helm upgrade flask-app my-private-repo/flask-app-chart --version 2.0.0 --namespace flask-app --atomic

注: Helm支持通过–atomic参数实现原子性操作。如果升级失败,Helm会自动回滚到之前的版本

验证升级是否成功

1
2
# curl flask-app.example.com/foo
hello foo v2

watch观察升级过程, 可以看出是滚动更新方式

1
2
3
4
5
6
7
8
9
10
11
12
13
watch "kubectl get pods -n flask-app"
NAMESPACE NAME READY STATUS RESTARTS AGE
flask-app flask-app-chart-flask-app-5c86b6ffdf-4jlqb 1/1 Running 0 31s
flask-app flask-app-chart-flask-app-5c86b6ffdf-kkw57 1/1 Running 0 30s
flask-app flask-app-chart-flask-app-5c86b6ffdf-qlnhv 1/1 Running 0 30s
flask-app flask-app-chart-flask-app-5c86b6ffdf-s9tr5 1/1 Running 0 31s
flask-app flask-app-chart-flask-app-5c86b6ffdf-w6776 1/1 Running 0 30s
flask-app flask-app-chart-flask-app-5c86b6ffdf-xwgtp 1/1 Running 0 31s
flask-app flask-app-chart-flask-app-5d44b77c54-72mhg 0/1 Terminating 0 3m17s
flask-app flask-app-chart-flask-app-5d44b77c54-gvgv7 0/1 Terminating 0 3m17s
flask-app flask-app-chart-flask-app-5d44b77c54-mrj5h 0/1 Terminating 0 3m17s
flask-app flask-app-chart-flask-app-5d44b77c54-r2bdp 1/1 Terminating 0 3m17s
flask-app flask-app-chart-flask-app-5d44b77c54-w7v67 1/1 Terminating 0 3m17s

使用Helm回滚到旧版本

查看历史记录

1
2
3
4
helm history flask-app -n flask-app
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 Sun Mar 23 19:15:11 2025 superseded flask-app-chart-1.0.0 1.0 Install complete
2 Sun Mar 23 19:17:57 2025 deployed flask-app-chart-2.0.0 2.0 Upgrade complete
  • REVISION 1: 最初安装的1.0的版本
  • REVISION 2: 升级到2.0的版本

执行回滚命令, 回滚到1.0版本

1
2
helm rollback flask-app 1 -n flask-app
Rollback was a success! Happy Helming!

验证回滚成功

1
2
3
4
5
# helm list -n flask-app
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
flask-app flask-app 3 2025-03-23 19:20:54.762690061 +0800 CST deployed flask-app-chart-1.0.0 1.0
# curl flask-app.example.com/foo
hello foo

总结

Helm vs 传统方式的对比

特性 传统方式 Helm
版本管理 手动维护多个 YAML 文件,容易混乱。 版本化管理,清晰记录每个版本的变化。
升级操作 需要手动删除旧版本并应用新版本,可能导致服务中断。 智能化更新,仅更改必要的部分,避免服务中断。
回滚操作 需要手动恢复旧版本的 YAML 文件,操作复杂且容易出错。 一键回滚到任意历史版本,简单高效。
差异追踪 难以知道两个版本之间的具体差异,容易遗漏更改。 Helm 自动计算差异,确保所有更改都被正确应用。
配置灵活性 需要手动编辑多个 YAML 文件,难以适应不同环境(如 dev、staging、prod)。 支持参数化配置,通过 values.yaml 动态调整,适应不同环境需求。
依赖管理 需要手动管理依赖关系,容易遗漏或冲突。 自动管理依赖关系,确保所有组件协同工作。

参考

https://helm.sh/docs/topics/charts/