返回首页

Golang横向:03 Helm Chart编写与管理

在 Kubernetes 里,部署一个应用从来不只是“把镜像跑起来”这么简单。一个完整的服务通常还会涉及 Deployment、Service、Ingress、ConfigMap、Secret、HPA,甚至初始化 Job、发布前检查和安装后的数据迁移。随着环境增多、配置变复杂、发布频率提高,直接维护一堆 YAML 文件会迅速变成一件低效且脆弱的事情。

Helm 的价值,就在于把这些“可复用、可参数化、可发布”的 Kubernetes 资源组织起来,形成标准化的应用交付包。对 Go 应用尤其如此:Go 服务通常天然适合容器化,配合 Helm 可以很好地落地多环境部署、版本管理和自动化发布。

本文将围绕“云原生篇 3:Helm Chart 编写与管理”这个主题,系统讲清楚以下内容:

  • Helm 核心概念:Chart、Release、Repository
  • 从零编写一个 Go 应用的 Helm Chart
  • values.yaml 设计与多环境覆盖
  • 模板语法:条件、循环、命名模板
  • Helm Hooks 与生命周期管理
  • Chart 测试与发布到 OCI Registry

全文示例均可直接运行或按需裁剪,适合作为团队内部 Helm Chart 编写规范的起点。

1. Helm 核心概念

在正式写 Chart 之前,先把 Helm 的三个核心概念厘清。

1.1 Chart 是什么

Chart 可以理解为一个 Kubernetes 应用的“安装包模板”。

它不是某一个固定环境下的最终 YAML,而是一组:

  • 模板文件
  • 默认配置
  • 元数据
  • 可选依赖项

组合在一起的发布单元。

一个 Chart 往往对应一个可部署应用,例如:

  • 一个 Go API 服务
  • 一个 MySQL 实例
  • 一个完整的监控组件

典型目录结构如下:

my-go-app/
├── Chart.yaml
├── values.yaml
├── charts/
├── templates/
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── serviceaccount.yaml
│   └── tests/
│       └── connection-test.yaml
└── .helmignore

其中:

  • Chart.yaml:描述 Chart 的基本信息
  • values.yaml:默认配置值
  • templates/:Kubernetes 资源模板
  • charts/:子 Chart 依赖
  • _helpers.tpl:公共命名模板和辅助函数

1.2 Release 是什么

Release 是某个 Chart 在 Kubernetes 集群里的一次“已安装实例”。

同一个 Chart,可以安装出多个 Release。例如:

  • go-web-dev
  • go-web-staging
  • go-web-prod

它们可以:

  • 安装在不同命名空间
  • 使用不同 values 配置
  • 指向不同镜像版本
  • 使用不同域名和资源配额

也就是说:Chart 是模板,Release 是实例。

例如下面这条命令:

helm install go-web-prod ./my-go-app -n prod

含义是:把本地 my-go-app 这个 Chart,安装成名为 go-web-prod 的 Release,并部署到 prod 命名空间。

1.3 Repository 是什么

Repository 是存放和分发 Chart 的仓库。

传统 Helm Repository 通过 index.yaml 管理 Chart 索引;而现在更常见的方式,是把 Chart 直接发布到 OCI Registry,例如:

  • Harbor
  • Docker Hub(支持 OCI)
  • GitHub Container Registry
  • 阿里云 ACR
  • 企业内部制品库

OCI Registry 的好处是:

  • 与镜像仓库统一管理
  • 权限体系更一致
  • 版本分发更标准
  • 更适合 CI/CD 自动化

例如:

helm push my-go-app-0.1.0.tgz oci://registry.example.com/helm

2. 从零准备一个 Go 示例应用

为了让 Helm Chart 更贴近实际使用,我们先准备一个极简 Go Web 服务。

2.1 示例目录结构

go-web/
├── Dockerfile
├── go.mod
└── main.go

2.2 main.go

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "os"
)

type Response struct {
    Message string `json:"message"`
    Version string `json:"version"`
    Env     string `json:"env"`
}

func main() {
    mux := http.NewServeMux()

    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        _ = json.NewEncoder(w).Encode(Response{
            Message: "hello from go-web",
            Version: getEnv("APP_VERSION", "v1.0.0"),
            Env:     getEnv("APP_ENV", "dev"),
        })
    })

    mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        _, _ = w.Write([]byte("ok"))
    })

    addr := ":8080"
    log.Printf("server listening on %s", addr)
    if err := http.ListenAndServe(addr, mux); err != nil {
        log.Fatalf("server exited: %v", err)
    }
}

