The Helm chart
สิ่งที่จะสร้าง
หัวข้อที่มีชื่อว่า “สิ่งที่จะสร้าง”deploy/helm/shopmicro/ — Helm chart ที่ package ทั้งระบบสำหรับ Kubernetes ทุก binary ที่ Module 13 containerize Service images → กลายเป็น Kubernetes Deployment: catalog, order, และ payment แยกกัน; notification และ notification-worker เป็น consumer ที่ไม่มี inbound port; และ gateway อยู่หน้าทุกตัวพร้อม Service (และ Ingress แบบ optional) บน HTTP 8080 gRPC service สองตัวที่ pod อื่นเรียกหา — catalog กับ order — ได้ ClusterIP Service เพื่อให้ชื่อ Service resolve ได้ภายใน cluster ส่วน ConfigMap ถือ env ที่ไม่ลับ (KAFKA_BROKERS, RABBITMQ_URL), Secret ถือ database URL และ templates/_helpers.tpl รวมชื่อกับ label ไว้ที่เดียวเพื่อ tag ทุก object ให้เหมือนกัน
ทุกอย่างขับด้วย values.yaml: image repository และ tag, จำนวน replica ต่อ service, และ endpoint ของ infrastructure ที่ใช้ร่วมกัน chart นี้ตั้งใจ ไม่ package Postgres, Kafka หรือ RabbitMQ เข้าไปด้วย แต่ถือว่ามีคนจัดการจากภายนอกแล้ว values.yaml จึงแค่ชี้ไปยังตำแหน่งที่ระบบเหล่านั้นรันอยู่ บทนี้สร้างและ lint chart ส่วน Deploying to the cluster → จะ install chart เข้า cluster จริงแล้วรัน curl smoke test เดียวกับที่ใช้มาทั้งคอร์ส เพียงแต่เปลี่ยนจาก go run เป็น pod
Docker Compose Compose stack → รันทั้ง 6 service บนเครื่องเดียวอยู่แล้ว Kubernetes ตอบคำถามที่ Compose ไม่ตอบ: พวกนี้รันข้ามหลายเครื่องอย่างไร, restart ตัวเองเมื่อ crash อย่างไร, roll out เวอร์ชันใหม่โดยไม่ downtime อย่างไร, และ scale notification-worker เป็นห้า replica โดยไม่แตะ service อื่นอย่างไร Helm chart คือวิธีอธิบายทั้งหมดนั้นเป็น package เดียวที่ version และ parameterize ได้ — helm install ครั้งเดียว แล้ว cluster reconcile ความเป็นจริงให้ตรงกับ manifest ที่ chart render จะเขียน Kubernetes YAML ดิบด้วยมือก็ได้ แต่ต้องก๊อป Deployment boilerplate เดิมหกครั้ง แล้วไล่แก้ image tag ข้ามไฟล์เป็นสิบทุกครั้งที่ release ส่วน templating ของ Helm ยุบทั้งหมดนั้นลงเหลือ values.yaml บวก template หนึ่งตัวต่อ object แต่ละ kind
สิ่งสำคัญที่สุดใน chart นี้คือรายละเอียดที่มองไม่เห็นตอน dev บนเครื่อง: CATALOG_GRPC_ADDR มีความหมายสองแบบ ขึ้นกับว่า pod ไหนเป็นคนอ่าน
ใน catalog pod ค่านี้คือ bind address เพราะ main ของ catalog ส่งต่อให้ net.Listen จึงต้องเป็น :50051 คือ listen บน port นั้นทุก interface
ส่วนใน order และ gateway pod ค่าเดียวกันนี้คือ dial address เพราะสอง service นั้นส่งต่อให้ grpc.NewClient เพื่อเข้าถึง Catalog จึงต้องเป็น catalog:50051 ที่เป็น DNS name ที่ Kubernetes ให้กับ Service ของ Catalog
ตอนรันบนเครื่อง สองบทบาทนี้ยุบเหลือ :50051 ค่าเดียว เพราะ net.Listen(":50051") กับการ dial localhost:50051 ชี้ไปที่ host เดียวกัน The gRPC Server → จึงใช้ค่าเดียวครอบสองบทบาทได้ แต่ใน cluster สองค่านี้ต่างกันจริง chart จึง set CATALOG_GRPC_ADDR ต่อ Deployment แทนที่จะ set ครั้งเดียวใน ConfigMap ที่ใช้ร่วมกัน เข้าใจว่า “ตัวแปรเดียวกันเป็น listen address ฝั่ง server และ dial address ฝั่ง client” ได้เมื่อไร การต่อ gRPC service ใน Kubernetes ก็เข้าใจง่ายไปกว่าครึ่ง
ที่เหลือเป็นมาตรฐาน และแต่ละทางเลือกมีเหตุผลของตัวเอง:
ConfigMapเทียบกับSecrethost ของKAFKA_BROKERSและRABBITMQ_URLไม่ใช่ความลับจึงอยู่ในConfigMapส่วน database URL พก credential มาด้วยจึงอยู่ในSecretKubernetes จัดการสองอย่างนี้ต่างกัน — อย่างน้อย Secret ก็ไม่โผล่ในget -o yamlธรรมดาโดย default และ encrypt at rest ได้ การแยกให้ชัดแบบนี้แปลว่า ConfigMap ที่รั่วจะไม่ลาก password ออกไปด้วย- Deployment ไหนได้
ServiceServiceมีไว้ให้ชื่อที่เสถียรและ load-balance traffic เข้าไปยัง กลุ่ม podcatalogและorderต้องมีเพราะ pod อื่น dial เข้ามา ส่วนgatewayต้องมีเพราะ client หรือ Ingress ยิงเข้ามา ขณะที่payment,notificationและnotification-workerเป็น consumer ล้วน ไม่มีใครเชื่อมเข้ามาหา มีแต่เอื้อม ออกไป หา Kafka และ RabbitMQ จึงไม่ได้ Service เลย เพราะ Service ที่ไม่มีคนใช้ก็เป็นแค่ขยะ - Readiness และ liveness probe gateway expose
/healthzไว้ grpc-gateway → probe จึงเป็นhttpGetธรรมดา ส่วน gRPC service ไม่ได้เสิร์ฟ HTTP chart นี้จึง probe ด้วยtcpSocketบน gRPC port ซึ่งพอจะบอกได้ว่า process listen อยู่ (ทางเลือกที่แกร่งกว่าคือ nativegrpcprobe ของ Kubernetes ซึ่งต้องให้ server implement servicegrpc.health.v1มาตรฐาน เพิ่มทีหลังเป็น follow-up เล็ก ๆ ที่คุ้มค่า ตามที่ระบุใน Pros & cons)
ข้อดีข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีข้อเสีย”Helm chart เทียบกับ kubectl apply manifest ที่เขียนเองดิบ ๆ (หรือ Kustomize)
- Pros:
values.yamlเดียว parameterize image tag, replica, และ resource ข้ามทุก object ดังนั้น release เป็นการแก้บรรทัดเดียวแทนที่จะ find-and-replace ข้ามไฟล์เป็นสิบ;helm install/upgrade/rollbackให้ release ที่ version, atomic พร้อมคำสั่ง rollback จริง; chart เป็น artifact ชิ้นเดียวที่ทีมอื่น install ได้โดยไม่ต้องอ่านโครงสร้างภายใน - Cons: Go-template-ใน-YAML ของ Helm อ่านและ debug ยากกว่า manifest แบน ๆ จริง ๆ —
{{- }}whitespace trim ที่หายไปหรือ.Valuespath ที่ผิดผลิต YAML ที่ invalid แบบสับสน; สำหรับ deployment เล็ก ๆ ที่ static, model overlay-a-base ของ Kustomize (ไม่มี templating language เลย) ง่ายกว่า และ manifest ดิบโปร่งใสที่สุด Helm คุ้มความซับซ้อนเมื่อคุณมี object ที่ parameterize เยอะและ release จริงให้จัดการ — ซึ่งระบบนี้มี
ถือ Postgres/Kafka/RabbitMQ เป็นแบบจัดการจากภายนอก เทียบกับ bundle เป็น subchart dependency ของ chart
- Pros: chart focus อยู่ที่ stateless service ของ ShopMicro เอง และ infra production มักถูกจัดการแยกอยู่แล้ว — cloud Postgres, managed Kafka — พร้อม backup, scaling และ lifecycle ของตัวเองที่ chart ของ stateless app ไม่ควรเป็นเจ้าของ
values.yamlจึงแค่ชี้ไปยังตำแหน่งเหล่านั้น - Cons: chart ไม่ self-contained —
helm installอย่างเดียวไม่ให้ระบบที่รันได้ คุณต้อง provision infra ก่อนแล้ว set endpoint — ส่วนการ bundle subchart Postgres/Kafka/RabbitMQ ของ Bitnami จะทำให้คำสั่งเดียวยกทุกอย่างขึ้น ซึ่งดีกว่าจริงสำหรับ demo หรือ test cluster ชั่วคราว trade คือ “รูปทรง production จริง” เทียบกับ “คำสั่งเดียวได้ทุกอย่าง” และคอร์สนี้เลือกอย่างแรก โดยมี subchart เป็นทางเลือกที่บันทึกไว้
ติดตั้ง
หัวข้อที่มีชื่อว่า “ติดตั้ง”1. deploy/helm/shopmicro/Chart.yaml
หัวข้อที่มีชื่อว่า “1. deploy/helm/shopmicro/Chart.yaml”apiVersion: v2name: shopmicrodescription: The ShopMicro microservices system — catalog, order, payment, notification, and the API gatewaytype: applicationversion: 0.1.0appVersion: "1.0.0"2. deploy/helm/shopmicro/values.yaml
หัวข้อที่มีชื่อว่า “2. deploy/helm/shopmicro/values.yaml”image: repository: shopmicro # images are shopmicro/<service>, built in Module 13 tag: latest pullPolicy: IfNotPresent
# Shared infrastructure is externally managed. Point these at wherever# Postgres, Kafka, and RabbitMQ run — in-cluster Service DNS is shown here.infra: kafkaBrokers: kafka:9092 rabbitmqURL: amqp://shopmicro:shopmicro@rabbitmq:5672/ postgres: host: postgres port: 5432 user: shopmicro password: shopmicro # override via --set or a values file in real use
services: catalog: replicas: 1 grpcPort: 50051 db: catalog order: replicas: 1 grpcPort: 50052 db: orders payment: replicas: 1 notification: replicas: 1 notificationWorker: replicas: 2 gateway: replicas: 2 httpPort: 8080 service: type: ClusterIP ingress: enabled: false host: shopmicro.local3. deploy/helm/shopmicro/templates/_helpers.tpl
หัวข้อที่มีชื่อว่า “3. deploy/helm/shopmicro/templates/_helpers.tpl”{{/* Common labels applied to every object. */}}{{- define "shopmicro.labels" -}}app.kubernetes.io/part-of: shopmicroapp.kubernetes.io/managed-by: {{ .Release.Service }}helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}{{- end -}}
{{/* Selector labels for a single service, given its name as the context. */}}{{- define "shopmicro.selectorLabels" -}}app.kubernetes.io/name: {{ .name }}app.kubernetes.io/part-of: shopmicro{{- end -}}
{{/* The Postgres URL for a given database name, assembled from infra values. */}}{{- define "shopmicro.pgurl" -}}postgres://{{ .root.Values.infra.postgres.user }}:{{ .root.Values.infra.postgres.password }}@{{ .root.Values.infra.postgres.host }}:{{ .root.Values.infra.postgres.port }}/{{ .db }}?sslmode=disable{{- end -}}4. deploy/helm/shopmicro/templates/configmap.yaml และ secret.yaml
หัวข้อที่มีชื่อว่า “4. deploy/helm/shopmicro/templates/configmap.yaml และ secret.yaml”apiVersion: v1kind: ConfigMapmetadata: name: shopmicro-config labels: {{- include "shopmicro.labels" . | nindent 4 }}data: KAFKA_BROKERS: {{ .Values.infra.kafkaBrokers | quote }} RABBITMQ_URL: {{ .Values.infra.rabbitmqURL | quote }}apiVersion: v1kind: Secretmetadata: name: shopmicro-db labels: {{- include "shopmicro.labels" . | nindent 4 }}type: OpaquestringData: CATALOG_DB_URL: {{ include "shopmicro.pgurl" (dict "root" $ "db" .Values.services.catalog.db) | quote }} ORDER_DB_URL: {{ include "shopmicro.pgurl" (dict "root" $ "db" .Values.services.order.db) | quote }}บันทึกเป็น templates/configmap.yaml และ templates/secret.yaml database URL ถูกประกอบจากค่า infra.postgres โดย helper shopmicro.pgurl ดังนั้น credential อยู่ที่เดียวเป๊ะ
5. deploy/helm/shopmicro/templates/catalog.yaml — pattern ของ gRPC service
หัวข้อที่มีชื่อว่า “5. deploy/helm/shopmicro/templates/catalog.yaml — pattern ของ gRPC service”apiVersion: apps/v1kind: Deploymentmetadata: name: catalog labels: {{- include "shopmicro.labels" . | nindent 4 }} app.kubernetes.io/name: catalogspec: replicas: {{ .Values.services.catalog.replicas }} selector: matchLabels: {{- include "shopmicro.selectorLabels" (dict "name" "catalog") | nindent 6 }} template: metadata: labels: {{- include "shopmicro.selectorLabels" (dict "name" "catalog") | nindent 8 }} spec: containers: - name: catalog image: "{{ .Values.image.repository }}/catalog:{{ .Values.image.tag }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - containerPort: {{ .Values.services.catalog.grpcPort }} env: # Bind address: catalog LISTENS on this port. - name: CATALOG_GRPC_ADDR value: ":{{ .Values.services.catalog.grpcPort }}" - name: CATALOG_DB_URL valueFrom: secretKeyRef: name: shopmicro-db key: CATALOG_DB_URL readinessProbe: tcpSocket: port: {{ .Values.services.catalog.grpcPort }} initialDelaySeconds: 3 livenessProbe: tcpSocket: port: {{ .Values.services.catalog.grpcPort }} initialDelaySeconds: 10---apiVersion: v1kind: Servicemetadata: name: catalog labels: {{- include "shopmicro.labels" . | nindent 4 }}spec: selector: {{- include "shopmicro.selectorLabels" (dict "name" "catalog") | nindent 4 }} ports: - port: {{ .Values.services.catalog.grpcPort }} targetPort: {{ .Values.services.catalog.grpcPort }}บันทึกเป็น templates/catalog.yaml Service ชื่อ catalog บน port 50051 คือสิ่งที่ทำให้ catalog:50051 resolve ได้จาก pod อื่น templates/order.yaml ใช้รูปทรงเดียวกันเป๊ะ ต่างกันสองจุด: bind ORDER_GRPC_ADDR: ":50052" และเพิ่ม env ที่ทำให้ Order เป็น client ของ Catalog และ Kafka:
env: - name: ORDER_GRPC_ADDR value: ":{{ .Values.services.order.grpcPort }}" # Dial address: order CONNECTS to Catalog's Service by name. - name: CATALOG_GRPC_ADDR value: "catalog:{{ .Values.services.catalog.grpcPort }}" - name: ORDER_DB_URL valueFrom: secretKeyRef: { name: shopmicro-db, key: ORDER_DB_URL } - name: KAFKA_BROKERS valueFrom: configMapKeyRef: { name: shopmicro-config, key: KAFKA_BROKERS }นั่นคือชื่อ CATALOG_GRPC_ADDR เดียวกับ catalog pod แต่ถือค่า dial (catalog:50051) แทนค่า bind (:50051) — ความต่างที่ Why ชี้ไว้ ทำให้เป็นรูปธรรม
6. Consumer Deployment — payment.yaml, notification.yaml, notification-worker.yaml
หัวข้อที่มีชื่อว่า “6. Consumer Deployment — payment.yaml, notification.yaml, notification-worker.yaml”ทั้งสองตัวใช้ pattern เดียวกับ catalog Deployment แต่ ไม่มี Service และไม่มี port เพราะไม่มีอะไรเชื่อมเข้ามา ต่างกันแค่ image กับ env ที่ดึงไปใช้:
payment:KAFKA_BROKERSจาก ConfigMap ไม่มี DB ไม่มี probe บน port (consumer ล้วนไม่มี readiness endpoint; ละ probe หรือเพิ่ม health port เล็ก ๆ ทีหลัง)notification:KAFKA_BROKERSและRABBITMQ_URLจาก ConfigMapnotification-worker:RABBITMQ_URLอย่างเดียว และreplicas: {{ .Values.services.notificationWorker.replicas }}(default เป็น 2 — การ scale-out แบบ competing-consumers ที่ The Send Worker → สร้างตอนนี้เป็นแค่จำนวน replica)
7. deploy/helm/shopmicro/templates/gateway.yaml
หัวข้อที่มีชื่อว่า “7. deploy/helm/shopmicro/templates/gateway.yaml”apiVersion: apps/v1kind: Deploymentmetadata: name: gateway labels: {{- include "shopmicro.labels" . | nindent 4 }} app.kubernetes.io/name: gatewayspec: replicas: {{ .Values.services.gateway.replicas }} selector: matchLabels: {{- include "shopmicro.selectorLabels" (dict "name" "gateway") | nindent 6 }} template: metadata: labels: {{- include "shopmicro.selectorLabels" (dict "name" "gateway") | nindent 8 }} spec: containers: - name: gateway image: "{{ .Values.image.repository }}/gateway:{{ .Values.image.tag }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - containerPort: {{ .Values.services.gateway.httpPort }} env: - name: GATEWAY_HTTP_ADDR value: ":{{ .Values.services.gateway.httpPort }}" - name: CATALOG_GRPC_ADDR value: "catalog:{{ .Values.services.catalog.grpcPort }}" - name: ORDER_GRPC_ADDR value: "order:{{ .Values.services.order.grpcPort }}" readinessProbe: httpGet: path: /healthz port: {{ .Values.services.gateway.httpPort }} initialDelaySeconds: 3 livenessProbe: httpGet: path: /healthz port: {{ .Values.services.gateway.httpPort }} initialDelaySeconds: 10---apiVersion: v1kind: Servicemetadata: name: gateway labels: {{- include "shopmicro.labels" . | nindent 4 }}spec: type: {{ .Values.services.gateway.service.type }} selector: {{- include "shopmicro.selectorLabels" (dict "name" "gateway") | nindent 4 }} ports: - port: {{ .Values.services.gateway.httpPort }} targetPort: {{ .Values.services.gateway.httpPort }}{{- if .Values.services.gateway.ingress.enabled }}---apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: gateway labels: {{- include "shopmicro.labels" . | nindent 4 }}spec: rules: - host: {{ .Values.services.gateway.ingress.host }} http: paths: - path: / pathType: Prefix backend: service: name: gateway port: number: {{ .Values.services.gateway.httpPort }}{{- end }}บันทึกเป็น templates/gateway.yaml gateway dial ไปที่ catalog:50051 และ order:50052 ที่เป็นชื่อ Service ทั้งคู่ ส่วน /healthz ของ gateway ขับ HTTP probe จริง {{- if .Values.services.gateway.ingress.enabled }} เป็นตัวคุม Ingress และปิดไว้โดย default บน laptop คุณจะใช้ port-forward ในบทหน้า ส่วนบน cluster จริงค่อยเปิดใช้งาน
ตรวจสอบผล
หัวข้อที่มีชื่อว่า “ตรวจสอบผล”คุณไม่ต้องมี cluster รันอยู่ก็เช็คได้ว่า chart ฟอร์แมตถูกต้อง เริ่มจาก lint ก่อน:
helm lint deploy/helm/shopmicro==> Linting deploy/helm/shopmicro1 chart(s) linted, 0 chart(s) failedแล้ว render template local ด้วยค่า default แล้วอ่าน YAML ที่ Kubernetes จะได้รับจริง — ไม่มี cluster เกี่ยว:
helm template shopmicro deploy/helm/shopmicro | grep -A2 "CATALOG_GRPC_ADDR"คุณควรเห็นตัวแปรเดียวกัน render สองแบบต่างกัน พิสูจน์การแยก bind-vs-dial:
- name: CATALOG_GRPC_ADDR value: ":50051" # in the catalog Deployment (bind)-- - name: CATALOG_GRPC_ADDR value: "catalog:50051" # in the order Deployment (dial)-- - name: CATALOG_GRPC_ADDR value: "catalog:50051" # in the gateway Deployment (dial)ยืนยันว่า consumer ไม่มี Service ด้วยการนับ — ห้า Service คือผิด สาม Service คือถูก (catalog, order, gateway):
helm template shopmicro deploy/helm/shopmicro | grep -c "^kind: Service"3Override ค่าโดยไม่แก้ไฟล์ เพื่อดู templating ทำงาน — เพิ่ม replica ของ worker:
helm template shopmicro deploy/helm/shopmicro \ --set services.notificationWorker.replicas=5 \ | grep -B4 "name: notification-worker" | grep replicas replicas: 5ตรวจสอบความเข้าใจ:
CATALOG_GRPC_ADDRเป็น:50051ใน Deployment หนึ่งและcatalog:50051ในอีกสองตัว ทำไมนั่นถูกไม่ใช่ bug และทำไม dev local ไม่เคยเผยความต่าง?- ทำไม
payment,notification, และnotification-workerไม่ได้Serviceส่วนcatalog,order, และgatewayได้? - database URL อยู่ใน
Secretและ broker address อยู่ในConfigMapอะไรจะพลาดถ้า DB URL อยู่ใน ConfigMap แทน? - การ scale
notification-workerเป็น 5 replica คือการแก้ค่าบรรทัดเดียว ทำไมถึงปลอดภัยสำหรับ service นี้โดยเฉพาะ และ The Send Worker → สร้างอะไรไว้ที่ทำให้ปลอดภัย?
deploy/helm/shopmicro/ package ทั้ง 6 binary ของ ShopMicro เป็น Helm chart เดียว: Deployment catalog, order, และ payment, notification และ notification-worker เป็น consumer ที่ไม่มี Service, และ Deployment gateway พร้อม Service (และ Ingress optional) บน 8080 — ทั้งหมด parameterize ผ่าน values.yaml สำหรับ image tag, replica, และ infra endpoint ที่จัดการจากภายนอก _helpers.tpl รวม label และประกอบ Postgres URL; ConfigMap พก KAFKA_BROKERS/RABBITMQ_URL ที่ไม่ลับ และ Secret พก DB URL แนวคิดสำคัญที่ทำให้ gRPC บน Kubernetes เข้าใจง่ายขึ้นคือ CATALOG_GRPC_ADDR เป็น bind address (:50051) ใน catalog pod แต่เป็น dial address (catalog:50051) ใน order และ gateway pod ตัวแปรเดียวกันแต่ set ต่อ Deployment เพราะ cluster แยกบทบาท listen กับ connect ที่ localhost ยุบรวมไว้
helm lint และ helm template พิสูจน์ว่า chart render manifest ออกมา valid โดยเห็นการแยก bind กับ dial และนับ Service ได้ครบสามตัวใน output ตั้งแต่ก่อน cluster จะได้แตะ บทถัดไป Deploying to the cluster → จะ install chart นี้เข้า Kubernetes cluster จริง รัน migration เป็น pre-install Job แล้วขับ order flow ด้วย curl ชุดเดิม — PENDING → CONFIRMED — โดยใช้ pod แทน go run