DNS และ Service Discovery
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”CoreDNS ให้ทุก Service มี DNS name ที่คาดเดาได้ ทำให้ Pod หากันด้วยชื่อแทนที่จะเป็น IP และ headless Service เปลี่ยนจาก IP เดียวที่ load balance ให้กลายเป็น IP ของแต่ละ Pod ที่อยู่เบื้องหลังโดยตรง
CoreDNS กับ DNS name ของ Service
หัวข้อที่มีชื่อว่า “CoreDNS กับ DNS name ของ Service”CoreDNS รันเป็น Deployment อยู่ใน cluster (ปกติอยู่ใน kube-system) และทำหน้าที่เป็น DNS server ภายในของ cluster ทุก Pod ถูกตั้งค่าผ่าน /etc/resolv.conf ให้ส่ง DNS query ไปที่ CoreDNS พอ Service ถูกสร้างขึ้น CoreDNS จะเริ่มตอบชื่อของ Service นั้นทันทีด้วยชื่อเต็มในรูปแบบ
<service-name>.<namespace>.svc.cluster.local# From inside any Pod in the clusternslookup payments.default.svc.cluster.localถ้าอยู่ใน namespace เดียวกัน ชื่อแบบสั้นก็ใช้ได้เหมือนกัน เพราะ DNS search path ของ Pod จะมี suffix svc.cluster.local ของ namespace ตัวเองต่อท้ายให้อัตโนมัติ
# From a Pod that lives in the same namespace as the "payments" Servicenslookup paymentscurl http://payments/healthสำหรับ Service ปกติ (ไม่ใช่ headless) ชื่อนั้นจะ resolve ไปที่ A/AAAA record เดียว คือ ClusterIP ของ Service ทุก request ที่ยิงไปชื่อนั้นก็ยังถูก load balance ไปยัง Pod เบื้องหลังผ่าน kube-proxy อยู่ดี DNS แค่พา client ไปหา virtual IP เท่านั้น
Headless Service: ข้าม load balancer ไปเลย
หัวข้อที่มีชื่อว่า “Headless Service: ข้าม load balancer ไปเลย”บางครั้ง client ต้องการเข้าถึง Pod ตัวใดตัวหนึ่งเจาะจง ไม่ใช่แค่ “Pod ตัวไหนก็ได้ที่ healthy อยู่หลัง Service” การตั้ง clusterIP: None จะสร้าง headless Service
apiVersion: v1kind: Servicemetadata: name: cachespec: clusterIP: None selector: app: cache ports: - port: 6379headless Service จะไม่มี virtual IP เลย แทนที่ด้วยการที่ CoreDNS ตอบกลับ IP ของทุก Pod ที่ match และพร้อมใช้งานโดยตรง query ยังคง resolve ชื่อเดิม cache.default.svc.cluster.local แต่คำตอบตอนนี้กลายเป็นลิสต์ของ Pod IP แทนที่จะเป็น ClusterIP เดียว
ทำไม StatefulSet ถึงต้องพึ่งสิ่งนี้
หัวข้อที่มีชื่อว่า “ทำไม StatefulSet ถึงต้องพึ่งสิ่งนี้”StatefulSet ให้แต่ละ Pod มี identity ที่เสถียร เช่น web-0, web-1, web-2 และจับคู่กับ headless Service (ผ่าน spec.serviceName) เพื่อให้แต่ละ Pod มี DNS record ของตัวเองที่เสถียรด้วย
<pod-name>.<service-name>.<namespace>.svc.cluster.local# Reach one specific ordinal Pod directly, not a load-balanced peernslookup web-0.cache.default.svc.cluster.localนี่คือสิ่งที่ระบบ stateful แบบ distributed ต้องการเป๊ะ ๆ database replica ต้องต่อกลับไปที่ peer ตัวเดิม ที่เคยต่อไว้ ไม่ใช่ตัวสุ่มที่ load balancer เลือกให้ ถ้าไม่มี headless Service DNS record รายตัวแบบนี้จะไม่มีทางเกิดขึ้นเลย
พิสูจน์จริง: svc, svc.ns, fqdn และ headless per-Pod record
หัวข้อที่มีชื่อว่า “พิสูจน์จริง: svc, svc.ns, fqdn และ headless per-Pod record”จาก Pod dnsutils จริง (registry.k8s.io/e2e-test-images/jessie-dnsutils:1.7) ยิงไปที่ Service web-clusterip จริง:
$ nslookup web-clusterip.default.svc.cluster.localServer: 10.96.0.10Address: 10.96.0.10#53
Name: web-clusterip.default.svc.cluster.localAddress: 10.96.196.12per-Pod A record จาก headless Service จริง web-headless (clusterIP: None):
$ nslookup web-headless.default.svc.cluster.localServer: 10.96.0.10Address: 10.96.0.10#53
Name: web-headless.default.svc.cluster.localAddress: 10.244.147.20Name: web-headless.default.svc.cluster.localAddress: 10.244.138.135query รูปแบบเดียวกัน แต่คำตอบคนละแบบสิ้นเชิง: Service ปกติคืน ClusterIP เดียว ส่วน headless Service คืน IP จริงของทุก Pod เบื้องหลังตรง ๆ ตรงตามกลไกของบทนี้เป๊ะ
ndots:5 กับ search: /etc/resolv.conf จริง และ lookup ที่เพิ่มขึ้นมา
หัวข้อที่มีชื่อว่า “ndots:5 กับ search: /etc/resolv.conf จริง และ lookup ที่เพิ่มขึ้นมา”$ cat /etc/resolv.confsearch default.svc.cluster.local svc.cluster.local cluster.localnameserver 10.96.0.10options ndots:5ndots:5 แปลว่า: ชื่อไหนก็ตามที่มีจุด น้อยกว่า 5 จุด จะถูกลอง suffix ทุกตัวใน search เรียงตามลำดับก่อน ก่อนที่ resolver จะลองชื่อตามที่พิมพ์มาจริง ๆ web-clusterip (0 จุด) เลยทำให้เกิดความพยายาม lookup เพิ่มอีกสามครั้ง (web-clusterip.default.svc.cluster.local, .svc.cluster.local, .cluster.local) ก่อนจะเจอตัวที่ถูกต้อง — มองไม่เห็นใน nslookup ที่สำเร็จครั้งเดียว แต่เป็น DNS round-trip เพิ่มจริงในทุก short-name lookup ทั่ว cluster ซึ่งเป็นเหตุผลที่ default นี้มักถูกปรับใน workload ที่ QPS สูง
dnsPolicy/dnsConfig แบบกำหนดเอง: resolv.conf จริงที่ต่างกัน
หัวข้อที่มีชื่อว่า “dnsPolicy/dnsConfig แบบกำหนดเอง: resolv.conf จริงที่ต่างกัน”apiVersion: v1kind: Podmetadata: name: custom-dns-demospec: dnsPolicy: None dnsConfig: nameservers: - 10.96.0.10 searches: - default.svc.cluster.local - svc.cluster.local - cluster.local options: - name: ndots value: '2' containers: - name: app image: registry.k8s.io/e2e-test-images/jessie-dnsutils:1.7 command: ['sleep', '3600']$ kubectl exec custom-dns-demo -- cat /etc/resolv.confsearch default.svc.cluster.local svc.cluster.local cluster.localnameserver 10.96.0.10options ndots:2dnsPolicy: None ยกการควบคุมทั้งหมดให้ dnsConfig — resolv.conf จริงของ Pod นี้แสดง ndots:2 แทนค่า default ของ cluster ที่ 5 พิสูจน์ว่า override มีผลจริงตามที่ config ไว้เป๊ะ การลด ndots เป็นวิธีปรับแต่งจริงที่พบบ่อยสำหรับ workload ที่คุยกับชื่อภายนอกแบบ fully-qualified เป็นหลัก เพื่อตัดความพยายาม search ภายใน cluster ที่เสียเปล่าตามที่อธิบายด้านบน
Corefile ของ CoreDNS: ConfigMap จริง
หัวข้อที่มีชื่อว่า “Corefile ของ CoreDNS: ConfigMap จริง”$ kubectl get configmap coredns -n kube-system -o jsonpath='{.data.Corefile}'.:53 { errors health { lameduck 5s } ready kubernetes cluster.local in-addr.arpa ip6.arpa { pods insecure fallthrough in-addr.arpa ip6.arpa ttl 30 } prometheus :9153 forward . /etc/resolv.conf { max_concurrent 1000 } cache 30 { disable success cluster.local disable denial cluster.local } loop reload loadbalance}นี่คือ plugin chain จริงที่ตอบทุก query ในบทนี้: plugin kubernetes serve cluster.local (พร้อม pods insecure เปิดให้มี DNS name แบบอิง Pod IP กับ TTL 30 วินาที) forward ส่งอะไรก็ตามที่อยู่นอก cluster.local ขึ้นไปยัง /etc/resolv.conf ของ Pod CoreDNS เอง และ cache 30 จำกัดว่าคำตอบถูก cache นานแค่ไหน
ExternalName: CNAME จริง พิสูจน์แล้ว
หัวข้อที่มีชื่อว่า “ExternalName: CNAME จริง พิสูจน์แล้ว”apiVersion: v1kind: Servicemetadata: name: external-apispec: type: ExternalName externalName: kubernetes.io$ nslookup external-api.default.svc.cluster.localServer: 10.96.0.10Address: 10.96.0.10#53
external-api.default.svc.cluster.local canonical name = kubernetes.io.Name: kubernetes.ioAddress: 15.197.167.90Name: kubernetes.ioAddress: 3.33.186.135CoreDNS ตอบด้วย CNAME จริงไปที่ kubernetes.io ซึ่ง resolver ก็ตามไปจน resolve ได้ A record จริงและปัจจุบันของ kubernetes.io เอง นี่คือสิ่งที่ ExternalName เป็นเป๊ะ ๆ: DNS-level alias เฉย ๆ ไม่มีอะไรมากกว่านั้น ไม่มี Service selector ไม่มี Endpoints/EndpointSlice ไม่มี load balancing — แค่ชื่อชี้ไปหาอีกชื่อหนึ่ง
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอนรีวิว manifest กับโค้ดที่เกี่ยวกับ DNS ที่ AI สร้าง ให้เช็ค
ExternalNameที่ชี้ไปที่ IP address ดิบแทนที่จะเป็น hostname — พิสูจน์ด้านบนแล้วว่าExternalNameสร้างแค่CNAMEเท่านั้น และCNAMEไปหา IP address ไม่ใช่ DNS ที่ valid IP ที่ตายตัวต้องใช้กลไกอื่น (headless Service กับEndpoints/EndpointSliceที่จัดการเองไม่ใช่ExternalName)- โค้ดที่คิดว่า
ndots/searchของทุก Pod ตรงกับ default ของ cluster โดยไม่เช็คว่ามี overridednsPolicy: None/dnsConfigหรือเปล่า — พิสูจน์ด้านบนแล้วว่าresolv.confจริงของ Pod ต่างจาก norm ของ cluster ได้เลยทั้งหมด - retry loop สำหรับ “DNS propagation delay” ที่ copy มาจากความเคยชินของ DNS สาธารณะ — คำตอบของ CoreDNS ถูกสร้างสดจาก state ของ EndpointSlice ทุกครั้งที่ query ไม่มี delay แบบ TTL-based propagation ที่ต้องรอแบบ DNS สาธารณะเลย
- hardcode
10.96.0.10เป็น “the” DNS server IP ของ cluster ใน application code แทนที่จะอ่านจาก/etc/resolv.confของ Pod เอง — เป็นของจริง (พิสูจน์ด้านบน) แต่เฉพาะ cluster นั้น ๆ (ClusterIP ของ Servicekube-dns) ไม่ใช่ค่าคงที่ universal
🐛 หา bug ในโค้ดนี้
apiVersion: v1kind: Servicemetadata:name: legacy-dbspec:type: ExternalNameexternalName: "10.20.30.40"เฉลย
externalName ถูกตั้งเป็น IP address ดิบ "10.20.30.40" พิสูจน์ด้านบนแล้วว่า ExternalName ทำงานด้วยการให้ CoreDNS ตอบ record CNAME ที่ชี้ไปที่ค่าของ externalName — แต่ target ของ CNAME ต้องเป็น hostname เสมอ ไม่ใช่ IP address เลย DNS ไม่มีแนวคิด “alias ชื่อนี้ไปที่ IP นั้น” ผ่าน CNAME แล้วแต่เวอร์ชันของ CoreDNS/upstream resolver ก็จะ fail ตอน validate, resolve ไม่ได้เลย หรือพฤติกรรมไม่แน่นอน — ไม่มีอันไหนเลยที่เป็น “pin Service นี้ไปที่ IP ตายตัว” ซึ่งเกือบแน่นอนว่าคือสิ่งที่ผู้เขียนต้องการ ทางแก้: สำหรับ external dependency ที่มี IP ตายตัว ให้สร้าง headless Service ที่ไม่มี selector พร้อม Endpoints/EndpointSlice ที่จัดการเองชี้ไปที่ IP จริง — นั่นคือกลไกที่รองรับจริงสำหรับ “ให้ IP นี้มี DNS name ภายใน cluster ที่เสถียร”
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ทุกคำตอบ DNS ใน cluster คือ “ถาม CoreDNS ซึ่งตัวมันเองก็แค่ Deployment ที่ watch Service/EndpointSlice” (พิสูจน์ด้านบนผ่าน Corefile จริง) — ไม่มี DNS control plane แยกต่างหากเลย ndots/search กำหนด ชื่อไหนที่ client ถามจริง ๆ (พิสูจน์ด้านบนด้วย resolv.conf จริงกับ lookup ที่เพิ่มขึ้นจากชื่อจุดน้อย) dnsPolicy/dnsConfig กำหนด Pod ใช้ server กับ search list ไหนเลย (พิสูจน์ด้านบนด้วยการ override ทั้งคู่) และ headless เทียบกับ Service ปกติกำหนด CoreDNS ตอบอะไรกลับมา (IP ของแต่ละ Pod เทียบกับ ClusterIP เดียว ทั้งคู่พิสูจน์ด้านบนด้วย nslookup จริง)
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ข้อดี | ต้นทุน |
|---|---|---|
ndots:5 + search list ของ cluster default | ชื่อสั้นภายใน namespace (web-clusterip) ใช้ได้เลย | FQDN ภายนอกที่มีจุดน้อยกว่า 5 เสีย lookup เพิ่มจากการลอง search suffix ของ cluster ก่อน พิสูจน์ด้านบน |
dnsConfig กำหนดเองที่ ndots ต่ำกว่า | ตัด lookup ภายใน cluster ที่เสียเปล่าสำหรับ workload ที่คุยกับภายนอกเยอะ พิสูจน์ด้านบน | ชื่อสั้นภายใน cluster ตอนนี้ต้องเขียนแบบ fully qualified ไม่งั้นอาจ resolve ไม่ได้ตามที่คาด |
| Headless Service | DNS เสถียรต่อ Pod จำเป็นสำหรับ StatefulSet peer discovery พิสูจน์ด้านบน | ไม่มี load balancing ที่ชั้น DNS เลย — client ต้องจัดการ A record หลายตัวเอง |
ExternalName | ชื่อสะอาดภายใน cluster สำหรับ external dependency พิสูจน์ด้านบนด้วย CNAME จริง | แค่ CNAME — ไม่มี health check ไม่มี load balancing ต้องเป็น hostname ที่ resolve ได้จริง ไม่ใช่ IP (พิสูจน์ด้านบน) |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “DNS ต้องใช้เวลา propagate ภายใน cluster เหมือน DNS สาธารณะ” คำตอบของ CoreDNS ถูกสร้างสดจาก state ของ EndpointSlice (พิสูจน์ด้านบนผ่าน plugin
kubernetesในCorefileจริง) ไม่ได้ cache แบบ TTL propagation ของ DNS สาธารณะ - “
ndotsมีผลแค่กับชื่อภายนอก” พิสูจน์ด้านบนแล้ว — มันเปลี่ยนพฤติกรรม (และต้นทุน) การ lookup ของทุกชื่อ ไม่ว่าใน cluster หรือไม่ ที่มีจุดน้อยกว่าค่าndots - “
ExternalNameชี้ไปที่ IP address ได้เพื่อ pin Service ไปที่ backend ตายตัว” พิสูจน์ด้านบนแล้ว — ทำไม่ได้ExternalNameสร้างแค่ CNAME ไปหา hostname เท่านั้น การ pin IP ตายตัวต้องใช้ headless Service กับEndpoints/EndpointSliceที่จัดการเองแทน
flowchart TB pod["Client Pod"] -->|"DNS query"| coredns["CoreDNS"] coredns -->|"normal Service"| vip["Service ClusterIP\n(load-balanced)"] coredns -->|"headless Service"| p0["web-0 IP"] coredns -->|"headless Service"| p1["web-1 IP"] coredns -->|"headless Service"| p2["web-2 IP"]