func getEnv(key, fallback string) string {
    if value, ok := os.LookupEnv(key); ok {
        return value
    }
    return fallback
}

2.3 go.mod

module example.com/go-web

go 1.22

2.4 Dockerfile

FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o server .

FROM alpine:3.20
WORKDIR /app
COPY --from=builder /app/server /app/server
EXPOSE 8080
ENTRYPOINT ["/app/server"]

构建并推送镜像后,假设镜像地址为:

registry.example.com/demo/go-web:v1.0.0

3. 从零编写一个 Go 应用的 Helm Chart

接下来开始编写 Chart。

3.1 初始化 Chart

可以直接使用 Helm 命令创建骨架:

helm create my-go-app

不过在实际项目里,很多团队会删掉默认模板,改成自己的风格。本文直接给出一套更适合 Go Web 服务的结构。

3.2 Chart 目录结构

my-go-app/
├── Chart.yaml
├── values.yaml
├── values-dev.yaml
├── values-prod.yaml
├── .helmignore
└── templates/
    ├── _helpers.tpl
    ├── deployment.yaml
    ├── service.yaml
    ├── ingress.yaml
    ├── serviceaccount.yaml
    ├── configmap.yaml
    ├── hpa.yaml
    ├── hooks-migrate.yaml
    └── tests/
        └── connection-test.yaml

3.3 Chart.yaml

Chart.yaml 用来定义 Chart 元信息。

apiVersion: v2
name: my-go-app
description: A Helm chart for deploying a Go web application
type: application
version: 0.1.0
appVersion: "1.0.0"
home: https://example.com/my-go-app
sources:
  - https://example.com/my-go-app.git
keywords:
  - go
  - kubernetes
  - helm
maintainers:
  - name: 杨晓明

这里需要注意两个版本:

  • version:Chart 自身版本
  • appVersion:应用版本,通常对应镜像版本或业务版本

3.4 .helmignore

.helmignore 的作用类似 .gitignore,避免无关文件打进 Chart 包里。

.git/
.gitignore
Dockerfile
README.md
*.swp
*.tmp

3.5 values.yaml

这是默认值文件,也是整个 Chart 的配置入口。

replicaCount: 2

image:
  repository: registry.example.com/demo/go-web
  tag: v1.0.0
  pullPolicy: IfNotPresent

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

serviceAccount:
  create: true
  annotations: {}
  name: ""

podAnnotations: {}
podLabels: {}

podSecurityContext: {}

securityContext:
  runAsNonRoot: true
  runAsUser: 10001
  allowPrivilegeEscalation: false

service:
  type: ClusterIP
  port: 80
  targetPort: 8080

containerPort: 8080

config:
  APP_ENV: dev
  APP_VERSION: v1.0.0

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

livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

ingress:
  enabled: false
  className: nginx
  annotations: {}
  hosts:
    - host: go-web.local
      paths:
        - path: /
          pathType: Prefix
  tls: []

autoscaling:
  enabled: false
  minReplicas: 2
  maxReplicas: 5
  targetCPUUtilizationPercentage: 80

extraEnv: []

nodeSelector: {}
tolerations: []
affinity: {}

hooks:
  migrate:
    enabled: false
    image: registry.example.com/demo/db-migrate:v1.0.0
    command:
      - /bin/sh
      - -c
      - ./migrate.sh

4. values.yaml 设计与多环境覆盖

values.yaml 看起来只是一个配置文件,但它实际上决定了 Chart 的可维护性上限。

一个好的 values 设计,至少应该满足:

  • 默认值清晰
  • 结构稳定
  • 便于多环境覆盖
  • 便于 CI/CD 动态注入
  • 尽量少把复杂逻辑写死在模板里

4.1 values 设计原则

建议遵循以下原则:

  1. 按关注点分组:如 imageserviceingressresources
  2. 默认值可本地运行:避免上来就一堆必填项
  3. 布尔开关显式化:如 ingress.enabledautoscaling.enabled
  4. 环境差异放 values 文件,不放模板判断
  5. 模板负责渲染,values 负责表达意图

4.2 开发环境覆盖文件

replicaCount: 1

image:
  tag: dev-latest

config:
  APP_ENV: dev
  APP_VERSION: dev-latest

