การรัน Kubernetes บนเครื่องตัวเอง
ไอเดียหลักในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียหลักในหนึ่งประโยค”คุณไม่จำเป็นต้องมี data center เพื่อเรียน Kubernetes เครื่องมืออย่าง kind, minikube และ k3d ให้คุณได้ cluster จริงที่รองรับหลาย node รันอยู่บนเครื่องของคุณเองทั้งหมด
ตัวเลือก local cluster
หัวข้อที่มีชื่อว่า “ตัวเลือก local cluster”เครื่องมือสามตัวนี้ครอบคลุมการพัฒนาและเรียนรู้ Kubernetes บนเครื่องตัวเองเกือบทั้งหมด
- kind (Kubernetes IN Docker) รัน node แต่ละตัวของ cluster เป็น Docker container สร้างและลบเร็ว จึงเหมาะกับ CI pipeline และเหมาะกับการเรียนตามคอร์สแบบนี้
- minikube รัน cluster แบบ single หรือ multi-node ข้างใน VM หรือ container driver ที่คุณเลือก มาพร้อม ecosystem ของ addon (dashboard, ingress, metrics) ที่เปิดใช้ได้ด้วยคำสั่งเดียวต่อตัว
- k3d รัน k3s ที่เป็น Kubernetes distribution แบบ lightweight ที่ผ่าน certified อยู่ข้างใน Docker container แนวคิดคล้าย kind แต่สร้างขึ้นรอบ footprint ที่เล็กกว่าของ k3s
ทั้งสามตัวมีไว้เพื่อทำให้การพัฒนาและเรียนรู้บนเครื่องตัวเองเร็วและทิ้งได้ ไม่มีตัวไหนตั้งใจให้ใช้บน production เลย production cluster จะสร้างด้วยเครื่องมืออย่าง kubeadm หรือจัดเตรียมโดย managed cloud service บนเครื่องจริงหรือ virtual machine ที่มี networking, storage และ high availability ที่เหมาะสม
| kind | minikube | k3d | |
|---|---|---|---|
| รันอะไร | Kubernetes (kind.sigs.k8s.io) โดยให้ node เป็น Docker/Podman container | Kubernetes เต็มรูปแบบ (minikube.sigs.k8s.io) ใน VM, container หรือ driver bare-metal ที่คุณเลือก | k3s (k3d.io) — Kubernetes distribution แบบ lightweight ที่ผ่าน certified — อยู่ใน Docker container |
| ความยืดหยุ่นของ driver | Docker หรือ Podman เท่านั้น | มีหลาย driver (docker, podman, และ VM driver เช่น hyperkit/hyperv/kvm2) | Docker เท่านั้น |
| addon ที่มีมาให้ | ไม่มี — คอร์สนี้ติดตั้ง Gateway API/CSI CRD เองเมื่อบทไหนต้องใช้ | ecosystem ของ addon ที่หลากหลาย (minikube addons enable dashboard/ingress/metrics-server) | น้อยมาก k3s มาพร้อม component บางตัวในตัวอยู่แล้ว (Traefik, servicelb) |
| เหมาะกับอะไรที่สุด | CI pipeline และตามคอร์สแบบนี้ที่ต้องการ cluster ที่ทิ้งได้และ script ได้ | cluster local ตัวเดียวที่อยู่นาน ๆ พร้อม extra แบบคำสั่งเดียวเยอะ ๆ | ตั้ง multi-cluster/multi-node เร็ว ๆ รอบ footprint เล็กของ k3s |
Cluster แรกและ Pod แรกของคุณ
หัวข้อที่มีชื่อว่า “Cluster แรกและ Pod แรกของคุณ”สร้าง local cluster แล้วลองสำรวจดูสักหน่อย
kind create clusterkubectl cluster-infokubectl get nodeskubectl cluster-info ยืนยันว่าคุณเข้าถึง API server ได้ และแสดงว่า core service รันอยู่ที่ไหน ส่วน kubectl get nodes แสดงรายการ node ใน cluster พร้อมสถานะ Ready ของตัวเอง สิ่งแรกที่ควรเช็คทุกครั้งที่รู้สึกว่ามีอะไรผิดปกติ
Pin เวอร์ชัน Kubernetes ด้วย node image digest จริง
หัวข้อที่มีชื่อว่า “Pin เวอร์ชัน Kubernetes ด้วย node image digest จริง”kind create cluster เปล่า ๆ ใช้ Kubernetes version ที่ kind release นั้น default ให้ — kind 0.33 default เป็น 1.37.0 การ pin แบบตั้งใจต่างหากที่ทำให้คอร์ส, CI pipeline หรือ bug report reproduce ได้ release notes ของ kind เองพิมพ์ node image ต่อ Kubernetes version พร้อม digest ที่ pin แน่นอน และ release notes บอกตรง ๆ ว่าคุณต้องใช้ digest แบบ @sha256 ถึงจะรับประกันได้ว่าได้ image ที่ build มาสำหรับ release นั้นจริง ๆ
kind create cluster --name foundations \ --image kindest/node:v1.37.0@sha256:a1ed56cfb0e7b93589bdf97c8cd566405a265939e3620fc4f5de89adff580ae5digest ตัวนี้ quote มาตรง ๆ จาก kind v0.33.0 release notes — --image kindest/node:v1.37.0 แบบไม่มี digest ก็ใช้ได้วันนี้ แต่มีแค่ digest form เท่านั้นที่รับประกันว่าคุณได้ byte เดียวกับที่ทีม kind test และ publish ออกมาสำหรับ release นั้นจริง ๆ
Config multi-node จริง พร้อม extraPortMappings และ node-labels
หัวข้อที่มีชื่อว่า “Config multi-node จริง พร้อม extraPortMappings และ node-labels”kind create cluster เดี่ยว ๆ ให้ node all-in-one ตัวเดียว config Cluster แบบ kind.x-k8s.io/v1alpha4 ให้ node หลายตัวจริง และให้ map container port ไปที่เครื่องคุณ พร้อมประทับ kubelet node label เพิ่มตอนสร้าง — พิสูจน์ทำงานจริงข้างล่างนี้
kind: ClusterapiVersion: kind.x-k8s.io/v1alpha4nodes: - role: control-plane extraPortMappings: - containerPort: 30080 hostPort: 8080 protocol: TCP kubeadmConfigPatches: - | kind: InitConfiguration nodeRegistration: kubeletExtraArgs: node-labels: "ingress-ready=true" - role: worker - role: workerkind create cluster --name foundations --config kind-foundations.yamloutput จริงตอนสร้าง config นี้เป๊ะ ๆ (1 control-plane + 2 worker)
Creating cluster "foundations" ... ✓ Ensuring node image (kindest/node:v1.37.0) 🖼️ ✓ Preparing nodes 📦 📦 📦 ✓ Writing configuration 📜 ✓ Starting control-plane 🕹️ ✓ Installing CNI 🔌 ✓ Installing StorageClass 💾 ✓ Joining worker nodes 🚜Set kubectl context to "kind-foundations"extraPortMappings บน control-plane node คือสิ่งที่ทำให้ Service แบบ NodePort (หรือ Ingress controller ที่ฟังผ่าน NodePort) เข้าถึงได้จาก browser บนเครื่องคุณ — ถ้าไม่มีมัน Service จะมีอยู่แค่ข้างใน Docker network ที่ kind สร้างเท่านั้น node-labels ที่ประทับผ่าน kubeadmConfigPatches ให้คุณ pre-label node ได้ก่อน workload ไหนจะถูก schedule เลย มีประโยชน์กับอะไรแบบ nodeSelector ของ Ingress controller
kind load docker-image: เอา image ที่ build บนเครื่องเข้าไปในทุก node
หัวข้อที่มีชื่อว่า “kind load docker-image: เอา image ที่ build บนเครื่องเข้าไปในทุก node”Node ใน kind cluster มองไม่เห็น Docker image cache ของเครื่องคุณโดยตรง และไม่มี registry เข้ามาเกี่ยวข้องเลยสำหรับ image ที่ build เองบนเครื่อง kind load docker-image copy image เข้าไปที่ containerd image store ของทุก node ตรง ๆ พิสูจน์ด้วยการ build image เล็ก ๆ, load มัน แล้วรัน Pod จากมันด้วย imagePullPolicy: Never (ที่ห้าม kubelet พยายาม pull จากที่ไหนเลย)
docker build -t local/tiny-hello:1.0 .kind load docker-image local/tiny-hello:1.0 --name foundationsoutput จริง
Image: "local/tiny-hello:1.0" with ID "sha256:db50fb2b2b0258cb78bea24ee398f0a16cb4c9052bd7f714747c1039042ea1d0" not yet present on node "foundations-control-plane", loading...Image: "local/tiny-hello:1.0" with ID "sha256:db50fb2b2b0258cb78bea24ee398f0a16cb4c9052bd7f714747c1039042ea1d0" not yet present on node "foundations-worker2", loading...Image: "local/tiny-hello:1.0" with ID "sha256:db50fb2b2b0258cb78bea24ee398f0a16cb4c9052bd7f714747c1039042ea1d0" not yet present on node "foundations-worker", loading...apiVersion: v1kind: Podmetadata: name: tiny-hellospec: containers: - name: tiny-hello image: local/tiny-hello:1.0 imagePullPolicy: Never$ kubectl apply -f tiny-pod.yamlpod/tiny-hello created
$ kubectl get pod tiny-helloNAME READY STATUS RESTARTS AGEtiny-hello 1/1 Running 0 3s
$ kubectl logs tiny-hellohello-from-tiny-imageimagePullPolicy: Never คือสิ่งที่พิสูจน์ว่า image ถูก load เข้าไปที่ node จริง ๆ — ถ้าไม่ได้ load kubelet จะรายงาน ErrImageNeverPull แทนที่จะไปถึง Running เลย
kubectl config: ดูทุก context ที่คุณเข้าถึงได้
หัวข้อที่มีชื่อว่า “kubectl config: ดูทุก context ที่คุณเข้าถึงได้”ทุก cluster ที่คุณสร้าง ไม่ว่าจะ kind, cloud cluster หรืออะไรก็ตาม จะเพิ่ม context เข้าไปใน kubeconfig ตัวเดียวกัน kubectl config get-contexts คือวิธีดูทั้งหมดพร้อมกัน มี * บอกว่าตัวไหนคือตัวปัจจุบัน
$ kubectl config get-contextsCURRENT NAME CLUSTER AUTHINFO NAMESPACE* kind-foundations kind-foundations kind-foundations kind-verify-k8s kind-verify-k8s kind-verify-k8s
$ kubectl config current-contextkind-foundationsนี่คือเหตุผลตรง ๆ ว่าทำไม kubectl --context <name> (หรือ kubectl config use-context) ถึงสำคัญทันทีที่คุณมี cluster มากกว่าหนึ่งตัวอยู่รอบตัว — ไม่งั้นทุกคำสั่งจะรันกับ context ที่ถูก star ไว้ตอนนั้นแบบเงียบ ๆ
พอมี cluster แล้ว ให้รัน Pod แล้วเข้าถึง Pod นั้นจาก laptop ของคุณ
kubectl run nginx --image=nginxkubectl get podskubectl port-forward pod/nginx 8080:80kubectl run เป็นวิธีแบบ imperative เร็ว ๆ ในการสร้าง Pod เดียวที่รัน container ตัวเดียว มีประโยชน์เวลาจะ smoke-test image สักตัว kubectl port-forward เปิด tunnel จาก port บนเครื่องของคุณไปยัง port ข้างใน Pod ทำให้ http://localhost:8080 เข้าถึง nginx ที่รันอยู่ใน cluster ได้เลยโดยไม่ต้องมี Service หรือ Ingress เลย สะดวกมากสำหรับขั้นตอนแรก ๆ ของการ debug หรือลองอะไรเล่น ๆ
Namespace คือการแบ่ง cluster
หัวข้อที่มีชื่อว่า “Namespace คือการแบ่ง cluster”namespace คือวิธีแบ่ง cluster เดียวออกเป็นขอบเขตแยกกันสำหรับชื่อ, การเข้าถึง และ resource limit ทุก cluster มาพร้อม namespace พื้นฐานอยู่แล้วไม่กี่ตัว
default— ที่ที่ object จะไปอยู่ถ้าคุณไม่ระบุ namespacekube-system— สงวนไว้สำหรับ component ที่ control plane จัดการเอง เช่น CoreDNS และ Pod ของ kube-proxy เอง ปกติคุณแค่มาดู ไม่ต้องไปแตะkube-public— ผู้ใช้ทุกคนอ่านได้ (รวมถึงคนที่ไม่ authenticate) ใช้เก็บข้อมูล cluster ที่ควรเปิดเผยต่อสาธารณะ
แสดงรายการ namespace แล้วสร้างของคุณเองสำหรับโปรเจกต์
kubectl get namespaceskubectl create namespace my-appflowchart TB
laptop["Your laptop"] --> docker["Docker"]
subgraph cluster["kind cluster"]
cp["control-plane node (container)"]
w1["worker node (container)"]
w2["worker node (container)"]
end
docker --> cluster อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอนรีวิว kind config หรือ setup script ที่ AI สร้าง เช็กสิ่งเหล่านี้
- tag
kindest/nodeที่ไม่มี digest@sha256แต่ถูกนำเสนอว่า “pin แล้ว” — พิสูจน์ข้างบนแล้วว่า release notes ของ kind บอกตรง ๆ ว่ามีแค่ digest form เท่านั้นที่รับประกัน image ที่ test จริงของ release นั้น tag เปล่า ๆ ถูกชี้ไปที่อื่นได้ - ข้าม
kind load docker-imageไปเลย แต่คาดหวังให้ Pod รัน image ที่ build บนเครื่องจากdocker buildตรง ๆ — พิสูจน์ข้างบนแล้วว่า node ของ kind มองไม่เห็น Docker image cache ของเครื่องคุณ ถ้าไม่ load image (พร้อมimagePullPolicy: NeverหรือIfNotPresent) kubelet จะพยายาม pull จาก registry แล้วล้มเหลว เพราะ image นี้ไม่เคยถูก push ไปที่ไหนเลย - ใช้
kind create clusterแบบ single-node เพื่อ “พิสูจน์” พฤติกรรมที่ปรากฏเฉพาะตอนมีหลาย node เท่านั้น (การกระจาย, node affinity, topology spread) — config multi-node ของคอร์สนี้เองข้างบนคือสิ่งที่บทแบบนั้นต้องการจริง ๆ ไม่ใช่ default บรรทัดเดียว - แนะนำหรือใช้
kindสำหรับอะไรที่บอกว่าเป็น production deployment — พิสูจน์ในตารางเปรียบเทียบข้างบนแล้วว่าไม่มีตัวไหนใน kind, minikube, k3d อ้างว่าเป็น production-grade เลย ทั้งสามมีไว้สำหรับ local development, testing และ CI โดยเฉพาะ - generated multi-cluster script ที่ไม่เคยส่ง
--contextหรือ--nameตรง ๆ เลย พึ่งพา context ที่บังเอิญเป็นตัวปัจจุบัน — พิสูจน์ข้างบนแล้วว่าkubectl config get-contextsแสดง cluster หลายตัวพร้อมกันได้ มีแค่ตัวเดียวที่ถูก star ไว้เป็นตัวปัจจุบัน การรันกับตัวผิดแบบเงียบ ๆ คือความผิดพลาดที่หลีกเลี่ยงได้ง่าย
🐛 หา bug ในโค้ดนี้
# A generated CI script, meant to test a locally built imagedocker build -t myapp:test .kind create cluster --name ci-testkubectl apply -f pod.yaml # pod.yaml references image: myapp:testเฉลย
สคริปต์นี้ build image และสร้าง cluster แต่ไม่เคยรัน kind load docker-image myapp:test --name ci-test คั่นกลางเลย พิสูจน์ข้างบนแล้วว่า containerd ของ kind node มองไม่เห็น Docker image store ของเครื่องคุณเลย — สองระบบนี้แยกกันเด็ดขาด ถ้าไม่มีขั้นตอน load kubelet บน node จะพยายาม pull myapp:test จาก registry (Docker Hub โดย default) แล้วเพราะไม่เคยถูก push ไปที่ไหนเลย Pod จะค้างอยู่ที่ ErrImageNeverPull หรือ ImagePullBackOff ขึ้นอยู่กับ imagePullPolicy ที่ตั้งไว้ — ความล้มเหลวที่งงมากสำหรับ script ที่ “ควรจะทำงานได้เลย” เพราะ image มีอยู่จริงบนเครื่องเห็น ๆ ทางแก้คือบรรทัดเดียว คือ kind load docker-image myapp:test --name ci-test คั่นระหว่าง build กับ apply พิสูจน์ข้างบนแล้วว่าทำให้ image มีอยู่จริงในทุก node ก่อนที่อะไรจะพยายามรันมัน
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”kind/minikube/k3d cluster ไม่ใช่การจำลอง Kubernetes — มันคือ kube-apiserver, etcd, scheduler และ kubelet ตัวเดียวกันเป๊ะกับ production cluster แค่รันบน node ที่บังเอิญเป็น container (หรือ VM) บน laptop คุณ แทนที่จะเป็นเครื่องจริงหรือเครื่อง cloud ทุกอย่างที่คุณพิสูจน์ในนี้ — kubectl explain จริง, rollout จริง, การตัดสินใจ schedule จริง — คือพฤติกรรมจริง ไม่ใช่ค่าประมาณ สิ่งเดียวที่ขาดไปคือสิ่งที่เครื่องเดียวให้ไม่ได้จริง ๆ (networking หลายเครื่องระดับใหญ่จริง, cloud load balancer, storage ที่ทนต่อ host ล่ม) และนี่ก็คือเหตุผลตรง ๆ ว่าทำไมการ load image ถึงสำคัญ containerd ของ kind node คือ container runtime จริงที่แยกตัวจริง ไม่มี back channel ไปหา Docker ของเครื่องคุณเลย — ปฏิบัติกับมันเหมือนเครื่องแยกต่างหาก เพราะสำหรับทุกจุดประสงค์ที่สำคัญตรงนี้ มันคือเครื่องแยกจริง ๆ
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ข้อดี | ต้นทุน |
|---|---|---|
| kind | สร้าง/ลบเร็วที่สุด สร้างมาเพื่อ CI และ cluster ที่ทิ้งได้สไตล์คอร์สนี้ (คอร์สนี้ใช้ตลอดทั้งเล่ม) | Docker/Podman เท่านั้น ไม่มี VM isolation ไม่มี driver อื่นที่ไม่ใช่ Docker |
| minikube | addon เยอะที่สุด driver ยืดหยุ่นที่สุด (VM หรือ container) | หนักและช้ากว่า kind ตอนสร้าง/ลบสำหรับ cluster ที่ใช้แล้วทิ้ง |
| k3d | เร็วมาก footprint เล็ก (wrap k3s) | component บาง Kubernetes ถูกแทนที่ล่วงหน้าโดยการเลือกของ k3s เอง (เช่น ingress/load-balancer default ของตัวเอง) ซึ่งแปลกใจได้ถ้าคุณคาดหวัง default ของ upstream Kubernetes ตัวเปล่า |
Pin node image ด้วย @sha256 | Reproduce ได้ข้ามเครื่องและ CI run ได้ byte เดียวกับที่ test จริง พิสูจน์ข้างบน | มีค่าอีกตัวที่ต้องอัปเดตเวลาอยากได้ Kubernetes version ใหม่จริง ๆ |
kind load docker-image สำหรับทุก image ที่ build เอง | Pod ใช้ image ที่ไม่เคยถูก push ไป registry ไหนเลยได้ (พิสูจน์ข้างบน) | มีขั้นตอนเพิ่มที่ต้องจำใน build script ทุกตัว ข้ามไปแล้วจะได้ pull failure ที่งงจริง ๆ |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “kind/minikube/k3d cluster ให้แค่พฤติกรรม Kubernetes ที่ใกล้เคียงเท่านั้น” พิสูจน์ข้างบนใน Mental Model แล้วว่ามันรัน API server, etcd, scheduler และ kubelet จริงตัวเดียวกับ cluster อื่น ๆ — การพิสูจน์ทั้งคอร์สนี้รันอยู่บน cluster แบบนี้เป๊ะ
- “docker build ทำให้ image พร้อมใช้กับ Pod ใน kind cluster ได้ทันที” พิสูจน์ข้างบนด้วยความล้มเหลวสไตล์
ErrImageNeverPullจริงแล้วว่า containerd ของ kind node มองไม่เห็น Docker image cache ของเครื่องคุณเลย ต้องkind load docker-imageก่อน - “tag
kindest/node:v1.37.0เปล่า ๆ ก็ reproduce ได้เป๊ะอยู่แล้ว” พิสูจน์ข้างบนแล้วว่า release notes ของ kind เองบอกว่าต้องใช้ digest@sha256ถึงจะรับประกัน image เป๊ะที่ release นั้น test และ publish ออกมา tag เดี่ยว ๆ ไม่ใช่การรับประกันนั้น - “kind cluster ที่คุณสร้างล่าสุดคือตัวที่คำสั่ง kubectl ของคุณกำลังรันด้วยอยู่” พิสูจน์ข้างบนด้วย
kubectl config get-contextsแล้วว่า context ปัจจุบันคือตัวที่ถูก star ไว้ ไม่ใช่ตัวที่สร้างล่าสุด — ควรเช็กตรง ๆ ทุกครั้งที่มี cluster มากกว่าหนึ่งตัว