article

Helm Charts ที่ทำให้ผมหยุดเกลียด Kubernetes

18 min read

วันที่เกลียด Kubernetes Deployment

ก่อนรู้จัก Helm ผมเกลียด Kubernetes มาก ๆ เพราะ deployment มันยุ่งยากแบบนี้:

# ทุกครั้งที่ deploy ต้องทำแบบนี้
kubectl apply -f namespace.yaml
kubectl apply -f configmap.yaml
kubectl apply -f secret.yaml
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f ingress.yaml

# แล้วต้องจำ order ด้วย เพราะบางอันต้องสร้างก่อน
kubectl create namespace myapp-production
kubectl create configmap app-config --from-file=config/ -n myapp-production
kubectl create secret generic app-secrets --from-literal=DB_PASSWORD=secret123 -n myapp-production
kubectl apply -f deployment.yaml -n myapp-production

ปัญหาที่เจอบ่อย:

  • Copy-paste Hell: แก้ deployment.yaml แล้วลืมแก้ service.yaml
  • Environment Management: มี dev, staging, production แต่ config ต่างกัน
  • Version Control: ไม่รู้ว่า version ไหน deploy อยู่
  • Rollback: เวลาอยากย้อนกลับ ต้อง manual ทุกอย่าง
  • Dependencies: ต้องจำว่าอะไรต้องสร้างก่อนอะไร

ตัวอย่างความยุ่งยาก:

# deployment-dev.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-dev
  namespace: myapp-dev
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp-dev
  template:
    spec:
      containers:
      - name: app
        image: myapp:v1.0.0-dev
        env:
        - name: DB_HOST
          value: "dev-db.example.com"
        - name: REDIS_URL
          value: "dev-redis.example.com"

---
# deployment-prod.yaml - เกือบเหมือนกันแต่ต่างกันนิดหน่อย
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-prod
  namespace: myapp-prod
spec:
  replicas: 5
  selector:
    matchLabels:
      app: myapp-prod
  template:
    spec:
      containers:
      - name: app
        image: myapp:v1.0.0
        env:
        - name: DB_HOST
          value: "prod-db.example.com"
        - name: REDIS_URL
          value: "prod-redis.example.com"

ผลลัพธ์: ใช้เวลา deploy นาน ผิดพลาดบ่อย และ maintenance ยาก! 😤

วันหนึ่งเพื่อนแนะนำ Helm บอกว่า “มันจะทำให้ K8s ง่ายขึ้นมาก”

ตอนแรกไม่เชื่อ… แต่พอลองแล้ว ชีวิตเปลี่ยนไปเลย! ⛵

Helm Basics: Package Manager สำหรับ Kubernetes

1. การติดตั้งและ Setup

# ติดตั้ง Helm (Windows)
choco install kubernetes-helm

# หรือ download จาก GitHub
# https://github.com/helm/helm/releases

# ตรวจสอบการติดตั้ง
helm version

# Add popular repositories
helm repo add stable https://charts.helm.sh/stable
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add nginx-ingress https://kubernetes.github.io/ingress-nginx
helm repo update

2. การใช้งาน Pre-built Charts

# ค้นหา chart ที่ต้องการ
helm search repo nginx
helm search repo postgresql

# ดูข้อมูล chart
helm show chart bitnami/postgresql
helm show values bitnami/postgresql

# Install chart
helm install my-postgres bitnami/postgresql \
  --set auth.postgresPassword=mypassword \
  --set primary.persistence.size=20Gi \
  --namespace database \
  --create-namespace

# ดู status
helm status my-postgres -n database

# List installations
helm list --all-namespaces

# Upgrade
helm upgrade my-postgres bitnami/postgresql \
  --set auth.postgresPassword=newpassword \
  --namespace database

# Rollback
helm rollback my-postgres 1 -n database

# Uninstall
helm uninstall my-postgres -n database

สร้าง Helm Chart แรก

1. Create Chart Structure

# สร้าง chart ใหม่
helm create myapp

# Structure ที่ได้
myapp/
├── Chart.yaml          # Chart metadata
├── values.yaml         # Default values
├── charts/            # Dependencies
└── templates/         # Kubernetes YAML templates
    ├── deployment.yaml
    ├── service.yaml
    ├── ingress.yaml
    ├── _helpers.tpl   # Template helpers
    └── NOTES.txt      # Installation notes

2. Chart.yaml Configuration

