第 40 篇 k8s之Helm:编写自定义 Helm Chart

IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。


在第 39 篇中,我们用 helm create 生成了一个 Chart 骨架,把 Flask + Redis 计数器应用改造成了 Helm Release,体验了"一条命令部署、升级、回滚"的便利。但 helm create 生成的模板是通用的 Nginx 示例,我们只是"改参数"而非"从零构建"。如果现在要你把一个全新的微服务应用 Chart 化,你能否独立完成?

今天这篇,我们就从零开始 ,手动编写一个完整的、生产级的 Helm Chart。从 Chart 的目录结构、Chart.yaml 元数据、values.yaml 设计原则,到 Go Template 模板语法、内置对象、流程控制、命名模板,再到 Chart 的打包和发布,全部覆盖。学完这篇,你将拥有将任意 K8s 应用封装为 Helm Chart 的能力。

一、Chart 的完整结构

一个规范的 Helm Chart 目录结构如下:

bash 复制代码
flask-redis-chart/
├── Chart.yaml          # Chart 元数据(名称、版本、描述)
├── values.yaml         # 默认配置值
├── values.schema.json  # 可选:JSON Schema 校验 values
├── charts/             # 子 Chart 依赖
├── templates/          # K8s 资源模板
│   ├── NOTES.txt       # helm install 后显示的信息
│   ├── _helpers.tpl    # 命名模板(可复用的模板片段)
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── configmap.yaml
│   ├── secret.yaml
│   ├── pvc.yaml
│   └── hpa.yaml
├── .helmignore         # 打包时忽略的文件
├── README.md           # 使用文档
└── LICENSE             # 可选:开源协议
  • Chart.yaml:Chart 的"身份证",包含名称、版本、API 版本等元数据

  • values.yaml :默认配置值,模板中的 {{ .Values.xxx }} 从这里取值

  • templates/:所有 K8s 资源模板,Helm 渲染后生成最终的 YAML 提交给 API Server

  • _helpers.tpl:存放可复用的模板宏,避免在多个文件中重复相同的模板代码

  • NOTES.txt:部署成功后的提示信息,指导用户如何使用部署好的服务

二、从零开始编写 Chart

我们从零开始构建一个 Flask + Redis 应用的 Helm Chart,不依赖 helm create

2.1 编写 Chart.yaml

bash 复制代码
apiVersion: v2
name: flask-redis-counter
description: A Helm chart for Flask + Redis Counter Application
type: application
version: 1.0.0
appVersion: "3.0"
maintainers:
  - name: IT策士
    email: itcishi@example.com
keywords:
  - flask
  - redis
  - counter
home: https://github.com/itcishi/flask-redis-counter

字段说明:

  • apiVersion: v2:Helm v3 的 Chart API 版本

  • name:Chart 名称,必须与目录名一致

  • version:Chart 自身的版本(SemVer 格式),Chart 模板变更时递增

  • appVersion:应用(镜像)的版本,与 version 独立,应用镜像升级时只需改此字段

  • type: applicationapplication 表示这是一个应用 Chart;library 表示这是一个库 Chart(只包含可复用的模板宏)

2.2 设计 values.yaml

好的 values.yaml 应该像一份"配置菜单"------结构清晰、注释详尽、有合理的默认值。这是 Helm Chart 设计中最见功力的部分:

bash 复制代码
# ============================================================
# Flask + Redis 计数器应用 ------ Helm Chart 默认配置
# ============================================================

# -- 全局配置
global:
  environment: production

# -- Flask 应用配置
flask:
  # -- 副本数
  replicaCount: 3

  # -- 镜像配置
  image:
    repository: flask-redis-counter
    tag: "3.0"
    pullPolicy: IfNotPresent

  # -- 服务配置
  service:
    type: ClusterIP
    port: 5000
    targetPort: 5000

  # -- 资源限制
  resources:
    requests:
      cpu: 100m
      memory: 128Mi
    limits:
      cpu: 500m
      memory: 256Mi

  # -- 健康检查
  probes:
    liveness:
      path: /health
      port: 5000
      periodSeconds: 15
      failureThreshold: 3
    readiness:
      path: /health
      port: 5000
      periodSeconds: 5
      failureThreshold: 2
    startup:
      path: /health
      port: 5000
      periodSeconds: 5
      failureThreshold: 12

  # -- 应用环境变量
  config:
    FLASK_ENV: production
    LOG_LEVEL: info
    REDIS_HOST: redis-service
    REDIS_PORT: "6379"

  # -- ServiceAccount
  serviceAccount:
    create: true
    name: flask-sa