ingress:
  enabled: true
  hosts:
    - host: go-web-dev.example.com
      paths:
        - path: /
          pathType: Prefix

resources:
  requests:
    cpu: 50m
    memory: 64Mi
  limits:
    cpu: 200m
    memory: 256Mi

autoscaling:
  enabled: false

文件名可保存为 values-dev.yaml

4.3 生产环境覆盖文件

replicaCount: 3

image:
  tag: v1.0.0

config:
  APP_ENV: prod
  APP_VERSION: v1.0.0

ingress:
  enabled: true
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: 20m
  hosts:
    - host: go-web.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: go-web-tls
      hosts:
        - go-web.example.com

resources:
  requests:
    cpu: 200m
    memory: 256Mi
  limits:
    cpu: 1000m
    memory: 1Gi

autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 10
  targetCPUUtilizationPercentage: 75

hooks:
  migrate:
    enabled: true
    image: registry.example.com/demo/db-migrate:v1.0.0

文件名可保存为 values-prod.yaml

4.4 多环境部署命令

开发环境:

helm upgrade --install go-web-dev ./my-go-app \
  -n dev --create-namespace \
  -f my-go-app/values.yaml \
  -f my-go-app/values-dev.yaml

生产环境:

helm upgrade --install go-web-prod ./my-go-app \
  -n prod --create-namespace \
  -f my-go-app/values.yaml \
  -f my-go-app/values-prod.yaml

如果需要在流水线中临时覆盖镜像 tag,还可以使用:

helm upgrade --install go-web-prod ./my-go-app \
  -n prod \
  -f my-go-app/values.yaml \
  -f my-go-app/values-prod.yaml \
  --set image.tag=v1.0.1

4.5 values 覆盖优先级

Helm 常见覆盖优先级从低到高为:

  • Chart 内置 values.yaml
  • -f values-xxx.yaml
  • 多个 -f 按顺序后者覆盖前者
  • --set
  • --set-string
  • --set-file

因此 CI/CD 通常会采用:

  • 仓库里维护基础 values.yaml
  • 不同环境维护独立 values 文件
  • 流水线运行时用 --set 覆盖镜像 tag、commit sha 等动态值

5. Helm 模板语法:条件、循环、命名模板

Helm 模板基于 Go Template,并额外集成了 Sprig 函数库。真正写 Chart 时,最常用的就是:

  • 条件判断
  • 循环渲染
  • 命名模板
  • 管道与函数
  • 缩进控制

5.1 _helpers.tpl:命名模板

先定义公共命名模板,后面所有资源都可以复用。

