Skip to main content

Kubernetes Fundamentals

สถาปัตยกรรมคร่าวๆ (Control Plane vs Node)​

Kubernetes cluster ประกอบด้วย 2 ส่วนหลัก:

  • Control Plane — "สมอง" ของ cluster มี kube-apiserver (ประตูเดียวที่ทุกคำสั่งต้องผ่าน), etcd (key-value store เก็บ state ทั้งหมดของ cluster), kube-scheduler (ตัดสินใจว่า Pod ไหนควรไปรันบน Node ไหน), kube-controller-manager (รัน control loop หลายตัว เช่น ReplicaSet controller, Node controller)
  • Node (Worker) — เครื่องที่รัน workload จริง มี kubelet (agent ที่คุย API server และ สั่ง container runtime), kube-proxy (จัดการ network rule ให้ Service ทำงาน), และ container runtime (เช่น containerd)

หัวใจสำคัญที่ควรเข้าใจ: Kubernetes ทำงานแบบ declarative + reconciliation loop — คุณบอก "สถานะที่ต้องการ" (desired state) ผ่าน YAML แล้ว controller จะพยายามปรับ "สถานะจริง" (actual state) ให้ตรงกันตลอดเวลา ไม่ใช่สั่งทีละคำสั่งแบบ imperative

🧠 จำง่ายๆ

"Control Plane = ผู้จัดการที่สั่งงาน, Node = ลูกน้องที่ลงมือทำ" — จำคู่ชื่อ-หน้าที่แบบย่อ: apiserver = พนักงานต้อนรับ (ประตูเดียวที่ทุกคนต้องผ่าน), etcd = ตู้เซฟ (เก็บ state จริง), scheduler = ฝ่ายจัดคิว (ตัดสินใจ Pod ไปไหน), kubelet = หัวหน้างานหน้างาน (สั่ง container runtime ตรงๆ บน Node)

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

"Declarative" ไม่ได้แปลว่า Kubernetes รู้ว่าจะ "ไปถึง" desired state ยังไงเองทั้งหมด — แต่ละ controller แก้ปัญหาแค่ ส่วนย่อยที่ตัวเองรับผิดชอบ แบบวนลูปอิสระ (control loop pattern) เช่น Deployment controller ไม่รู้จักเรื่อง networking เลย มันแค่คอยเช็คว่า ReplicaSet ตรงกับ desired count ของตัวเองไหม — ส่วน Service/Endpoint controller ก็วนลูปแยกต่างหากโดยไม่รู้จัก Deployment เลย ระบบทั้งหมดทำงานถูกเพราะ control loop หลายสิบตัวทำงานคู่ขนานกันแบบไม่รู้จักกัน โดยตรง แค่ต่างฝ่ายต่างดู state ใน etcd ผ่าน apiserver — นี่คือสถาปัตยกรรมที่เรียกว่า "eventually consistent via independent reconciliation loops" ซึ่งเป็นเหตุผลที่ Kubernetes scale ได้ดีมาก (ไม่มี single orchestrator ที่ต้องรู้ทุกอย่าง)

Pod — หน่วยที่เล็กที่สุดที่ deploy ได้​

Pod คือกลุ่มของ container ตั้งแต่ 1 ตัวขึ้นไปที่ share network namespace และ storage เดียวกัน (เข้าถึงกันผ่าน localhost ได้) ส่วนใหญ่ 1 Pod = 1 container หลัก แต่บางครั้งมี "sidecar container" เสริม (เช่น log shipper, service mesh proxy)

apiVersion: v1
kind: Pod
metadata:
name: my-app
labels:
app: my-app
spec:
containers:
- name: app
image: my-app:1.0.0
ports:
- containerPort: 8080
ข้อควรระวัง

ในทางปฏิบัติ แทบไม่สร้าง Pod ตรงๆ เอง — Pod ที่สร้างเดี่ยวๆ เมื่อตายแล้วจะไม่ถูกสร้างใหม่ ให้อัตโนมัติ ต้องสร้างผ่าน controller เช่น Deployment หรือ Job เสมอ ในงานจริง

🧠 จำง่ายๆ

"Pod = ห้องพักเดียวกัน, container = รูมเมท" — container ใน Pod เดียวกันแชร์ "ที่อยู่" เดียวกัน (network namespace/IP) คุยกันผ่าน localhost ได้เหมือนรูมเมทคุยกันในห้องเดียวกัน แต่แยก filesystem ส่วนตัวกันคนละ container (เหมือนมีตู้เสื้อผ้าแยกคนละใบ)

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

