ExternalDNS trên Kubernetes: Tự động đồng bộ DNS Record lên Cloudflare và Route53

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

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 sang policy: 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.

Share: