Chapter 6: Helm Package Management

Haiyue
25min

Chapter 6: Helm Package Management

Learning Objectives
  • Understand Helm’s design philosophy and core concepts
  • Master the creation and usage of Helm Charts
  • Learn to deploy and manage applications using Helm
  • Become proficient in Chart version management and rollback

Key Concepts

What is Helm

Helm is the package manager for Kubernetes, similar to apt/yum for Linux or npm for Node.js. It simplifies the deployment and management of Kubernetes applications.

🔄 正在渲染 Mermaid 图表...

Helm Core Concepts

🔄 正在渲染 Mermaid 图表...

Helm Architecture

🔄 正在渲染 Mermaid 图表...

Installing Helm

# macOS (using Homebrew)
brew install helm

# Linux (using script)
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# Windows (using Chocolatey)
choco install kubernetes-helm

# Or download binary
# https://github.com/helm/helm/releases

# Verify installation
helm version
# version.BuildInfo{Version:"v3.12.0", ...}

Using Helm Repositories

# Add official repositories
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

# Update repository index
helm repo update

# List added repositories
helm repo list

# Search for Charts
helm search repo nginx
helm search repo mysql --versions  # Show all versions

# Search Artifact Hub (public repositories)
helm search hub prometheus

# View Chart information
helm show chart bitnami/nginx
helm show values bitnami/nginx  # View default values
helm show all bitnami/nginx     # View all information

# Remove repository
helm repo remove stable

Deploying Applications

Basic Deployment

# Install a Chart
helm install my-nginx bitnami/nginx

# Specify namespace
helm install my-nginx bitnami/nginx -n web --create-namespace

# View Release status
helm status my-nginx

# List all Releases
helm list
helm list -A  # All namespaces

# View Release history
helm history my-nginx

# Uninstall Release
helm uninstall my-nginx
helm uninstall my-nginx --keep-history  # Keep history

Custom Configuration

# Method 1: Command line parameters
helm install my-nginx bitnami/nginx \
  --set replicaCount=3 \
  --set service.type=LoadBalancer \
  --set resources.requests.memory=256Mi

# Method 2: Values file
cat > my-values.yaml <<EOF
replicaCount: 3
service:
  type: LoadBalancer
  port: 80
resources:
  requests:
    memory: 256Mi
    cpu: 100m
  limits:
    memory: 512Mi
    cpu: 200m
ingress:
  enabled: true
  hostname: nginx.example.com
EOF

helm install my-nginx bitnami/nginx -f my-values.yaml

# Method 3: Multiple values files (later overrides earlier)
helm install my-nginx bitnami/nginx \
  -f values-base.yaml \
  -f values-production.yaml

# View final rendered templates (without deployment)
helm template my-nginx bitnami/nginx -f my-values.yaml

# Simulate installation (dry-run)
helm install my-nginx bitnami/nginx -f my-values.yaml --dry-run --debug

Upgrade and Rollback

# Upgrade Release
helm upgrade my-nginx bitnami/nginx -f my-values.yaml

# Upgrade with new parameters
helm upgrade my-nginx bitnami/nginx --set replicaCount=5

# Install or upgrade (idempotent operation)
helm upgrade --install my-nginx bitnami/nginx -f my-values.yaml

# View history versions
helm history my-nginx
# REVISION    STATUS      DESCRIPTION
# 1           superseded  Install complete
# 2           superseded  Upgrade complete
# 3           deployed    Upgrade complete

# Rollback to specific version
helm rollback my-nginx 2

# Rollback to previous version
helm rollback my-nginx

# View differences between two versions
helm diff revision my-nginx 2 3  # Requires helm-diff plugin

Creating Helm Charts

Chart Structure

mychart/
├── Chart.yaml          # Chart metadata
├── values.yaml         # Default configuration values
├── charts/             # Dependent Charts
├── templates/          # Template files
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── configmap.yaml
│   ├── secret.yaml
│   ├── _helpers.tpl    # Template helper functions
│   ├── NOTES.txt       # Post-installation notes
│   └── tests/          # Test templates
│       └── test-connection.yaml
├── .helmignore         # Files to ignore when packaging
└── README.md           # Documentation

Creating a Chart

