Skip to content

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:

  1. 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.

  2. 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ả:

  1. kube-proxy không tạo iptables rules cho Service này — vì không có VIP để intercept
  2. 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
  3. 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

yaml
apiVersion: v1
kind: Service
metadata:
  name: external-postgres
spec:
  clusterIP: None
  ports:
  - port: 5432

Headless Service không có selector không auto-discover Pods. Thay vào đó, bạn manually tạo EndpointSlice:

yaml
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: 5432

DNS resolution của external-postgres.default.svc.cluster.local trả về 203.0.113.10203.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:

yaml
apiVersion: v1
kind: Service
metadata:
  name: external-analytics
  namespace: backend
spec:
  type: ExternalName
  externalName: analytics.company.com

Khi 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 query analytics.company.com đến upstream DNS
  • Default: 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:

  1. Tạo Service postgres.production.svc.cluster.local trỏ đến external DB db.old-datacenter.com
  2. Applications kết nối đến Kubernetes DNS name
  3. Khi migration hoàn tất, thay Service thành regular Service trỏ đến in-cluster Postgres
  4. 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ử:

yaml
spec:
  type: ExternalName
  externalName: analytics.company.com

Khi 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:

  1. Pod cũ bị terminate → Endpoint bị removed từ EndpointSlice
  2. Pod mới được tạo và trở thành Ready
  3. CoreDNS nhận watch event về EndpointSlice changes
  4. CoreDNS cập nhật in-memory DNS cache
  5. 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

yaml
# 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: 3

Kế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.local

Kafka 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:9092

Hoặc đơn giản hơn, để Kafka client tự discover:

bootstrap.servers=kafka.messaging.svc.cluster.local:9092

Client nhận 3 IPs từ DNS, connect đến một trong số đó, và nhận danh sách brokers đầy đủ từ Kafka protocol itself.

References