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

前言

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

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

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

环境准备

创建一个Flask应用

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

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

文件结构

flask-app/
├── app.py
├── requirements.txt
└── Dockerfile

app.py

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

Flask

Dockerfile

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"]

构建并测试镜像

构建镜像

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

本地运行容器测试

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

测试OK后, 推送镜像到repo

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

配置Kubernetes环境

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

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

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 文件,添加以下配置以忽略私有镜像仓库的证书验证

[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 服务以应用更改

systemctl restart containerd

创建Helm Chart

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

helm create flask-app-chart

生成的文件结构如下:

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

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

# 全局名称覆盖
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

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

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模板, 支持集群外部访问

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

{{/*
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 到仓库之前,可以先在本地进行测试部署

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

验证OK

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

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

helm -n flask-app uninstall flask-app

发布Chart到仓库

检查Chart的正确性

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

打包Chart

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

上传 Chart 到私有 Helm 仓库

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

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

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

通过Helm Repo安装Chart

添加私有 Helm 仓库

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

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

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

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

验证安装是否成功

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

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

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

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)

构建并推送新版本镜像

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

# 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

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

打包并上传新的Chart

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版本

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

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

验证升级是否成功

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

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

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回滚到旧版本

查看历史记录

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版本

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

验证回滚成功

# 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/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

pcj_888

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值