# myapp/Chart.yaml
apiVersion: v2
name: myapp
description: My awesome application Helm chart
type: application
version: 0.1.0
appVersion: "1.0.0"

maintainers:
  - name: Your Name
    email: your.email@example.com

keywords:
  - web
  - api
  - microservice

home: https://github.com/yourorg/myapp
sources:
  - https://github.com/yourorg/myapp

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

3. Values.yaml - Configuration Management

# myapp/values.yaml
replicaCount: 1

image:
  repository: myapp
  tag: ""
  pullPolicy: IfNotPresent

nameOverride: ""
fullnameOverride: ""

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

podAnnotations: {}

podSecurityContext:
  fsGroup: 2000

securityContext:
  capabilities:
    drop:
    - ALL
  readOnlyRootFilesystem: true
  runAsNonRoot: true
  runAsUser: 1000

service:
  type: ClusterIP
  port: 80
  targetPort: 3000

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

resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 250m
    memory: 256Mi

autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 100
  targetCPUUtilizationPercentage: 80

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

# Application-specific values
app:
  env: development
  debug: true
  
database:
  host: ""
  port: 5432
  name: myapp
  user: myapp

redis:
  enabled: true
  host: ""
  port: 6379

postgresql:
  enabled: true
  auth:
    postgresPassword: "changeme"
    database: myapp
    username: myapp
    password: "changeme"
  primary:
    persistence:
      enabled: true
      size: 8Gi

monitoring:
  enabled: false
  serviceMonitor:
    enabled: false

Template Engineering

1. Deployment Template

# myapp/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 }}
  selector:
    matchLabels:
      {{- include "myapp.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      {{- with .Values.podAnnotations }}
      annotations:
        {{- 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
          livenessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: http
            initialDelaySeconds: 5
            periodSeconds: 5
          env:
            - name: NODE_ENV
              value: {{ .Values.app.env | quote }}
            - name: DEBUG
              value: {{ .Values.app.debug | quote }}
            {{- if .Values.postgresql.enabled }}
            - name: DATABASE_URL
              value: "postgresql://{{ .Values.postgresql.auth.username }}:{{ .Values.postgresql.auth.password }}@{{ include "myapp.fullname" . }}-postgresql:5432/{{ .Values.postgresql.auth.database }}"
            {{- else }}
            - name: DATABASE_HOST
              value: {{ .Values.database.host | quote }}
            - name: DATABASE_PORT
              value: {{ .Values.database.port | quote }}
            - name: DATABASE_NAME
              value: {{ .Values.database.name | quote }}
            - name: DATABASE_USER
              value: {{ .Values.database.user | quote }}
            {{- end }}
            {{- if .Values.redis.enabled }}
            - name: REDIS_URL
              value: "redis://{{ include "myapp.fullname" . }}-redis-master:6379"
            {{- else if .Values.redis.host }}
            - name: REDIS_URL
              value: "redis://{{ .Values.redis.host }}:{{ .Values.redis.port }}"
            {{- end }}
          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 }}

2. Service Template

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

3. ConfigMap Template

# myapp/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "myapp.fullname" . }}-config
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
data:
  app.properties: |
    # Application Configuration
    app.name={{ include "myapp.fullname" . }}
    app.version={{ .Chart.AppVersion }}
    app.environment={{ .Values.app.env }}
    
    # Database Configuration
    {{- if .Values.postgresql.enabled }}
    database.type=postgresql
    database.host={{ include "myapp.fullname" . }}-postgresql
    database.port=5432
    database.name={{ .Values.postgresql.auth.database }}
    {{- else }}
    database.type=external
    database.host={{ .Values.database.host }}
    database.port={{ .Values.database.port }}
    database.name={{ .Values.database.name }}
    {{- end }}
    
    # Feature Flags
    features.monitoring={{ .Values.monitoring.enabled }}
    features.autoscaling={{ .Values.autoscaling.enabled }}

4. Ingress Template

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

5. Helper Templates

# myapp/templates/_helpers.tpl
{{/*
Expand the name of the chart.
*/}}
{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Create a default fully qualified app name.
*/}}
{{- 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 }}

{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "myapp.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Common labels
*/}}
{{- 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 }}

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