# -- Redis 配置
redis:
  # -- 是否部署 Redis(设为 false 可使用外部 Redis)
  enabled: true

  image:
    repository: redis
    tag: alpine

  # -- 持久化存储
  persistence:
    enabled: true
    size: 1Gi
    storageClass: standard

  service:
    name: redis-service
    port: 6379

# -- Ingress 配置
ingress:
  enabled: true
  className: nginx
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
  hosts:
    - host: counter.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: counter-tls
      hosts:
        - counter.example.com

设计要点:

  • 将配置分组flask.*redis.* 层次清晰,避免顶级字段爆炸

  • 提供合理的默认值:所有字段都有默认值,用户不传即可运行

  • 注释说明:每个字段的含义和用途

  • 开关设计redis.enabled 允许用户选择使用内置 Redis 或外部 Redis 服务

  • 探针参数可配置:不同环境可能需要调整探针的灵敏度和超时时间

三、Go Template 模板语法详解

Helm 使用 Go 的 text/template 引擎来渲染模板。以下是编写 Chart 时必须掌握的语法。

3.1 基本取值

bash 复制代码
# 访问 values.yaml 中的值
{{ .Values.flask.replicaCount }}

# 管道操作(类似 Unix 管道,将前一个命令的输出作为后一个命令的输入)
{{ .Values.flask.config.FLASK_ENV | quote }}

# 默认值
{{ .Values.flask.image.tag | default "latest" }}

管道是 Helm 模板中最常用的特性之一。| 将左侧的值传递给右侧的函数处理。quote 函数为字符串添加引号,避免 YAML 解析错误。

3.2 内置对象

除了 .Values,Helm 还提供了丰富的内置对象:

3.3 流程控制

bash 复制代码
# if/else 条件
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
# ... 其余 Ingress 配置
{{- end }}

# with 作用域(简化深层取值)
{{- with .Values.flask.resources }}
resources:
  {{- toYaml . | nindent 2 }}
{{- end }}

# range 遍历
{{- range .Values.ingress.hosts }}
  - host: {{ .host | quote }}
{{- end }}

{{--}} 中的连字符表示去除模板标签前后的空白符,让生成的 YAML 更整洁。nindent 2 表示在内容前添加换行并缩进 2 个空格。

3.4 常用函数

Helm 提供了 Go 模板的完整函数库,还扩展了 Sprig 函数库(共 70+ 函数)。以下是最高频使用的几个:

四、编写模板文件

现在用学到的语法,为 Flask + Redis 应用编写核心模板。

4.1 _helpers.tpl(命名模板)

命名模板用于定义可复用的模板宏------在一个地方定义,多个模板文件中引用:

