Headless Services và ExternalName Services — DNS Thay Vì Virtual IP
Tại Sao Hai Loại Service Này Quan Trọng
Kubernetes Service thông thường là một virtual IP abstraction: DNS trả về ClusterIP, kube-proxy DNAT traffic đến Pods thực. Client không biết có bao nhiêu Pods hay IPs của chúng là gì. Đây là behavior đúng cho hầu hết stateless workloads.
Tuy nhiên, có hai trường hợp mà virtual IP abstraction này là sai:
Stateful workloads (databases, Kafka, ZooKeeper) cần client kết nối đến Pod cụ thể — không phải load balancer ngẫu nhiên. Kafka consumer cần stick với một broker. Database client cần kết nối đến primary, không phải replica.
External services nằm ngoài cluster cần được integrate vào cluster DNS mà không phải thay đổi code — nhưng không có ClusterIP để assign.
Headless Services và ExternalName Services là hai cơ chế giải quyết hai bài toán này mà không phá vỡ mô hình DNS discovery.
Internal Model — Headless Services
ClusterIP: None — Ý Nghĩa Kỹ Thuật
Khi bạn set spec.clusterIP: None, Kubernetes không assign ClusterIP cho Service. Điều này có một chuỗi hệ quả:
- kube-proxy không tạo iptables rules cho Service này — vì không có VIP để intercept
- CoreDNS không trả về một A record đơn — thay vào đó nó trả về nhiều A records, mỗi record là IP của một Pod matching selector
- Load balancing xảy ra tại client, không phải tại kube-proxy. Client nhận danh sách Pod IPs từ DNS và tự quyết định connect đến đâu
DNS Resolution Với Headless Service
Giả sử headless Service kafka-headless có 3 Pods ở namespace messaging:
kafka-headless.messaging.svc.cluster.local →
A 10.0.1.10 (kafka-0)
A 10.0.1.11 (kafka-1)
A 10.0.1.12 (kafka-2)CoreDNS trả về cả ba A records trong một response. Client nhận response và — tùy theo implementation của DNS resolver — có thể:
- Dùng record đầu tiên
- Round-robin qua các records
- Implement client-side load balancing
Thứ tự records trong DNS response không guaranteed theo Kubernetes spec. CoreDNS có loadbalance plugin để shuffle thứ tự, nhưng client không nên assume thứ tự nhất định.
Per-Pod DNS Records — Stable Addressability
Đây là feature quan trọng nhất của headless Services. Khi kết hợp với spec.hostname hoặc StatefulSet (tự động assign hostname):
<hostname>.<service-name>.<namespace>.svc.cluster.local → IP của Pod đóRecord này:
- Stable: hostname không đổi dù Pod restart (với StatefulSet)
- Direct: trỏ đến Pod IP, không qua kube-proxy
- Updated: khi Pod restart với IP mới, DNS record được cập nhật tự động
Cho phép client maintain persistent connection đến Pod cụ thể bằng DNS name thay vì IP. Đây là nền tảng cho mọi stateful cluster workload (databases, message brokers, distributed caches).
Headless Service Không Có Selector
apiVersion: v1
kind: Service
metadata:
name: external-postgres
spec:
clusterIP: None
ports:
- port: 5432Headless Service không có selector không auto-discover Pods. Thay vào đó, bạn manually tạo EndpointSlice:
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: external-postgres-1
namespace: default
labels:
kubernetes.io/service-name: external-postgres
addressType: IPv4
endpoints:
- addresses:
- "203.0.113.10"
- "203.0.113.11"
ports:
- name: postgres
port: 5432DNS resolution của external-postgres.default.svc.cluster.local trả về 203.0.113.10 và 203.0.113.11. Dùng để expose external endpoints với cluster DNS interface — hybrid giữa headless và ExternalName.
Client-Side Load Balancing — Hệ Quả Và Gotchas
Vì không có kube-proxy LB, client phải tự xử lý. Các libraries phổ biến handle điều này khác nhau:
gRPC: gRPC có client-side LB built-in và hiểu round-robin qua danh sách A records từ headless Service. Đây là pattern recommended cho gRPC services — dùng headless Service thay vì regular Service để gRPC LB hoạt động đúng.
HTTP/TCP thông thường: HTTP client thường chỉ connect đến IP đầu tiên trong DNS response và giữ connection. Không có actual load balancing trừ khi connection pool được implement.
Database drivers: Kafka, Cassandra, Elasticsearch client libraries hiểu danh sách seed nodes và tự implement cluster discovery. Headless Service là cách tiêu chuẩn để cung cấp danh sách này.
Internal Model — ExternalName Services
CNAME Thay Vì A Record
ExternalName Service là Service không có Pods, không có ClusterIP, và không có endpoints. Thay vào đó, nó là một mapping từ cluster DNS name sang external DNS name:
apiVersion: v1
kind: Service
metadata:
name: external-analytics
namespace: backend
spec:
type: ExternalName
externalName: analytics.company.comKhi Pod query external-analytics.backend.svc.cluster.local, CoreDNS trả về:
external-analytics.backend.svc.cluster.local →
CNAME analytics.company.com
analytics.company.com →
A 203.0.113.50Đây là hai DNS queries riêng biệt. Query đầu tiên đến CoreDNS trả về CNAME. Query thứ hai để resolve CNAME target phụ thuộc vào dnsPolicy của Pod:
ClusterFirst(mặc định): CoreDNS forward queryanalytics.company.comđến upstream DNSDefault: Pod dùng node's DNS resolver
Tại Sao ExternalName Hữu Ích
Database migration pattern: Khi di chuyển database từ external sang in-cluster, có thể có downtime nếu phải thay đổi connection string trong tất cả applications. Với ExternalName:
- Tạo Service
postgres.production.svc.cluster.localtrỏ đến external DBdb.old-datacenter.com - Applications kết nối đến Kubernetes DNS name
- Khi migration hoàn tất, thay Service thành regular Service trỏ đến in-cluster Postgres
- Zero code change ở applications
Multi-cloud và hybrid: Applications trong GKE cần reach Cloud SQL, Memorystore, hoặc services trong VPC khác. ExternalName Service giúp abstract external endpoints behind cluster DNS.
TLS Gotcha — ExternalName Và SNI
Đây là footgun phổ biến nhất với ExternalName Services. Giả sử:
spec:
type: ExternalName
externalName: analytics.company.comKhi HTTPS client kết nối đến external-analytics.backend.svc.cluster.local:443, nó nhận CNAME resolve về analytics.company.com. Nhưng TLS handshake xảy ra với IP của analytics.company.com, và SNI (Server Name Indication) trong TLS ClientHello sẽ là gì?
Tùy implementation:
- Một số HTTP clients gửi SNI là
external-analytics.backend.svc.cluster.local— server sẽ từ chối vì cert không cover tên này - Một số clients gửi SNI là
analytics.company.com(sau CNAME resolution) — hoạt động đúng
Behavior không guaranteed và phụ thuộc vào HTTP client library. Đây là lý do ExternalName Services có giới hạn — nó chỉ an toàn với plain HTTP hoặc khi bạn kiểm soát được cách client xử lý TLS.
Giải pháp thay thế an toàn hơn: dùng headless Service không có selector + manual EndpointSlice với IP của external service, kết hợp với custom backend annotations.
Không Có kube-proxy Rules
ExternalName Service không tạo kube-proxy iptables rules. Không có VIP, không có connection tracking, không có DNAT. Đây là pure DNS mechanism. Nếu external service không reachable từ Pod network (vì firewall, VPC peering), DNS resolution sẽ thành công nhưng connection sẽ fail — và error message không rõ ràng.
Constraints Và Failure Modes
Headless Service: DNS Record Update Latency
Khi Pod bị replaced (restart, rescheduling), DNS record cho kafka-0.kafka-headless... cần được cập nhật với IP mới. Quá trình:
- Pod cũ bị terminate → Endpoint bị removed từ EndpointSlice
- Pod mới được tạo và trở thành Ready
- CoreDNS nhận watch event về EndpointSlice changes
- CoreDNS cập nhật in-memory DNS cache
- Client re-query DNS và nhận IP mới
Tổng latency từ bước 1 đến bước 5: thường 1-5 giây trong cluster bình thường, có thể cao hơn khi cluster under load. Trong khoảng thời gian này, clients đang connect đến IP cũ sẽ nhận connection refused.
Mitigations:
- Client implement retry với exponential backoff
- Application-level health check và circuit breaker
- Với database drivers: connection pool validation trước khi dùng connection
Headless Service: Không Có Health Check Tại DNS Layer
CoreDNS trả về Pod IP trong A records dựa trên readiness probe. Nhưng readiness probe có thể có độ trễ. Worst case: Pod pass readiness probe nhưng application chưa thực sự ready (probe quá đơn giản). Client nhận IP và connect ngay → connection fail.
Thiết kế readiness probe phải phản ánh đúng trạng thái application ready, không phải chỉ "process đang chạy".
ExternalName: Không Resolve Recursive
Nếu externalName trỏ đến một hostname mà bản thân cũng là CNAME:
external-service → analytics.company.com (CNAME)
analytics.company.com → lb.company.com (CNAME)
lb.company.com → 203.0.113.50 (A)CoreDNS trả về CNAME đầu tiên (analytics.company.com), client phải tự follow chain. Behavior phụ thuộc vào resolver của client — một số follow đến A record, một số dừng ở CNAME đầu tiên.
ExternalName Và Port Mapping
ExternalName Service không hỗ trợ port mapping. Nếu external service lắng nghe port 9200 nhưng cluster convention là port 80, không thể dùng ExternalName để remap. Giải pháp: dùng headless Service không có selector với manual EndpointSlice và regular Service với port mapping.
Minh Họa: Kafka Cluster Discovery
# Headless Service cho Kafka brokers
apiVersion: v1
kind: Service
metadata:
name: kafka
namespace: messaging
spec:
clusterIP: None
selector:
app: kafka
ports:
- name: broker
port: 9092
targetPort: 9092
- name: controller
port: 9093
targetPort: 9093
---
# StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: kafka
namespace: messaging
spec:
serviceName: kafka # Phải khớp tên headless Service
replicas: 3Kết quả DNS records được tạo tự động:
kafka.messaging.svc.cluster.local → 10.0.1.10, 10.0.1.11, 10.0.1.12
kafka-0.kafka.messaging.svc.cluster.local → 10.0.1.10
kafka-1.kafka.messaging.svc.cluster.local → 10.0.1.11
kafka-2.kafka.messaging.svc.cluster.local → 10.0.1.12
SRV: _broker._tcp.kafka.messaging.svc.cluster.local → 9092 kafka-0.kafka.messaging.svc.cluster.local
SRV: _broker._tcp.kafka.messaging.svc.cluster.local → 9092 kafka-1.kafka.messaging.svc.cluster.local
SRV: _broker._tcp.kafka.messaging.svc.cluster.local → 9092 kafka-2.kafka.messaging.svc.cluster.localKafka producer/consumer config:
bootstrap.servers=kafka-0.kafka.messaging.svc.cluster.local:9092,kafka-1.kafka.messaging.svc.cluster.local:9092,kafka-2.kafka.messaging.svc.cluster.local:9092Hoặc đơn giản hơn, để Kafka client tự discover:
bootstrap.servers=kafka.messaging.svc.cluster.local:9092Client nhận 3 IPs từ DNS, connect đến một trong số đó, và nhận danh sách brokers đầy đủ từ Kafka protocol itself.