{{/*
Create the name of the service account to use
*/}}
{{- define "myapp.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "myapp.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

{{/*
Generate database URL based on configuration
*/}}
{{- define "myapp.databaseUrl" -}}
{{- if .Values.postgresql.enabled }}
postgresql://{{ .Values.postgresql.auth.username }}:{{ .Values.postgresql.auth.password }}@{{ include "myapp.fullname" . }}-postgresql:5432/{{ .Values.postgresql.auth.database }}
{{- else }}
postgresql://{{ .Values.database.user }}:{{ .Values.database.password }}@{{ .Values.database.host }}:{{ .Values.database.port }}/{{ .Values.database.name }}
{{- end }}
{{- end }}

Environment-Specific Values

1. Development Environment

# values-dev.yaml
replicaCount: 1

image:
  tag: "latest"
  pullPolicy: Always

app:
  env: development
  debug: true

resources:
  limits:
    cpu: 100m
    memory: 128Mi
  requests:
    cpu: 50m
    memory: 64Mi

ingress:
  enabled: true
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
  hosts:
    - host: myapp-dev.local
      paths:
        - path: /
          pathType: Prefix

postgresql:
  enabled: true
  auth:
    postgresPassword: "dev123"
    password: "dev123"
  primary:
    persistence:
      enabled: false

redis:
  enabled: true
  replica:
    replicaCount: 0
  auth:
    enabled: false

monitoring:
  enabled: false

2. Staging Environment

# values-staging.yaml
replicaCount: 2

image:
  tag: "v1.0.0-rc"
  pullPolicy: IfNotPresent

app:
  env: staging
  debug: false

resources:
  limits:
    cpu: 250m
    memory: 256Mi
  requests:
    cpu: 100m
    memory: 128Mi

ingress:
  enabled: true
  className: "nginx"
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-staging"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
  hosts:
    - host: myapp-staging.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: myapp-staging-tls
      hosts:
        - myapp-staging.example.com

postgresql:
  enabled: true
  auth:
    postgresPassword: "staging-secure-password"
    password: "staging-secure-password"
  primary:
    persistence:
      enabled: true
      size: 10Gi
      storageClass: "ssd"

redis:
  enabled: true
  auth:
    enabled: true
    password: "staging-redis-password"
  replica:
    replicaCount: 1

monitoring:
  enabled: true
  serviceMonitor:
    enabled: true

3. Production Environment

# values-prod.yaml
replicaCount: 5

image:
  tag: "v1.0.0"
  pullPolicy: IfNotPresent

app:
  env: production
  debug: false

resources:
  limits:
    cpu: 1000m
    memory: 1Gi
  requests:
    cpu: 500m
    memory: 512Mi

autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 20
  targetCPUUtilizationPercentage: 70

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: myapp.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: myapp-prod-tls
      hosts:
        - myapp.example.com

# Use external managed databases
postgresql:
  enabled: false

database:
  host: "prod-db.amazonaws.com"
  port: 5432
  name: "myapp_production"
  user: "myapp_user"

redis:
  enabled: false
  host: "prod-redis.amazonaws.com"
  port: 6379

nodeSelector:
  node-type: "application"

tolerations:
  - key: "dedicated"
    operator: "Equal"
    value: "application"
    effect: "NoSchedule"

affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 100
      podAffinityTerm:
        labelSelector:
          matchExpressions:
          - key: app.kubernetes.io/name
            operator: In
            values:
            - myapp
        topologyKey: kubernetes.io/hostname

monitoring:
  enabled: true
  serviceMonitor:
    enabled: true
    interval: 30s

Deployment Workflows

1. Development Deployment

# Deploy to development
helm upgrade --install myapp-dev ./myapp \
  --namespace myapp-dev \
  --create-namespace \
  --values values-dev.yaml \
  --set image.tag=latest

# Debug template rendering
helm template myapp-dev ./myapp \
  --values values-dev.yaml \
  --debug

# Dry run
helm upgrade --install myapp-dev ./myapp \
  --namespace myapp-dev \
  --values values-dev.yaml \
  --dry-run --debug

# Check status
helm status myapp-dev -n myapp-dev
kubectl get pods -n myapp-dev

2. CI/CD Pipeline Integration

# .github/workflows/deploy.yml
name: Deploy to Kubernetes

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    
    - name: Lint Helm Chart
      run: |
        helm lint ./helm/myapp
        
    - name: Test Helm Templates
      run: |
        helm template test ./helm/myapp \
          --values ./helm/myapp/values-dev.yaml \
          --debug

  deploy-dev:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
    steps:
    - uses: actions/checkout@v3
    
    - name: Configure kubectl
      uses: azure/k8s-set-context@v1
      with:
        method: kubeconfig
        kubeconfig: ${{ secrets.KUBE_CONFIG }}
    
    - name: Deploy to Development
      run: |
        helm upgrade --install myapp-dev ./helm/myapp \
          --namespace myapp-dev \
          --create-namespace \
          --values ./helm/myapp/values-dev.yaml \
          --set image.tag=${{ github.sha }}

  deploy-staging:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
    - uses: actions/checkout@v3
    
    - name: Configure kubectl
      uses: azure/k8s-set-context@v1
      with:
        method: kubeconfig
        kubeconfig: ${{ secrets.KUBE_CONFIG }}
    
    - name: Deploy to Staging
      run: |
        helm upgrade --install myapp-staging ./helm/myapp \
          --namespace myapp-staging \
          --create-namespace \
          --values ./helm/myapp/values-staging.yaml \
          --set image.tag=${{ github.sha }} \
          --wait --timeout=300s

  deploy-prod:
    needs: [test, deploy-staging]
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')
    environment: production
    steps:
    - uses: actions/checkout@v3
    
    - name: Configure kubectl
      uses: azure/k8s-set-context@v1
      with:
        method: kubeconfig
        kubeconfig: ${{ secrets.PROD_KUBE_CONFIG }}
    
    - name: Deploy to Production
      run: |
        helm upgrade --install myapp-prod ./helm/myapp \
          --namespace myapp-prod \
          --create-namespace \
          --values ./helm/myapp/values-prod.yaml \
          --set image.tag=${GITHUB_REF#refs/tags/} \
          --wait --timeout=600s

Advanced Helm Patterns

1. Chart Dependencies

# Chart.yaml
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
    
  - name: nginx-ingress
    version: "4.x.x"
    repository: https://kubernetes.github.io/ingress-nginx
    condition: ingress.controller.enabled
    
  - name: cert-manager
    version: "1.x.x"
    repository: https://charts.jetstack.io
    condition: certManager.enabled
# Update dependencies
helm dependency update ./myapp

# Build dependencies
helm dependency build ./myapp

# List dependencies
helm dependency list ./myapp

2. Hooks และ Tests

# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "myapp.fullname" . }}-test-connection"
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  restartPolicy: Never
  containers:
    - name: wget
      image: busybox
      command: ['wget']
      args: ['{{ include "myapp.fullname" . }}:{{ .Values.service.port }}/health']
# templates/hooks/pre-install-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: "{{ include "myapp.fullname" . }}-db-migrate"
  labels:
    {{- include "myapp.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:
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: db-migrate
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
        command:
          - npm
          - run
          - migrate
        env:
          - name: DATABASE_URL
            value: {{ include "myapp.databaseUrl" . }}
# Run tests
helm test myapp-dev -n myapp-dev

# Run tests with logs
helm test myapp-dev -n myapp-dev --logs

3. Conditional Resources

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

4. Multi-environment Secrets Management

# templates/secret.yaml
{{- if .Values.secrets.create }}
apiVersion: v1
kind: Secret
metadata:
  name: {{ include "myapp.fullname" . }}-secrets
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
type: Opaque
data:
  {{- range $key, $value := .Values.secrets.data }}
  {{ $key }}: {{ $value | b64enc | quote }}
  {{- end }}
{{- end }}
# values-prod.yaml
secrets:
  create: false  # Use external secret management in prod
  
# values-dev.yaml
secrets:
  create: true
  data:
    DB_PASSWORD: "dev123"
    API_KEY: "dev-api-key"

Helm Chart Repository

1. Create Chart Repository

# Package chart
helm package ./myapp

# Create index file
helm repo index . --url https://mycompany.github.io/helm-charts

# Serve locally for testing
helm serve --repo-path .

2. GitHub Pages Repository

# .github/workflows/release.yml
name: Release Charts

on:
  push:
    branches:
      - main

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: Configure Git
        run: |
          git config user.name "$GITHUB_ACTOR"
          git config user.email "$GITHUB_ACTOR@users.noreply.github.com"

      - name: Install Helm
        uses: azure/setup-helm@v3

      - name: Run chart-releaser
        uses: helm/chart-releaser-action@v1.5.0
        env:
          CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

3. Using Private Repository

# Add private repository with authentication
helm repo add mycompany https://charts.mycompany.com \
  --username myuser \
  --password mypass

# Or using token
helm repo add mycompany https://charts.mycompany.com \
  --pass-credentials

# Search and install
helm search repo mycompany
helm install myapp mycompany/myapp

Production Best Practices

1. Security Hardening

# Security-focused values
securityContext:
  runAsNonRoot: true
  runAsUser: 65534
  runAsGroup: 65534
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false
  capabilities:
    drop:
      - ALL

podSecurityContext:
  seccompProfile:
    type: RuntimeDefault
  fsGroup: 65534

# Network policies
networkPolicy:
  enabled: true
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              name: nginx-ingress
      ports:
        - protocol: TCP
          port: 3000
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              name: database
      ports:
        - protocol: TCP
          port: 5432

2. Resource Management

# Proper resource limits
resources:
  limits:
    cpu: "1"
    memory: "1Gi"
    ephemeral-storage: "2Gi"
  requests:
    cpu: "500m"
    memory: "512Mi"
    ephemeral-storage: "1Gi"

# Quality of Service
qosClass: Guaranteed  # Achieved by setting limits = requests

# Pod Disruption Budget
podDisruptionBudget:
  enabled: true
  minAvailable: 2
  # or maxUnavailable: 1

3. Monitoring และ Observability

# templates/servicemonitor.yaml
{{- if .Values.monitoring.serviceMonitor.enabled }}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: {{ include "myapp.fullname" . }}
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
spec:
  selector:
    matchLabels:
      {{- include "myapp.selectorLabels" . | nindent 6 }}
  endpoints:
    - port: http
      path: /metrics
      interval: {{ .Values.monitoring.serviceMonitor.interval | default "30s" }}
      scrapeTimeout: {{ .Values.monitoring.serviceMonitor.scrapeTimeout | default "10s" }}
{{- end }}

4. Backup และ Recovery

# templates/cronjob-backup.yaml
{{- if .Values.backup.enabled }}
apiVersion: batch/v1
kind: CronJob
metadata:
  name: {{ include "myapp.fullname" . }}-backup
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
spec:
  schedule: {{ .Values.backup.schedule | quote }}
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - name: backup
            image: {{ .Values.backup.image }}
            command:
              - /bin/bash
              - -c
              - |
                pg_dump $DATABASE_URL > /backup/backup-$(date +%Y%m%d-%H%M%S).sql
                aws s3 cp /backup/ s3://{{ .Values.backup.s3Bucket }}/ --recursive
            env:
              - name: DATABASE_URL
                value: {{ include "myapp.databaseUrl" . }}
            volumeMounts:
              - name: backup-storage
                mountPath: /backup
          volumes:
            - name: backup-storage
              emptyDir: {}
{{- end }}

Troubleshooting และ Debugging

1. Common Issues

# Template rendering issues
helm template myapp ./myapp --debug

# Values validation
helm lint ./myapp

# Check what will be deployed
helm diff upgrade myapp ./myapp --values values-prod.yaml

# Debug failed deployment
kubectl describe pod -l app.kubernetes.io/name=myapp -n myapp-prod
kubectl logs -l app.kubernetes.io/name=myapp -n myapp-prod --previous

# Check Helm release status
helm status myapp-prod -n myapp-prod
helm history myapp-prod -n myapp-prod

# Get generated manifests
helm get manifest myapp-prod -n myapp-prod
helm get values myapp-prod -n myapp-prod

# Rollback if needed
helm rollback myapp-prod 1 -n myapp-prod

2. Debugging Templates

# Add debug information to templates
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "myapp.fullname" . }}-debug
data:
  debug-info.yaml: |
    chart:
      name: {{ .Chart.Name }}
      version: {{ .Chart.Version }}
    release:
      name: {{ .Release.Name }}
      namespace: {{ .Release.Namespace }}
      service: {{ .Release.Service }}
    values:
{{- .Values | toYaml | nindent 6 }}

3. Validation และ Testing

# JSON Schema validation (values.schema.json)
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "replicaCount": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100
    },
    "image": {
      "type": "object",
      "properties": {
        "repository": {
          "type": "string",
          "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$"
        },
        "tag": {
          "type": "string",
          "pattern": "^[a-zA-Z0-9._-]+$"
        }
      },
      "required": ["repository"]
    }
  },
  "required": ["image"]
}