# Create new Chart
helm create mychart

# View generated files
tree mychart

Chart.yaml

apiVersion: v2
name: mychart
description: A Helm chart for my application
type: application
version: 0.1.0        # Chart version
appVersion: "1.0.0"   # Application version

# Maintainer information
maintainers:
- name: Your Name
  email: your@email.com
  url: https://example.com

# Keywords (for searching)
keywords:
- web
- nginx

# Project homepage
home: https://github.com/example/mychart

# Icon
icon: https://example.com/icon.png

# Dependencies
dependencies:
- name: mysql
  version: "9.x.x"
  repository: https://charts.bitnami.com/bitnami
  condition: mysql.enabled
- name: redis
  version: "17.x.x"
  repository: https://charts.bitnami.com/bitnami
  condition: redis.enabled

values.yaml

# Application configuration
replicaCount: 1

image:
  repository: nginx
  pullPolicy: IfNotPresent
  tag: ""  # Defaults to appVersion

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

# Service configuration
service:
  type: ClusterIP
  port: 80

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

# Resource limits
resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 200m
    memory: 256Mi

# Autoscaling
autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80

# Node selector
nodeSelector: {}

# Tolerations
tolerations: []

# Affinity
affinity: {}

# Application configuration
config:
  logLevel: info
  database:
    host: localhost
    port: 5432

# Dependency configuration
mysql:
  enabled: false
  auth:
    rootPassword: "secret"

redis:
  enabled: false

