Architecture: Control Plane และ Node
ไอเดียหลักในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียหลักในหนึ่งประโยค”control plane ของ cluster ตัดสินใจและเก็บ state ส่วน node รัน Pod และทุกการตัดสินใจ ทุก byte ของ state นั้น ต้องผ่านประตูเดียวคือ API server
Control plane คือสมองของ cluster
หัวข้อที่มีชื่อว่า “Control plane คือสมองของ cluster”control plane คือ component ชุดเล็ก ๆ ที่มักรันอยู่ด้วยกัน และร่วมกันตัดสินใจว่า cluster ควรทำอะไร พร้อมจดจำสิ่งที่ตัดสินใจไปแล้ว
- kube-apiserver คือประตูหน้าของ cluster เปิด REST API ที่ทุก component อื่น รวมถึงทุกคำสั่ง
kubectlที่คุณรัน ต้องคุยด้วย ทำหน้าที่ตรวจสอบและ authenticate request และเป็น component เดียวเท่านั้นที่คุยกับ etcd โดยตรง - etcd คือ distributed key-value store ที่มีความ consistent เก็บ cluster state ทั้งหมด ทุก object ที่คุณสร้าง สถานะปัจจุบันของตัวเอง ทุกอย่าง ถ้า etcd หายไปโดยไม่มี backup ความจำของ cluster ก็หายไปด้วย
- kube-scheduler คอยดู Pod ที่เพิ่งสร้างใหม่และยังไม่มี node กำหนด แล้วตัดสินใจว่าแต่ละตัวควรรันบน node ไหน โดยดูจาก resource requirement, constraint และ load ปัจจุบันของ cluster
- kube-controller-manager รัน controller ในตัวทั้งหมดเป็น process เดียว เช่น node controller (สังเกตเมื่อ node เข้าไม่ถึง) replication controller (รักษาจำนวน Pod replica ให้ถูกต้อง) endpoint controller (อัปเดต endpoint ของ Service ให้ทันสมัย) และอื่น ๆ ตรงนี้คือที่ที่ reconciliation loop จากบทก่อนหน้าทำงานจริง
- cloud-controller-manager เป็น optional และมีเฉพาะบน managed cloud cluster ทำหน้าที่เชื่อม concern ระดับ cluster เช่น load balancer และ node lifecycle เข้ากับ API ของ cloud provider
คุณดู Pod ของ control plane เองได้บน cluster สไตล์ kubeadm เหมือน workload ทั่วไป
kubectl get pods -n kube-systemNode component คือที่ที่ Pod รันจริง
หัวข้อที่มีชื่อว่า “Node component คือที่ที่ Pod รันจริง”ทุก node ใน cluster รันสามอย่างนี้
- kubelet คือ node agent เป็น component ที่ทำให้ container ตาม Pod spec ที่ assign มาให้ node นี้รันอยู่จริงและ healthy kubelet เริ่ม container, restart เมื่อ container ตาย, รัน probe ของตัวเอง และรายงานสถานะของ node กับ Pod กลับไปยัง API server ไม่มีอะไรรันบน node ได้ ถ้าไม่มี kubelet เป็นคนสั่ง
- kube-proxy ดูแล network rule บน node ที่ทำให้ Service ทำงานได้ คือ route traffic ที่ยิงไปยัง virtual IP ของ Service ไปหา Pod ตัวใดตัวหนึ่งที่ match ไม่ว่าจะรันอยู่ที่ไหน
- container runtime เช่น containerd หรือ CRI-O คือตัวที่ pull image และรัน container จริง ๆ โดยคุยกับ kubelet ผ่าน Container Runtime Interface (CRI)
ตรวจสอบ component และสถานะของ node โดยตรงได้ด้วย
kubectl describe node <node-name>ทุกอย่างคุยกับ etcd ผ่าน API server เท่านั้น
หัวข้อที่มีชื่อว่า “ทุกอย่างคุยกับ etcd ผ่าน API server เท่านั้น”นี่คือข้อเท็จจริงเชิง architecture ที่ควรจำ kube-scheduler, kube-controller-manager, kubelet และทุกอย่างอื่น ไม่เคยแตะ etcd โดยตรง ทุกตัวอ่านและเขียน cluster state ผ่านการเรียก API server เท่านั้น ที่เป็น single source of truth และเป็นผู้มีอำนาจเดียวของทั้ง cluster ทำให้ Kubernetes มีจุดเดียวที่สม่ำเสมอสำหรับ authentication, authorization, validation และ watch notification controller ทุกตัวและ kubelet ทุกตัว จริง ๆ แล้วก็แค่เป็น client ของ REST API ตัวเดียวกับที่คุณใช้กับ kubectl
controller รู้ว่ามีอะไรเปลี่ยนได้ยังไงจริง ๆ: watch ไม่ใช่ poll
หัวข้อที่มีชื่อว่า “controller รู้ว่ามีอะไรเปลี่ยนได้ยังไงจริง ๆ: watch ไม่ใช่ poll”controller ทุกตัวและ kubectl get --watch ทุกครั้ง เปิด HTTP connection แบบยาว ๆ ไปที่ API server แล้วรับ stream ของ event ADDED/MODIFIED/DELETED ทันทีที่มีอะไรเปลี่ยน — ไม่ได้ poll เป็น loop เลย --output-watch-events แสดง event type คู่กับ column ปกติ พิสูจน์ด้วย watch ที่เริ่มบน namespace แล้วสร้างและลบ Pod ระหว่างที่มันรันอยู่
kubectl get pods -n foundations-demo --watch --output-watch-events -o widestream จริง (Pod ที่มีอยู่แล้วจะมาเป็น ADDED ก่อน จากนั้น Pod ใหม่เอี่ยมจะเล่น lifecycle ทั้งหมดเป็นชุด event MODIFIED จบด้วย DELETED)
EVENT NAME READY STATUS RESTARTS AGE IP NODEADDED web-69d5569566-fmcq2 1/1 Running 0 19s 10.244.2.10 foundations-workerADDED watchtest 0/1 Pending 0 0s <none> <none>MODIFIED watchtest 0/1 Pending 0 0s <none> foundations-worker2MODIFIED watchtest 0/1 ContainerCreating 0 0s <none> foundations-worker2MODIFIED watchtest 1/1 Running 0 1s 10.244.1.9 foundations-worker2MODIFIED watchtest 1/1 Terminating 0 3s 10.244.1.9 foundations-worker2MODIFIED watchtest 0/1 Completed 0 3s 10.244.1.9 foundations-worker2DELETED watchtest 0/1 Completed 0 4s 10.244.1.9 foundations-worker2ทุกบรรทัดคือ API server push การเปลี่ยนแปลงลงมาผ่าน connection ที่เปิดค้างไว้แล้ว — นี่คือกลไกเดียวกันเป๊ะที่ kube-controller-manager, kube-scheduler และ kubelet ใช้ตอบสนอง cluster state แทนที่จะถาม “มีอะไรเปลี่ยนหรือยัง” เป็น loop กลไกที่ลึกกว่านี้ — resourceVersion, watch bookmark และวิธีที่ informer สร้าง local cache ทับ stream เดียวกันนี้ — อยู่ในบท internals บทนี้แค่ต้องการให้คุณรู้ว่า “controller ตอบสนอง” หมายถึง “controller watch”
kubectl get --raw: คุยกับ API server แบบไม่มีอะไรคั่นกลาง
หัวข้อที่มีชื่อว่า “kubectl get --raw: คุยกับ API server แบบไม่มีอะไรคั่นกลาง”kubectl get --raw <path> ส่ง request ตรงไปที่ path ของ API server แล้วพิมพ์ response ดิบ ๆ ออกมา ไม่มีการจัด format เป็น object หรือ table — มีประโยชน์กับ endpoint ที่ไม่ใช่ resource ปกติเลย
$ kubectl get --raw /healthzok
$ kubectl get --raw /version{ "major": "1", "minor": "37", "gitVersion": "v1.37.0", "gitCommit": "f54c212e3a2f75d674b717a9b29052b20b60aefc", "buildDate": "2026-08-26T10:44:25Z", "goVersion": "go1.26.6", "platform": "linux/arm64"}-v=8: ดู HTTP request จริงที่ kubectl ยิง
หัวข้อที่มีชื่อว่า “-v=8: ดู HTTP request จริงที่ kubectl ยิง”ทุกคำสั่ง kubectl ข้างใต้คือ HTTP call ไปที่ API server — -v=8 พิมพ์ request line, header และ response body จริงที่ kubectl ส่งและรับ ซึ่งเป็นวิธีที่เร็วที่สุดที่จะเลิกเดาว่าคำสั่ง “จริง ๆ แล้วทำอะไร”
$ kubectl get pods -n foundations-demo -v=8...I0926 15:34:31.288997 round_trippers.go:527] "Request" verb="GET" url="https://127.0.0.1:52255/api/v1/namespaces/foundations-demo/pods?limit=500"I0926 15:34:31.296968 round_trippers.go:632] "Response" status="200 OK" headers=<...>I0926 15:34:31.297057 helper.go:113] "Response Body" body="{\"kind\":\"Table\",\"apiVersion\":\"meta.k8s.io/v1\", ...}"kubectl get ที่ไม่ใส่ -o จริง ๆ แล้วขอ meta.k8s.io/v1, Kind=Table จาก server — column formatting ที่คุณเห็นใน kubectl get pods ธรรมดา ถูกคำนวณฝั่ง server แล้วส่งกลับมาเป็น structured data ไม่ได้เดาเอาเองฝั่ง client จาก object list เปล่า ๆ
etcd อยู่ตรงไหนจริง ๆ ใน kind cluster
หัวข้อที่มีชื่อว่า “etcd อยู่ตรงไหนจริง ๆ ใน kind cluster”kind รัน control plane แบบเดียวกับที่ kubeadm ทำทุกที่ คือเป็น static Pod ที่ถูกกำหนดด้วยไฟล์ manifest ธรรมดาที่ kubelet เฝ้าดูตรงบน disk ไม่ผ่าน API server บน control-plane node
$ docker exec foundations-control-plane ls /etc/kubernetes/manifests/etcd.yamlkube-apiserver.yamlkube-controller-manager.yamlkube-scheduler.yamletcd.yaml จริง (ตัดให้สั้น) — นี่คือ command line จริงที่ etcd ใช้ start ไม่มี abstraction คั่นกลางเลย
$ docker exec foundations-control-plane cat /etc/kubernetes/manifests/etcd.yamlapiVersion: v1kind: Podmetadata: name: etcd namespace: kube-systemspec: containers: - command: - etcd - --advertise-client-urls=https://172.19.0.7:2379 - --data-dir=/var/lib/etcd - --initial-cluster=foundations-control-plane=https://172.19.0.7:2380 - --listen-client-urls=https://127.0.0.1:2379,https://172.19.0.7:2379kind node image ไม่มี binary etcdctl มาให้ member list เลยใช้ไม่ได้ข้างในตัว node แบบที่อาจใช้ได้บน kubeadm cluster ที่คุณสร้างเองแล้วติดตั้ง etcd tooling เต็มชุด ทางเลือกตรง ๆ สำหรับ “etcd ยัง healthy ไหม” บน kind คือดู static Pod ของมันผ่าน API server เหมือนของอย่างอื่น
$ kubectl -n kube-system get podsNAME READY STATUS RESTARTS AGEetcd-foundations-control-plane 1/1 Running 0 3m24skube-apiserver-foundations-control-plane 1/1 Running 0 3m24skube-controller-manager-foundations-control-plane 1/1 Running 0 3m23skube-scheduler-foundations-control-plane 1/1 Running 0 3m24skubelet กับ container runtime บน node จริง
หัวข้อที่มีชื่อว่า “kubelet กับ container runtime บน node จริง”บน worker node kubelet กับ containerd เป็น host process คนละตัวกัน kubelet ไม่เคยรัน container เองเลย มันสั่ง containerd (ผ่าน CRI) ให้รันแทน แล้ว containerd ก็ส่งต่อแต่ละ container ให้ process containerd-shim-runc-v2
$ docker exec foundations-worker sh -c 'ps aux | grep -E "kubelet|containerd" | grep -v grep'root 130 /usr/local/bin/containerdroot 242 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/config.yamlroot 294 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id bf542e6a6f4bd107dc83be43e581da8a6b297fb39d17fbfb1291a33f75834384หนึ่ง shim process ต่อหนึ่ง container ที่กำลังรัน แต่ละตัวเป็นลูกโดยตรงของ containerd ไม่ใช่ของ kubelet — งานของ kubelet จบแค่บอก containerd ว่าควรมีอะไรรันอยู่ ส่วน containerd (และ shim ข้างใต้) ต่างหากที่เป็นเจ้าของ process tree ที่รันอยู่จริง
flowchart TB
subgraph cp["Control Plane"]
api["kube-apiserver"]
etcd["etcd"]
sched["kube-scheduler"]
cm["kube-controller-manager"]
end
subgraph n1["Node"]
kubelet["kubelet"]
proxy["kube-proxy"]
cri["container runtime (containerd / CRI-O)"]
end
sched --> api
cm --> api
api --> etcd
kubelet --> api
proxy --> api อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอนรีวิว architecture diagram, runbook หรือคำแนะนำ debug ที่ AI สร้าง เช็กสิ่งเหล่านี้
- คำแนะนำให้
curlหรือคุยกับ etcd ตรง ๆ จาก application หรือ script — พิสูจน์ข้างบนแล้วว่ามีแค่ API server เท่านั้นที่ควรแตะ etcd port ของ etcd บน cluster ที่ healthy ไม่ใช่สิ่งที่ workload ของคุณเองควรไปชี้ถึงเลย - “poll API server ทุก N วินาทีเพื่อจับการเปลี่ยนแปลง” เป็น design ของ controller หรือ automation script — พิสูจน์ข้างบนแล้วว่ากลไกจริงที่ built-in controller ทุกตัวใช้คือ watch (connection ที่เปิดค้างไว้ยาว ๆ รับ event
ADDED/MODIFIED/DELETED) ไม่ใช่ poll ถ้า controller ที่ generate มา poll แปลว่ามันกำลังอธิบายอะไรที่ช้าและหนักกว่าสิ่งที่ Kubernetes เองทำมาก - คำแนะนำ debug สำหรับ managed cloud cluster (EKS/GKE/AKS) ที่บอกให้
docker execเข้า control-plane node หรืออ่าน/etc/kubernetes/manifests/etcd.yaml— พิสูจน์ข้างบนแล้วว่า path นี้จริงสำหรับ cluster สไตล์ kubeadm (รวมถึง kind) แต่ control plane ของ managed cluster ไม่ได้รันเป็น Pod บน node ที่ shell เข้าไปได้เลย คำสั่งแบบนี้ไม่มีอยู่ที่นั่น - สมมติว่า
etcdctlมีอยู่ในทุก node ของ Kubernetes — พิสูจน์ข้างบนแล้วว่าไม่ได้เป็นส่วนหนึ่งของ kind node image ทางเลือก (kubectl -n kube-system get pods) คือทางแทนที่ใช้ได้จริง ไม่ใช่การประนีประนอม - สับสนระหว่าง kubelet กับ container runtime — พิสูจน์ข้างบนแล้วว่า kubelet แค่เรียก containerd (ผ่าน CRI) containerd กับ shim process ของมันต่างหากที่เป็นเจ้าของ process tree ที่รันอยู่จริง
🐛 หา bug ในโค้ดนี้
# From a generated "custom health checker" scriptETCD_ENDPOINT="https://172.19.0.7:2379"curl --cacert /etc/kubernetes/pki/etcd/ca.crt "$ETCD_ENDPOINT/health"# ...then feed the result straight into a scaling decisionเฉลย
สคริปต์นี้คุยกับ etcd ตรง ๆ จากนอก control plane ข้าม API server ไปเลย พิสูจน์ข้างบนแล้วบน kind cluster จริง kube-scheduler, kube-controller-manager, kubelet และทุกคำสั่ง kubectl เข้าถึง etcd ผ่านประตูเดียวเท่านั้นคือ API server — ไม่มีอะไรอื่นควรเปิด client connection ไปหามันเลย แม้จะไม่พูดถึงว่า client cert ของ etcd (/etc/kubernetes/pki/etcd/*) ไม่ใช่สิ่งที่ application ควรถืออยู่แล้ว การเอา scaling decision ไปอิงกับการเช็ก health ของ etcd ดิบ ๆ ก็ทิ้งการรับประกันทุกอย่าง (auth, validation, watch notification) ที่ API server มีอยู่เพื่อให้ ทางแก้คือ kubectl get --raw /healthz (หรือ client-go call เทียบเท่ากับ API server) ซึ่งพิสูจน์ข้างบนแล้วว่าเป็นวิธีจริงที่รองรับสำหรับถามว่า “API เข้าถึงได้ไหม” โดยไม่แตะ etcd เลย
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”มองว่า control plane เป็นฐานข้อมูล (etcd) ที่มีประตูหน้าเดียวเป๊ะ (API server) และมี client กลุ่มเล็ก ๆ ยืนอยู่หน้าประตูนั้น ทำสองอย่างเดิมตลอดเวลา คือ อ่าน state ปัจจุบันผ่าน watch และเขียนกลับด้วย delta เล็ก ๆ ที่จำเป็นเพื่อขยับ state ไปหาสิ่งที่ต้องการ kube-scheduler, kube-controller-manager, kubelet, kube-proxy และ kubectl เอง เหมือนกันเป๊ะในมุมของ etcd — ทุกตัวก็แค่เป็น client ที่ watch-แล้ว-patch ของ REST API ตัวเดียวกัน ไม่มีประตูหลัง ไม่มีสิทธิ์พิเศษ พอเข้าใจตรงนี้แล้ว “ทำไมอ่าน etcd ตรง ๆ บน managed cluster ไม่ได้” กับ “ทำไม kubectl get --watch ดูเหมือนไม่หยุดสักที” ก็ตอบตัวเองทั้งคู่ ไม่มีทางเข้าอื่น และไม่มี polling loop ให้หยุด
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ข้อดี | ต้นทุน |
|---|---|---|
| API server เดียวเป็น etcd client ตัวเดียว | จุดเดียวที่สม่ำเสมอสำหรับ auth, validation และ watch notification (พิสูจน์ข้างบน) | availability ของทุก component ถูกจำกัดด้วย availability ของ API server — ไม่มี component ไหนมีทาง fallback ส่วนตัวไปหา state ได้ |
| Watch แทน poll | ตอบสนองการเปลี่ยนแปลงเกือบทันที พิสูจน์ข้างบนด้วย event stream จริง ไม่เสีย request “มีอะไรเปลี่ยนหรือยัง” ทิ้ง | controller ต้องจัดการ watch connection ที่หลุดหรือหมดอายุแล้ว re-list ซึ่งเป็น state ที่ต้องจัดการให้ถูกต้องมากกว่า poll loop แบบ naive |
| Static Pod สำหรับ control plane (kubeadm/kind) | เรียบง่าย ตรวจสอบได้ เป็น YAML file ที่ kubelet เฝ้าดูบน disk จริง ๆ พิสูจน์ได้ด้วย docker exec + cat ข้างบน | ไม่ใช่วิธีที่ managed cloud cluster ทำงานเลย คำแนะนำ debug ที่เขียนจาก kind/kubeadm ใช้ข้ามไปไม่ได้ |
| kubelet ส่งต่อให้ containerd ผ่าน CRI แทนที่จะรัน container เอง | runtime สลับได้ (containerd, CRI-O) โดยไม่ต้องเปลี่ยน kubelet | มี process boundary เพิ่มอีกจุด และมีอะไรอีกอย่างที่ out of sync ได้ (kubelet ขึ้นแต่ containerd แย่ลง หรือกลับกัน) |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “Application หรือ script คุยกับ etcd ตรง ๆ เพื่ออ่านเร็ว ๆ ได้ ถ้ามี cert” พิสูจน์ข้างบนแล้วว่าไม่มีอะไรนอกจาก API server ที่ควรแตะ etcd และ SpotTheBug ข้างบนแสดงเหตุผลตรง ๆ ว่าทำไมถึงสำคัญ — การรับประกันทุกอย่าง (auth, validation, watch notification) ถูกข้ามไปทันทีที่ทำแบบนั้น
- “Controller จับการเปลี่ยนแปลงด้วยการ poll API server เป็นช่วง ๆ” พิสูจน์ข้างบนด้วย
--output-watch-eventsstream จริงแล้วว่ากลไกจริงคือ watch connection ที่เปิดค้างไว้รับ push event ไม่ใช่ request-response poll loop - “ทุก Kubernetes cluster มี
/etc/kubernetes/manifests/ให้ตรวจสอบ control plane” พิสูจน์ข้างบนแล้วว่าจริงสำหรับ kind และ cluster ที่สร้างด้วย kubeadm แต่ไม่จริงสำหรับ managed cloud cluster (EKS/GKE/AKS) ที่ control plane ไม่ได้เป็น Pod บน node ที่คุณเข้าถึงได้เลย - “kubelet เป็นคนรัน container” พิสูจน์ข้างบนแล้วว่า kubelet เรียก containerd ผ่าน Container Runtime Interface containerd (และ shim process ต่อ container ของมัน) ต่างหากที่เป็นเจ้าของ process ที่รันอยู่จริง