เคสจริง: จาก Manual Deploy สู่ Helm

ก่อนใช้ Helm

# ต้องจำคำสั่งยาวๆ และทำเป็น script
#!/bin/bash
ENV=${1:-dev}

kubectl create namespace myapp-$ENV || true

kubectl create configmap app-config \
  --from-literal=NODE_ENV=$ENV \
  --from-literal=DEBUG=true \
  -n myapp-$ENV \
  --dry-run=client -o yaml | kubectl apply -f -

kubectl create secret generic app-secrets \
  --from-literal=DB_PASSWORD=$DB_PASSWORD \
  --from-literal=API_KEY=$API_KEY \
  -n myapp-$ENV \
  --dry-run=client -o yaml | kubectl apply -f -

envsubst < deployment-template.yaml | kubectl apply -f - -n myapp-$ENV
envsubst < service-template.yaml | kubectl apply -f - -n myapp-$ENV

ปัญหา:

  • Environment variables ต้องจัดการเอง
  • Template substitution ไม่ flexible
  • ไม่มี rollback mechanism
  • Version tracking ยาก
  • Dependencies ต้องจัดการ manual

หลังใช้ Helm

# Development
helm upgrade --install myapp-dev ./myapp \
  -n myapp-dev --create-namespace \
  -f values-dev.yaml \
  --set image.tag=$BUILD_VERSION