bash 复制代码
{{/*
Create a default fully qualified app name.
*/}}
{{- define "flask-redis.fullname" -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{/*
Common labels
*/}}
{{- define "flask-redis.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
{{- end -}}

这些标签是 K8s 推荐的标准标签,用于资源分组和版本追踪。include 调用命名模板时必须传递 .(当前上下文),否则模板内无法访问 .Values

4.2 configmap.yaml

bash 复制代码
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-config
  labels:
    {{- include "flask-redis.labels" . | nindent 4 }}
data:
  {{- range $key, $value := .Values.flask.config }}
  {{ $key }}: {{ $value | quote }}
  {{- end }}

range 遍历 flask.config 下的所有键值对,动态生成 ConfigMap 的 data 字段。这样当用户新增配置项时,模板自动识别,无需手动修改 YAML。

4.3 deployment.yaml(核心模板)

bash 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "flask-redis.fullname" . }}
  labels:
    {{- include "flask-redis.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.flask.replicaCount }}
  selector:
    matchLabels:
      app: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app: {{ .Release.Name }}
    spec:
      {{- if .Values.flask.serviceAccount.create }}
      serviceAccountName: {{ .Values.flask.serviceAccount.name }}
      {{- end }}
      containers:
        - name: flask
          image: "{{ .Values.flask.image.repository }}:{{ .Values.flask.image.tag }}"
          imagePullPolicy: {{ .Values.flask.image.pullPolicy }}
          ports:
            - containerPort: {{ .Values.flask.service.port }}
          envFrom:
            - configMapRef:
                name: {{ .Release.Name }}-config
          {{- with .Values.flask.resources }}
          resources:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          {{- if .Values.flask.probes.liveness }}
          livenessProbe:
            httpGet:
              path: {{ .Values.flask.probes.liveness.path }}
              port: {{ .Values.flask.probes.liveness.port }}
            periodSeconds: {{ .Values.flask.probes.liveness.periodSeconds }}
            failureThreshold: {{ .Values.flask.probes.liveness.failureThreshold }}
          {{- end }}
          {{- if .Values.flask.probes.readiness }}
          readinessProbe:
            httpGet:
              path: {{ .Values.flask.probes.readiness.path }}
              port: {{ .Values.flask.probes.readiness.port }}
            periodSeconds: {{ .Values.flask.probes.readiness.periodSeconds }}
            failureThreshold: {{ .Values.flask.probes.readiness.failureThreshold }}
          {{- end }}

几个设计要点:

  • include "flask-redis.fullname" . 使用命名模板生成资源名称,确保同一 Release 的所有资源前缀一致

  • envFrom.configMapRef 将整个 ConfigMap 作为环境变量注入,一行配置覆盖所有键值对,比逐项枚举 env 更简洁

  • withtoYaml 配合使用,当资源配置不为空时优雅地插入 YAML 块

  • 探针通过 if 条件控制,用户可以将某个探针设为空值来禁用

4.4 条件化部署:redis.yaml

通过 redis.enabled 开关,让用户选择使用内置 Redis 还是外部 Redis:

bash 复制代码
{{- if .Values.redis.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
  labels:
    {{- include "flask-redis.labels" . | nindent 4 }}
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
        - name: redis
          image: "{{ .Values.redis.image.repository }}:{{ .Values.redis.image.tag }}"
          ports:
            - containerPort: 6379
          {{- if .Values.redis.persistence.enabled }}
          volumeMounts:
            - name: redis-data
              mountPath: /data
      {{- if .Values.redis.persistence.enabled }}
      volumes:
        - name: redis-data
          persistentVolumeClaim:
            claimName: redis-pvc
      {{- end }}
---
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.redis.service.name }}
spec:
  type: ClusterIP
  selector:
    app: redis
  ports:
    - port: {{ .Values.redis.service.port }}
---
{{- if .Values.redis.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: redis-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: {{ .Values.redis.persistence.size }}
  storageClassName: {{ .Values.redis.persistence.storageClass }}
{{- end }}
{{- end }}

redis.enabled: false 时,整个 Redis Deployment、Service、PVC 都不会被创建。用户可以自行部署 Redis 并在 flask.config.REDIS_HOST 中指定外部服务的地址。

4.5 NOTES.txt

bash 复制代码
{{- if .Values.ingress.enabled }}
✅ Flask + Redis Counter 已部署!

应用名称: {{ .Release.Name }}
访问地址: http{{ if .Values.ingress.tls }}s{{ end }}://{{ index .Values.ingress.hosts 0 "host" }}

{{- else }}
✅ Flask + Redis Counter 已部署!

应用名称: {{ .Release.Name }}
Service 端口: {{ .Values.flask.service.port }}
使用端口转发访问:
  kubectl port-forward svc/{{ .Release.Name }}-service {{ .Values.flask.service.port }}:{{ .Values.flask.service.port }}
{{- end }}

📊 查看状态:
  helm status {{ .Release.Name }}
  kubectl get pods -l app={{ .Release.Name }}

📈 查看日志:
  kubectl logs -f deployment/{{ .Release.Name }}

🔄 升级:
  helm upgrade {{ .Release.Name }} . -f custom-values.yaml

五、打包与发布

5.1 本地验证

在打包之前,必须确保 Chart 模板语法正确、渲染结果符合预期:

bash 复制代码
# 语法检查
helm lint .

# 查看渲染后的 YAML
helm template flask-counter . --debug

# 安装到集群(干运行模式,不实际部署)
helm install flask-counter . --dry-run --debug

5.2 打包

bash 复制代码
helm package .
# Successfully packaged chart and saved it to: flask-redis-counter-1.0.0.tgz

helm package 将 Chart 目录打包为一个 .tgz 文件,并验证 Chart.yaml 中的版本号是否与文件名一致。

5.3 发布到 Chart 仓库

方式一:发布到 GitHub Pages(免费、最常用)

bash 复制代码
# 1. 创建 gh-pages 分支
git checkout --orphan gh-pages

# 2. 生成 Helm 仓库索引
helm repo index . --url https://<你的GitHub用户名>.github.io/flask-redis-chart

# 3. 提交 index.yaml 和 .tgz 文件,推送到 gh-pages 分支

方式二:发布到 OCI 兼容的容器镜像仓库

Helm v3.8+ 支持将 Chart 存储为 OCI 镜像(与容器镜像使用相同的仓库和认证机制),简化了 Chart 仓库的托管:

bash 复制代码
# 登录到 OCI 仓库
helm registry login registry-1.docker.io

# 打包并推送
helm package .
helm push flask-redis-counter-1.0.0.tgz oci://registry-1.docker.io/<你的用户名>

5.4 版本更新

当修改 Chart 模板或 values 后,需要更新版本号并重新打包:

bash 复制代码
# 修改 Chart.yaml 中的 version
vim Chart.yaml  # version: 1.0.0 → 1.1.0

# 重新打包
helm package .

六、常用 Helm 命令速查

七、本篇总结

  • Chart 的完整结构:Chart.yaml 是身份、values.yaml 是菜单、templates/ 是配方、_helpers.tpl 是可复用的模板宏。

  • 模板语法核心{{ .Values.xxx }} 取值、| 管道传递函数、if/else/with/range 流程控制、include 调用命名模板、toYamlnindent 美化输出。

  • 设计原则 :values 提供合理默认值并支持深度自定义,用条件开关(enabled)控制可选组件,命名模板消除重复代码。

  • 打包与发布helm lint/template/install --dry-run 三级验证确保正确性,helm package 打包为标准 .tgz 文件,可通过 GitHub Pages 或 OCI 仓库分发。

下一篇------第 41 篇:监控:Metrics Server 与 Prometheus 快速上手,我们将进入 K8s 可观测性领域,从部署 Metrics Server 开始,逐步搭建 Prometheus + Grafana 监控栈,让集群和应用的运行状态不再是一个黑盒。

想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维 !

相关推荐
曾几何时`1 小时前
Go(四)Channel
开发语言·后端·golang
橘子星2 小时前
前端薅数据神器 Fetch:不用翻墙,在线拿捏后端与 AI 接口
前端·后端
用户925807911482 小时前
画图理解mysql日志机制
java·后端
huzhongqiang2 小时前
120行代码实现一个极简 Agent
后端·agent
XIAOHEZIcode2 小时前
进程、会话与终端——一次真实的 Linux Session 解剖
linux·后端·命令行
枕星而眠2 小时前
【数据结构】树与二叉树基础知识点总结
数据结构·c++·后端·算法·运维开发
极光技术熊2 小时前
从零构建在线Excel:一个Java全栈工程师的实战记录
前端·后端
小谢小哥2 小时前
68-持续集成详解
java·后端·架构
foggyprojects2 小时前
列表里要带子表统计值时,为什么需要 QM 聚合型 JOIN
后端