{{- define "my-go-app.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{- define "my-go-app.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name (include "my-go-app.name" .) | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}

{{- define "my-go-app.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" -}}
{{- end -}}

{{- define "my-go-app.labels" -}}
helm.sh/chart: {{ include "my-go-app.chart" . }}
app.kubernetes.io/name: {{ include "my-go-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}

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

{{- define "my-go-app.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{- default (include "my-go-app.fullname" .) .Values.serviceAccount.name -}}
{{- else -}}
{{- default "default" .Values.serviceAccount.name -}}
{{- end -}}
{{- end -}}

这里的 define 就是在声明命名模板,后续通过 include 复用。这样做的好处是:

  • 统一名称生成逻辑
  • 统一 label 风格
  • 降低模板重复度
  • 便于团队标准化

5.2 Deployment 模板

下面是核心的 templates/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-go-app.fullname" . }}
  labels:
    {{- include "my-go-app.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "my-go-app.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "my-go-app.selectorLabels" . | nindent 8 }}
        {{- with .Values.podLabels }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
      {{- with .Values.podAnnotations }}
      annotations:
        {{- toYaml . | nindent 8 }}
      {{- end }}
    spec:
      serviceAccountName: {{ include "my-go-app.serviceAccountName" . }}
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      securityContext:
        {{- toYaml .Values.podSecurityContext | nindent 8 }}
      containers:
        - name: {{ include "my-go-app.name" . }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 12 }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.containerPort }}
              protocol: TCP
          envFrom:
            - configMapRef:
                name: {{ include "my-go-app.fullname" . }}
          {{- with .Values.extraEnv }}
          env:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          livenessProbe:
            {{- toYaml .Values.livenessProbe | nindent 12 }}
          readinessProbe:
            {{- toYaml .Values.readinessProbe | nindent 12 }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}

这个模板里已经包含了几个非常典型的写法。

条件判断

{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}

如果开启了 HPA,就不再显式设置 Deployment 的 replicas

with 作用域切换

{{- with .Values.podAnnotations }}
annotations:
  {{- toYaml . | nindent 8 }}
{{- end }}

with 的含义是:当值存在且非空时,进入该作用域,并把当前 . 切换为该对象。

toYaml 与 nindent

{{- toYaml .Values.resources | nindent 12 }}

这是 Helm 模板最常见的组合之一:

  • toYaml:把对象转为 YAML
  • nindent:换行并按指定空格数缩进

5.3 ConfigMap 模板

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "my-go-app.fullname" . }}
  labels:
    {{- include "my-go-app.labels" . | nindent 4 }}
data:
  {{- range $key, $value := .Values.config }}
  {{ $key }}: {{ $value | quote }}
  {{- end }}

这里用到了 range 循环,把 config 下的键值对全部渲染到 ConfigMap 中。

循环语法

{{- range $key, $value := .Values.config }}
{{ $key }}: {{ $value | quote }}
{{- end }}

这种方式非常适合:

  • 环境变量
  • 注解集合
  • labels 集合
  • hosts/path 列表

5.4 Service 模板

apiVersion: v1
kind: Service
metadata:
  name: {{ include "my-go-app.fullname" . }}
  labels:
    {{- include "my-go-app.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: {{ .Values.service.targetPort }}
      protocol: TCP
      name: http
  selector:
    {{- include "my-go-app.selectorLabels" . | nindent 4 }}

5.5 Ingress 模板

{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "my-go-app.fullname" . }}
  labels:
    {{- include "my-go-app.labels" . | nindent 4 }}
  {{- with .Values.ingress.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
spec:
  ingressClassName: {{ .Values.ingress.className }}
  {{- if .Values.ingress.tls }}
  tls:
    {{- toYaml .Values.ingress.tls | nindent 4 }}
  {{- end }}
  rules:
    {{- range .Values.ingress.hosts }}
    - host: {{ .host | quote }}
      http:
        paths:
          {{- range .paths }}
          - path: {{ .path }}
            pathType: {{ .pathType }}
            backend:
              service:
                name: {{ include "my-go-app.fullname" $ }}
                port:
                  number: {{ $.Values.service.port }}
          {{- end }}
    {{- end }}
{{- end }}

这里有两个重点:

  1. 最外层 if 控制是否生成 Ingress
  2. 嵌套 range 渲染多 host、多 path

同时这里还用到了 $

  • . 代表当前作用域
  • $ 代表根作用域

因为进入 range 之后,当前 . 已经不再是全局上下文,因此访问全局对象时通常要用 $

5.6 ServiceAccount 模板

{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
  name: {{ include "my-go-app.serviceAccountName" . }}
  labels:
    {{- include "my-go-app.labels" . | nindent 4 }}
  {{- with .Values.serviceAccount.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
{{- end }}

5.7 HPA 模板

{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: {{ include "my-go-app.fullname" . }}
  labels:
    {{- include "my-go-app.labels" . | nindent 4 }}
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: {{ include "my-go-app.fullname" . }}
  minReplicas: {{ .Values.autoscaling.minReplicas }}
  maxReplicas: {{ .Values.autoscaling.maxReplicas }}
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}

6. Helm Hooks 与生命周期管理

在很多真实场景中,应用发布并不只是“安装 Deployment”。例如:

  • 发布前执行数据库迁移
  • 安装后写入初始化数据
  • 卸载前做清理
  • 升级前做兼容检查

这类逻辑就适合用 Helm Hooks。

6.1 Helm Hooks 支持的常见阶段

Helm 常见 Hook 阶段包括:

  • pre-install
  • post-install
  • pre-upgrade
  • post-upgrade
  • pre-delete
  • post-delete
  • pre-rollback
  • post-rollback
  • test

本质上,Hook 是通过资源注解来声明执行时机。

6.2 发布前执行数据库迁移

假设我们希望在安装或升级前执行一次迁移 Job,可以写一个 Hook 模板。

{{- if .Values.hooks.migrate.enabled }}
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "my-go-app.fullname" . }}-migrate-{{ .Release.Revision }}
  labels:
    {{- include "my-go-app.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": pre-install,pre-upgrade
    "helm.sh/hook-weight": "-5"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  backoffLimit: 1
  template:
    metadata:
      labels:
        {{- include "my-go-app.selectorLabels" . | nindent 8 }}
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: {{ .Values.hooks.migrate.image | quote }}
          command:
            {{- toYaml .Values.hooks.migrate.command | nindent 12 }}
{{- end }}

上面三个 Hook 注解非常关键:

  • helm.sh/hook:定义触发时机
  • helm.sh/hook-weight:定义多个 Hook 的执行顺序,越小越先执行
  • helm.sh/hook-delete-policy:定义 Hook 资源删除策略

6.3 生命周期管理建议

在使用 Hooks 时,建议把握几个原则:

  1. Hook 只做短任务:例如迁移、检查、初始化,不要跑长时间常驻任务
  2. Job 必须可重试且幂等:避免升级失败后状态不一致
  3. 要设置删除策略:否则 Job 资源会越积越多
  4. 不要把核心业务部署依赖写得过重:否则发布链路会变脆弱
  5. 把失败视为发布失败的一部分:Hook 失败不是“小问题”,而是发布失败

6.4 安装、升级、回滚、卸载常用命令

安装:

helm install go-web-dev ./my-go-app -n dev --create-namespace

升级:

helm upgrade go-web-dev ./my-go-app -n dev -f my-go-app/values-dev.yaml

回滚:

helm rollback go-web-dev 1 -n dev

卸载:

helm uninstall go-web-dev -n dev

查看 Release 历史:

helm history go-web-dev -n dev

7. Chart 测试:Lint、模板渲染、安装验证

Chart 不应该等到部署时才发现有问题。至少要建立三层检查:

  • 语法和规范检查
  • 模板渲染检查
  • 安装后连通性检查

7.1 helm lint

helm lint 是最基础的一步。

helm lint ./my-go-app

如果要带某个环境配置一起检查:

helm lint ./my-go-app -f ./my-go-app/values-prod.yaml

7.2 helm template

helm template 会把 Chart 渲染成最终 YAML,但不实际安装到集群。

helm template go-web-dev ./my-go-app \
  -n dev \
  -f ./my-go-app/values.yaml \
  -f ./my-go-app/values-dev.yaml

这一步非常适合在 CI 中:

  • 检查 YAML 是否符合预期
  • 对照 diff 审核资源变化
  • 配合 kubectl apply --dry-run=client 做进一步验证

7.3 dry-run 安装验证

helm upgrade --install go-web-dev ./my-go-app \
  -n dev --create-namespace \
  -f ./my-go-app/values-dev.yaml \
  --dry-run --debug

--debug 会输出更多渲染上下文,排查模板问题时非常有帮助。

7.4 Helm Test

Helm 支持用 test Hook 编写安装后的验证任务。下面写一个最常见的服务连通性测试。

apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "my-go-app.fullname" . }}-test-connection"
  labels:
    {{- include "my-go-app.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
spec:
  containers:
    - name: wget
      image: busybox:1.36
      command:
        - sh
        - -c
        - >-
          wget -qO- http://{{ include "my-go-app.fullname" . }}:{{ .Values.service.port }}/healthz | grep ok
  restartPolicy: Never

安装完成后,执行:

helm test go-web-dev -n dev

如果测试成功,说明至少服务在集群内可以被访问。

7.5 一个可直接执行的本地校验脚本

团队里通常会把校验动作固化成脚本,方便开发、测试、CI 统一使用。

#!/usr/bin/env bash
set -euo pipefail

CHART_DIR="./my-go-app"

helm lint "${CHART_DIR}"
helm lint "${CHART_DIR}" -f "${CHART_DIR}/values-dev.yaml"
helm lint "${CHART_DIR}" -f "${CHART_DIR}/values-prod.yaml"

helm template go-web-dev "${CHART_DIR}" \
  -n dev \
  -f "${CHART_DIR}/values.yaml" \
  -f "${CHART_DIR}/values-dev.yaml" >/dev/null

helm template go-web-prod "${CHART_DIR}" \
  -n prod \
  -f "${CHART_DIR}/values.yaml" \
  -f "${CHART_DIR}/values-prod.yaml" >/dev/null

echo "helm chart validation passed"

8. Chart 打包与发布到 OCI Registry

在团队协作中,Chart 一旦稳定下来,就应该像镜像一样被版本化和分发,而不是每次都依赖源码目录部署。

8.1 打包 Chart

helm package ./my-go-app

执行后会生成类似文件:

my-go-app-0.1.0.tgz

8.2 登录 OCI Registry

以企业镜像仓库为例:

helm registry login registry.example.com \
  -u your-username \
  -p your-password

更安全的方式是通过标准输入传密码:

export HELM_REGISTRY_PASSWORD="your-password"
printf '%s' "${HELM_REGISTRY_PASSWORD}" | helm registry login registry.example.com \
  -u your-username \
  --password-stdin

8.3 推送到 OCI Registry

helm push my-go-app-0.1.0.tgz oci://registry.example.com/helm

推送成功后,Chart 地址类似于:

oci://registry.example.com/helm/my-go-app

8.4 从 OCI Registry 拉取和安装

查看 Chart 信息:

helm show chart oci://registry.example.com/helm/my-go-app --version 0.1.0

安装指定版本:

helm install go-web-prod oci://registry.example.com/helm/my-go-app \
  --version 0.1.0 \
  -n prod --create-namespace \
  -f ./my-go-app/values-prod.yaml

拉取到本地:

helm pull oci://registry.example.com/helm/my-go-app --version 0.1.0

8.5 CI/CD 发布思路

一个比较常见的自动化流程如下:

  1. 开发提交 Chart 变更
  2. CI 执行 helm lint
  3. CI 执行 helm template
  4. 如有测试集群,可执行试安装验证
  5. 使用 helm package 打包
  6. .tgz 推送到 OCI Registry
  7. 部署流水线按版本拉取并安装

例如在 CI 中:

#!/usr/bin/env bash
set -euo pipefail

CHART_DIR="./my-go-app"
CHART_VERSION="0.1.0"
REGISTRY="registry.example.com"
REPO="oci://${REGISTRY}/helm"

helm lint "${CHART_DIR}"
helm template go-web-ci "${CHART_DIR}" -f "${CHART_DIR}/values.yaml" >/dev/null
helm package "${CHART_DIR}" --version "${CHART_VERSION}"
printf '%s' "${HELM_REGISTRY_PASSWORD}" | helm registry login "${REGISTRY}" \
  -u "${HELM_REGISTRY_USERNAME}" \
  --password-stdin
helm push "my-go-app-${CHART_VERSION}.tgz" "${REPO}"

9. 一套完整的实战操作顺序

如果你希望把本文内容真正落地到项目里,可以按下面顺序执行:

第一步:构建并推送 Go 应用镜像

docker build -t registry.example.com/demo/go-web:v1.0.0 ./go-web
docker push registry.example.com/demo/go-web:v1.0.0

第二步:校验 Chart 模板

helm lint ./my-go-app
helm template go-web-dev ./my-go-app -f ./my-go-app/values-dev.yaml

第三步:安装到开发环境

helm upgrade --install go-web-dev ./my-go-app \
  -n dev --create-namespace \
  -f ./my-go-app/values.yaml \
  -f ./my-go-app/values-dev.yaml

第四步:执行安装后测试

helm test go-web-dev -n dev

第五步:打包并发布 Chart

helm package ./my-go-app
helm push my-go-app-0.1.0.tgz oci://registry.example.com/helm

第六步:生产环境按版本安装

helm upgrade --install go-web-prod oci://registry.example.com/helm/my-go-app \
  --version 0.1.0 \
  -n prod --create-namespace \
  -f ./my-go-app/values-prod.yaml

10. 结语

Helm 的核心价值,不在于“少写几份 YAML”,而在于把 Kubernetes 应用的部署方式真正工程化、标准化、可复用化。

对于 Go 应用来说,一个质量足够高的 Helm Chart,至少应该具备这些特征:

  • Chart 元数据清晰,版本语义明确
  • values.yaml 结构稳定,适合多环境覆盖
  • 模板复用合理,命名模板统一
  • 条件和循环只服务于渲染,不把业务逻辑塞进模板
  • Hook 用于补足生命周期动作,而不是制造发布复杂度
  • 能被 lint、template、test、package、push 这条链路稳定消费

当团队把 Helm Chart 当作正式交付物来维护时,Kubernetes 发布流程才会真正变得可协作、可审查、可追踪、可自动化。


📝 版权声明:本文为原创技术博客,转载请注明出处。

如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!

上一篇

Golang横向:02 Kubernetes 部署模型与实战

下一篇

Golang横向:04 CI/CD 流水线设计