# Production
helm upgrade --install myapp-prod ./myapp \
  -n myapp-prod --create-namespace \
  -f values-prod.yaml \
  --set image.tag=$RELEASE_VERSION \
  --wait --timeout=300s

# Rollback if needed
helm rollback myapp-prod 1 -n myapp-prod

ประโยชน์ที่ได้:

  • Deployment Time: จาก 15 นาที เหลือ 2 นาที
  • Error Rate: จาก 30% เหลือ <5%
  • Rollback Time: จาก 30 นาที เหลือ 1 นาที
  • Environment Consistency: 100% identical templates
  • Team Velocity: ทำให้ทีมใหม่ deploy ได้ใน 1 วัน แทน 1 สัปดาห์

สรุป: Helm ที่เปลี่ยนวิธีคิดเรื่อง K8s

ก่อนรู้จัก Helm:

  • Kubernetes = ยุ่งยาก ซับซ้อน 😤
  • Deployment = copy-paste hell
  • Environment management = nightmare
  • Rollback = manual และช้า
  • Team collaboration = ยาก

หลังรู้จัก Helm:

  • Package Management: เหมือน npm สำหรับ Kubernetes
  • Template Engine: Flexible และ DRY
  • Release Management: Version control สำหรับ deployments
  • Environment Parity: Dev/Staging/Prod เหมือนกัน
  • Team Collaboration: ใครก็ deploy ได้แบบเดียวกัน

