目录

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

Helm 包管理:从入门到实战

系列第九篇。Helm 是 K8s 的包管理器,让你可以将复杂的 K8s 应用打包、版本化、分发和管理。


目录

  1. Helm 是什么
  2. 安装与基础命令
  3. Chart 结构详解
  4. 模板语法
  5. Values 管理
  6. 从零开发一个 Go 应用 Chart
  7. Helm Hooks
  8. 依赖管理
  9. 多环境发布策略
  10. 常用公共 Chart
  11. 小结

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