Kubernetes architecture แบ่งคลัสเตอร์ออกเป็นสองฝั่ง ฝั่งแรกคือ control plane ที่เก็บ desired state และเป็นผู้ตัดสินใจ อีกฝั่งคือ worker node ที่รัน container ของคุณจริง ๆ ทุก component คุยกันผ่าน API server เพียงตัวเดียว และทุกการเปลี่ยนแปลงเดินตาม loop เดียวกัน: มีคนบันทึกว่าต้องการอะไร, controller เห็นว่าของจริงยังไม่ตรงกับที่ต้องการ, แล้ว agent บน node ก็ลงมือทำให้ตรง
พอเข้าใจว่าแต่ละ component ทำหน้าที่อะไร คุณจะตอบได้ทันทีว่าทำไมคลัสเตอร์ยังรับ traffic ได้ตอน control plane ล่ม, ทำไมการเสีย etcd ถึงเป็นหายนะ และต้องไปดูตรงไหนเมื่อ Pod ค้างอยู่ที่ Pending
สองฝั่งของ Kubernetes architecture
CONTROL PLANE
kubectl --> kube-apiserver <--> etcd
^ scheduler
^ controller-manager
^ cloud-controller-manager (cloud only)
|
| watch assigned Pods, report status
|
WORKER NODES (one set per node)
kubelet --> container runtime (CRI) --> Pods
kube-proxy --> Service routing rulesถ้าใช้ managed service อย่าง EKS, GKE หรือ AKS ผู้ให้บริการจะดูแล control plane ให้ และคุณจะเห็นแค่ API endpoint ส่วนคลัสเตอร์ที่ตั้งเองด้วย kubeadm, component ของ control plane จะรันเป็น static Pod อยู่ใน namespace kube-system
Component ฝั่ง control plane
kube-apiserver: ประตูเดียวของคลัสเตอร์
API server เป็น REST API ที่ทุกอย่างต้องผ่าน ไม่ว่าจะเป็น kubectl, controller, scheduler หรือ kubelet ทุกตัว ทุก request เดินผ่าน pipeline เดียวกัน:
- Authentication: ใครเป็นคนเรียก (certificate, token, OIDC)
- Authorization: คนนี้มีสิทธิ์ทำสิ่งนี้ไหม ส่วนใหญ่ตัดสินด้วย RBAC
- Admission: mutating webhook และ plugin แก้ไข object ได้ จากนั้น validating webhook มีสิทธิ์ปฏิเสธ
- Persistence: เขียน object ลง etcd
API server ยังให้บริการ watch ด้วย คือ component ต่าง ๆ subscribe รอรับการเปลี่ยนแปลงแทนการ poll ซ้ำ ๆ ตัว API server เองเป็น stateless จึง scale ได้ด้วยการรันหลาย replica หลัง load balancer ถ้ามันล่ม คุณจะแก้ไขอะไรในคลัสเตอร์ไม่ได้เลย แต่ Pod ที่รันอยู่แล้วยังทำงานต่อตามปกติ
etcd: source of truth ของคลัสเตอร์
etcd เป็น key-value store แบบกระจายที่เก็บทุก object ในคลัสเตอร์ ตั้งแต่ Deployment, Pod, Service, ConfigMap, Secret ไปจนถึงข้อมูล node มีแค่ API server เท่านั้นที่คุยกับ etcd ตรง ๆ ถ้า etcd หายไปโดยไม่มี backup คลัสเตอร์ก็เท่ากับความจำเสื่อม ไม่รู้แล้วว่าอะไรควรรันอยู่บ้าง
etcd ใช้ Raft consensus และต้องมีสมาชิกเกินครึ่ง (quorum) จึงจะเขียนข้อมูลได้ คลัสเตอร์ production จึงรัน 3 หรือ 5 member โดย 3 ตัวทนเสียได้ 1 ตัว และ 5 ตัวทนเสียได้ 2 ตัว การเพิ่มเป็น 4 ตัวไม่ได้ช่วยให้ทนขึ้น เพราะ quorum ขยับจาก 2 เป็น 3 ตาม
ถ้าดูแลคลัสเตอร์เอง ให้ตั้ง snapshot ตามรอบ และซ้อม restore จริงด้วย:
etcdctl snapshot save /backup/etcd-$(date +%F).db \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.keySecret ถูกเก็บแบบ base64 ไม่ได้เข้ารหัส เว้นแต่คุณเปิด encryption at rest ที่ API server ใครที่อ่านไฟล์ backup ของ etcd ได้ ก็อ่าน Secret ของคุณได้ทั้งหมด
kube-scheduler: ตัดสินว่า Pod ควรไปอยู่ node ไหน
Scheduler คอย watch หา Pod ที่ยังไม่มี node แล้วทำสองขั้น ขั้นแรก filter ตัด node ที่รับไม่ได้ออก เช่น CPU หรือ memory ที่ request ไว้ไม่พอ, มี taint ที่ Pod ไม่ tolerate หรือ node selector และ affinity ไม่ตรง ขั้นที่สอง score node ที่เหลือแล้วเลือกตัวที่ดีที่สุด จากนั้นเขียนผลเป็น binding กลับไปที่ API server มันแค่ตัดสินใจ ไม่ได้ไปสั่งรัน container เอง เรื่อง requests, limits และ QoS ที่มีผลต่อการตัดสินใจนี้มีรายละเอียดอยู่ในบทความ Kubernetes requests, limits และ scheduling
kube-controller-manager: รวม reconcile loop ไว้ในที่เดียว
Binary ตัวนี้รัน controller หลายตัวรวมกัน ทั้ง Deployment, ReplicaSet, StatefulSet, DaemonSet, Job, Node lifecycle, EndpointSlice, ServiceAccount และอื่น ๆ ทุกตัววน loop แบบเดียวกัน:
- ดู current state ผ่าน watch
- เทียบกับ desired state ใน
specของ object - ลงมือแก้ให้ตรง แล้วเขียนผลลง
status
Loop นี้คือที่มาของ self-healing ลองลบ Pod ที่เป็นของ ReplicaSet ดู แล้ว ReplicaSet controller จะสร้างตัวใหม่มาแทนทันที
cloud-controller-manager: ตัวเชื่อมกับ cloud
ส่วนนี้เก็บ logic เฉพาะของแต่ละ cloud เช่น ลบ node object เมื่อ VM หายไป, ตั้งค่า route และสร้าง cloud load balancer ให้ Service ประเภท LoadBalancer มีเฉพาะเมื่อรันบน cloud เท่านั้น บน bare metal, Service แบบ LoadBalancer จะค้างอยู่ที่ <pending> จนกว่าจะติดตั้งตัวทำหน้าที่แทนอย่าง MetalLB ดูรายละเอียดได้ใน Kubernetes Service types
Component ฝั่ง worker node
kubelet: agent ที่ลงมือทำงานจริง
kubelet รันอยู่บนทุก node คอย watch หา Pod ที่ถูกจัดมาให้ node ของตัวเอง แล้วทำให้เกิดขึ้นจริง: สั่ง container runtime ดึง image, mount volume, start container, รัน probe และรายงานสถานะกลับ ถ้า node เริ่มขาด memory หรือ disk, kubelet จะ evict Pod ออกเพื่อปกป้อง node
Container runtime และ CRI
kubelet ไม่ได้รัน container เอง แต่สั่งผ่าน container runtime ด้วยมาตรฐาน Container Runtime Interface (CRI) ปัจจุบันแทบทั้งหมดใช้ containerd หรือ CRI-O ส่วน dockershim ที่เคยให้ Kubernetes คุยกับ Docker Engine ตรง ๆ ถูกถอดออกตั้งแต่เวอร์ชัน 1.24 แต่ image ที่ build ด้วย Docker ยังใช้ได้เหมือนเดิม เพราะเป็น OCI image มาตรฐาน
ตอน Pod เริ่มทำงาน runtime จะเรียก CNI plugin ของคลัสเตอร์ (Calico, Cilium, Flannel ฯลฯ) เพื่อสร้าง network interface และแจก IP ให้ Pod
kube-proxy: routing ของ Service บนแต่ละ node
kube-proxy คอย watch Service และ EndpointSlice แล้วเขียนกฎบน node ด้วย iptables, IPVS หรือโหมด nftables ที่ใหม่กว่า เพื่อให้ traffic ที่ยิงไปยัง virtual IP ของ Service ไปถึง Pod ที่พร้อมรับงาน ในโหมดเหล่านี้ kernel เป็นคนส่งต่อ packet ส่วน kube-proxy แค่ดูแลกฎให้เป็นปัจจุบัน CNI บางตัวอย่าง Cilium สามารถแทนที่ kube-proxy ทั้งหมดด้วย eBPF ได้
เกิดอะไรขึ้นเมื่อสั่ง kubectl apply
ลองตาม Deployment ที่มี 3 replica ตั้งแต่ต้นจนจบ:
kubectlส่ง manifest ไปที่ API server- API server ทำ authentication, authorization, admission, validate แล้วเก็บลง etcd จากนั้น
kubectlแสดงdeployment.apps/web createdซึ่งแปลว่า object ถูกบันทึกแล้ว แต่ยังไม่มีอะไรรันเลย - Deployment controller เห็น Deployment ใหม่ จึงสร้าง ReplicaSet
- ReplicaSet controller เห็นว่า ReplicaSet ต้องการ 3 Pod แต่ยังไม่มีสักตัว จึงสร้าง Pod object 3 ตัวที่ยังไม่มี node
- Scheduler เห็น Pod ที่ยังไม่ถูกจัดวาง เลือก node ให้แต่ละตัว แล้วเขียน binding
- kubelet บน node ที่ถูกเลือกเห็นว่ามี Pod ของตัวเอง runtime ดึง image, CNI แจก IP แล้ว container ก็เริ่มทำงาน
- เมื่อ readiness probe ผ่าน kubelet จะรายงานว่า Pod
Readyแล้ว EndpointSlice controller เพิ่ม IP ของ Pod เข้า Service ที่ตรงกัน และ kube-proxy บนทุก node ก็อัปเดตกฎตาม
ไม่มี component ไหนเรียกหากันตรง ๆ ทุกตัวแค่ watch API server แล้วตอบสนอง:
kubectl apply -f deployment.yaml
kubectl get events --watch
kubectl get pods -o wide --watchลำดับนี้ยังบอกด้วยว่าเมื่อมีอะไรค้าง ควรไปดูที่ไหน:
| อาการ | ลำดับหยุดอยู่ตรงไหน |
|---|---|
Pod ค้าง Pending | Scheduler: ไม่มี node ที่เหมาะ (resource, taint, affinity, PVC ที่ยังไม่ bind) |
ค้าง ContainerCreating นาน | kubelet, runtime, CNI หรือการ mount volume บน node |
ImagePullBackOff | runtime ดึง image ไม่ได้: ชื่อ image, tag หรือ credential ของ registry ผิด |
Running แต่ไม่ Ready | readiness probe ไม่ผ่าน |
| Service ไม่ตอบ | ไม่มี Pod ที่ ready อยู่ใน EndpointSlice หรือ selector ไม่ตรง |
ส่วน Pod ที่ restart วนไม่จบเป็นอีกเรื่องหนึ่ง อ่านต่อได้ที่ การ debug CrashLoopBackOff และ probe
Control traffic กับ data traffic
Kubernetes architecture แยก traffic สองแบบออกจากกันชัดเจน:
- Control traffic คือการบริหารคลัสเตอร์ เช่น
kubectl apply, kubelet รายงานสถานะ, scheduler เขียน binding ทั้งหมดนี้วิ่งผ่าน API server - Data traffic คือ request จริงของผู้ใช้ วิ่งจาก load balancer เข้า node แล้วกฎของ kube-proxy ส่งต่อไปยัง Pod เส้นทางนี้ไม่แตะ API server หรือ etcd เลย
การแยกแบบนี้สำคัญทั้งเรื่อง scale และความทนทาน ถ้าทุก request ของผู้ใช้ต้องผ่าน API server มันจะกลายเป็นคอขวดทันที และเมื่อ control plane ล่ม แอปของคุณก็ยังให้บริการผู้ใช้ได้
แต่ "ยังให้บริการได้" ไม่ได้แปลว่า "ปกติดี" ระหว่างที่ control plane ล่ม ถ้ามี node ตายจะไม่มีการย้าย Pod ไปที่อื่น, deploy หรือ scale ไม่ได้ และ endpoint ของ Service จะไม่อัปเดต Pod ที่กลายเป็น unready จึงยังได้รับ traffic ต่อไป สิ่งเดียวที่ยังทำได้คือ kubelet restart container ที่ crash ภายใน node ของตัวเอง
Networking model ของ Kubernetes
โมเดลเครือข่ายมีกฎพื้นฐานสามข้อ:
- Pod ทุกตัวได้ IP ของตัวเอง
- Pod คุยกับ Pod อื่นได้ทุกตัว ไม่ว่าจะอยู่ node ไหน โดยไม่ต้องผ่าน NAT
- Agent บน node เช่น kubelet เข้าถึง Pod ทุกตัวบน node นั้นได้
CNI plugin เป็นตัวทำให้กฎเหล่านี้เป็นจริง ต่อยอดจาก Pod networking ก็จะมี:
- Service: virtual IP ที่นิ่ง วางไว้หน้า Pod ที่ IP เปลี่ยนได้ตลอด
- CoreDNS: ชื่อแบบ
orders.shop.svc.cluster.local - NetworkPolicy: firewall ระหว่าง Pod ซึ่งจะมีผลก็ต่อเมื่อ CNI plugin รองรับ
- Ingress: routing ระดับ HTTP ตาม host และ path เข้าสู่คลัสเตอร์
Namespace: จัดระเบียบคลัสเตอร์ที่ใช้ร่วมกัน
Namespace คือขอบเขตของชื่อและ policy สองทีมมี Service ชื่อ api ได้พร้อมกันถ้าอยู่คนละ namespace และ namespace ยังเป็นที่ผูก RBAC, ResourceQuota, LimitRange และ NetworkPolicy:
apiVersion: v1
kind: Namespace
metadata:
name: team-payments
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: compute
namespace: team-payments
spec:
hard:
requests.cpu: "8"
requests.memory: 16Gi
limits.memory: 32Gi
pods: "50"ทุกคลัสเตอร์เริ่มต้นด้วย default, kube-system (control plane และ add-on), kube-public และ kube-node-lease (heartbeat ของ node) resource บางชนิดเป็น cluster-scoped ไม่อยู่ใน namespace ไหนเลย เช่น Node, PersistentVolume, StorageClass, ClusterRole, CustomResourceDefinition และตัว Namespace เอง ดูรายการทั้งหมดได้ด้วย kubectl api-resources --namespaced=false
Namespace อย่างเดียวไม่ใช่ security boundary โดยค่าเริ่มต้น Pod ต่าง namespace ยังคุยกันได้ ถ้าต้องการแยกกันจริงต้องใช้ร่วมกับ RBAC, NetworkPolicy และ Pod Security Admission
คำสั่งสำหรับสำรวจ architecture ของคลัสเตอร์
kubectl get nodes -o wide # nodes, versions, runtime
kubectl get pods -n kube-system # control plane and add-ons
kubectl get --raw='/readyz?verbose' # API server health checks
kubectl describe node <node-name> # capacity, allocations, conditionsPattern watch-แล้ว-reconcile แบบเดียวกันนี้คือวิธีขยายความสามารถของ Kubernetes ด้วย: Operator เพิ่ม custom resource ชนิดใหม่พร้อม controller ที่ดูแลมัน อ่านต่อที่ Kubernetes Operator และ CRD
คำถามที่พบบ่อย
Control plane กับ worker node ต่างกันอย่างไร
Control plane เก็บ state ของคลัสเตอร์และตัดสินใจว่าอะไรควรรันที่ไหน ส่วน worker node คือที่ที่ Pod รันจริง โดย kubelet บนแต่ละ node เป็นคนทำตามสิ่งที่ control plane บันทึกไว้
ถ้า control plane ของ Kubernetes ล่ม จะเกิดอะไรขึ้น
Pod ที่รันอยู่ยังทำงานและรับ traffic ต่อได้ แต่ deploy, scale หรือแก้ไขอะไรไม่ได้, Pod บน node ที่ตายจะไม่ถูกย้าย และ endpoint ของ Service จะไม่อัปเดตจนกว่า control plane จะกลับมา
ทำไม etcd ต้องมีจำนวน member เป็นเลขคี่
etcd ต้องได้เสียงข้างมากจึงจะเขียนข้อมูลได้ 3 member ทนเสียได้ 1 ตัว 5 member ทนเสียได้ 2 ตัว การเพิ่มเป็นเลขคู่ทำให้ quorum สูงขึ้นโดยไม่ได้ทนความเสียหายได้มากขึ้น
Kubernetes ยังใช้ Docker อยู่ไหม
ไม่ได้ใช้เป็น runtime แล้ว ตั้งแต่ 1.24 Kubernetes คุยกับ containerd หรือ CRI-O ผ่าน CRI แต่ image ที่ build ด้วย Docker เป็น OCI image จึงรันได้เหมือนเดิม
Namespace เป็น security boundary หรือเปล่า
ไม่ใช่ถ้าใช้อย่างเดียว มันแค่แบ่งขอบเขตชื่อ, RBAC และ quota ส่วน traffic ข้าม namespace ยังวิ่งได้จนกว่าจะเพิ่ม NetworkPolicy
สรุปสิ่งที่ควรจำ
- API server คือทางเข้าเดียว และ etcd คือที่เก็บข้อมูลเดียว ต้องปกป้องและ backup etcd เสมอ
- Controller และ scheduler แค่เขียนลง API server ส่วนงานจริงบน node เป็นหน้าที่ของ kubelet กับ runtime
kubectl applyสำเร็จแปลว่า object ถูกบันทึกแล้ว ไม่ได้แปลว่ารันแล้ว ให้ไล่ตามลำดับ controller, scheduler, kubelet, endpoint- Traffic ของผู้ใช้ไม่ผ่าน control plane ดังนั้นเมื่อ control plane ล่ม สิ่งที่หยุดคือการเปลี่ยนแปลง ไม่ใช่การให้บริการ
- Namespace ช่วยจัดระเบียบ แต่การแยกกันจริงต้องมี RBAC และ NetworkPolicy ด้วย
ถ้าอยากลองฝึกแนวคิดเหล่านี้บนคลัสเตอร์จริง คอร์ส DevOps ฟรีของ Vectorkub พาทำทีละขั้นตอน
