目录

Kubernetes-09 Helm 包管理:从入门到实战

1. Helm 是什么

Helm 是 K8s 的包管理器,类比:

  • Linux 的 apt/yum
  • Node.js 的 npm
  • Go 的 go mod

解决的问题:

没有 Helm 时,部署一个有多个组件的应用(Deployment + Service + Ingress + ConfigMap + HPA + RBAC…)需要维护多个独立的 YAML 文件,没有版本管理,多环境配置困难。

Helm 的核心概念:

概念 类比 描述
Chart npm package 包含应用所有 K8s 资源的模板集合
Release 安装的 package Chart 安装到集群后的一个实例
Repository npm registry 存储和分发 Chart 的仓库
Values 配置文件 自定义 Chart 的参数

2. 安装与基础命令

# 安装 Helm
brew install helm  # macOS
# 或
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# 验证
helm version

2.1 仓库管理

# 添加常用仓库
helm repo add stable https://charts.helm.sh/stable
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx

# 更新仓库索引
helm repo update

# 搜索 Chart
helm search repo redis
helm search repo mysql

# 查看 Chart 信息
helm show chart bitnami/redis
helm show values bitnami/redis    # 查看所有可配置参数

2.2 安装与管理 Release

# 安装(最简单形式)
helm install my-redis bitnami/redis

# 安装时覆盖参数
helm install my-redis bitnami/redis \
  --set auth.password=mypassword \
  --set replica.replicaCount=1

# 使用 values 文件
helm install my-redis bitnami/redis -f redis-values.yaml

# 指定命名空间
helm install my-redis bitnami/redis \
  --namespace production \
  --create-namespace

# 查看所有 Release
helm list
helm list -A           # 所有命名空间
helm list -n production

# 查看 Release 状态
helm status my-redis

# 升级
helm upgrade my-redis bitnami/redis --set replica.replicaCount=3

# 升级(不存在则安装)
helm upgrade --install my-redis bitnami/redis -f values.yaml

# 回滚
helm rollback my-redis 1    # 回滚到版本 1
helm rollback my-redis      # 回滚到上一版本

# 查看历史版本
helm history my-redis

# 卸载
helm uninstall my-redis
helm uninstall my-redis --keep-history  # 保留历史记录(可以回滚)

2.3 调试命令

# 渲染模板(不安装,只看生成的 YAML)
helm template my-app ./my-chart -f values.yaml

# 渲染并校验
helm template my-app ./my-chart | kubectl apply --dry-run=client -f -

# lint 检查
helm lint ./my-chart
helm lint ./my-chart -f values.yaml

# 渲染特定模板
helm template my-app ./my-chart --show-only templates/deployment.yaml

3. Chart 结构详解

# 创建新 Chart
helm create go-app

# 目录结构
go-app/
├── Chart.yaml           # Chart 元数据
├── values.yaml          # 默认 Values
├── values.schema.json   # Values 的 JSON Schema 校验(可选)
├── README.md            # 文档(可选)
├── LICENSE              # 许可证(可选)
├── charts/              # 依赖的子 Chart
├── crds/                # Custom Resource Definitions
└── templates/           # K8s 资源模板
    ├── NOTES.txt        # 安装后显示的帮助信息
    ├── _helpers.tpl     # 模板辅助函数(不会渲染为 K8s 资源)
    ├── deployment.yaml
    ├── service.yaml
    ├── ingress.yaml
    ├── configmap.yaml
    ├── hpa.yaml
    ├── serviceaccount.yaml
    └── tests/           # 测试模板
        └── test-connection.yaml

Chart.yaml

apiVersion: v2          # Helm 3 用 v2
name: go-app
description: A Go web application
type: application       # application | library

# 版本:Chart 版本(每次修改 Chart 时递增)
version: 1.2.0

# 应用版本(应用程序的版本,如镜像 tag)
appVersion: "2.5.0"

keywords:
- go
- web
- api

maintainers:
- name: Platform Team
  email: platform@company.com

dependencies:
- name: redis
  version: "~18.0"
  repository: https://charts.bitnami.com/bitnami
  condition: redis.enabled

4. 模板语法

Helm 模板使用 Go Template 语法,并扩展了 Sprig 函数库。

4.1 基本语法

