Home > Blog > tech

Helm Chart สร้างเอง คืออะไร? สอนสร้าง Custom Helm Chart สำหรับ K8s Application 2026

Helm Chart Creation Guide K8s 2026
2026-04-16 | tech | 3600 words

Helm เป็น Package Manager ของ Kubernetes เหมือน apt สำหรับ Ubuntu หรือ npm สำหรับ Node.js การสร้าง Helm Chart เองทำให้สามารถ Package Application พร้อม Configuration ทั้งหมดเป็นหน่วยเดียว Deploy ซ้ำได้ แชร์กับทีมได้ และ Version control ได้

บทความนี้สอนสร้าง Custom Helm Chart ตั้งแต่เริ่มต้น จนถึง Publish ขึ้น Registry สำหรับใช้งานจริงในปี 2026

ทำไมต้องสร้าง Helm Chart เอง?

helm create — เริ่มต้นสร้าง Chart

# สร้าง Chart scaffold
helm create my-webapp

# โครงสร้างที่ได้:
my-webapp/
  Chart.yaml          # Metadata ของ Chart
  values.yaml         # Default configuration values
  charts/             # Chart dependencies
  templates/          # Kubernetes manifest templates
    deployment.yaml
    service.yaml
    ingress.yaml
    hpa.yaml
    serviceaccount.yaml
    _helpers.tpl       # Named templates (helper functions)
    NOTES.txt          # Post-install instructions
    tests/
      test-connection.yaml
  .helmignore          # Files to ignore when packaging

Chart.yaml — Metadata ของ Chart

# Chart.yaml
apiVersion: v2                    # Helm 3 ใช้ v2
name: my-webapp                   # ชื่อ Chart
description: A web application Helm chart
type: application                 # application หรือ library
version: 1.2.0                   # Chart version (SemVer)
appVersion: "3.5.1"              # App version ที่ deploy

# Optional
home: https://github.com/myorg/my-webapp
icon: https://example.com/icon.png
sources:
  - https://github.com/myorg/my-webapp
maintainers:
  - name: DevOps Team
    email: devops@example.com
keywords:
  - webapp
  - api
  - microservice

# Dependencies
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

Version Strategy

Versionเมื่อเปลี่ยนตัวอย่าง
Chart version (version)เมื่อแก้ไข Chart (templates, values, etc.)1.0.0 → 1.1.0 → 2.0.0
App version (appVersion)เมื่อ Application มี Release ใหม่3.5.0 → 3.5.1 → 3.6.0

values.yaml — Configuration Design

# values.yaml — ออกแบบให้ครอบคลุมทุก Environment
replicaCount: 2

image:
  repository: myorg/my-webapp
  tag: ""                         # Default ใช้ appVersion จาก Chart.yaml
  pullPolicy: IfNotPresent

imagePullSecrets: []

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

service:
  type: ClusterIP
  port: 80
  targetPort: 8080

ingress:
  enabled: false
  className: nginx
  annotations: {}
  hosts:
    - host: my-webapp.example.com
      paths:
        - path: /
          pathType: Prefix
  tls: []

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi

autoscaling:
  enabled: false
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80

env: []
envFrom: []

configMap:
  enabled: false
  data: {}

secret:
  enabled: false
  data: {}

persistence:
  enabled: false
  storageClass: ""
  accessMode: ReadWriteOnce
  size: 10Gi

healthcheck:
  liveness:
    path: /healthz
    port: 8080
    initialDelaySeconds: 30
    periodSeconds: 10
  readiness:
    path: /ready
    port: 8080
    initialDelaySeconds: 5
    periodSeconds: 5

postgresql:
  enabled: false

redis:
  enabled: false

Templates — Deployment, Service, Ingress

templates/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-webapp.fullname" . }}
  labels:
    {{- include "my-webapp.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "my-webapp.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "my-webapp.selectorLabels" . | nindent 8 }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      serviceAccountName: {{ include "my-webapp.serviceAccountName" . }}
      containers:
        - name: {{ .Chart.Name }}
          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.healthcheck }}
          livenessProbe:
            httpGet:
              path: {{ .Values.healthcheck.liveness.path }}
              port: {{ .Values.healthcheck.liveness.port }}
            initialDelaySeconds: {{ .Values.healthcheck.liveness.initialDelaySeconds }}
            periodSeconds: {{ .Values.healthcheck.liveness.periodSeconds }}
          readinessProbe:
            httpGet:
              path: {{ .Values.healthcheck.readiness.path }}
              port: {{ .Values.healthcheck.readiness.port }}
            initialDelaySeconds: {{ .Values.healthcheck.readiness.initialDelaySeconds }}
            periodSeconds: {{ .Values.healthcheck.readiness.periodSeconds }}
          {{- end }}
          {{- with .Values.resources }}
          resources:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          {{- with .Values.env }}
          env:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          {{- with .Values.envFrom }}
          envFrom:
            {{- toYaml . | nindent 12 }}
          {{- end }}

