Kubernetes Networking
Service — Stable Endpoint สำหรับ Pod ที่ตายเกิดใหม่ตลอดเวลา
Pod มี IP ของตัวเองแต่ IP นั้นเปลี่ยนทุกครั้งที่ Pod ถูกสร้างใหม่ (restart, reschedule,
scale) — Service คือ abstraction ที่ให้ stable IP/DNS name คงที่ ที่ route ไปยัง Pod
กลุ่มหนึ่งผ่าน label selector โดยอัตโนมัติ
apiVersion: v1
kind: Service
metadata:
name: my-app
spec:
selector:
app: my-app
ports:
- port: 80 # port ที่ Service เปิด
targetPort: 8080 # port ที่ container ฟังจริง
type: ClusterIP
Service Types
| Type | ใช้เมื่อ |
|---|---|
ClusterIP (default) | เข้าถึงได้เฉพาะภายใน cluster — ใช้สำหรับ internal communication ระหว่าง service |
NodePort | เปิด port คงที่ (30000-32767) บนทุก Node — ใช้ตอน dev/test หรือเมื่อไม่มี LoadBalancer |
LoadBalancer | ขอ external load balancer จาก cloud provider (AWS ELB, GCP LB) — ใช้ expose service ออกอินเทอร์เน็ต |
ExternalName | ทำ DNS CNAME ไปยัง external service ภายนอก cluster (ไม่มี selector/proxy เกี่ยวข้อง) |
NodePort และ LoadBalancer include พฤติกรรมของ ClusterIP ด้วยเสมอ (เป็น superset
ไม่ใช่แยกจากกัน)
"4 ชนิด Service เรียงจากในสุดไปนอกสุด": ClusterIP (ในบ้าน) → NodePort (หน้าประตูรั้วบ้านทุก หลัง) → LoadBalancer (ป้ายถนนใหญ่ชี้เข้ามา) → ExternalName (ป้ายบอกทางไปบ้านคนอื่นเลย ไม่ใช่ บ้านตัวเอง) ยิ่งนอกยิ่งเปิดกว้างสู่ภายนอกมากขึ้น
Service ที่ type: ClusterIP ยังมีอีกโหมดที่คนมักไม่รู้จัก: clusterIP: None เรียกว่า
Headless Service — ตอนนั้น Service จะไม่มี virtual IP เลย แต่ CoreDNS จะ return IP
ของ Pod ทุกตัวที่ match selector โดยตรง (A record หลายตัว) แทนที่จะ load-balance ผ่าน
kube-proxy เป็นสิ่งที่ StatefulSet ใช้เป็นมาตรฐาน เพราะต้องการให้ client resolve ไปหา Pod
ตัวใดตัวหนึ่งเจาะจงได้ (เช่น db-0.db.default.svc.cluster.local ชี้ไปที่ Pod db-0 ตรงๆ)
ไม่ใช่โดน load-balance ไปสุ่ม Pod เหมือน Service ปกติ — เป็นกลไกที่ทำให้ StatefulSet มี
"stable network identity" ได้จริงตามที่เอกสารอ้างถึง
kube-proxy & Service Routing
kube-proxy รันบนทุก Node มีหน้าที่เขียน network rule (ส่วนใหญ่ผ่าน iptables หรือ IPVS
ในเวอร์ชันใหม่) ให้ traffic ที่ยิงไปยัง Service IP ถูก load-balance ไปยัง Pod จริงที่ backend —
นี่คือกลไกเบื้องหลังที่ทำให้ Service "ดูเหมือน" มี IP เดียวแต่จริงๆ กระจายไปหลาย Pod
"kube-proxy ไม่ใช่ proxy จริงๆ (ตั้งแต่โหมด iptables)" ชื่อหลอกคนทั่วไปให้คิดว่า traffic
วิ่งผ่านมันเหมือน reverse proxy ทั่วไป แต่จริงๆ มันแค่ "เขียนกฎจราจร" ไว้ล่วงหน้าแล้วก็เดินจาก
ไป — traffic จริงวิ่งตามกฎ iptables/IPVS ที่ kernel จัดการเอง ไม่ได้วิ่งผ่านตัว process
kube-proxy เลย
โหมด iptables (ซึ่งเคยเป็น default มานาน) มีข้อจำกัดเรื่อง performance ที่ชัดเจน: กฎ
iptables rule ที่ kube-proxy สร้างเป็น linear chain ที่ kernel ต้อง traverse ทีละกฎจนกว่า
จะเจอ match — เมื่อ cluster มี Service เยอะมาก (หลักพันตัว) เวลา lookup ต่อ packet จะช้าลง
แบบ O(n) โหมด IPVS (ที่เป็นทางเลือกใหม่กว่า) ใช้ hash table ภายใน kernel ทำให้ lookup เป็น
O(1) ไม่ว่าจำนวน Service จะเยอะแค่ไหน — cluster ขนาดใหญ่ระดับ production มักสลับไปใช้ IPVS
ด้วยเหตุผลนี้โดยเฉพาะ แต่ cluster เล็กๆ ส่วนใหญ่ไม่เคยรู้สึกถึงความต่างเลยเพราะ Service มีไม่
ถึงจุดที่ performance ต่างกันชัดเจน
DNS ภายใน Cluster
Kubernetes มี internal DNS (CoreDNS) ที่ resolve ชื่อ Service อัตโนมัติ — Pod ใน namespace
เดียวกันเรียก my-app ได้เลย ส่วนข้าม namespace ต้องใช้ my-app.<namespace>.svc.cluster.local
# จาก Pod ใดๆ ใน namespace "production"
curl http://my-app # resolve ภายใน namespace เดียวกัน
curl http://my-app.staging.svc.cluster.local # ข้าม namespace
"ชื่อสั้นใช้ในบ้าน ชื่อเต็มใช้ข้ามบ้าน" — เหมือนเรียกชื่อเล่นได้ถ้าอยู่บ้านเดียวกัน (namespace
เดียวกัน) แต่ถ้าข้ามบ้านต้องเรียกชื่อเต็มพร้อมนามสกุล (<service>.<namespace>.svc.cluster.local)
ถึงจะรู้ว่าหมายถึงใคร
Pod ที่ curl ชื่อ service สั้นๆ แล้วทำงานช้าผิดปกติ (latency สูงกว่าที่ควร) มักมีสาเหตุมาจาก
ndots:5 ซึ่งเป็นค่า default ใน /etc/resolv.conf ของทุก Pod — มันทำให้ query domain ที่
มี dot น้อยกว่า 5 ตัว (เช่น my-app มี 0 dot, หรือแม้แต่ external domain เต็มรูปแบบอย่าง
api.github.com ที่มีแค่ 2 dot) ถูก ลองต่อท้ายด้วย search domain ภายใน cluster ก่อนเสมอ
(เช่น my-app.default.svc.cluster.local ก่อน แล้วค่อย fallback ไปลองแบบอื่น) ทำให้ query ไป
external domain หนึ่งครั้งกลายเป็นต้อง query DNS 4-5 รอบ กว่าจะ resolve สำเร็จจริง —
เป็นสาเหตุ latency ที่พบบ่อยมากในระบบที่เรียก external API เยอะ วิธีแก้คือใส่ . ต่อท้าย FQDN
(api.github.com.) หรือปรับ dnsConfig.options ของ Pod โดยตรง
Ingress — Layer 7 Routing เข้า Cluster
Service ประเภท LoadBalancer เปลือง external LB ต่อ 1 service (แพงและไม่ยืดหยุ่น) —
Ingress แก้ปัญหานี้ด้วยการเป็น reverse proxy ตัวเดียว ที่ route traffic เข้าหลาย Service
ตาม hostname/path โดยใช้ external LB เพียงตัวเดียว
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: api.example.com
http:
paths:
- path: /users
pathType: Prefix
backend:
service:
name: user-service
port:
number: 80
- path: /orders
pathType: Prefix
backend:
service:
name: order-service
port:
number: 80
Ingress object เป็นแค่ "spec" — ต้องมี Ingress Controller (เช่น nginx-ingress,
Traefik, หรือของ cloud provider) รันอยู่ใน cluster ถึงจะทำงานได้จริง ไม่ใช่ built-in
feature ที่ทำงานเองโดยไม่ต้องติดตั้งอะไรเพิ่ม
"Ingress = ใบสั่งงาน, Ingress Controller = คนลงมือทำจริง" — เขียน YAML อย่างเดียวไม่มีอะไร เกิดขึ้น เหมือนเขียนใบสั่งงานทิ้งไว้บนโต๊ะ ต้องมี "พนักงาน" (controller) มาอ่านใบสั่งงานแล้ว ไปตั้งค่า reverse proxy จริงให้
Ingress Controller แต่ละเจ้าตีความ annotation ต่างกัน (เพราะ Ingress spec มาตรฐานของ
Kubernetes เองค่อนข้าง minimal ไม่ครอบคลุม feature ขั้นสูงอย่าง rate limiting, canary
deployment) ทำให้ annotation อย่าง nginx.ingress.kubernetes.io/... ใช้ได้กับ nginx-ingress
เท่านั้น ย้ายไปใช้ Traefik จะต้องเขียน annotation คนละชุดใหม่ทั้งหมด — ปัญหา vendor lock-in นี้
คือเหตุผลหลักที่ Kubernetes สร้าง API ใหม่ชื่อ Gateway API (kind: Gateway, HTTPRoute)
ขึ้นมาแทนที่ Ingress ในระยะยาว โดยออกแบบให้ feature ขั้นสูงเหล่านี้เป็นส่วนหนึ่งของ spec
มาตรฐานแทนที่จะต้องพึ่ง annotation เฉพาะของแต่ละ vendor — รู้เรื่องนี้แล้วตอบคำถาม "Ingress
กับ Gateway API ต่างกันยังไง" ได้ลึกกว่าคนทั่วไปมาก
NetworkPolicy — Firewall ระดับ Pod
Default ของ Kubernetes คือ Pod ทุกตัวคุยกันได้หมด (flat network, allow-all) —
NetworkPolicy ใช้จำกัด traffic เข้า/ออก Pod แบบ allow-list (ต้องมี CNI plugin ที่รองรับ
เช่น Calico, Cilium — ไม่ใช่ทุก CNI รองรับ)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-only-frontend
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
NetworkPolicy ข้างบน: Pod ที่มี label app: backend รับ traffic ได้ เฉพาะ จาก Pod ที่มี
label app: frontend บน port 8080 เท่านั้น — Pod อื่นทั้งหมดถูกบล็อก (เพราะเมื่อมี
NetworkPolicy ที่ match selector หนึ่งตัว จะเปลี่ยนจาก allow-all เป็น deny-all-except-listed
สำหรับ Pod นั้นทันที)
"ไม่มี NetworkPolicy = ประตูเปิดหมด, มี 1 ข้อ = ล็อกประตูแล้วแง้มเฉพาะที่ระบุ" — กฎข้อสำคัญคือ "additive" (บวกเพิ่ม ไม่ใช่แทนที่กัน) ถ้ามีหลาย NetworkPolicy match Pod เดียวกัน traffic จะผ่าน ได้ถ้าอย่างน้อยหนึ่งข้อ อนุญาต (union ของทุกกฎ ไม่ใช่ intersection)
policyTypes: [Ingress] ในตัวอย่างข้างบนควบคุมแค่ traffic ขาเข้า — traffic ขาออก
(egress) จาก Pod backend ยังเปิดกว้างเหมือนเดิมทุกประการ (เพราะไม่ได้ระบุ Egress ใน
policyTypes) หลายทีมเข้าใจผิดว่าตั้ง NetworkPolicy แล้ว Pod นั้น "ปลอดภัย" ทั้งสองทิศทาง
ทั้งที่จริงๆ ต้องประกาศ Egress แยกต่างหากถ้าต้องการจำกัดขาออกด้วย และที่สำคัญกว่านั้น:
NetworkPolicy ทำงานที่ layer 3/4 เท่านั้น (IP + port) ไม่รู้จัก HTTP path, header, หรือ
mTLS identity เลย — ถ้าต้องการ fine-grained authorization ระดับ application (เช่น "endpoint
นี้เท่านั้นที่เรียกได้") ต้องใช้ service mesh (Istio, Linkerd) เสริมเข้ามา NetworkPolicy
อย่างเดียวให้ได้แค่ coarse-grained network segmentation เท่านั้น
Pod IPs are ephemeral; a Service gives a stable virtual IP/DNS name that load-balances to
matching Pods via kube-proxy rewriting node-level routing rules (iptables is O(n) per lookup;
IPVS is O(1), which matters at high Service counts). A Headless Service (clusterIP: None)
skips the virtual IP entirely and returns individual Pod IPs via DNS — the mechanism behind
StatefulSet's stable per-replica addressing. ClusterIP is internal-only; NodePort/
LoadBalancer build on top of it. Watch out for ndots:5 in Pod DNS config inflating external
DNS lookups into 4-5 queries. Ingress is an L7 reverse-proxy spec needing a separate Ingress
Controller — vendor-specific annotations for advanced features (rate limiting, canary) are why
Kubernetes is moving toward the standardized Gateway API. By default all Pods can reach each
other; NetworkPolicy rules are additive (union, not intersection) across multiple matching
policies, only cover L3/L4 (not HTTP-level authorization — that needs a service mesh), and
ingress/egress must be declared separately in policyTypes.