วันที่เกลียด 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 เป็นเรื่องสนุก แทนที่จะเป็นฝันร้าย! ⛵🚀