Vấn đề DNS Mà Ít Ai Nhắc Đến
Bạn đang có một Kubernetes cluster hoạt động trơn tru. Ingress controller đã triển khai, service đang chạy, pod khỏe mạnh. Rồi bạn nhận ra — mỗi lần deploy thứ gì mới, bạn vẫn phải tự tay vào Cloudflare hay Route53 để tạo DNS record. Nếu IP của LoadBalancer thay đổi (và điều này xảy ra, đặc biệt với managed cluster), bạn lại phải cập nhật record đó một lần nữa.
Nguyên nhân gốc rễ là Kubernetes không có cơ chế tích hợp để giao tiếp với các DNS provider bên ngoài. Cluster của bạn biết về IP và hostname, nhưng nó hoàn toàn không hay biết gì về sự tồn tại của Cloudflare hay Route53. Đây chính xác là khoảng trống mà ExternalDNS được tạo ra để lấp đầy.
ExternalDNS theo dõi các resource Kubernetes của bạn — cụ thể là Ingress object và Service kiểu LoadBalancer — và đồng bộ hostname bạn định nghĩa ở đó trực tiếp lên DNS provider. Tạo một Ingress với host app.example.com, ExternalDNS tự động tạo A record trỏ đến IP load balancer của bạn. Xóa Ingress đó, DNS record cũng biến mất theo.
Tôi đã áp dụng cách tiếp cận này trong môi trường production chạy trên cả EKS lẫn GKE, và kết quả luôn ổn định — không còn DNS record lỗi thời, không cập nhật thủ công, và giảm đáng kể công việc nhàm chán cho cả team.
Cài đặt
Điều kiện tiên quyết
Trước khi cài ExternalDNS, hãy đảm bảo bạn có:
- Kubernetes cluster đang chạy (1.19+)
- kubectl đã cấu hình với quyền truy cập cluster
- Tài khoản DNS provider — Cloudflare hoặc AWS Route53
- Helm 3 đã cài đặt (phương pháp cài khuyên dùng)
Cài bằng Helm
Cách gọn nhất để chạy ExternalDNS là dùng Bitnami Helm chart. Thêm repo và cập nhật:
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
Bạn sẽ tùy chỉnh cài đặt dựa trên DNS provider của mình. Các phần bên dưới hướng dẫn riêng cho Cloudflare và Route53 — chọn phần phù hợp với bạn.
Cấu hình
Tùy chọn A: Cloudflare
Đầu tiên, tạo API token trong Cloudflare dashboard. Vào My Profile → API Tokens → Create Token và cấp các quyền sau:
- Zone → DNS → Edit
- Zone → Zone → Read
Giới hạn phạm vi token cho zone (domain) cụ thể mà bạn muốn ExternalDNS quản lý. Tránh dùng Global API Key — token có phạm vi giới hạn an toàn hơn nhiều trong production và giảm thiểu rủi ro nếu credential bị lộ.
Tạo Kubernetes secret chứa Cloudflare token của bạn:
kubectl create namespace external-dns
kubectl create secret generic cloudflare-api-token \
--from-literal=apiToken=YOUR_CLOUDFLARE_API_TOKEN \
-n external-dns
Tạo file values-cloudflare.yaml với cấu hình ExternalDNS:
provider: cloudflare
cloudflare:
secretName: cloudflare-api-token
proxied: false # Đặt true nếu muốn dùng Cloudflare proxy (biểu tượng đám mây cam)
domainFilters:
- example.com # Thay bằng domain thực của bạn
policy: sync # "sync" = tạo + xóa, "upsert-only" = chỉ tạo/cập nhật
sources:
- ingress
- service
txtOwnerId: k8s-cluster-prod # Định danh duy nhất cho cluster này
logLevel: info
Cài ExternalDNS:
helm install external-dns bitnami/external-dns \
-n external-dns \
-f values-cloudflare.yaml
Tùy chọn B: AWS Route53
Với Route53, ExternalDNS cần quyền IAM để quản lý DNS record. Trên EKS, cách được ưu tiên là dùng IAM Roles for Service Accounts (IRSA). Với các môi trường khác, một IAM user chuyên dụng kèm access key cũng hoạt động tốt.
Tạo IAM policy document và đính kèm vào IAM user hoặc role của bạn:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets"
],
"Resource": [
"arn:aws:route53:::hostedzone/*"
]
},
{
"Effect": "Allow",
"Action": [
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
"route53:ListTagsForResource"
],
"Resource": ["*"]
}
]
}
Lưu IAM credential vào Kubernetes secret:
kubectl create namespace external-dns
kubectl create secret generic aws-credentials \
--from-literal=access-key-id=YOUR_AWS_ACCESS_KEY \
--from-literal=secret-access-key=YOUR_AWS_SECRET_KEY \
-n external-dns
Tạo file values-route53.yaml:
provider: aws
aws:
credentials:
secretKey: aws-credentials
accessKey: access-key-id
secretAccessKey: secret-access-key
region: us-east-1 # Region AWS của bạn
zoneType: public # "public" hoặc "private"
domainFilters:
- example.com
policy: sync
sources:
- ingress
- service
txtOwnerId: k8s-cluster-prod
logLevel: info
Cài ExternalDNS:
helm install external-dns bitnami/external-dns \
-n external-dns \
-f values-route53.yaml
Thêm Annotation cho Ingress và Service
ExternalDNS tự động đọc hostname từ các resource Kubernetes của bạn. Với Ingress object, nó lấy trường spec.rules[].host mà không cần cấu hình thêm. Với LoadBalancer service, thêm annotation để khai báo tên DNS:
# Ingress — ExternalDNS tự động đọc trường host
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
namespace: default
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app
port:
number: 80
# LoadBalancer Service với DNS annotation
apiVersion: v1
kind: Service
metadata:
name: my-service
namespace: default
annotations:
external-dns.alpha.kubernetes.io/hostname: service.example.com
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
selector:
app: my-app
Một chi tiết đáng nắm rõ: ExternalDNS tạo TXT record song song với mỗi DNS entry. Các TXT record này đóng vai trò nhãn sở hữu — đây là cách ExternalDNS phân biệt record nào do nó quản lý với record do người khác tạo. Giá trị txtOwnerId được nhúng vào các TXT record này. Nếu bạn chạy nhiều cluster cùng trỏ vào một DNS zone, mỗi cluster phải có txtOwnerId riêng biệt để tránh xung đột.
Kiểm tra & Giám sát
Kiểm tra Deployment
Xác nhận ExternalDNS đang chạy và hoạt động bình thường:
kubectl get pods -n external-dns
kubectl logs -n external-dns -l app.kubernetes.io/name=external-dns --tail=50
Khi ExternalDNS xử lý một Ingress hay Service, bạn sẽ thấy các log entry như sau:
level=info msg="Desired change: CREATE app.example.com A [203.0.113.42]"
level=info msg="Desired change: CREATE app.example.com TXT [\"heritage=external-dns,...\"]"
level=info msg="2 record(s) in zone example.com. were successfully updated"
Kiểm tra DNS Record
Dùng dig để xác nhận record đã xuất hiện trong DNS:
# Kiểm tra A record
dig app.example.com +short
# Kiểm tra TXT ownership record
dig TXT "externaldns-app.example.com" +short
Thay đổi thường lan truyền trong vòng một đến hai phút. ExternalDNS chạy vòng lặp đồng bộ mỗi 60 giây theo mặc định. Bạn có thể điều chỉnh thông số này bằng cài đặt interval trong Helm values nếu cần đồng bộ nhanh hơn trong quá trình phát triển, hoặc chậm hơn để giảm API call trong production.
Xử lý Sự cố Thường gặp
Nếu record không được tạo, hãy kiểm tra những điểm sau trước tiên:
- domainFilters sai: ExternalDNS bỏ qua mọi hostname không khớp với danh sách
domainFilters. Kiểm tra lại host trong Ingress có khớp chính xác với filter không. - Thiếu quyền: Với Route53, xác nhận IAM policy đã được đính kèm đúng user hoặc role. Với Cloudflare, kiểm tra phạm vi API token có bao gồm đúng zone không.
- Policy đặt là upsert-only: Với
policy: upsert-only, ExternalDNS sẽ không xóa record khi bạn xóa Ingress. Chuyển sangpolicy: syncđể quản lý toàn bộ vòng đời. - Ingress chưa được gán external IP: ExternalDNS chờ có IP trong
status.loadBalancer.ingress. Nếu Ingress controller chưa gán IP, ExternalDNS không có dữ liệu để làm việc — hãy kiểm tra Ingress controller trước.
Prometheus Metrics
ExternalDNS expose Prometheus metrics trên port 7979. Nếu Prometheus đang chạy trong cluster của bạn, bật scraping bằng cách thêm pod annotation vào Helm values:
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "7979"
Các metric chính cần theo dõi là external_dns_controller_last_sync_timestamp_seconds (phát hiện vòng lặp đồng bộ bị treo) và external_dns_registry_endpoints_total (theo dõi số lượng record ExternalDNS đang quản lý). Một alert rule đơn giản — kích hoạt khi timestamp đồng bộ cuối cùng vượt quá năm phút — sẽ phát hiện sự cố credential hoặc DNS provider bị gián đoạn trước khi ảnh hưởng đến người dùng.
Chạy ExternalDNS đã giúp tôi loại bỏ hoàn toàn một mảng công việc vận hành nhàm chán. Không còn những sự cố “tại sao domain này không resolve được” chỉ vì ai đó quên cập nhật DNS record sau deployment. Cluster tự quản lý DNS của mình, và cả team có thể tập trung vào những việc thực sự quan trọng.