Template Functions — include, tpl, toYaml

# include — เรียก Named template
{{ include "my-webapp.fullname" . }}
# ใช้แทน {{ template "my-webapp.fullname" . }} เพราะ include ส่งผลลัพธ์เป็น string
# → pipe ต่อกับ function อื่นได้ เช่น | nindent 4

# toYaml — แปลง Go object เป็น YAML string
{{- toYaml .Values.resources | nindent 12 }}
# .Values.resources = map[string]interface{} → แปลงเป็น YAML

# nindent — indent + newline ข้างหน้า
{{- toYaml .Values.env | nindent 12 }}
# nindent 12 = newline + 12 spaces indent

# tpl — render template string จาก values
# values.yaml: customAnnotation: "app-{{ .Release.Name }}"
{{ tpl .Values.customAnnotation . }}
# → render ค่า .Release.Name แทน placeholder

# default — ค่า default ถ้าเป็น empty
{{ .Values.image.tag | default .Chart.AppVersion }}

# quote — ครอบ string ด้วย double quotes
{{ .Values.image.tag | quote }}

# required — error ถ้าค่า empty
{{ required "image.repository is required" .Values.image.repository }}

Flow Control — if/else, range, with

# if/else
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
...
{{- end }}

# if/else/if
{{- if eq .Values.service.type "NodePort" }}
  nodePort: {{ .Values.service.nodePort }}
{{- else if eq .Values.service.type "LoadBalancer" }}
  loadBalancerIP: {{ .Values.service.loadBalancerIP }}
{{- end }}

# range — loop
{{- range .Values.ingress.hosts }}
  - host: {{ .host | quote }}
    http:
      paths:
        {{- range .paths }}
        - path: {{ .path }}
          pathType: {{ .pathType }}
          backend:
            service:
              name: {{ include "my-webapp.fullname" $ }}
              port:
                number: {{ $.Values.service.port }}
        {{- end }}
{{- end }}

# with — change scope
{{- with .Values.resources }}
resources:
  {{- toYaml . | nindent 2 }}
{{- end }}

Named Templates — _helpers.tpl

# templates/_helpers.tpl
# ไฟล์ที่ขึ้นต้นด้วย _ จะไม่ถูก render เป็น Kubernetes manifest
# ใช้สำหรับเก็บ Named templates ที่เรียกซ้ำได้

