- 1. สิ่งที่จะทำได้หลังจบเวิร์กช็อป
- 2. K3d, K3s และ Docker ทำงานร่วมกันอย่างไร
- 3. เตรียมเครื่องและเลือกเวอร์ชัน
- 4. โครงสร้างไฟล์และวิธีใช้งาน
- Lab 1 — สร้างคลัสเตอร์แบบหลาย node
- Lab 2 — สร้างแอปและนำ image เข้า node
- Lab 3 — Deploy ด้วย ConfigMap, Deployment และ Service
- Lab 4 — เปิด HTTP ผ่าน Ingress
- Lab 5 — เพิ่ม Pod, อัปเดตแอป และ rollback
- Lab 6 — เปลี่ยน ConfigMap โดยไม่ build image ใหม่
- Lab 7 — เปรียบเทียบ Ingress กับ NodePort
- Lab 8 — ทดลองเก็บข้อมูลด้วย PVC
- 5. ตรวจสอบและแก้ปัญหาอย่างเป็นลำดับ
- 6. หยุด เริ่ม และลบคลัสเตอร์
- 7. แบบฝึกหัดท้ายเวิร์กช็อป
- 8. แนวทางต่อยอด
- 9. สถานะการตรวจสอบของชุดไฟล์
- เอกสารอ้างอิง
K3d ช่วยให้เราสร้างคลัสเตอร์ Kubernetes สำหรับเรียนรู้และพัฒนาบนเครื่องตนเองได้ โดยใช้ K3s เป็น Kubernetes distribution และใช้ Docker container เป็น node เราจึงฝึกใช้งาน Kubernetes หลาย node ได้โดยไม่ต้องเตรียม VM แยกทุกเครื่อง [1]
บทความนี้พาทำแอป HTTP ขนาดเล็กด้วย Python สร้าง Docker image นำเข้า K3d แล้วเปิดใช้งานผ่าน Ingress จากนั้นทดลองเพิ่มจำนวน Pod อัปเดตแอป และย้อนกลับเวอร์ชันเดิม พร้อมแบบฝึกหัด ConfigMap, NodePort และ PersistentVolumeClaim
ขอบเขต: คลัสเตอร์สำหรับการเรียนรู้บนเครื่องเดียว ไม่ใช่แบบติดตั้งระบบ production
#1. สิ่งที่จะทำได้หลังจบเวิร์กช็อป
- อธิบายบทบาทของ Docker, K3d, K3s และ kubectl ได้
- สร้างคลัสเตอร์ 1 server และ 2 agents ด้วยไฟล์ตั้งค่า
- Build image และนำ image เข้า containerd ของ node ด้วย
k3d image import - Deploy แอปพร้อม ConfigMap, resource requests/limits และ health probes
- เชื่อมต่อแอปผ่าน Service, Ingress และ NodePort
- ทดลอง scale, rolling update, rollback และตรวจสอบเหตุขัดข้อง
- สังเกตความต่างระหว่างอายุของ Pod กับอายุของข้อมูลใน PVC
#2. K3d, K3s และ Docker ทำงานร่วมกันอย่างไร
| เครื่องมือ/องค์ประกอบ | บทบาทในเวิร์กช็อป |
|---|---|
| Docker | รัน container ที่เป็น node และ container ของ K3d load balancer |
| K3d | จัดการการสร้าง หยุด เริ่ม และลบคลัสเตอร์ K3s บน Docker |
| K3s | Kubernetes distribution ที่ทำงานอยู่ภายใน node |
| containerd ภายใน node | รัน workload container ของ Pod |
| kubectl | ส่งคำสั่งไปยัง Kubernetes API |
| Traefik | Ingress controller สำหรับรับ HTTP เข้าสู่แอป |
| Service | ให้จุดเชื่อมต่อภายในที่เลือก Pod ผ่าน label |
จุดที่ต้องเข้าใจ: Docker image บน host กับ image ใน containerd ของ K3s node เป็นคนละ image store การ docker build สำเร็จจึงยังไม่เพียงพอ ต้อง import image หรือใช้ registry ที่ node เข้าถึงได้ [5][10]
การมี 3 node บน Docker host เดียวช่วยให้ฝึกการจัดวาง workload ได้ แต่ทุก node ยังใช้ทรัพยากรจากเครื่องเดียวกัน และคลัสเตอร์ 1 server ไม่ได้มี control plane แบบ HA
#สถาปัตยกรรมที่ใช้ฝึก
flowchart TD B["Browser :8080"] --> LB["K3d serverlb :80"] LB --> T["Traefik ผ่าน K3s ServiceLB"] T --> S["Service web :80"] S --> P1["Pod web A :8080"] S --> P2["Pod web B :8080"] D["Docker image บน host"] --> I["k3d image import"] I --> P1 I --> P2
เส้นทางส่วนล่างแสดงการเตรียม image ให้ node ก่อนสร้าง Pod ส่วนเส้นทางด้านบนแสดง HTTP โดยย่อ ไม่ได้แสดงทุก network hop ภายใน Kubernetes
K3d serverlb, K3s ServiceLB และ Traefik ทำคนละหน้าที่: serverlb ส่งต่อพอร์ตไปยัง server node, ServiceLB ช่วยเปิดพอร์ตของ LoadBalancer Service บน node และ Traefik อ่านกฎ Ingress เพื่อส่ง HTTP ไป backend แอป [3][4]
#3. เตรียมเครื่องและเลือกเวอร์ชัน
#เครื่องมือและทรัพยากร
- Docker Engine หรือ Docker Desktop ที่เปิดทำงานแล้ว และใช้ Linux containers
- K3d, kubectl, curl และ Bash
- อินเทอร์เน็ตสำหรับดาวน์โหลด image และเครื่องมือ
- แนวทางจัดสรรสำหรับห้องปฏิบัติการ: CPU 4 cores, RAM ให้ Docker 4–6 GB และพื้นที่ว่างประมาณ 8 GB; ปรับตามเครื่องและ workload จริง ตัวเลขนี้เป็นข้อเสนอสำหรับเวิร์กช็อป ไม่ใช่ข้อกำหนดขั้นต่ำของโครงการ
- ใช้ Docker daemon บนเครื่องเดียวกับที่รันคำสั่ง เพราะบทความทดสอบผ่าน
127.0.0.1
Windows: ใช้ WSL2 กับ Docker Desktop เปิด WSL integration สำหรับ Linux distribution ที่ใช้งาน และรันคำสั่งทั้งหมดใน Bash ของ WSL2
macOS: เปิด Docker Desktop ก่อน และใช้ Terminal หากใช้เครื่อง Apple Silicon ให้เลือก binary arm64
Linux: ติดตั้ง Docker ตาม เอกสาร Docker Engine แล้วตรวจว่า user ที่ใช้งานเรียก docker info ได้
#เวอร์ชันอ้างอิงของชุดฝึก
| รายการ | เวอร์ชัน/ค่าในชุดไฟล์ |
|---|---|
| K3d | v5.9.0 เป็นรุ่นอ้างอิงสำหรับติดตั้ง |
| K3s node image | rancher/k3s:v1.35.2-k3s1 |
| kubectl สำหรับคำสั่งติดตั้งด้านล่าง | v1.35.2 |
| K3d configuration API | k3d.io/v1alpha5 |
| Python base image | python:3.13-alpine |
| Image แอปที่เราสร้าง | demo/k3d-web:1.0 และ demo/k3d-web:2.0 |
ตรวจสอบเอกสารทางการวันที่ 16 ตุลาคม 2569 (Asia/Bangkok): เลือก K3d จากหน้ารุ่นเผยแพร่ และเลือก K3s tag ตามตัวอย่าง configuration ของโครงการ [2][11] ตารางนี้เป็น baseline สำหรับทำตามเอกสาร ไม่ใช่การรับรองว่าเป็น patch ล่าสุดหรือผ่านการทดสอบคลัสเตอร์จริงแล้ว
kubectl ควรอยู่ภายในระยะต่างกันหนึ่ง minor version จาก API server ตาม version skew policy; สำหรับชุดฝึกนี้ใช้ 1.35.x เพื่อให้ตรงกัน [9] หากใช้ package manager ที่ติดตั้งรุ่นใหม่กว่า ให้ตรวจเวอร์ชันก่อนเริ่ม
Base image tags อาจเปลี่ยนเนื้อหาได้ เมื่อต้องทำการทดลองที่ต้องทำซ้ำตรงกันทุกบิต ควรบันทึก image digest และใช้ digest แทน tag
#ติดตั้ง K3d บน Linux/macOS/WSL2
ดาวน์โหลด install script จากโครงการ ตรวจสอบไฟล์ แล้วติดตั้งเวอร์ชันที่กำหนด [1]
curl -fsSL \
https://raw.githubusercontent.com/k3d-io/k3d/v5.9.0/install.sh \
-o install-k3d.sh
less install-k3d.sh
TAG=v5.9.0 bash install-k3d.sh
k3d version
สคริปต์อาจขอสิทธิ์สำหรับเขียน binary ลง /usr/local/bin หากใช้ macOS และต้องการใช้ Homebrew แทน สามารถ brew install k3d ได้ แต่เวอร์ชันที่ได้ขึ้นกับ formula ในขณะนั้น
#ติดตั้ง kubectl แบบเลือก OS และสถาปัตยกรรม
คำสั่งต่อไปนี้เลือก Linux หรือ macOS จากเครื่องที่รัน และตรวจ SHA-256 ก่อนติดตั้ง โดยใช้แนวทางจากเอกสาร Kubernetes [8]
KUBECTL_VERSION=v1.35.2
OS_NAME=$(uname -s | tr '[:upper:]' '[:lower:]')
CPU_ARCH=$(uname -m)
case "$CPU_ARCH" in
x86_64) CPU_ARCH=amd64 ;;
aarch64|arm64) CPU_ARCH=arm64 ;;
*) echo "Unsupported architecture: $CPU_ARCH"; exit 1 ;;
esac
case "$OS_NAME" in
linux|darwin) ;;
*) echo "Use Linux, macOS or WSL2"; exit 1 ;;
esac
curl -fsSLo kubectl \
"https://dl.k8s.io/release/$KUBECTL_VERSION/bin/$OS_NAME/$CPU_ARCH/kubectl"
curl -fsSLo kubectl.sha256 \
"https://dl.k8s.io/release/$KUBECTL_VERSION/bin/$OS_NAME/$CPU_ARCH/kubectl.sha256"
if command -v sha256sum >/dev/null 2>&1; then
printf '%s kubectl\n' "$(cat kubectl.sha256)" | sha256sum --check || exit 1
else
printf '%s kubectl\n' "$(cat kubectl.sha256)" | shasum -a 256 --check || exit 1
fi
# ทำคำสั่งต่อเมื่อ checksum แสดง OK เท่านั้น
sudo install -m 0755 kubectl /usr/local/bin/kubectl
kubectl version --client
#ตรวจสอบก่อนเริ่ม
docker info
docker version
k3d version
kubectl version --client
curl --version
ใช้ curl รุ่นที่รองรับ --retry-all-errors สำหรับสคริปต์ smoke test หากเครื่องเก่ากว่านั้นให้อัปเดต curl หรือทดสอบด้วย curl -fsS http://127.0.0.1:8080/ โดยตรง
| พอร์ตบนเครื่อง | เชื่อมไปที่ | ใช้ทำอะไร |
|---|---|---|
127.0.0.1:6550 |
Kubernetes API | kubectl เข้าคลัสเตอร์ |
127.0.0.1:8080 |
serverlb พอร์ต 80 | HTTP ผ่าน Ingress |
127.0.0.1:8081 |
agent-0 พอร์ต 30080 | แบบฝึกหัด NodePort |
หากพอร์ตถูกใช้อยู่ ให้ปรับ k3d.yaml ก่อนสร้างคลัสเตอร์ และแก้ URL ในคำสั่งหรือสคริปต์ทดสอบให้ตรงกัน
#4. โครงสร้างไฟล์และวิธีใช้งาน
แตก K3d-Workshop-Files.zip แล้วเข้าโฟลเดอร์ k3d-workshop ไฟล์สำคัญมีดังนี้
| ไฟล์ | เนื้อหา |
|---|---|
k3d.yaml |
ตั้งค่าคลัสเตอร์และพอร์ต |
app/server.py |
แอป HTTP ส่ง JSON และ health endpoints |
app/Dockerfile |
สร้าง image แบบ non-root |
k8s/00-namespace.yaml |
Namespace demo |
k8s/10-configmap.yaml |
ข้อความที่ส่งให้แอป |
k8s/20-deployment.yaml |
Pod template, replicas, probes และทรัพยากร |
k8s/30-service.yaml |
ClusterIP Service |
k8s/40-ingress.yaml |
กฎ Ingress ที่ใช้ Traefik |
extras/nodeport.yaml |
Service แบบ NodePort |
extras/storage.yaml |
PVC และ workload เขียนข้อมูล |
scripts/00-preflight.sh ถึง 05-rollback.sh |
คำสั่งช่วยทำ lab ตามลำดับ |
scripts/99-cleanup.sh |
ลบคลัสเตอร์เมื่อจบการฝึก |
VALIDATION.md |
รายงานการตรวจสอบและสิ่งที่ยังไม่ได้รันจริง |
ทุกคำสั่ง kubectl ในบทความระบุ --context k3d-workshop เพื่อเลือกคลัสเตอร์นี้โดยตรง ส่วนไฟล์ config ไม่เปลี่ยน current context เดิมโดยอัตโนมัติ
หากต้องการใช้สคริปต์ เริ่มด้วย:
cd k3d-workshop
bash scripts/00-preflight.sh
#Lab 1 — สร้างคลัสเตอร์แบบหลาย node
เวลาแนะนำ: 20 นาที
ไฟล์ k3d.yaml ใช้ configuration API v1alpha5 ซึ่งยังเป็นรูปแบบ alpha ตามเอกสาร K3d จึงควรเก็บเวอร์ชันเครื่องมือควบคู่กับไฟล์ตั้งค่า [2]
apiVersion: k3d.io/v1alpha5
kind: Simple
metadata:
name: workshop
servers: 1
agents: 2
image: rancher/k3s:v1.35.2-k3s1
kubeAPI:
host: "127.0.0.1"
hostIP: "127.0.0.1"
hostPort: "6550"
ports:
- port: "127.0.0.1:8080:80"
nodeFilters:
- loadbalancer
- port: "127.0.0.1:8081:30080"
nodeFilters:
- agent:0
options:
k3d:
wait: true
timeout: "180s"
kubeconfig:
updateDefaultKubeconfig: true
switchCurrentContext: false
สร้างคลัสเตอร์และรอ node พร้อม:
k3d cluster create --config k3d.yaml
kubectl --context k3d-workshop wait \
--for=condition=Ready nodes --all --timeout=180s
kubectl --context k3d-workshop -n kube-system rollout status \
deployment/traefik --timeout=180s
k3d cluster list
k3d node list
kubectl --context k3d-workshop get nodes -o wide
kubectl --context k3d-workshop get pods -n kube-system
kubectl --context k3d-workshop get ingressclass
หรือใช้ bash scripts/01-cluster.sh แทนชุดคำสั่งสร้างด้านบน เลือกวิธีใดวิธีหนึ่ง เพื่อไม่สร้างคลัสเตอร์ชื่อเดิมซ้ำ
ผลที่ควรเห็นคือ Kubernetes node จำนวน 3 ตัว ได้แก่ server-0, agent-0 และ agent-1 อยู่ในสถานะ Ready ส่วน container serverlb เป็นองค์ประกอบของ K3d จึงไม่ใช่ Kubernetes node เพิ่มอีกหนึ่งตัว
จุดตรวจ: get ingressclass มี traefik และ Deployment ของ Traefik พร้อมทำงาน
#Lab 2 — สร้างแอปและนำ image เข้า node
เวลาแนะนำ: 25 นาที
แอปใช้ Python standard library เพื่อให้เห็นขั้นตอน container และ Kubernetes ชัดเจน ไม่ต้องติดตั้ง framework เพิ่ม
| URL | ผลลัพธ์ |
|---|---|
/ |
JSON มี title, version, pod และ node |
/healthz |
HTTP 200 สำหรับ liveness |
/readyz |
HTTP 200 สำหรับ readiness |
| URL อื่น | HTTP 404 |
"""Small, dependency-free HTTP app for a local Kubernetes workshop."""
import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlsplit
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
path = urlsplit(self.path).path
if path in ("/healthz", "/readyz"):
payload, status = {"status": "ok"}, 200
elif path == "/":
payload, status = {
"title": os.getenv("APP_TITLE", "K3d Workshop"),
"version": os.getenv("APP_VERSION", "1.0"),
"pod": os.getenv("POD_NAME", "local"),
"node": os.getenv("NODE_NAME", "local"),
}, 200
else:
payload, status = {"error": "not found"}, 404
body = (json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
if __name__ == "__main__":
port = int(os.getenv("PORT", "8080"))
print(f"Listening on 0.0.0.0:{port}", flush=True)
ThreadingHTTPServer(("0.0.0.0", port), Handler).serve_forever()
Dockerfile รับค่า version ตอน build และใช้ user ID 10001:
FROM python:3.13-alpine
ARG APP_VERSION=1.0
ENV APP_VERSION=${APP_VERSION} \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
COPY --chown=10001:10001 server.py /app/server.py
USER 10001:10001
EXPOSE 8080
CMD ["python", "/app/server.py"]
Build แล้ว import เข้า คลัสเตอร์ workshop:
docker build --build-arg APP_VERSION=1.0 \
-t demo/k3d-web:1.0 app
k3d image import demo/k3d-web:1.0 --cluster workshop
เราใช้ image tag ที่ระบุชัดเจน พร้อม imagePullPolicy: IfNotPresent ใน Deployment เพื่อให้ node ใช้ image ที่ import แล้ว ไม่เลือก latest ซึ่งทำให้พฤติกรรม pull ต่างออกไป การ import นี้เป็นความสามารถของ K3d โดยตรง [5]
บน Apple Silicon ให้ build สำหรับสถาปัตยกรรมเดียวกับ node ที่ใช้อยู่ การกำหนด --platform linux/amd64 โดยไม่จำเป็นอาจทำให้ต้องพึ่ง emulation หรือรันไม่ได้บน node อีกสถาปัตยกรรม
จุดตรวจ: คำสั่ง import สำเร็จก่อนทำ Lab 3
#Lab 3 — Deploy ด้วย ConfigMap, Deployment และ Service
เวลาแนะนำ: 30 นาที
ConfigMap เก็บค่าตั้งค่าที่ไม่ใช่ความลับ ในที่นี้เก็บข้อความชื่อแอป ส่วน Pod รับค่า POD_NAME และ NODE_NAME จาก Downward API [7]
apiVersion: v1
kind: ConfigMap
metadata:
name: web-config
namespace: demo
data:
APP_TITLE: "เรียนรู้ Kubernetes ด้วย K3d"
Deployment กำหนด replicas เริ่มต้น 2 และใช้ rolling update:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: demo
spec:
replicas: 2
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: web
image: demo/k3d-web:1.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
envFrom:
- configMapRef:
name: web-config
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
readinessProbe:
httpGet:
path: /readyz
port: http
initialDelaySeconds: 2
periodSeconds: 3
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 250m
memory: 96Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
Readiness ใช้ตัดสินว่า Pod พร้อมรับ traffic หรือยัง ส่วน liveness ใช้ตรวจว่าต้อง restart container หรือไม่ [6] ตัวอย่างนี้ทั้งสอง endpoint คืนค่า 200 เพื่อสาธิตการตั้งค่า ในระบบจริง readiness อาจตรวจ dependency ที่จำเป็นและใช้เงื่อนไขต่างจาก liveness
ค่าทรัพยากร เช่น cpu: 50m หมายถึง 0.05 CPU สำหรับ request; ส่วน limits เป็นเพดานที่ตั้งไว้สำหรับ container ตัวอย่าง ไม่ใช่ขนาดที่เหมาะกับทุกแอป
Service รับพอร์ต 80 แล้วส่งต่อไปพอร์ตชื่อ http ซึ่งตรงกับ 8080 ใน Pod:
apiVersion: v1
kind: Service
metadata:
name: web
namespace: demo
spec:
type: ClusterIP
selector:
app: web
ports:
- name: http
port: 80
targetPort: http
Apply ตามลำดับ:
kubectl --context k3d-workshop apply -f k8s/00-namespace.yaml
kubectl --context k3d-workshop apply -f k8s/10-configmap.yaml
kubectl --context k3d-workshop apply -f k8s/20-deployment.yaml
kubectl --context k3d-workshop apply -f k8s/30-service.yaml
kubectl --context k3d-workshop -n demo rollout status \
deployment/web --timeout=180s
kubectl --context k3d-workshop -n demo get deployment,replicaset,pod,service
kubectl --context k3d-workshop -n demo get pods -o wide
จุดตรวจ: มี Pod web 2 ตัว Running และ READY 1/1 ไม่จำเป็นต้องอยู่คนละ node เพราะตัวอย่างนี้ไม่ได้บังคับ anti-affinity
#Lab 4 — เปิด HTTP ผ่าน Ingress
เวลาแนะนำ: 20 นาที
K3s มี Traefik เป็นองค์ประกอบที่ติดตั้งมาด้วยในค่าปกติของคลัสเตอร์ ตัวอย่างนี้ไม่ได้ปิด Traefik [4] ไฟล์ Ingress ใช้ ingressClassName: traefik และไม่กำหนด host จึงเรียกด้วย IP ของเครื่องได้:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
namespace: demo
spec:
ingressClassName: traefik
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
kubectl --context k3d-workshop apply -f k8s/40-ingress.yaml
kubectl --context k3d-workshop -n demo get ingress
curl -fsS http://127.0.0.1:8080/
curl -fsS http://127.0.0.1:8080/healthz
curl -fsS http://127.0.0.1:8080/readyz
ตัวอย่างผลลัพธ์ ไม่ใช่ log จากการรันคลัสเตอร์จริง:
{"title":"เรียนรู้ Kubernetes ด้วย K3d","version":"1.0","pod":"web-xxxxxxxxxx-xxxxx","node":"k3d-workshop-agent-0"}
ค่า pod และ node ขึ้นกับ scheduler และชื่อที่ Kubernetes สร้าง Ingress อาจใช้เวลาสักครู่หลัง apply ก่อนเริ่มส่ง traffic
แนวทาง map พอร์ต host ไปที่ loadbalancer และเปิดแอปด้วย Ingress ตรงกับตัวอย่างในเอกสาร K3d [3]
#ทางเลือกสำหรับทดสอบ: port-forward
ถ้าอยากตรวจว่า Service ทำงานก่อนตรวจ Ingress ให้เปิด Terminal อีกหน้าต่าง:
kubectl --context k3d-workshop -n demo port-forward \
service/web 18080:80 --address 127.0.0.1
จากนั้นเรียก curl -fsS http://127.0.0.1:18080/ และกด Ctrl+C เพื่อจบ port-forward
#ใช้สคริปต์ให้ครบเส้นทาง
สำหรับผู้ที่ยังไม่ได้ build/deploy ด้วยคำสั่งรายขั้น สามารถใช้:
bash scripts/02-deploy.sh
bash scripts/03-smoke-test.sh
02-deploy.sh ทำ build, import และ apply ครบถึง Ingress การรันซ้ำจะนำ Deployment กลับเป็น image 1.0 และ replicas 2 ตามไฟล์ตั้งต้น
#Lab 5 — เพิ่ม Pod, อัปเดตแอป และ rollback
เวลาแนะนำ: 25 นาที
#5.1 เพิ่ม replicas
kubectl --context k3d-workshop -n demo scale deployment/web --replicas=3
kubectl --context k3d-workshop -n demo rollout status \
deployment/web --timeout=180s
kubectl --context k3d-workshop -n demo get pods -l app=web -o wide
for i in $(seq 1 6); do
curl -fsS http://127.0.0.1:8080/
done
อาจเห็นค่า pod ต่างกันในการเรียกหลายครั้ง แต่ไม่ควรใช้จำนวน request เพียงเล็กน้อยสรุปว่าสัดส่วนกระจายเท่ากัน การเปิด connection และพฤติกรรม load balancing มีผลด้วย
#5.2 สร้างเวอร์ชัน 2.0 และ rolling update
docker build --build-arg APP_VERSION=2.0 \
-t demo/k3d-web:2.0 app
k3d image import demo/k3d-web:2.0 --cluster workshop
kubectl --context k3d-workshop -n demo set image \
deployment/web web=demo/k3d-web:2.0
kubectl --context k3d-workshop -n demo rollout status \
deployment/web --timeout=180s
kubectl --context k3d-workshop -n demo rollout history deployment/web
curl -fsS http://127.0.0.1:8080/
หรือใช้ bash scripts/04-update.sh ผลที่คาดหวังคือ "version": "2.0"
maxUnavailable: 0 และ maxSurge: 1 ทำให้ Deployment พยายามคงจำนวน Pod ที่พร้อม และสร้าง Pod เพิ่มระหว่างเปลี่ยนรุ่น [12] เครื่องต้องมีทรัพยากรเหลือสำหรับ Pod ที่เพิ่มชั่วคราวด้วย จึงไม่ควรตีความว่าเป็นการรับประกัน zero downtime ทุกสถานการณ์
#5.3 ย้อนกลับทันทีหลังเปลี่ยน image
kubectl --context k3d-workshop -n demo rollout undo deployment/web
kubectl --context k3d-workshop -n demo rollout status \
deployment/web --timeout=180s
curl -fsS http://127.0.0.1:8080/
หรือใช้ bash scripts/05-rollback.sh ควรกลับเป็น version 1.0 หากไม่มีการเปลี่ยน Pod template อื่นระหว่าง update กับ undo
rollout undo ย้อน Pod template ไป revision ก่อนหน้า ไม่ได้ย้อน ConfigMap ที่แยกอยู่ ไม่ย้อนเนื้อหาข้อมูลใน PVC และไม่ใช้สำหรับย้อนจำนวน replicas
จุดตรวจ: บันทึก image tag ก่อน update, หลัง update และหลัง rollback โดยตรวจเพิ่มเติมด้วย:
kubectl --context k3d-workshop -n demo get deployment web \
-o jsonpath='{.spec.template.spec.containers[0].image}'
echo
คำสั่ง set image แก้ live resource แต่ไฟล์ 20-deployment.yaml ยังเป็น 1.0 หากต้องการเก็บ 2.0 เป็น desired state ระยะยาว ต้องแก้ไฟล์นั้นด้วย
#Lab 6 — เปลี่ยน ConfigMap โดยไม่ build image ใหม่
เวลาแนะนำ: 15 นาที
แก้ค่าใน k8s/10-configmap.yaml:
data:
APP_TITLE: "สวัสดีจากห้องปฏิบัติการ K3d"
kubectl --context k3d-workshop apply -f k8s/10-configmap.yaml
kubectl --context k3d-workshop -n demo rollout restart deployment/web
kubectl --context k3d-workshop -n demo rollout status \
deployment/web --timeout=180s
curl -fsS http://127.0.0.1:8080/
ตัวอย่างนี้รับ ConfigMap เป็น environment variable ค่าใน Pod เดิมจึงไม่เปลี่ยนอัตโนมัติ ต้องให้ Pod ใหม่เริ่มทำงาน [7] การ restart นี้ทำให้เกิด revision ของ Pod template เพิ่ม จึงควรทำหลังจบแบบฝึกหัด rollback
#Lab 7 — เปรียบเทียบ Ingress กับ NodePort
เวลาแนะนำ: 10 นาที
คลัสเตอร์ได้ map พอร์ต host 8081 ไป agent-0:30080 ไว้ตั้งแต่แรก Apply Service เสริม:
kubectl --context k3d-workshop apply -f extras/nodeport.yaml
kubectl --context k3d-workshop -n demo get service web-nodeport
curl -fsS http://127.0.0.1:8081/
| วิธี | URL ของ lab | ลักษณะการเข้าถึง |
|---|---|---|
| Ingress | http://127.0.0.1:8080/ |
HTTP routing ผ่าน Traefik และ Service |
| NodePort | http://127.0.0.1:8081/ |
Docker map ไป NodePort 30080 ของ node |
| port-forward | http://127.0.0.1:18080/ |
เปิด tunnel ชั่วคราวด้วย kubectl |
การสร้าง NodePort Service เพียงอย่างเดียวไม่ทำให้พอร์ตนั้นปรากฏบน host เมื่อใช้ Docker node จึงต้องมี Docker port mapping ตามที่กำหนดใน k3d.yaml [3]
#Lab 8 — ทดลองเก็บข้อมูลด้วย PVC
เวลาแนะนำ: 15 นาที
แบบฝึกหัดนี้ใช้ local-path StorageClass ของ K3s ให้ workload เขียนเวลาลงไฟล์ ทุก 5 วินาที และคง PVC เดิมไว้เมื่อสร้าง Pod ใหม่ [13]
kubectl --context k3d-workshop get storageclass
kubectl --context k3d-workshop apply -f extras/storage.yaml
kubectl --context k3d-workshop -n demo rollout status \
deployment/notes --timeout=180s
kubectl --context k3d-workshop -n demo get pvc
kubectl --context k3d-workshop -n demo exec deployment/notes \
-- sh -c 'cat /data/events.txt'
kubectl --context k3d-workshop -n demo delete pod -l app=notes
kubectl --context k3d-workshop -n demo rollout status \
deployment/notes --timeout=180s
kubectl --context k3d-workshop -n demo exec deployment/notes \
-- sh -c 'cat /data/events.txt'
ผลที่ต้องสังเกตคือไฟล์ยังมีบรรทัดเดิม และค่อย ๆ มีบรรทัดเวลาใหม่เพิ่มหลัง Pod ถูกสร้างใหม่ ถ้ารันติดกันเร็วมากอาจยังมีข้อมูลเพียงบรรทัดเดียว ให้รอสักครู่แล้วอ่านอีกครั้ง
หาก PVC แสดง Pending ก่อนมี Pod ใช้งาน อาจเป็นพฤติกรรม WaitForFirstConsumer ให้ตรวจ Pod และ Events ประกอบ
ขอบเขตการคงอยู่ของข้อมูล: local-path ใช้ storage ของ node ในกรณี K3d คือพื้นที่ภายใน Docker node container การลบคลัสเตอร์แล้วสร้างใหม่ไม่ได้รับประกันว่าข้อมูลนี้จะอยู่ต่อ การใช้ PVC ไม่เท่ากับการทำ backup และไม่ได้เปลี่ยน local storage ให้เป็น shared storage ข้ามทุก node
หากต้องคงข้อมูลข้ามการลบคลัสเตอร์ ต้องออกแบบ host bind mount, ตำแหน่ง node และการกู้คืนให้ชัดเจน หรือใช้ระบบ storage ภายนอก แบบฝึกหัดนี้ไม่รวมการตั้งค่าส่วนนั้น
#5. ตรวจสอบและแก้ปัญหาอย่างเป็นลำดับ
เริ่มดู Pod และ Events ก่อน แล้วจึงตรวจเส้นทาง Service/Ingress:
kubectl --context k3d-workshop -n demo get pods -o wide
kubectl --context k3d-workshop -n demo describe deployment web
kubectl --context k3d-workshop -n demo logs deployment/web --tail=50
kubectl --context k3d-workshop -n demo get events \
--sort-by=.metadata.creationTimestamp
kubectl --context k3d-workshop -n demo get service,ingress
kubectl --context k3d-workshop -n demo get endpointslices \
-l kubernetes.io/service-name=web
kubectl --context k3d-workshop -n kube-system get pods,service
docker ps --filter name=k3d-workshop
| อาการ | สิ่งที่ควรตรวจ | แนวทางแก้ |
|---|---|---|
| ติดต่อ Docker ไม่ได้ | docker info |
เปิด daemon/Desktop และตรวจ WSL integration |
| สร้างคลัสเตอร์ไม่ได้เพราะ bind port | พอร์ต 6550, 8080, 8081 | เปลี่ยน host port ใน config ก่อนสร้าง |
ImagePullBackOff |
ชื่อ image/tag, Events, cluster ที่ import | import ให้ครบทุก node ด้วยชื่อ cluster ที่ตรง |
CrashLoopBackOff |
logs และ describe pod |
ตรวจ Python error, command, permission และ limits |
Pod Pending |
Events, resource requests, PVC | เพิ่มทรัพยากรหรือแก้ storage ตามสาเหตุ |
| Ingress คืน 404 | กฎ Ingress, class, path และ log | ตรวจว่าใช้ Traefik และมี backend ตรงชื่อ |
| Ingress คืน 502/503 | Ready Pod และ EndpointSlice | แก้ selector, targetPort หรือ readiness |
| ต่อ 8080 ไม่ได้ | Docker mapping และ Traefik | ตรวจ k3d.yaml กับ container ports |
| เปลี่ยน ConfigMap แต่ค่าเดิม | Pod รับค่าเป็น env | apply ConfigMap แล้ว restart Deployment |
| ใช้ image tag เดิมแต่โค้ดไม่เปลี่ยน | cache ที่ node/Pod | สร้าง tag ใหม่, import แล้ว set image |
| Apple Silicon ขึ้น exec format error | image platform กับ node architecture | build สำหรับสถาปัตยกรรมที่ตรงกัน |
| kubectl ชี้ผิด cluster | context ที่ใช้ในคำสั่ง | ระบุ --context k3d-workshop |
ถ้า port-forward ใช้ได้ แต่ Ingress ใช้ไม่ได้ ให้มุ่งตรวจ Ingress controller, routing และ host mapping ต่อ แทนการแก้ตัวแอปทันที
#6. หยุด เริ่ม และลบคลัสเตอร์
หยุดชั่วคราวแล้วกลับมาใช้คลัสเตอร์เดิม:
k3d cluster stop workshop
k3d cluster start workshop
kubectl --context k3d-workshop wait \
--for=condition=Ready nodes --all --timeout=180s
เมื่อตรวจข้อมูลที่ต้องการเก็บเรียบร้อยและต้องการจบ lab:
k3d cluster delete workshop
หรือ bash scripts/99-cleanup.sh --yes คำสั่งนี้ลบเฉพาะคลัสเตอร์ชื่อ workshop รวม workload และข้อมูล local ของ lab ที่อยู่ใน node; ไม่ลบ Docker image ที่เรา build บน host
#7. แบบฝึกหัดท้ายเวิร์กช็อป
| งาน | หลักฐานที่ส่ง |
|---|---|
| เปลี่ยน APP_TITLE เป็นชื่อโครงการของตนเอง | JSON ที่มี title ใหม่ |
| เพิ่ม replicas เป็น 4 | รายการ Pod ที่ Ready 4 ตัว |
| Build image 3.0 และ deploy | image tag และ JSON version 3.0 |
| Rollback ไป revision ที่ต้องการ | rollout history และ tag หลังย้อนกลับ |
| เปลี่ยน Service selector ชั่วคราวแล้ววิเคราะห์ปัญหา | Events/EndpointSlice และคำอธิบายสาเหตุ |
| เปรียบเทียบ Ingress กับ NodePort | URL, พอร์ต และอธิบายเส้นทาง |
| ลบ Pod ของ notes แต่คง PVC | ข้อมูลก่อนและหลังสร้าง Pod ใหม่ |
เมื่อจบการทดลอง selector ให้แก้กลับเป็น app: web ก่อนส่งงาน และอย่าลืมปรับ desired state ในไฟล์ให้ตรงกับผลที่ต้องการคงไว้
#คำถามอภิปราย
- เหตุใด Docker build แล้ว Pod จึงยังหา image ไม่พบได้?
- Kubernetes node กับ Docker container
serverlbต่างกันอย่างไร? - ทำไม ConfigMap ที่ส่งเป็น environment variable ต้องสร้าง Pod ใหม่จึงเห็นค่าใหม่?
- การเพิ่ม replicas เป็น 4 ทำให้ control plane เป็น HA หรือไม่ เพราะเหตุใด?
- ทำไม rollout undo จึงไม่สามารถย้อนข้อมูลในฐานข้อมูลได้?
- หากต้องใช้ local registry แทน image import ต้องแก้ขั้นตอนใด?
#8. แนวทางต่อยอด
เมื่อใช้ image import คล่องแล้ว สามารถต่อยอดเป็น local registry โดยให้ host push image เข้า registry และให้ containerd ของ node pull จาก endpoint ที่เข้าถึงได้ [10] ต้องระวังว่า localhost ภายใน node หมายถึง node นั้น ไม่ใช่ host เครื่องของเรา
หัวข้อที่เหมาะกับเวิร์กช็อปถัดไป ได้แก่ Helm, GitOps ด้วย Argo CD, CI ที่สร้างคลัสเตอร์ชั่วคราว, Prometheus/Grafana และการออกแบบ persistent storage ที่กู้คืนได้
#9. สถานะการตรวจสอบของชุดไฟล์
ชุดนี้ตรวจไวยากรณ์ Python/Bash, ตรวจ k3d.yaml กับ JSON Schema ทางการ, โหลด YAML และตรวจความสอดคล้องของ label, selector, port, image tag และ namespace รวมถึงทดสอบแอป HTTP บนเครื่องผู้จัดทำ รายการผลจริงอยู่ใน VALIDATION.md ภายใน ZIP
#เอกสารอ้างอิง
เอกสารเป็นการอธิบายและออกแบบ lab ใหม่ โดยอ้างอิงพฤติกรรมเครื่องมือจากแหล่งทางการ ตรวจสอบวันที่ 16 ตุลาคม 2569
- K3d Overview, Requirements and Installation
- K3d — Using Config Files
- K3d — Exposing Services
- K3s — Networking Services: Traefik and ServiceLB
- K3d — Image Import
- Kubernetes — Liveness, Readiness and Startup Probes
- Kubernetes — ConfigMaps
- Kubernetes — Install kubectl on Linux และ macOS
- Kubernetes — Version Skew Policy
- K3d — Using Image Registries
- K3d — Official Releases
- Kubernetes — Deployments
- K3s — Storage