ข้ามไปยังเนื้อหา

Deploying to the cluster

ไม่มีโค้ด application ใหม่ — บทนี้เอา chart ที่ The Helm chart → สร้างมารันจริง บน kind cluster local: load image shopmicro/* ที่ Module 13 สร้าง Service images →, เพิ่ม template ใหม่หนึ่งตัว — migration Job ที่รันเป็น Helm pre-install/pre-upgrade hook เพื่อให้ schema มีอยู่ก่อน service ใด ๆ เริ่ม — helm install, รอทุก pod ผ่าน probe, port-forward gateway, แล้วรัน curl flow เดียวกับที่ทั้งคอร์สใช้: สร้าง product, วางคำสั่งซื้อ, poll จนเป็น CONFIRMED ความต่างคือคราวนี้ saga รันข้าม pod ที่ Kubernetes schedule กู้ตัวเองและ rolling-upgrade ได้ แทนที่จะเป็นหก terminal go run

ทุกอย่างที่ chart declare ไว้ยังเฉื่อยอยู่ จนกว่าจะมีอะไรมา reconcile กับ cluster จริง นั่นคือสิ่งที่ helm install trigger และเป็นเนื้อหาทั้งหมดของบทนี้ มีสามอย่างที่กลายเป็นจริงตรงนี้ และแต่ละอันคุ้มค่าที่จะนั่งดู:

  • Migration ต้องรันก่อน service ที่ต้องการ table catalog และ order เปิด Postgres pool และคาดว่า schema มีอยู่; service ที่เริ่มกับ database ว่างเปล่าจะ crash ตอน dev local Infra & Compose → คุณรัน migrate ... up ด้วยมือก่อน ใน cluster ไม่มี “ด้วยมือ” — ดังนั้น chart พก Job ที่ annotate เป็น Helm pre-install,pre-upgrade hook ซึ่ง Helm รัน และรอ ก่อนจะ apply Deployment ลำดับการทำงานที่ implicit ตอน shell prompt กลายเป็น dependency ที่ declare ชัด
  • Probe คุม readiness และ readiness คุม traffic Service หน้า gateway ส่ง request เฉพาะ pod ที่ผ่าน readinessProbe; pod ที่ยังเริ่มอยู่ หรือ pod ที่ /healthz fail จะถูกกันออกจาก rotation อัตโนมัติ นี่คือ cluster บังคับใช้วินัย “อย่า route ไปสิ่งที่ยังไม่พร้อม” เดียวกันที่ resilience layer Circuit breakers → บังคับใช้ที่ระดับ client — สองชั้นของแนวคิดเดียวกัน
  • เวอร์ชันใหม่ roll out โดยไม่ downtime helm upgrade พร้อม image tag หรือจำนวน replica ที่เปลี่ยน trigger rolling update: Kubernetes ยก pod ใหม่ขึ้น รอให้ผ่าน readiness แล้วค่อยรื้อ pod เก่า Service ของ gateway จึงมี pod ที่พร้อมอยู่เบื้องหลังเสมอ ส่วนที่ทำให้ครึ่ง teardown ของ rollout สะอาดคือ graceful shutdown บน SIGTERM ที่ทุก main implement ไว้ grpc-gateway → Kubernetes ส่ง SIGTERM เข้ามา process ทำงานที่ค้างให้จบแล้วค่อย exit จึงไม่มี request ไหนโดนตัดกลางคัน

เราเลือก kind (Kubernetes-in-Docker) เป็น cluster ตรงนี้เพราะรันอยู่ใน Docker ที่คุณมีอยู่แล้ว ไม่ต้องมี cloud account และที่สำคัญคือ load image ที่ build ในเครื่องตรงเข้า cluster ได้ด้วย kind load จึงไม่ต้อง push ขึ้น registry แค่เพื่อทดลอง chart

ถ้าเป็น cluster จริง คุณจะ push shopmicro/* ขึ้น registry แล้ว set image.repository ตามนั้น นอกนั้นไม่มีอะไรใน chart เปลี่ยนเลย

kind load docker-image (cluster local อ่าน image local) เทียบกับ push image ไป registry

  • Pros: ไม่ต้อง setup registry และไม่มี network round trip — build image เสร็จก็ใช้ใน cluster ได้ในไม่กี่วินาที ทำให้ loop edit-build-deploy เร็วพอจะ iterate กับ chart จริง เหมาะกับ dev บนเครื่องและ CI test cluster
  • Cons: วิธีนี้ใช้ได้เพราะ node ของ kind คือ Docker ในเครื่อง ส่วน kubelet ของ cluster หลาย node จริงอ่าน image cache บน laptop คุณไม่ได้ ดังนั้น deployment ที่ไม่ใช่ local ย่อมต้องมี registry และต้องจัดการเรื่อง imagePullPolicy/imagePullSecrets ที่ kind ให้ข้ามไป ความสะดวกนี้จริง แต่ใช้ได้เฉพาะบนเครื่อง

Migration เป็น Helm pre-install hook Job เทียบกับ initContainer บนแต่ละ service หรือ migrate ด้วยมือ

  • Pros: hook รันครั้งเดียวเป๊ะต่อ release ก่อน service pod ใดจะเริ่ม และ Helm block รอจนกว่า Job จะสำเร็จ จึงมีจุดเดียวที่กำหนดลำดับและ declare ไว้ชัดว่า schema เป็นเวอร์ชันปัจจุบัน ยิ่งกว่านั้น migration ที่ fail จะทำให้ helm install fail ดัง ๆ แทนที่จะทิ้ง service ที่เริ่มมาครึ่ง ๆ ไว้ให้ crash-loop
  • Cons: hook Job คือ object เพิ่มอีกตัวพร้อม image และ failure mode ของตัวเอง แถมรันบน ทุก upgrade แม้ไม่มีอะไรเปลี่ยน migration จึงต้อง idempotent ซึ่ง migrate ... up เป็นอยู่แล้ว ทางเลือกอื่นแย่กว่า: initContainer ต่อ service วาง migration ไว้ติดกับ service ที่ต้องใช้ก็จริง แต่กลายเป็นรัน migration เดิม N ครั้งแข่งกันข้าม replica ส่วนการรัน “ด้วยมือ” ก็เอาไม่อยู่ทันทีที่มีมากกว่าหนึ่ง environment สำหรับ chart ที่ตั้งใจให้ install ซ้ำได้ hook จึงเป็นตัวเลือกที่แย่น้อยที่สุดในสามแบบ
apiVersion: batch/v1
kind: Job
metadata:
name: shopmicro-migrate
labels:
{{- include "shopmicro.labels" . | nindent 4 }}
annotations:
# Run before the Deployments on every install and upgrade, and wait for
# success before proceeding. delete-policy cleans up the old Job first.
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": before-hook-creation
spec:
backoffLimit: 3
template:
metadata:
labels:
{{- include "shopmicro.selectorLabels" (dict "name" "migrate") | nindent 8 }}
spec:
restartPolicy: Never
containers:
- name: migrate
# shopmicro/migrate bundles the migrate CLI with the migrations/
# tree — built in the next section. A cluster can't bind-mount the
# host's migrations the way the Compose stack does, so they're baked
# into an image instead.
image: "{{ .Values.image.repository }}/migrate:{{ .Values.image.tag }}"
command: ["/bin/sh", "-c"]
args:
- |
migrate -path /migrations/catalog -database "$CATALOG_DB_URL" up && \
migrate -path /migrations/order -database "$ORDER_DB_URL" up
env:
- name: CATALOG_DB_URL
valueFrom:
secretKeyRef: { name: shopmicro-db, key: CATALOG_DB_URL }
- name: ORDER_DB_URL
valueFrom:
secretKeyRef: { name: shopmicro-db, key: ORDER_DB_URL }

บันทึกเป็น templates/migrate-job.yaml โดย hook-weight: "-5" จัดลำดับ Job นี้ไว้ก่อน hook ตัวอื่น ส่วน pre-install,pre-upgrade ทำให้ Helm รัน Job แล้ว block รอจนเสร็จก่อนจะ apply Deployment catalog และ order จึงไม่มีทางเริ่มขึ้นมาพร้อม database ที่ยังไม่ migrate

Terminal window
kind create cluster --name shopmicro

image ของทั้งหก service มาจาก Service images → ใน Module 13 แต่ chart ยังต้องการอีกหนึ่งตัวที่ service image จงใจไม่ bundle ไว้ — migrations image ที่จับคู่ migrate CLI เข้ากับ SQL tree build image ตัวนี้ตอนนี้เลย โดยใช้ migrations/ เป็น build context เพื่อให้ root .dockerignore ที่ exclude migrations ออกจาก service build ไม่มีผล:

Terminal window
docker build -t shopmicro/migrate:latest -f - migrations <<'EOF'
FROM migrate/migrate:v4.17.1
COPY . /migrations
EOF

แล้ว load ทุก image ที่ chart อ้างถึงเข้า node ของ cluster เพื่อให้ imagePullPolicy: IfNotPresent เจอ image ในเครื่องได้เลยโดยไม่ต้องมี registry:

Terminal window
for svc in catalog order payment notification notification-worker gateway migrate; do
kind load docker-image shopmicro/$svc:latest --name shopmicro
done

chart คาดว่า Postgres, Kafka, และ RabbitMQ เข้าถึงได้ที่ชื่อใน values.yaml The Helm chart → สำหรับ cluster local ทางที่เร็วสุดคือ chart ของ Bitnami:

Terminal window
helm install postgres oci://registry-1.docker.io/bitnamicharts/postgresql \
--set auth.username=shopmicro,auth.password=shopmicro,auth.database=shopmicro \
--set fullnameOverride=postgres
helm install kafka oci://registry-1.docker.io/bitnamicharts/kafka \
--set fullnameOverride=kafka --set listeners.client.protocol=PLAINTEXT
helm install rabbitmq oci://registry-1.docker.io/bitnamicharts/rabbitmq \
--set auth.username=shopmicro,auth.password=shopmicro --set fullnameOverride=rabbitmq

(Catalog และ Order ใช้ Postgres instance เดียวกันร่วมกันด้วยสอง database, catalog และ orders — สร้าง database ที่สองเมื่อ Postgres ขึ้นแล้ว หรือชี้ infra.postgres ไปสอง host ถ้าคุณอยากให้แยกกัน)

Install chart ได้เลย Helm จะรัน migration Job ก่อน รอจนเสร็จ แล้วค่อย apply ที่เหลือ:

Terminal window
helm install shopmicro deploy/helm/shopmicro

ดู pod ทยอยขึ้นมาและผ่าน probe — READY 1/1 แปลว่า readinessProbe เขียวแล้ว:

Terminal window
kubectl get pods
NAME READY STATUS RESTARTS AGE
shopmicro-migrate-abcde 0/1 Completed 0 40s
catalog-6f9c8b7d5-2xk4p 1/1 Running 0 35s
order-7d4b9c6f8-9wq2n 1/1 Running 0 35s
payment-5c8d7b4f9-lm6rt 1/1 Running 0 35s
notification-6b7d9f8c5-pk3wq 1/1 Running 0 35s
notification-worker-8f9c7b6d4-aa11b 1/1 Running 0 35s
notification-worker-8f9c7b6d4-bb22c 1/1 Running 0 35s
gateway-7f8d9c6b5-zz99x 1/1 Running 0 35s
gateway-7f8d9c6b5-yy88w 1/1 Running 0 35s

migration Job แสดง Completed เพราะรันครั้งเดียวแล้ว exit ส่วน notification-worker สองตัวและ gateway สองตัวคือค่า replicas จาก values.yaml ต่อไป port-forward Service ของ gateway มาที่ laptop:

Terminal window
kubectl port-forward svc/gateway 8080:8080

ทีนี้รัน flow เดียวกับที่คอร์สใช้มาตั้งแต่ REST Mapping → — สร้าง product, วางคำสั่งซื้อ, poll สถานะ — ต่างกันแค่ตอนนี้ทุก hop เป็น pod:

Terminal window
curl -s -X POST localhost:8080/v1/products \
-H 'Content-Type: application/json' \
-d '{"name":"Coffee Mug","description":"350ml ceramic mug","price_cents":1299,"stock":50}'
Terminal window
curl -s -X POST localhost:8080/v1/orders \
-H 'Content-Type: application/json' \
-d '{"customer_id":"cust-1","items":[{"product_id":"8f14e45f-ceea-4c9d-b2a5-0c1e3f4a9b21","quantity":2}]}'

Poll คำสั่งซื้ออีกไม่กี่วินาทีต่อมา — นานพอให้ loop outbox relay → Kafka → Payment → Kafka → saga The Saga Handler → รันข้าม pod:

Terminal window
curl -s localhost:8080/v1/orders/3a7c9e21-1e4d-4b8a-9c6e-2f8b1d5a7c90
{
"id": "3a7c9e21-1e4d-4b8a-9c6e-2f8b1d5a7c90",
"status": "ORDER_STATUS_CONFIRMED",
"totalCents": "2598"
}

PENDING → CONFIRMED รันใน cluster ทั้งหมด — ทั้งระบบ ทุก service และทั้งสอง broker deploy ด้วย helm install เดียว ทีนี้พิสูจน์ rolling upgrade เปลี่ยนค่าแล้ว upgrade:

Terminal window
helm upgrade shopmicro deploy/helm/shopmicro --set services.gateway.replicas=3
Terminal window
kubectl rollout status deployment/gateway
deployment "gateway" successfully rolled out

Kubernetes ยก gateway pod ตัวที่สามขึ้น รอ readiness /healthz ของ pod นั้นผ่าน แล้ว Service ก็ balance ข้ามทั้งสามตัว ไม่มี request หล่นเลย เพราะ pod เก่าจะออกจาก rotation หลัง pod ใหม่พร้อมแล้วเท่านั้น สุดท้าย รื้อทั้งหมด:

Terminal window
helm uninstall shopmicro
release "shopmicro" uninstalled

แล้วยืนยันว่า chart ยัง lint สะอาดหลังเพิ่ม Job template ใหม่:

Terminal window
helm lint deploy/helm/shopmicro

ไม่มี failure แปลว่าสำเร็จ

ตรวจสอบความเข้าใจ:

  • migration Job เป็น pre-install,pre-upgrade hook อะไรจะพังถ้าเปลี่ยนเป็น template ธรรมดา (ไม่มี hook annotation) ที่ Helm apply พร้อมกับ Deployment?
  • kubectl get pods แสดง gateway เป็น READY 1/1 หลังผ่านไปไม่กี่วินาที ในช่วงไม่กี่วินาทีก่อนหน้านั้น Service ทำอะไรกับ gateway pod และทำไม?
  • helm upgrade ที่เปลี่ยน image tag roll out ได้โดยไม่ downtime ชิ้นไหนใน main ของแต่ละ service ที่ทำให้ครึ่ง teardown ของ rollout สะอาด และ signal ไหนเป็นตัว trigger?
  • kind load docker-image ให้คุณข้าม registry ทั้งหมด ทำไมนั่นทำงานได้กับ kind แต่ไม่ได้กับ cluster หลาย node จริง?

helm install shopmicro deploy/helm/shopmicro ยกทั้งระบบขึ้นบน kind cluster โดย pre-install hook Job รัน migrate ... up ให้ทั้งสอง database และ Helm block รอ Job นั้น catalog กับ order จึงไม่มีทางเริ่มขึ้นมาพร้อม schema ว่างเปล่า จากนั้นทุก Deployment ก็ roll out และแต่ละ pod จะเข้าร่วม Service ก็ต่อเมื่อ readinessProbe ผ่าน (/healthz สำหรับ gateway, tcpSocket สำหรับ gRPC service)

image ไปถึง cluster ด้วย kind load docker-image โดยไม่ต้องมี registry และ curl flow เดียวกับที่คอร์สใช้มาตลอดก็ขับคำสั่งซื้อจาก PENDING เป็น CONFIRMED ข้าม pod ได้ทั้งหมด loop outbox-relay → Kafka → Payment → saga รันเป๊ะเหมือนตอนอยู่บนเครื่อง

helm upgrade --set services.gateway.replicas=3 พิสูจน์ zero-downtime rolling update โดยมี graceful SIGTERM shutdown เป็นตัวทำให้ teardown สะอาด และ helm uninstall ลบ release ทิ้ง ตอนนี้ ShopMicro รันใน cluster ได้แบบเดียวกับที่รันบน laptop โดยอธิบายไว้ครั้งเดียวเป็น chart ที่ version ได้ บทถัดไป Wrap-up → จะถอยกลับมามองทุกอย่างที่คุณสร้าง — service, broker สองตัว, saga และ outbox, resilience layer และ deployment นี้ — พร้อม trade-off เบื้องหลังแต่ละอัน และทิศทางต่อไปของระบบ