# Chart name
{{- define "my-webapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

# Full name (release + chart)
{{- define "my-webapp.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}

# Common labels
{{- define "my-webapp.labels" -}}
helm.sh/chart: {{ include "my-webapp.chart" . }}
{{ include "my-webapp.selectorLabels" . }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}

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

# Service Account name
{{- define "my-webapp.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "my-webapp.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

Chart Dependencies

# Chart.yaml — dependencies section
dependencies:
  - name: postgresql
    version: "12.12.10"
    repository: "https://charts.bitnami.com/bitnami"
    condition: postgresql.enabled
  - name: redis
    version: "17.15.6"
    repository: "https://charts.bitnami.com/bitnami"
    condition: redis.enabled
    alias: cache           # ใช้ .Values.cache แทน .Values.redis

# อัพเดท Dependencies
helm dependency update ./my-webapp
# → download charts ลงใน charts/ directory

# ล็อค Version
helm dependency build ./my-webapp
# → สร้าง Chart.lock

# values.yaml — configure dependencies
postgresql:
  enabled: true
  auth:
    postgresPassword: "secretpass"
    database: "myapp"
  primary:
    persistence:
      size: 20Gi

redis:
  enabled: false

Chart Hooks — Pre/Post Install/Upgrade

# templates/hooks/db-migration.yaml
# Hook ที่รัน Database migration ก่อน Upgrade
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "my-webapp.fullname" . }}-db-migrate
  annotations:
    "helm.sh/hook": pre-upgrade,pre-install
    "helm.sh/hook-weight": "-5"        # ลำดับ (ตัวน้อยรันก่อน)
    "helm.sh/hook-delete-policy": hook-succeeded  # ลบ Job หลังสำเร็จ
spec:
  template:
    spec:
      containers:
        - name: migrate
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          command: ["python", "manage.py", "migrate"]
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: {{ include "my-webapp.fullname" . }}-db
                  key: url
      restartPolicy: Never
  backoffLimit: 3
Hookเมื่อรันตัวอย่างใช้งาน
pre-installก่อน install resourcesสร้าง namespace, secrets
post-installหลัง install ทั้งหมดส่ง notification, seed data
pre-upgradeก่อน upgradeDatabase migration
post-upgradeหลัง upgradeClear cache, notification
pre-deleteก่อนลบ ReleaseBackup data
post-deleteหลังลบ ReleaseCleanup external resources
pre-rollbackก่อน rollbackDatabase rollback
testhelm testIntegration test

Testing — helm lint, template, test

# 1. helm lint — ตรวจ syntax
helm lint ./my-webapp
# ==> Linting ./my-webapp
# [INFO] Chart.yaml: icon is recommended
# 1 chart(s) linted, 0 chart(s) failed

# 2. helm template — render template ดู output
helm template my-release ./my-webapp --values custom-values.yaml
# → แสดง Kubernetes YAML ที่จะ apply

# 3. helm template + kubeval — validate output
helm template my-release ./my-webapp | kubeval --strict

# 4. helm test — รัน Test Pod
# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "my-webapp.fullname" . }}-test"
  annotations:
    "helm.sh/hook": test
spec:
  containers:
    - name: wget
      image: busybox
      command: ['wget']
      args: ['{{ include "my-webapp.fullname" . }}:{{ .Values.service.port }}']
  restartPolicy: Never

# รัน test
helm test my-release

# 5. chart-testing (ct) — CI/CD testing
# ติดตั้ง: pip install chart-testing
ct lint --charts ./my-webapp
ct install --charts ./my-webapp

Packaging & Publishing

Helm Package

# Package chart เป็น .tgz
helm package ./my-webapp
# → my-webapp-1.2.0.tgz

# Package พร้อม sign (GPG)
helm package --sign --key 'My Key' --keyring ~/.gnupg/pubring.gpg ./my-webapp

Publish ไปที่ OCI Registry (แนะนำ 2026)

# Login to OCI Registry
helm registry login ghcr.io -u myuser

# Push chart
helm push my-webapp-1.2.0.tgz oci://ghcr.io/myorg/charts

# Pull chart
helm pull oci://ghcr.io/myorg/charts/my-webapp --version 1.2.0

# Install from OCI
helm install my-release oci://ghcr.io/myorg/charts/my-webapp --version 1.2.0

Publish ไปที่ GitHub Pages

# 1. สร้าง gh-pages branch
# 2. helm package ./my-webapp
# 3. helm repo index . --url https://myorg.github.io/charts
# 4. Push index.yaml + .tgz ไป gh-pages

# ผู้ใช้ add repo:
helm repo add myorg https://myorg.github.io/charts
helm repo update
helm install my-release myorg/my-webapp

ChartMuseum (Self-hosted)

# Deploy ChartMuseum
docker run -d -p 8080:8080   -e STORAGE=local   -e STORAGE_LOCAL_ROOTDIR=/charts   -v /data/chartmuseum:/charts   ghcr.io/helm/chartmuseum:v0.16.1

# Push chart
curl --data-binary "@my-webapp-1.2.0.tgz" http://chartmuseum:8080/api/charts

# Add repo
helm repo add mymuseum http://chartmuseum:8080
helm install my-release mymuseum/my-webapp

Helm Chart Best Practices

Practiceทำไม
ใช้ include แทน templateinclude ส่ง string กลับ pipe ต่อได้ (nindent, quote)
ใช้ {{- (dash) ตัด whitespaceลด blank lines ใน output YAML
ตั้ง values.yaml ให้มี Default ที่ใช้งานได้helm install ได้เลยโดยไม่ต้อง --set อะไร
ใส่ required สำหรับ mandatory valuesError ชัดเจนเมื่อไม่ได้ใส่ค่าที่จำเป็น
ใช้ SemVer สำหรับ versionMAJOR.MINOR.PATCH ชัดเจน
เขียน NOTES.txtผู้ใช้เห็น Instructions หลัง install
เขียน Testตรวจสอบว่า Deploy สำเร็จ
ใช้ .helmignoreไม่ Pack ไฟล์ที่ไม่จำเป็น (.git, tests, docs)
Document ทุก value ใน values.yamlผู้ใช้รู้ว่าตั้งค่าอะไรได้บ้าง
ใช้ OCI Registry (2026+)Standard ใหม่ รองรับ RBAC signing verification
เริ่มต้น: helm create my-chart → แก้ values.yaml → แก้ templates → helm lint → helm template → helm install → helm test สร้าง Chart แรกภายใน 30 นาที

สรุป

การสร้าง Custom Helm Chart ทำให้ Kubernetes Deployment เป็นระบบ ทำซ้ำได้ แชร์ได้ และ Version control ได้ เริ่มจาก helm create แก้ไข Chart.yaml, values.yaml, templates ใส่ Flow control (if/range/with) สร้าง Named templates ใน _helpers.tpl เพิ่ม Hooks สำหรับ Migration ทดสอบด้วย lint + template + test แล้ว Package + Publish ขึ้น OCI Registry หรือ GitHub Pages

Helm Chart ที่ดีคือ Chart ที่ helm install ได้เลยโดยไม่ต้อง Config อะไร (sensible defaults) แต่สามารถ Customize ได้ทุกอย่างผ่าน values.yaml เมื่อต้องการ


Back to Blog | iCafe Forex | SiamLanCard | Siam2R