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 ในไฟล์ให้ตรงกับผลที่ต้องการคงไว้

#คำถามอภิปราย

  1. เหตุใด Docker build แล้ว Pod จึงยังหา image ไม่พบได้?
  2. Kubernetes node กับ Docker container serverlb ต่างกันอย่างไร?
  3. ทำไม ConfigMap ที่ส่งเป็น environment variable ต้องสร้าง Pod ใหม่จึงเห็นค่าใหม่?
  4. การเพิ่ม replicas เป็น 4 ทำให้ control plane เป็น HA หรือไม่ เพราะเหตุใด?
  5. ทำไม rollout undo จึงไม่สามารถย้อนข้อมูลในฐานข้อมูลได้?
  6. หากต้องใช้ 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

  1. K3d Overview, Requirements and Installation
  2. K3d — Using Config Files
  3. K3d — Exposing Services
  4. K3s — Networking Services: Traefik and ServiceLB
  5. K3d — Image Import
  6. Kubernetes — Liveness, Readiness and Startup Probes
  7. Kubernetes — ConfigMaps
  8. Kubernetes — Install kubectl on Linux และ macOS
  9. Kubernetes — Version Skew Policy
  10. K3d — Using Image Registries
  11. K3d — Official Releases
  12. Kubernetes — Deployments
  13. K3s — Storage