自定义 Chart 开发
核心问题:怎样为自己的应用从零开发一个 Helm Chart,并发布到私有仓库供团队共用?
从零创建 Chart
# 使用 helm create 生成脚手架
helm create my-app
# 生成目录结构,包含 deployment、service、ingress、hpa、serviceaccount 模板
# 查看结构
tree my-app/
# my-app/
# ├── Chart.yaml
# ├── values.yaml
# ├── templates/
# │ ├── _helpers.tpl
# │ ├── deployment.yaml
# │ ├── hpa.yaml
# │ ├── ingress.yaml
# │ ├── NOTES.txt
# │ ├── service.yaml
# │ ├── serviceaccount.yaml
# │ └── tests/
# │ └── test-connection.yaml
# └── charts/
开发 ConfigMap 模板
# templates/configmap.yaml
{{- if .Values.config }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "my-app.fullname" . }}-config
namespace: {{ .Release.Namespace }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
data:
{{- range $key, $value := .Values.config }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- if .Values.appConfig }}
app.yml: |
{{- toYaml .Values.appConfig | nindent 4 }}
{{- end }}
{{- end }}
开发 Ingress 模板
# templates/ingress.yaml
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "my-app.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "my-app.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}
NOTES.txt:安装后说明
{{/* templates/NOTES.txt */}}
恭喜!{{ .Chart.Name }} v{{ .Chart.AppVersion }} 已成功安装。
Release 名称:{{ .Release.Name }}
命名空间:{{ .Release.Namespace }}
{{- if .Values.ingress.enabled }}
访问地址:
{{- range .Values.ingress.hosts }}
- https://{{ .host }}
{{- end }}
{{- else if eq .Values.service.type "LoadBalancer" }}
获取 Service 地址:
kubectl get service {{ include "my-app.fullname" . }} -n {{ .Release.Namespace }}
{{- else }}
本地端口转发测试:
kubectl port-forward service/{{ include "my-app.fullname" . }} 8080:{{ .Values.service.port }} -n {{ .Release.Namespace }}
然后访问 http://localhost:8080
{{- end }}
查看日志:
kubectl logs -l "app.kubernetes.io/name={{ .Chart.Name }},app.kubernetes.io/instance={{ .Release.Name }}" \
-n {{ .Release.Namespace }} --tail=50 -f
Chart 测试
# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
name: {{ include "my-app.fullname" . }}-test
namespace: {{ .Release.Namespace }}
annotations:
"helm.sh/hook": test # 标记为 Helm 测试
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
restartPolicy: Never
containers:
- name: test
image: curlimages/curl:8.4.0
command: ['curl', '-f', 'http://{{ include "my-app.fullname" . }}:{{ .Values.service.port }}/healthz']
# 运行测试(在 install 之后)
helm test my-api -n production
# 查看测试 Pod 日志
kubectl logs my-api-test -n production
Chart Hooks:生命周期钩子
# templates/db-migrate-job.yaml(在 upgrade 前运行数据库迁移)
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "my-app.fullname" . }}-db-migrate
namespace: {{ .Release.Namespace }}
annotations:
"helm.sh/hook": pre-upgrade,pre-install # 何时运行
"helm.sh/hook-weight": "-5" # 权重(越小越先运行)
"helm.sh/hook-delete-policy": before-hook-creation
spec:
ttlSecondsAfterFinished: 3600
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: {{ include "my-app.image" . }}
command: ["node", "scripts/migrate.js"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "my-app.fullname" . }}-secrets
key: database-url
Hook 类型:
| Hook | 触发时机 |
|---|---|
pre-install | helm install 前 |
post-install | helm install 后 |
pre-upgrade | helm upgrade 前 |
post-upgrade | helm upgrade 后 |
pre-rollback | helm rollback 前 |
post-rollback | helm rollback 后 |
pre-delete | helm uninstall 前 |
test | helm test 时 |
打包与发布到私有仓库
# 打包 Chart
helm package ./my-app
# 生成:my-app-1.2.3.tgz
# 方式 1:OCI Registry(推荐,Harbor / ECR / GHCR)
helm push my-app-1.2.3.tgz oci://registry.example.com/helm-charts
helm install my-api oci://registry.example.com/helm-charts/my-app \
--version 1.2.3 \
-n production
# 方式 2:GitHub Pages(静态文件仓库)
# 在 gh-pages 分支存放 .tgz 和 index.yaml
helm repo index . --url https://myorg.github.io/helm-charts
git add . && git commit -m "Release my-app v1.2.3"
git push origin gh-pages
# 添加并使用
helm repo add myorg https://myorg.github.io/helm-charts
helm install my-api myorg/my-app --version 1.2.3
# 方式 3:Harbor(私有 OCI Registry)
helm registry login harbor.example.com
helm push my-app-1.2.3.tgz oci://harbor.example.com/library
CI/CD 中自动发布 Chart
# .github/workflows/helm-release.yml
name: Release Helm Chart
on:
push:
tags: ['v*']
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 更新 Chart 版本
run: |
TAG=${GITHUB_REF#refs/tags/v}
sed -i "s/^version:.*/version: \"$TAG\"/" charts/my-app/Chart.yaml
sed -i "s/^appVersion:.*/appVersion: \"v$TAG\"/" charts/my-app/Chart.yaml
- name: Helm Lint
run: helm lint charts/my-app
- name: 打包 Chart
run: helm package charts/my-app --destination ./dist
- name: 推送到 OCI Registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
helm registry login registry.example.com -u ${{ secrets.REGISTRY_USER }} --password-stdin
helm push dist/my-app-*.tgz oci://registry.example.com/helm-charts
下一章:GitOps 与 ArgoCD