Template Syntax

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "mychart.fullname" . }}
  labels:
    {{- include "mychart.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "mychart.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
      labels:
        {{- include "mychart.selectorLabels" . | nindent 8 }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      containers:
      - name: {{ .Chart.Name }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
        imagePullPolicy: {{ .Values.image.pullPolicy }}
        ports:
        - name: http
          containerPort: 80
          protocol: TCP
        env:
        - name: LOG_LEVEL
          value: {{ .Values.config.logLevel | quote }}
        - name: DB_HOST
          value: {{ .Values.config.database.host | quote }}
        {{- if .Values.mysql.enabled }}
        - name: MYSQL_HOST
          value: {{ include "mychart.fullname" . }}-mysql
        {{- end }}
        livenessProbe:
          httpGet:
            path: /health
            port: http
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: http
          initialDelaySeconds: 5
          periodSeconds: 5
        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 }}

Helper Templates (_helpers.tpl)

# templates/_helpers.tpl

{{/*
Generate application full name
*/}}
{{- define "mychart.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 "mychart.labels" -}}
helm.sh/chart: {{ include "mychart.chart" . }}
{{ include "mychart.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

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

{{/*
Chart name and version
*/}}
{{- define "mychart.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Application name
*/}}
{{- define "mychart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

Service Template

# templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ include "mychart.fullname" . }}
  labels:
    {{- include "mychart.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  ports:
  - port: {{ .Values.service.port }}
    targetPort: http
    protocol: TCP
    name: http
  selector:
    {{- include "mychart.selectorLabels" . | nindent 4 }}

Ingress Template

# templates/ingress.yaml
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "mychart.fullname" . }}
  labels:
    {{- include "mychart.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 "mychart.fullname" $ }}
                port:
                  number: {{ $.Values.service.port }}
          {{- end }}
    {{- end }}
{{- end }}

ConfigMap Template

# templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "mychart.fullname" . }}
  labels:
    {{- include "mychart.labels" . | nindent 4 }}
data:
  config.yaml: |
    logLevel: {{ .Values.config.logLevel }}
    database:
      host: {{ .Values.config.database.host }}
      port: {{ .Values.config.database.port }}
    {{- if .Values.config.extra }}
    extra:
      {{- toYaml .Values.config.extra | nindent 6 }}
    {{- end }}

NOTES.txt

# 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 }}{{ (index $host.paths 0).path }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
  export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "mychart.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 }}
     NOTE: It may take a few minutes for the LoadBalancer IP to be available.
           You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "mychart.fullname" . }}'
  export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "mychart.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
  echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
  export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "mychart.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
  export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
  echo "Visit http://127.0.0.1:8080 to use your application"
  kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

Chart Testing

# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "mychart.fullname" . }}-test-connection"
  labels:
    {{- include "mychart.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
spec:
  containers:
  - name: wget
    image: busybox
    command: ['wget']
    args: ['{{ include "mychart.fullname" . }}:{{ .Values.service.port }}']
  restartPolicy: Never
# Run Chart tests
helm test my-release

# View test results
kubectl logs my-release-mychart-test-connection

Chart Packaging and Publishing

# Check Chart syntax
helm lint mychart

# Package Chart
helm package mychart
# Generates mychart-0.1.0.tgz

# Generate index.yaml (for repository)
helm repo index .

# Install local Chart
helm install my-release ./mychart
helm install my-release ./mychart-0.1.0.tgz

# Push to Chart repository (ChartMuseum example)
curl --data-binary "@mychart-0.1.0.tgz" http://chartmuseum:8080/api/charts

# Push to OCI repository
helm push mychart-0.1.0.tgz oci://registry.example.com/charts

Dependency Management

# Download dependencies
helm dependency update mychart

# View dependencies
helm dependency list mychart

# Dependencies are downloaded to charts/ directory
ls mychart/charts/
# Dependency definition in Chart.yaml
dependencies:
- name: mysql
  version: "9.x.x"
  repository: https://charts.bitnami.com/bitnami
  condition: mysql.enabled
  tags:
  - database
- name: redis
  version: "17.x.x"
  repository: https://charts.bitnami.com/bitnami
  condition: redis.enabled
  tags:
  - cache
# Enable/disable dependencies in values.yaml
mysql:
  enabled: true
  auth:
    database: myapp
    username: appuser

redis:
  enabled: false

# Or using tags
tags:
  database: true
  cache: false

Practical Exercise

Creating a Complete Web Application Chart

# Create Chart
helm create webapp

# Edit files
# webapp/Chart.yaml
apiVersion: v2
name: webapp
description: A complete web application with frontend, backend, and database
type: application
version: 1.0.0
appVersion: "2.0.0"

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
# webapp/values.yaml
# Global configuration
global:
  environment: production
  domain: example.com

# Frontend configuration
frontend:
  replicaCount: 2
  image:
    repository: myapp/frontend
    tag: "2.0.0"
  resources:
    requests:
      cpu: 100m
      memory: 128Mi

# Backend configuration
backend:
  replicaCount: 3
  image:
    repository: myapp/backend
    tag: "2.0.0"
  resources:
    requests:
      cpu: 200m
      memory: 256Mi

# Ingress configuration
ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
  - host: www.example.com
    paths:
    - path: /
      pathType: Prefix
      service: frontend
    - path: /api
      pathType: Prefix
      service: backend
  tls:
  - hosts:
    - www.example.com
    secretName: webapp-tls

# Database configuration
postgresql:
  enabled: true
  auth:
    database: webapp
    username: webapp
    existingSecret: webapp-db-secret
  primary:
    persistence:
      size: 10Gi

# Cache configuration
redis:
  enabled: true
  auth:
    enabled: false
  master:
    persistence:
      size: 1Gi
# webapp/templates/frontend-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "webapp.fullname" . }}-frontend
  labels:
    {{- include "webapp.labels" . | nindent 4 }}
    app.kubernetes.io/component: frontend
spec:
  replicas: {{ .Values.frontend.replicaCount }}
  selector:
    matchLabels:
      {{- include "webapp.selectorLabels" . | nindent 6 }}
      app.kubernetes.io/component: frontend
  template:
    metadata:
      labels:
        {{- include "webapp.selectorLabels" . | nindent 8 }}
        app.kubernetes.io/component: frontend
    spec:
      containers:
      - name: frontend
        image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag }}"
        ports:
        - name: http
          containerPort: 80
        env:
        - name: API_URL
          value: "http://{{ include "webapp.fullname" . }}-backend:8080"
        resources:
          {{- toYaml .Values.frontend.resources | nindent 12 }}

---
# webapp/templates/backend-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "webapp.fullname" . }}-backend
  labels:
    {{- include "webapp.labels" . | nindent 4 }}
    app.kubernetes.io/component: backend
spec:
  replicas: {{ .Values.backend.replicaCount }}
  selector:
    matchLabels:
      {{- include "webapp.selectorLabels" . | nindent 6 }}
      app.kubernetes.io/component: backend
  template:
    metadata:
      labels:
        {{- include "webapp.selectorLabels" . | nindent 8 }}
        app.kubernetes.io/component: backend
    spec:
      containers:
      - name: backend
        image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}"
        ports:
        - name: http
          containerPort: 8080
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: {{ include "webapp.fullname" . }}-secrets
              key: database-url
        {{- if .Values.redis.enabled }}
        - name: REDIS_URL
          value: "redis://{{ include "webapp.fullname" . }}-redis-master:6379"
        {{- end }}
        resources:
          {{- toYaml .Values.backend.resources | nindent 12 }}
# Update dependencies
helm dependency update webapp

# Check templates
helm template webapp ./webapp -f values-production.yaml

# Deploy application
helm upgrade --install webapp ./webapp \
  --namespace webapp \
  --create-namespace \
  -f values-production.yaml

# View status
helm status webapp -n webapp
kubectl get all -n webapp

Multi-Environment Deployment

# values-development.yaml
global:
  environment: development

frontend:
  replicaCount: 1
  image:
    tag: "latest"

backend:
  replicaCount: 1
  image:
    tag: "latest"

ingress:
  enabled: false

postgresql:
  enabled: true
  primary:
    persistence:
      size: 1Gi

redis:
  enabled: false
# values-staging.yaml
global:
  environment: staging
  domain: staging.example.com

frontend:
  replicaCount: 2

backend:
  replicaCount: 2

ingress:
  enabled: true
  hosts:
  - host: staging.example.com
    paths:
    - path: /
      pathType: Prefix
      service: frontend

postgresql:
  primary:
    persistence:
      size: 5Gi

redis:
  enabled: true
# Deploy to different environments
helm upgrade --install webapp-dev ./webapp \
  -f values-development.yaml \
  -n development

helm upgrade --install webapp-staging ./webapp \
  -f values-staging.yaml \
  -n staging

helm upgrade --install webapp-prod ./webapp \
  -f values-production.yaml \
  -n production

Helm Plugins

# Install common plugins

# helm-diff: View upgrade differences
helm plugin install https://github.com/databus23/helm-diff

# helm-secrets: Manage encrypted values
helm plugin install https://github.com/jkroepke/helm-secrets

# helm-s3: Use S3 as Chart repository
helm plugin install https://github.com/hypnoglow/helm-s3

# View installed plugins
helm plugin list

# Using helm-diff
helm diff upgrade my-release ./mychart -f values.yaml

# Using helm-secrets
helm secrets enc secrets.yaml
helm secrets dec secrets.yaml
helm upgrade --install my-release ./mychart -f secrets://secrets.yaml

Common Troubleshooting

# View Release details
helm get all my-release
helm get values my-release
helm get manifest my-release
helm get notes my-release
helm get hooks my-release

# Debug template rendering
helm template my-release ./mychart --debug

# View rendered specific template
helm template my-release ./mychart -s templates/deployment.yaml

# Check Chart syntax
helm lint ./mychart

# Simulate installation
helm install my-release ./mychart --dry-run --debug

# View installation failure reason
helm status my-release
kubectl describe pod -l app.kubernetes.io/instance=my-release
Best Practices
  1. Version Control: Store Charts in Git repositories
  2. Semantic Versioning: Follow semver conventions for version management
  3. Safe Defaults: values.yaml should have sensible default values
  4. Complete Documentation: Write clear README and NOTES.txt
  5. Test Coverage: Write Chart tests to verify correct deployment
  6. Sensitive Data: Use helm-secrets or external secret management

Summary

Through this chapter, you should have mastered:

  • Helm Concepts: Understanding the relationship between Charts, Releases, and Repositories
  • Deployment Management: Using Helm to install, upgrade, and rollback applications
  • Chart Development: Creating custom Charts and using template syntax
  • Dependency Management: Managing Chart dependencies and sub-Charts
  • Best Practices: Multi-environment configuration, version management, and plugin usage

In the next chapter, we will learn about monitoring and logging, mastering the use of Prometheus, Grafana, and EFK/ELK.