ข้อดีที่ได้จริง:

  • Productivity เพิ่ม 10x: Deploy ง่าย รวดเร็ว น่าเชื่อถือ
  • Error ลด 90%: Template-driven ลด human error
  • Onboarding เร็วขึ้น: คนใหม่เข้าใจง่าย deploy ได้ทันที
  • Infrastructure as Code: Version control ทุกอย่าง
  • Standardization: Pattern เดียวกันทุก project

Best Practices ที่เรียนรู้:

  • Values-driven design: แยก configuration ออกจาก template
  • Environment parity: ใช้ template เดียว แต่ values ต่างกัน
  • Security by default: ตั้ง security context ที่เหมาะสม
  • Resource management: กำหนด limits และ requests เสมอ
  • Testing: ใช้ helm test และ dry-run ก่อน deploy

Anti-patterns ที่หลีกเลี่ยง:

  • Hardcode values ใน template
  • ไม่ใช้ helper functions
  • ไม่ทำ validation
  • ไม่มี resource limits
  • ไม่ test templates

Helm เหมือน Package Manager ที่ทำให้ Kubernetes เป็นมิตร

มันเปลี่ยน Kubernetes จาก “เครื่องมือที่ซับซ้อน” เป็น “แพลตฟอร์มที่ใช้งานง่าย”

ตอนนี้ไม่สามารถคิดถึง K8s deployment โดยไม่มี Helm ได้เลย!

เพราะมันทำให้การ deploy เป็นเรื่องสนุก แทนที่จะเป็นฝันร้าย! ⛵🚀