ทุก Pod มี container ที่มองไม่เห็นอยู่เสมอ เรียกว่า "pause container" (หรือ infra container) ซึ่งมีหน้าที่แค่ถือ network namespace ไว้ให้ container อื่นในกลุ่มมาเกาะ มันไม่ทำ อะไรเลยนอกจาก sleep ตลอดชีวิต — เหตุผลที่ต้องมี: ถ้า container หลักใน Pod restart (เช่น crash แล้ว kubelet สร้างใหม่) network namespace จะไม่หายไปด้วย เพราะ pause container ยังถือมันไว้อยู่ ทำให้ Pod ยังคง IP เดิมแม้ container ข้างในจะ restart กี่ครั้งก็ตาม docker ps บน Node จะเห็น container ชื่อ pause เพิ่มมาเงียบๆ คู่กับทุก Pod เสมอ ซึ่งเป็นรายละเอียดที่ engineer ส่วนใหญ่ ไม่เคยสังเกต

Deployment & ReplicaSet​

Deployment คือตัวจัดการ stateless workload ที่พบบ่อยที่สุด มันไม่ได้คุม Pod ตรงๆ แต่คุม ReplicaSet อีกที ซึ่ง ReplicaSet คุมจำนวน Pod ให้ตรงตาม replicas ที่กำหนด

Deployment (จัดการ rollout/rollback, version history)
└── ReplicaSet (จัดการจำนวน Pod ให้ตรงตาม desired count)
└── Pod × N
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: my-app:1.0.0

เมื่อ update image แล้ว apply ใหม่ Deployment จะสร้าง ReplicaSet ใหม่ แล้วค่อยๆ scale ตัวใหม่ ขึ้น + scale ตัวเก่าลง (rolling update) — นี่คือที่มาของ rollback ได้ทันที เพราะ ReplicaSet เก่ายังถูกเก็บ history ไว้ (kubectl rollout undo deployment/my-app)

🧠 จำง่ายๆ

"Deployment = ผู้จัดการโครงการ, ReplicaSet = หัวหน้าทีมย่อยแต่ละเวอร์ชัน" — ทุกครั้งที่เปลี่ยน image จะได้หัวหน้าทีมคนใหม่ (ReplicaSet ใหม่) ส่วนหัวหน้าทีมคนเก่ายังไม่ถูกไล่ออก (แค่ scale down เหลือ 0 คน) เผื่อต้องเรียกกลับมาทำงานอีก (rollback)

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

ReplicaSet เก่าที่ scale เหลือ 0 ไม่ได้ถูกเก็บไว้ตลอดไป — ค่า default spec.revisionHistoryLimit ของ Deployment คือ 10 (เก็บแค่ 10 revision ล่าสุด) ถ้า deploy บ่อยมาก (เช่นวันละหลายรอบใน CI/CD) ReplicaSet เก่าสุดจะถูกลบทิ้งอัตโนมัติเงียบๆ ทำให้ kubectl rollout undo ย้อนกลับไปได้ไกลสุดแค่ 10 เวอร์ชันเท่านั้น ทีมที่ deploy ถี่มาก บางทีมปรับค่านี้ให้สูงขึ้น หรือพึ่ง image tag history ใน container registry แทนการพึ่ง Kubernetes revision history เพียงอย่างเดียว

Labels & Selectors — กลไกเชื่อมทุกอย่างเข้าด้วยกัน​

Kubernetes ไม่มีการอ้างอิงแบบ ID ตรงๆ ระหว่าง object ส่วนใหญ่ — ใช้ label + selector แทนทั้งหมด (loosely-coupled by design) เช่น Deployment หา Pod ของตัวเองผ่าน selector.matchLabels, Service หา Pod ปลายทางผ่าน spec.selector

# Pod ที่มี label app: my-app, tier: backend
metadata:
labels:
app: my-app
tier: backend
---
# Service ที่เลือก Pod ด้วย label เดียวกัน
spec:
selector:
app: my-app

ถ้า label ไม่ match กัน Service จะหา endpoint ไม่เจอ (เป็นสาเหตุ bug อันดับต้นๆ ที่เจอในงานจริง)

🧠 จำง่ายๆ

"Label คือป้ายชื่อติดเสื้อ, Selector คือเกณฑ์เรียกชื่อ" — Kubernetes ไม่เคยถาม "นี่ object ID อะไร" แต่ถามว่า "ใครใส่เสื้อป้าย app: my-app บ้าง" ทุก object ที่ "แต่งตัวตรงเกณฑ์" จะถูก นับรวมโดยอัตโนมัติ แม้จะถูกสร้างคนละเวลา คนละวิธีก็ตาม

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

