云原生技术08-Helm 3:Kubernetes的“Yum/Apt“——包管理so easy,手把手教你写第一个Helm Chart
「知识图谱生成工具」:一键将文件夹内容变身为交互式知识图谱的免安装桌面工具(文末附免费下载链接)-CSDN博客
CSDN AI数字营销功能实测:CSDN AI内容创作,10分钟从技术选题到成文,技术博主最值得开通的功能,没有之一-CSDN博客
目录
- 开篇:YAML地狱逃生指南
- Helm架构:Chart → Release → Revision
- Helm v3革命:Tiller的葬礼
- Chart解剖学:三个文件走天下
- Kustomize:Overlay模式的魔法
- 最佳实践:Helm + Kustomize组合拳
- 实战:从零写一个生产级Chart
- 文末三件套
开篇:YAML地狱逃生指南
你是否遇到过部署应用时要改几十个YAML文件、环境切换时配置混乱、版本回滚找不到历史记录的崩溃?
想象一下这个场景:你刚入职新公司,老大扔给你一套微服务架构,包含20个Deployment、15个Service、8个ConfigMap、5个Secret。测试环境要改数据库地址,生产环境要调整资源限制,每次上线都像在玩"大家来找茬"——找不完,根本找不完。
这时候Helm出现了,就像Kubernetes世界的"Yum/Apt",让你像安装软件一样部署应用。本文将从零开始教你写生产级Helm Chart,告别复制粘贴,拥抱包管理。
💡效率技巧:Chart平均大小不到1MB,Rollback回滚时间小于10秒。这意味着什么?意味着你可以像玩Git一样玩K8s部署——错了就回滚,比撤销微信消息还快。
Helm架构:Chart → Release → Revision
核心概念图解
┌─────────────────────────────────────────────────────────────────┐ │ Helm 架构全景图 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Chart │ ───▶ │ Release │ ───▶ │ Revision │ │ │ │ (安装包) │ │ (实例) │ │ (版本) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ templates/ │ │ my-app-v1 │ │ revision 1 │ │ │ │ values.yaml│ │ my-app-v2 │ │ revision 2 │ │ │ │ Chart.yaml │ │ │ │ revision 3 │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ 类比: 类比: 类比: │ │ .deb / .rpm 包 安装的软件实例 Git commit历史 │ │ │ └─────────────────────────────────────────────────────────────────┘三要素详解
| 概念 | 类比 | 作用 |
|---|---|---|
| Chart | 软件安装包(.deb/.rpm) | 包含所有K8s资源模板的集合 |
| Release | 安装后的软件实例 | Chart在集群中的运行实例,可多次安装 |
| Revision | Git commit | 每次upgrade产生的版本,支持回滚 |
Chart是模板,Release是实例,Revision是历史——记住这个三角关系,你就掌握了Helm的灵魂。
⚠️避坑警告:很多新手会把Chart和Release搞混。Chart是静态的代码包(存在本地或仓库),Release是动态的运行实例(存在K8s集群)。就像你下载的nginx-1.20.deb是Chart,而
apt install nginx后运行的进程是Release。
Helm v3革命:Tiller的葬礼
架构对比:v2 vs v3
┌────────────────────────────────────────────────────────────────────┐ │ Helm v2(已入土) │ ├────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │ │ │ Helm │ ───▶ │ Tiller │ ───▶ │ Kubernetes API │ │ │ │ Client │ │ (Server) │ │ Server │ │ │ └──────────┘ └──────────┘ └──────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ 需要集群管理员 │ │ │ │ 权限 + RBAC │ │ │ │ 配置复杂 │ │ │ └──────────────┘ │ │ │ └────────────────────────────────────────────────────────────────────┘ ┌────────────────────────────────────────────────────────────────────┐ │ Helm v3(现行版) │ ├────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────┐ ┌──────────────────────┐ │ │ │ Helm │ ───▶ │ Kubernetes API │ │ │ │ Client │ │ Server │ │ │ └──────────┘ └──────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ 直接操作K8s │ │ │ │ 无需Tiller │ │ │ │ 安全简单 │ │ │ └──────────────┘ │ │ │ └────────────────────────────────────────────────────────────────────┘v3的核心改进
1. 移除Tiller
- v2:Tiller作为集群内Pod运行,需要集群管理员权限,RBAC配置复杂得像在写论文
- v3:Helm直接调用K8s API,权限跟着kubeconfig走,简单得像在作弊
2. Release级别管理
# v2:Tiller存储Release信息在ConfigMap(全局可见,安全隐患) # v3:Release信息存储在Release自身的Namespace中 # 查看Release存储位置 kubectl get secret -n <release-namespace> -l owner=helm3. 安全性提升
- 不再需要给Tiller开集群管理员权限
- 每个Release的Secret只有对应Namespace权限可见
- RBAC粒度细到令人发指
💡效率技巧:如果你还在用Helm v2,赶紧升级吧。v2已经停止维护,就像还在用Windows XP一样——不是不能用,是没必要。
Chart解剖学:三个文件走天下
Chart目录结构
myapp/ ├── Chart.yaml # Chart的"身份证" ├── values.yaml # 默认配置值 ├── values-dev.yaml # 开发环境覆盖配置 ├── values-prod.yaml # 生产环境覆盖配置 ├── charts/ # 依赖的Chart │ └── mysql-8.0.0.tgz ├── templates/ # K8s资源模板 │ ├── _helpers.tpl # 辅助模板函数 │ ├── deployment.yaml │ ├── service.yaml │ ├── ingress.yaml │ ├── configmap.yaml │ ├── secret.yaml │ ├── hpa.yaml # 水平自动扩缩容 │ ├── pdb.yaml # Pod中断预算 │ └── NOTES.txt # 安装后提示信息 └── README.mdChart.yaml:Chart的身份证
apiVersion: v2 # v2表示Helm 3,v1是Helm 2 name: myapp description: A Helm chart for Kubernetes type: application # application或library version: 1.2.3 # Chart版本(语义化版本) appVersion: "2.0.0" # 应用的版本 icon: https://example.com/icon.png keywords: - web - microservice home: https://github.com/example/myapp sources: - https://github.com/example/myapp maintainers: - name: John Doe email: john@example.com dependencies: - name: mysql version: "8.0.x" repository: "https://charts.bitnami.com/bitnami" condition: mysql.enabled - name: redis version: "16.x.x" repository: "https://charts.bitnami.com/bitnami" alias: cache⚠️避坑警告:
version和appVersion是两个完全不同的东西!version是Chart本身的版本(改模板就升这个),appVersion是里面应用的版本(升级应用就升这个)。很多新手搞混,导致回滚时一脸懵逼。
values.yaml:配置的中心化
# 全局配置 replicaCount: 3 # 镜像配置 image: repository: nginx pullPolicy: IfNotPresent tag: "1.20.0" # 镜像拉取密钥 imagePullSecrets: [] nameOverride: "" fullnameOverride: "" # Service配置 service: type: ClusterIP port: 80 targetPort: 8080 # Ingress配置 ingress: enabled: true className: "nginx" annotations: cert-manager.io/cluster-issuer: "letsencrypt" nginx.ingress.kubernetes.io/ssl-redirect: "true" hosts: - host: myapp.example.com paths: - path: / pathType: Prefix tls: - secretName: myapp-tls hosts: - myapp.example.com # 资源限制 resources: limits: cpu: 1000m memory: 1Gi requests: cpu: 100m memory: 128Mi # 自动扩缩容 autoscaling: enabled: true minReplicas: 3 maxReplicas: 10 targetCPUUtilizationPercentage: 80 targetMemoryUtilizationPercentage: 80 # Pod中断预算 podDisruptionBudget: enabled: true minAvailable: 2 # 健康检查 livenessProbe: httpGet: path: /health port: http initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /ready port: http initialDelaySeconds: 5 periodSeconds: 5 # 配置和密钥 config: logLevel: info maxConnections: 100 secrets: dbPassword: "" apiKey: "" # 数据库依赖配置 mysql: enabled: true auth: rootPassword: "" database: myapp username: myapp password: "" primary: persistence: enabled: true size: 10Gitemplates/deployment.yaml:模板的艺术
apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "myapp.fullname" . }} labels: {{- include "myapp.labels" . | nindent 4 }} spec: {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} {{- end }} selector: matchLabels: {{- include "myapp.selectorLabels" . | nindent 6 }} template: metadata: annotations: # 配置变更自动滚动更新 checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} checksum/secrets: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} labels: {{- include "myapp.selectorLabels" . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "myapp.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 env: - name: LOG_LEVEL value: {{ .Values.config.logLevel | quote }} - name: DB_HOST {{- if .Values.mysql.enabled }} value: {{ include "myapp.fullname" . }}-mysql {{- else }} value: {{ .Values.externalDatabase.host | quote }} {{- end }} - name: DB_PASSWORD valueFrom: secretKeyRef: name: {{ include "myapp.fullname" . }}-db key: password livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: {{- toYaml .Values.readinessProbe | nindent 12 }} resources: {{- toYaml .Values.resources | nindent 12 }} volumeMounts: - name: config mountPath: /etc/myapp/config.yaml subPath: config.yaml volumes: - name: config configMap: name: {{ include "myapp.fullname" . }}-config_helpers.tpl:模板函数库
{{/* 生成Chart全名 */}} {{- define "myapp.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 }} {{/* Chart名称 */}} {{- define "myapp.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* 通用标签 */}} {{- define "myapp.labels" -}} helm.sh/chart: {{ include "myapp.chart" . }} {{ include "myapp.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* 选择器标签 */}} {{- define "myapp.selectorLabels" -}} app.kubernetes.io/name: {{ include "myapp.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* Chart标签 */}} {{- define "myapp.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* ServiceAccount名称 */}} {{- define "myapp.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} {{- default (include "myapp.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }}💡效率技巧:
_helpers.tpl是Chart的瑞士军刀。把常用的命名逻辑、标签生成抽离到这里,可以让你的模板代码减少50%,而且更容易维护。记住:不要重复自己(DRY)。
Kustomize:Overlay模式的魔法
为什么需要Kustomize?
Helm擅长包管理和模板化,Kustomize擅长环境差异化配置。它们不是竞争关系,而是互补关系。
想象一下:你用Helm打包了一个应用,但开发环境、测试环境、生产环境的配置差异很大——域名不同、资源限制不同、副本数不同。用Helm的--values可以搞定,但当差异变得复杂时,Kustomize的Overlay模式更清晰。
Kustomize架构
┌─────────────────────────────────────────────────────────────────────┐ │ Kustomize Overlay 架构 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────┐ │ │ │ Base(基础) │ ← 通用配置,所有环境共享 │ │ │ deployment.yaml│ │ │ │ service.yaml │ │ │ │ kustomization.yaml │ │ │ └────────┬────────┘ │ │ │ │ │ ┌──────┴──────┬──────────────┐ │ │ ▼ ▼ ▼ │ │ ┌────────┐ ┌──────────┐ ┌──────────┐ │ │ │ dev │ │ staging │ │ prod │ ← Overlay(覆盖层) │ │ │replica:1│ │ replica:2│ │ replica:5│ │ │ │cpu:100m │ │ cpu:500m │ │ cpu:2000m│ │ │ └────────┘ └──────────┘ └──────────┘ │ │ │ │ 原理:Base + Patch = 最终配置 │ │ │ └─────────────────────────────────────────────────────────────────────┘Kustomize目录结构
k8s/ ├── base/ # 基础配置 │ ├── kustomization.yaml │ ├── deployment.yaml │ ├── service.yaml │ └── configmap.yaml ├── overlays/ │ ├── dev/ # 开发环境 │ │ ├── kustomization.yaml │ │ ├── replica-patch.yaml │ │ └── resource-patch.yaml │ ├── staging/ # 测试环境 │ │ ├── kustomization.yaml │ │ ├── replica-patch.yaml │ │ └── ingress-patch.yaml │ └── prod/ # 生产环境 │ ├── kustomization.yaml │ ├── replica-patch.yaml │ ├── resource-patch.yaml │ ├── hpa-patch.yaml │ └── pdb-patch.yaml └── components/ # 可复用组件 ├── monitoring/ ├── security/ └── logging/Base层配置
# base/kustomization.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - deployment.yaml - service.yaml - configmap.yaml commonLabels: app.kubernetes.io/name: myapp app.kubernetes.io/managed-by: kustomize images: - name: myapp newTag: latest# base/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: myapp spec: replicas: 1 selector: matchLabels: app: myapp template: metadata: labels: app: myapp spec: containers: - name: myapp image: myapp:latest ports: - containerPort: 8080 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512MiOverlay层配置
# overlays/prod/kustomization.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: production namePrefix: prod- resources: - ../../base - hpa.yaml - pdb.yaml patchesStrategicMerge: - replica-patch.yaml - resource-patch.yaml - config-patch.yaml configMapGenerator: - name: myapp-config behavior: merge literals: - LOG_LEVEL=warn - MAX_CONNECTIONS=1000 secretGenerator: - name: myapp-secrets literals: - DB_PASSWORD=prod-secret-password replicas: - name: myapp count: 5 images: - name: myapp newTag: v2.0.0# overlays/prod/resource-patch.yaml apiVersion: apps/v1 kind: Deployment metadata: name: myapp spec: template: spec: containers: - name: myapp resources: requests: cpu: 500m memory: 512Mi limits: cpu: 2000m memory: 2Gi# overlays/prod/hpa.yaml apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: myapp-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: prod-myapp minReplicas: 5 maxReplicas: 20 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70💡效率技巧:Kustomize的
patchesStrategicMerge是神器。它可以智能合并配置,比如你在Base里定义了环境变量列表,在Overlay里添加新的变量,Kustomize会自动合并而不是覆盖。这比Helm的模板替换更优雅。
最佳实践:Helm + Kustomize组合拳
组合架构图
┌─────────────────────────────────────────────────────────────────────────┐ │ Helm + Kustomize 组合架构 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ Helm Chart │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ │ │ │ │ Chart.yaml │ │ values.yaml │ │ templates/ │ │ │ │ │ │ (元数据) │ │ (默认值) │ │ ┌─────────────────┐ │ │ │ │ │ └─────────────┘ └─────────────┘ │ │ deployment.yaml │ │ │ │ │ │ │ │ service.yaml │ │ │ │ │ │ 职责:包管理、版本控制、依赖管理 │ │ ... │ │ │ │ │ │ 输出:渲染后的K8s YAML清单 │ └─────────────────┘ │ │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ Kustomize Overlay │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ │ │ │ │ base/ │ │ dev/ │ │ prod/ │ │ │ │ │ │ (基础清单) │ │ (开发覆盖) │ │ (生产覆盖) │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────────────────┘ │ │ │ │ │ │ │ │ 职责:环境差异化、配置覆盖、安全加固 │ │ │ │ 输出:最终应用到集群的YAML │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ Kubernetes Cluster │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ 口诀:Helm管分发,Kustomize管差异 │ │ │ └─────────────────────────────────────────────────────────────────────────┘实战流程
# 步骤1:用Helm渲染模板(不安装) helm template myapp ./myapp-chart \ --values ./myapp-chart/values.yaml \ --output-dir ./rendered # 步骤2:用Kustomize应用环境覆盖 cd k8s/overlays/prod kustomize build . | kubectl apply -f - # 或者一步到位 helm template myapp ./myapp-chart | kustomize build ./overlays/prod | kubectl apply -f -项目实战目录结构
project/ ├── helm-charts/ # Helm Charts仓库 │ ├── myapp/ │ │ ├── Chart.yaml │ │ ├── values.yaml │ │ └── templates/ │ └── common/ # 通用Library Chart │ ├── Chart.yaml │ └── templates/ │ ├── _resources.tpl │ ├── _probes.tpl │ └── _labels.tpl ├── k8s-manifests/ # Kustomize配置 │ ├── base/ │ │ ├── kustomization.yaml │ │ └── helm-generated/ # Helm渲染输出 │ └── overlays/ │ ├── dev/ │ ├── staging/ │ └── prod/ ├── helmfile.yaml # Helmfile配置(多环境管理) └── scripts/ ├── deploy.sh # 部署脚本 └── rollback.sh # 回滚脚本Helmfile:多环境Helm管理
# helmfile.yaml releases: # 开发环境 - name: myapp-dev namespace: dev chart: ./helm-charts/myapp values: - ./helm-charts/myapp/values.yaml - ./values/dev.yaml set: - name: replicaCount value: 1 - name: resources.requests.cpu value: 100m # 测试环境 - name: myapp-staging namespace: staging chart: ./helm-charts/myapp values: - ./helm-charts/myapp/values.yaml - ./values/staging.yaml set: - name: replicaCount value: 2 # 生产环境 - name: myapp-prod namespace: production chart: ./helm-charts/myapp values: - ./helm-charts/myapp/values.yaml - ./values/prod.yaml set: - name: replicaCount value: 5 - name: autoscaling.enabled value: true hooks: - events: ["presync"] showlogs: true command: "/bin/sh" args: ["-c", "echo 'Deploying to PRODUCTION - are you sure?'"] repositories: - name: bitnami url: https://charts.bitnami.com/bitnami helmDefaults: timeout: 600 recreatePods: false force: false# 部署所有环境 helmfile sync # 仅部署生产环境 helmfile -l name=myapp-prod sync # 查看差异 helmfile diff # 回滚 helmfile rollback⚠️避坑警告:不要试图用Helm做所有的事情,也不要试图用Kustomize做所有的事情。Helm的模板逻辑太复杂会变得难以维护,Kustomize没有版本控制和依赖管理。记住:Helm管分发,Kustomize管差异。
实战:从零写一个生产级Chart
步骤1:创建Chart骨架
# 创建Chart helm create myapp cd myapp # 查看生成的结构 tree .步骤2:完善Chart.yaml
apiVersion: v2 name: myapp description: Production-ready Helm chart for MyApp type: application version: 1.0.0 appVersion: "2.0.0" kubeVersion: ">=1.19.0-0" keywords: - web - microservice - golang home: https://github.com/example/myapp sources: - https://github.com/example/myapp maintainers: - name: DevOps Team email: devops@example.com dependencies: - name: postgresql version: "12.x.x" repository: "https://charts.bitnami.com/bitnami" condition: postgresql.enabled - name: redis version: "17.x.x" repository: "https://charts.bitnami.com/bitnami" condition: redis.enabled annotations: category: Application步骤3:配置values.yaml
# 全局配置 replicaCount: 3 # 镜像配置 image: repository: myregistry/myapp pullPolicy: IfNotPresent tag: "" # 镜像拉取密钥 imagePullSecrets: - name: regcred nameOverride: "" fullnameOverride: "" # ServiceAccount serviceAccount: create: true annotations: {} name: "" # Pod安全 podSecurityContext: fsGroup: 2000 runAsNonRoot: true seccompProfile: type: RuntimeDefault securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: true runAsNonRoot: true runAsUser: 1000 # Service service: type: ClusterIP port: 80 targetPort: 8080 annotations: {} # Ingress ingress: enabled: true className: nginx annotations: cert-manager.io/cluster-issuer: letsencrypt-prod nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/rate-limit: "100" hosts: - host: api.example.com paths: - path: / pathType: Prefix tls: - secretName: api-tls hosts: - api.example.com # 资源限制 resources: limits: cpu: 1000m memory: 1Gi requests: cpu: 100m memory: 128Mi # 自动扩缩容 autoscaling: enabled: true minReplicas: 3 maxReplicas: 20 targetCPUUtilizationPercentage: 70 targetMemoryUtilizationPercentage: 80 behavior: scaleDown: stabilizationWindowSeconds: 300 policies: - type: Percent value: 10 periodSeconds: 60 scaleUp: stabilizationWindowSeconds: 0 policies: - type: Percent value: 100 periodSeconds: 15 - type: Pods value: 4 periodSeconds: 15 selectPolicy: Max # Pod中断预算 podDisruptionBudget: enabled: true minAvailable: 2 # 健康检查 livenessProbe: httpGet: path: /health port: http initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /ready port: http initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 startupProbe: httpGet: path: /health port: http initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 30 # 配置 config: logLevel: info logFormat: json maxConnections: 1000 timeout: 30s # 数据库配置 postgresql: enabled: true auth: postgresPassword: "" database: myapp username: myapp password: "" primary: persistence: enabled: true size: 10Gi storageClass: fast-ssd redis: enabled: true auth: enabled: true password: "" master: persistence: enabled: true size: 5Gi # 监控 metrics: enabled: true port: 9090 path: /metrics serviceMonitor: enabled: true namespace: monitoring interval: 30s步骤4:编写Deployment模板
# templates/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "myapp.fullname" . }} labels: {{- include "myapp.labels" . | nindent 4 }} spec: {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} {{- end }} revisionHistoryLimit: 10 strategy: type: RollingUpdate rollingUpdate: maxSurge: 25% maxUnavailable: 0 selector: matchLabels: {{- include "myapp.selectorLabels" . | nindent 6 }} template: metadata: annotations: # 强制滚动更新 checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} checksum/secrets: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} prometheus.io/scrape: "{{ .Values.metrics.enabled }}" prometheus.io/port: "{{ .Values.metrics.port }}" prometheus.io/path: "{{ .Values.metrics.path }}" {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} labels: {{- include "myapp.selectorLabels" . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "myapp.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.metrics.enabled }} - name: metrics containerPort: {{ .Values.metrics.port }} protocol: TCP {{- end }} env: - name: LOG_LEVEL value: {{ .Values.config.logLevel | quote }} - name: LOG_FORMAT value: {{ .Values.config.logFormat | quote }} - name: MAX_CONNECTIONS value: {{ .Values.config.maxConnections | quote }} - name: TIMEOUT value: {{ .Values.config.timeout | quote }} - name: DB_HOST {{- if .Values.postgresql.enabled }} value: {{ include "myapp.fullname" . }}-postgresql {{- else }} value: {{ .Values.externalDatabase.host | quote }} {{- end }} - name: DB_PORT value: "5432" - name: DB_NAME value: {{ .Values.postgresql.auth.database | quote }} - name: DB_USER value: {{ .Values.postgresql.auth.username | quote }} - name: DB_PASSWORD valueFrom: secretKeyRef: name: {{ include "myapp.fullname" . }}-db key: password - name: REDIS_HOST {{- if .Values.redis.enabled }} value: {{ include "myapp.fullname" . }}-redis-master {{- else }} value: {{ .Values.externalRedis.host | quote }} {{- end }} - name: REDIS_PASSWORD valueFrom: secretKeyRef: name: {{ include "myapp.fullname" . }}-redis key: password livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: {{- toYaml .Values.readinessProbe | nindent 12 }} startupProbe: {{- toYaml .Values.startupProbe | nindent 12 }} resources: {{- toYaml .Values.resources | nindent 12 }} volumeMounts: - name: tmp mountPath: /tmp - name: config mountPath: /etc/myapp readOnly: true volumes: - name: tmp emptyDir: {} - name: config configMap: name: {{ include "myapp.fullname" . }}-config topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: ScheduleAnyway labelSelector: matchLabels: {{- include "myapp.selectorLabels" . | nindent 14 }} - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: {{- include "myapp.selectorLabels" . | nindent 14 }} affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app.kubernetes.io/name operator: In values: - {{ include "myapp.name" . }} topologyKey: kubernetes.io/hostname步骤5:打包和发布
# 更新依赖 helm dependency update # 打包Chart helm package . # 验证Chart helm lint . # 模板渲染测试 helm template myapp . --debug # 本地安装测试 helm install myapp-test . --dry-run --debug # 推送到Chart仓库 helm push myapp-1.0.0.tgz oci://registry.example.com/charts💡效率技巧:生产级Chart必须包含PodDisruptionBudget(保证升级时最少可用实例)、HPA(自动扩缩容)、TopologySpreadConstraints(跨可用区/节点分布)、以及合理的健康检查配置。这些不是"锦上添花",是"保命必备"。
文末三件套
1. 【源码获取】
关注此系列获取后续更新,后台回复’helm’获取完整Chart源码和配套Kustomize配置。
2. 【思考题】
你的Chart维护有哪些痛点?是模板逻辑太复杂?还是多环境配置管理混乱?欢迎在评论区分享你的踩坑经历。
3. 【系列预告】
- K8s平台篇:从零搭建生产级K8s集群
- 安全扫描篇:镜像安全、合规检查、漏洞管理
- 可观测性篇:Prometheus + Grafana + ELK全链路监控
总结
| 工具 | 擅长 | 不擅长 |
|---|---|---|
| Helm | 包管理、版本控制、依赖管理、模板化 | 复杂的环境差异化配置 |
| Kustomize | 环境差异化、配置覆盖、无模板化 | 依赖管理、版本控制 |
| Helm + Kustomize | 全都要 | 学习曲线稍陡 |
核心口诀:Helm管分发,Kustomize管差异。两者结合,天下无敌。
最后送大家一句话:YAML写得好,半夜不用搞。祝大家的K8s之路,少踩坑,多上线!
标签:Helm, Kustomize, Chart, 包管理, Kubernetes部署, Helmfile