# templates/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  # 变量引用
  name: {{ .Release.Name }}-go-app
  namespace: {{ .Release.Namespace }}
  labels:
    # 引用辅助函数(定义在 _helpers.tpl)
    {{- include "go-app.labels" . | nindent 4 }}
  annotations:
    # 条件语句
    {{- if .Values.podAnnotations }}
    {{- toYaml .Values.podAnnotations | nindent 4 }}
    {{- end }}
spec:
  # 三元运算符
  replicas: {{ .Values.replicaCount | default 1 }}

  selector:
    matchLabels:
      {{- include "go-app.selectorLabels" . | nindent 6 }}

  template:
    metadata:
      labels:
        {{- include "go-app.selectorLabels" . | nindent 8 }}
        {{- if .Values.podLabels }}
        {{- toYaml .Values.podLabels | nindent 8 }}
        {{- end }}
    spec:
      containers:
      - name: go-app
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
        imagePullPolicy: {{ .Values.image.pullPolicy }}
        ports:
        - containerPort: {{ .Values.service.targetPort }}
        resources:
          {{- toYaml .Values.resources | nindent 10 }}

4.2 内置对象

.Release.Name       → Release 名称(helm install 时的名字)
.Release.Namespace  → Release 所在命名空间
.Release.IsInstall  → 是否是首次安装
.Release.IsUpgrade  → 是否是升级

.Chart.Name         → Chart 名称
.Chart.Version      → Chart 版本
.Chart.AppVersion   → 应用版本

.Values             → values.yaml 中的值(或 -f 指定的)

.Files              → 访问 Chart 中的非模板文件
.Capabilities       → K8s 集群的版本信息

4.3 _helpers.tpl(模板函数)