matchLabels คือ syntax ย่อของ matchExpressions ที่ทรงพลังกว่ามาก — matchExpressions รองรับ operator In, NotIn, Exists, DoesNotExist ทำให้เขียน selector แบบซับซ้อนได้ เช่น "เลือก Pod ที่ env เป็น prod หรือ staging แต่ ไม่ใช่ canary":

selector:
matchExpressions:
- {key: env, operator: In, values: [prod, staging]}
- {key: track, operator: NotIn, values: [canary]}

คนส่วนใหญ่ใช้แค่ matchLabels (AND เงื่อนไขแบบ exact match เท่านั้น) ไม่รู้ว่า matchExpressions ทำ OR-like logic ผ่าน In ได้ ทำให้บางครั้งไปออกแบบ label scheme ที่ซับซ้อน เกินจำเป็นเพราะไม่รู้ว่า selector เองก็ยืดหยุ่นพอแล้ว

Namespace — การแบ่งขอบเขตเชิงตรรกะ​

Namespace ใช้แบ่ง cluster หนึ่งออกเป็นหลาย "ห้อง" เชิงตรรกะ (เช่น dev, staging, prod) เพื่อแยก resource quota, RBAC, และลดโอกาสชื่อชนกัน — แต่ ไม่ใช่ security boundary ที่สมบูรณ์ (Pod ข้าม namespace ยัง network กันถึงได้โดย default ถ้าไม่มี NetworkPolicy กั้น)

kubectl get pods -n production
kubectl config set-context --current --namespace=production
🧠 จำง่ายๆ

"Namespace คือห้องกั้นด้วยม่าน ไม่ใช่กำแพง" — แยกมองเห็น/จัดการง่าย (RBAC, quota, ชื่อไม่ชน) แต่เสียงยังลอดผ่านม่านได้ (network traffic ยังคุยข้ามห้องได้โดย default) ถ้าอยากได้กำแพงจริง ต้องสร้างเพิ่มเอง (NetworkPolicy)

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

ไม่ใช่ทุก object เป็น namespaced — Node, PersistentVolume (ไม่ใช่ Claim), ClusterRole, และ Namespace เองเป็น cluster-scoped (ไม่ขึ้นกับ namespace ไหนเลย) วิธีเช็คเร็วๆ ว่า object ชนิดไหน namespaced หรือไม่:

kubectl api-resources --namespaced=false

คนที่ไม่รู้เรื่องนี้มักงงว่าทำไม kubectl get node -n production ไม่ error แต่ก็ไม่กรองอะไร เลย (เพราะ -n ถูกเพิกเฉยสำหรับ resource ที่ cluster-scoped) และทำไม ClusterRole ถึงใช้ ชื่อซ้ำกับ namespace ไหนก็ไม่ได้เพราะมันไม่ได้ผูกกับ namespace ใดเลยตั้งแต่ต้น

ReplicaSet ไม่ใช่ตัวเดียวที่คุมจำนวน Pod​

Controllerใช้เมื่อ
DeploymentStateless app ทั่วไป (web server, API)
StatefulSetต้องการ stable network identity + persistent storage ต่อ instance (database)
DaemonSetต้องรัน Pod 1 ตัวต่อ Node ทุกตัว (log agent, monitoring agent)
Job / CronJobงานที่รันจบแล้วจบ (batch processing, scheduled task)

(รายละเอียดแต่ละตัวอยู่ใน Workload Patterns)

English Summary

Kubernetes is declarative: you describe desired state in YAML, and controllers continuously reconcile actual state to match it — crucially, via many independent control loops that don't know about each other, each watching shared state through the API server rather than one central orchestrator. A Pod is the smallest deployable unit (one or more co-located containers sharing network/storage via a hidden "pause container" that preserves the Pod's IP across container restarts) but is rarely created directly — it's managed via a Deployment (which owns a ReplicaSet, which owns Pods) for rolling updates and rollback history, though old ReplicaSets are pruned after revisionHistoryLimit (default 10). Everything connects through labels and selectors — matchExpressions supports In/NotIn/Exists beyond simple matchLabels AND- matching. Namespaces provide logical separation (quota, RBAC) but not a hard network boundary, and not every resource is even namespaced (Nodes, PVs, ClusterRoles are cluster-scoped).