{{/*
Expand the name of the chart.
*/}}
{{- define "go-app.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Create a default fully qualified app name.
*/}}
{{- define "go-app.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

{{/*
Common labels
*/}}
{{- define "go-app.labels" -}}
helm.sh/chart: {{ include "go-app.chart" . }}
{{ include "go-app.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/*
Selector labels
*/}}
{{- define "go-app.selectorLabels" -}}
app.kubernetes.io/name: {{ include "go-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

4.4 常用模板技巧

# 空白控制(- 消除左右空白/换行)
{{- "去掉左边空白" }}
{{ "去掉右边空白" -}}
{{- "两边都去掉" -}}

# nindent:缩进并在前面加换行
{{- toYaml .Values.resources | nindent 10 }}

# indent:只缩进(不加换行)
{{- toYaml .Values.resources | indent 10 }}

# if/else
{{- if .Values.ingress.enabled }}
...ingress yaml...
{{- end }}

{{- if eq .Values.env "production" }}
  LOG_LEVEL: error
{{- else if eq .Values.env "staging" }}
  LOG_LEVEL: warn
{{- else }}
  LOG_LEVEL: debug
{{- end }}

# range(遍历)
env:
{{- range $key, $val := .Values.extraEnvs }}
- name: {{ $key }}
  value: {{ $val | quote }}
{{- end }}

# with(修改作用域)
{{- with .Values.nodeSelector }}
nodeSelector:
  {{- toYaml . | nindent 2 }}
{{- end }}

# 字符串函数
{{ "hello" | upper }}         # → HELLO
{{ "HELLO" | lower }}         # → hello
{{ "hello world" | title }}   # → Hello World
{{ printf "%s-%s" "foo" "bar" }} # → foo-bar
{{ .Values.name | trunc 63 }} # 截取前 63 字符
{{ .Values.name | replace "-" "_" }} # 替换字符

# 默认值
{{ .Values.port | default 8080 }}
{{ .Values.tag | default .Chart.AppVersion }}

# 类型转换
{{ .Values.port | toString }}
{{ .Values.count | int }}
{{ "true" | toBool }}

# required(必填参数)
{{ required "image.repository is required" .Values.image.repository }}

5. Values 管理

5.1 values.yaml 设计

# 基本信息
replicaCount: 3

image:
  repository: myregistry.io/go-app
  tag: ""                  # 空则使用 Chart.AppVersion
  pullPolicy: IfNotPresent

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

# Service Account
serviceAccount:
  create: true
  annotations: {}
  name: ""

# Pod 配置
podAnnotations:
  prometheus.io/scrape: "true"
  prometheus.io/port: "9090"

podLabels: {}

podSecurityContext:
  runAsNonRoot: true
  runAsUser: 1000
  fsGroup: 2000

securityContext:
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false
  capabilities:
    drop:
    - ALL

# 探针
livenessProbe:
  httpGet:
    path: /healthz
    port: http
  initialDelaySeconds: 10
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /ready
    port: http
  initialDelaySeconds: 5
  periodSeconds: 5

# 资源
resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi

# HPA
autoscaling:
  enabled: false
  minReplicas: 2
  maxReplicas: 20
  targetCPUUtilizationPercentage: 70
  targetMemoryUtilizationPercentage: 80

# Service
service:
  type: ClusterIP
  port: 80
  targetPort: 8080

# Ingress
ingress:
  enabled: false
  className: "nginx"
  annotations: {}
  hosts:
  - host: myapp.example.com
    paths:
    - path: /
      pathType: Prefix
  tls: []

# 节点调度
nodeSelector: {}
tolerations: []
affinity: {}

# 环境变量
env: {}
  # APP_ENV: production
  # LOG_LEVEL: info

# 额外挂载
extraVolumes: []
extraVolumeMounts: []

# 应用特有配置
config:
  serverPort: 8080
  logLevel: info
  enableMetrics: true
  metricsPort: 9090

5.2 values.schema.json(参数校验)

{
  "$schema": "http://json-schema.org/draft-07/schema",
  "type": "object",
  "required": ["image"],
  "properties": {
    "replicaCount": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100
    },
    "image": {
      "type": "object",
      "required": ["repository"],
      "properties": {
        "repository": {
          "type": "string",
          "minLength": 1
        },
        "tag": {
          "type": "string"
        },
        "pullPolicy": {
          "type": "string",
          "enum": ["Always", "IfNotPresent", "Never"]
        }
      }
    },
    "service": {
      "type": "object",
      "properties": {
        "type": {
          "type": "string",
          "enum": ["ClusterIP", "NodePort", "LoadBalancer"]
        }
      }
    }
  }
}

6. 从零开发一个 Go 应用 Chart

helm create go-api
cd go-api

templates/configmap.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "go-api.fullname" . }}
  labels:
    {{- include "go-api.labels" . | nindent 4 }}
data:
  app.yaml: |
    server:
      port: {{ .Values.config.serverPort }}
    log:
      level: {{ .Values.config.logLevel }}
    metrics:
      enabled: {{ .Values.config.enableMetrics }}
      port: {{ .Values.config.metricsPort }}
    {{- if .Values.config.extra }}
    {{- toYaml .Values.config.extra | nindent 4 }}
    {{- end }}    

templates/deployment.yaml(完整版):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "go-api.fullname" . }}
  labels:
    {{- include "go-api.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "go-api.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      annotations:
        # ConfigMap 变化时触发重启
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
        {{- with .Values.podAnnotations }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
      labels:
        {{- include "go-api.selectorLabels" . | nindent 8 }}
        {{- with .Values.podLabels }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      serviceAccountName: {{ include "go-api.serviceAccountName" . }}
      securityContext:
        {{- toYaml .Values.podSecurityContext | nindent 8 }}
      containers:
        - name: {{ .Chart.Name }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 12 }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.service.targetPort }}
              protocol: TCP
            {{- if .Values.config.enableMetrics }}
            - name: metrics
              containerPort: {{ .Values.config.metricsPort }}
              protocol: TCP
            {{- end }}
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            {{- range $key, $val := .Values.env }}
            - name: {{ $key }}
              value: {{ $val | quote }}
            {{- end }}
          {{- if .Values.livenessProbe }}
          livenessProbe:
            {{- toYaml .Values.livenessProbe | nindent 12 }}
          {{- end }}
          {{- if .Values.readinessProbe }}
          readinessProbe:
            {{- toYaml .Values.readinessProbe | nindent 12 }}
          {{- end }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          volumeMounts:
            - name: config
              mountPath: /etc/app
              readOnly: true
            - name: tmp
              mountPath: /tmp
            {{- with .Values.extraVolumeMounts }}
            {{- toYaml . | nindent 12 }}
            {{- end }}
      volumes:
        - name: config
          configMap:
            name: {{ include "go-api.fullname" . }}
        - name: tmp
          emptyDir: {}
        {{- with .Values.extraVolumes }}
        {{- 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 }}

templates/NOTES.txt:

1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
  http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
  export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "go-api.fullname" . }})
  export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
  echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
  export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "go-api.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
  echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
  kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ include "go-api.fullname" . }} 8080:{{ .Values.service.port }}
  echo "Visit http://127.0.0.1:8080"
{{- end }}

7. Helm Hooks

Hooks 允许在 Release 生命周期的特定时间点执行操作。

# templates/db-migrate.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "go-api.fullname" . }}-db-migrate
  annotations:
    # pre-install: 安装前执行
    # post-install: 安装后执行
    # pre-upgrade: 升级前执行(数据库迁移最佳位置)
    # post-upgrade: 升级后执行
    # pre-delete: 删除前执行
    # post-delete: 删除后执行
    # pre-rollback: 回滚前执行
    # post-rollback: 回滚后执行
    "helm.sh/hook": pre-upgrade,pre-install
    "helm.sh/hook-weight": "-5"           # 执行顺序(数字越小越先执行)
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  ttlSecondsAfterFinished: 300
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: db-migrate
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
        command: ["/app/migrate", "--up"]
        env:
        - name: DATABASE_DSN
          valueFrom:
            secretKeyRef:
              name: {{ include "go-api.fullname" . }}-secret
              key: dsn

8. 依赖管理

# Chart.yaml
dependencies:
- name: redis
  version: "~18.0"
  repository: https://charts.bitnami.com/bitnami
  condition: redis.enabled    # values.yaml 中的 redis.enabled 控制是否安装

- name: postgresql
  version: "~13.0"
  repository: https://charts.bitnami.com/bitnami
  condition: postgresql.enabled
# values.yaml
redis:
  enabled: true
  auth:
    enabled: true
    password: "redis-password"
  master:
    persistence:
      size: 5Gi

postgresql:
  enabled: false
# 下载依赖
helm dependency update ./go-api

# 查看依赖
helm dependency list ./go-api

9. 多环境发布策略

9.1 多 values 文件覆盖

helm upgrade --install go-api ./chart \
  -f values.yaml \
  -f values-production.yaml \
  --set image.tag=$CI_COMMIT_SHA
# values-production.yaml(只覆盖与 base 不同的部分)
replicaCount: 5

resources:
  requests:
    cpu: 500m
    memory: 512Mi
  limits:
    cpu: 2
    memory: 2Gi

autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 50

ingress:
  enabled: true
  hosts:
  - host: api.myapp.com
    paths:
    - path: /
      pathType: Prefix
  tls:
  - secretName: myapp-tls
    hosts:
    - api.myapp.com

config:
  logLevel: warn

9.2 Kustomize + Helm(helmfile)

# helmfile.yaml
environments:
  development:
    values:
    - environments/development.yaml
  staging:
    values:
    - environments/staging.yaml
  production:
    values:
    - environments/production.yaml

releases:
- name: go-api
  namespace: {{ .Environment.Name }}
  chart: ./charts/go-api
  values:
  - values.yaml
  - values/{{ .Environment.Name }}.yaml
  set:
  - name: image.tag
    value: {{ requiredEnv "IMAGE_TAG" }}
# 部署到生产
IMAGE_TAG=v1.2.0 helmfile -e production sync

10. 常用公共 Chart

# Ingress Nginx(反向代理)
helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx --create-namespace

# cert-manager(自动 TLS)
helm upgrade --install cert-manager jetstack/cert-manager \
  --namespace cert-manager --create-namespace \
  --set installCRDs=true

# Prometheus Stack(监控)
helm upgrade --install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
  --namespace monitoring --create-namespace

# Loki Stack(日志)
helm upgrade --install loki grafana/loki-stack \
  --namespace monitoring

# External Secrets Operator
helm upgrade --install external-secrets \
  external-secrets/external-secrets \
  --namespace external-secrets-operator --create-namespace

# ArgoCD(GitOps)
helm upgrade --install argocd argo/argo-cd \
  --namespace argocd --create-namespace

11. 小结

Helm 工作流

开发 Chart → helm lint(校验)→ helm template(预览)
         → helm upgrade --install(部署)
         → helm history(查看历史)
         → helm rollback(回滚)

常用命令速查

# 仓库
helm repo add <name> <url>
helm repo update
helm search repo <keyword>

# 安装/升级
helm upgrade --install <release> <chart> -f values.yaml --namespace <ns>

# 查看
helm list -A
helm status <release>
helm history <release>

# 回滚
helm rollback <release> [revision]

# 调试
helm template <release> <chart> -f values.yaml
helm lint <chart>

# 开发
helm create <chart-name>
helm package <chart-dir>
helm push <package> oci://registry.io/charts