Skip to content

Split-Horizon DNS: Phân giải Internal so với External

Tại sao điều này quan trọng

Split-Horizon DNS là mô hình kiến trúc (architectural pattern) cơ bản cho mọi hệ thống production có cả lưu lượng truy cập nội bộ (internal traffic) lẫn bên ngoài (external traffic). Quan niệm sai lầm phổ biến là: "Chúng ta có mạng nội bộ, có mạng bên ngoài, nên có thể dùng chung domain."

Thực tế: Nếu triển khai không đúng, hậu quả:

  • Vòng lặp định tuyến (Routing loops): Các máy khách nội bộ (internal clients) phân giải ra IP bên ngoài (external IP) → bắt buộc kết nối vòng qua internet → tăng độ trễ (latency) gấp 10 lần
  • Lỗi vượt tường lửa (Firewall breaches): Các máy khách nội bộ (internal clients) bỏ qua tường lửa (firewall) vì cố gắng kết nối trực tiếp tới IP bên ngoài (external IP)
  • Độ phức tạp vận hành (Operational complexity): Phải duy trì tính nhất quán dữ liệu giữa 2 zone
  • Hỗn loạn khi gặp sự cố (Failover chaos): Nếu IP bên ngoài (external IP) bị sập, các máy khách nội bộ cũng không thể truy cập được (nếu cấu hình sai)
  • Điểm mù bảo mật (Security blind spots): Các dịch vụ nội bộ (internal services) bị lộ thông tin qua hệ thống DNS bên ngoài (external DNS)
  • Đồng bộ hóa TTL (TTL synchronization): Máy khách (client) lưu bộ nhớ đệm (cache) các bản ghi lỗi thời xuyên qua các ranh giới mạng

Thực tế trong production: Hơn 70% công ty triển khai (implement) sai mô hình này. Kết quả dẫn đến: tăng độ trễ (latency), ngừng hoạt động (outages), và các sự cố bảo mật nghiêm trọng.

Kiến Trúc Split-Horizon

Khái Niệm

Split-horizon DNS (hay còn gọi là "split-view DNS") sẽ trả về câu trả lời DNS khác nhau tùy thuộc vào nguồn gốc của truy vấn (query):

Truy vấn nội bộ (Query nội bộ từ 10.0.0.0/8):
  dig api.example.com
  → Kết quả: 10.0.1.5 (địa chỉ IP nội bộ)

Truy vấn bên ngoài (Query bên ngoài từ internet):
  dig api.example.com
  → Kết quả: 35.201.100.50 (địa chỉ IP công khai)

Sử dụng cùng một domain name, nhưng nhận được câu trả lời khác nhau.

Tại Sao Cần Thiết

Kịch bản 1: Không áp dụng Split-Horizon (Scenario 1)
  
├── Public Zone: api.example.com → 35.201.100.50
└── Các VM nội bộ (Internal VMs) thực hiện phân giải qua public DNS
    → Lưu lượng truy cập (Traffic) nội bộ đi vòng qua internet
    → Vượt qua ranh giới mạng một cách không cần thiết
    → Tăng độ trễ - latency (thêm 10-100ms)
    → Lãng phí băng thông (bandwidth)
    → Dễ gặp sự cố với các quy tắc tường lửa (firewall rules)

Kịch bản 2: Áp dụng Split-Horizon Chuẩn (Scenario 2)

├── Public Zone: api.example.com → 35.201.100.50 (hướng ra ngoài internet)
├── Private Zone: api.example.com → 10.0.1.5 (dành cho nội bộ)
└── Các VM nội bộ (Internal VMs) phân giải qua private zone
    → Định tuyến (Routing) nội bộ diễn ra trực tiếp
    → Độ trễ (Latency) đạt mức tối thiểu
    → Đảm bảo bảo mật (không đi qua internet)

Triển Khai (Implementation)

Mô Hình Hai Zone

bash
# Bước 1: Tạo public zone
gcloud dns managed-zones create api-public \
  --dns-name=api.example.com \
  --visibility=public

# Bước 2: Tạo private zone (sử dụng cùng domain!)
gcloud dns managed-zones create api-internal \
  --dns-name=api.example.com \
  --visibility=private \
  --networks=projects/PROJECT_ID/global/networks/default

# Bước 3: Thêm các record public
gcloud dns record-sets transaction start --zone=api-public
gcloud dns record-sets transaction add 35.201.100.50 \
  --name=api.example.com \
  --type=A \
  --ttl=300 \
  --zone=api-public
gcloud dns record-sets transaction execute --zone=api-public

# Bước 4: Thêm các record nội bộ
gcloud dns record-sets transaction start --zone=api-internal
gcloud dns record-sets transaction add 10.0.1.5 \
  --name=api.example.com \
  --type=A \
  --ttl=300 \
  --zone=api-internal
gcloud dns record-sets transaction execute --zone=api-internal

Quy trình phân giải (Resolution Flow)

┌─────────────────────────────────────────┐
│      Lựa chọn Resolver (Selection)      │
└────────┬────────────────────────────┬───┘
         │                            │
    ┌────▼─────┐              ┌──────▼────┐
    │ Nội bộ   │              │ Bên ngoài │
    │ (10.x.x) │              │ (internet)│
    └────┬─────┘              └──────┬────┘
         │                           │
    ┌────▼──────────────┐    ┌──────▼────────┐
    │ VPC Resolver      │    │ Public Recursor│
    │ (169.254.169.254) │    │ (8.8.8.8 v.v.) │
    └────┬──────────────┘    └──────┬────────┘
         │                          │
    ┌────▼──────────────┐    ┌──────▼───────────┐
    │ Private Zone      │    │ Public Zone       │
    │ api.example.com   │    │ api.example.com   │
    │ → 10.0.1.5       │    │ → 35.201.100.50  │
    └───────────────────┘    └──────────────────┘
         
         ↓ Result             ↓ Result
    
    10.0.1.5 (Trực tiếp)   35.201.100.50 (Internet)

Cơ chế hoạt động của Private Zone

Các Private zone chỉ thực hiện phân giải (resolve) trong phạm vi các VPC được cấp quyền:

Trình phân giải nội bộ của VPC (VPC Internal Resolver - 169.254.169.254):
  - Mỗi VPC đều được tích hợp sẵn trình phân giải (built-in resolver)
  - Xử lý các truy vấn DNS (DNS queries) xuất phát từ tài nguyên trong VPC
  - Thực hiện kiểm tra các private zone được liên kết với VPC trước tiên
  - Chuyển sang sử dụng public DNS nếu không tìm thấy bản ghi

Cấu hình:
├── VPC "default" (10.0.0.0/8)
└── Private Zone "api.example.com" (gắn vào)

    Mọi tài nguyên trong VPC "default" → phân giải (resolve) "api.example.com" → trả về 10.0.1.5

VPC "secondary" (172.16.0.0/12)
└── CHƯA được liên kết vào zone "api.example.com"

    Tài nguyên trong VPC "secondary" → không thể phân giải (resolve) được "api.example.com"

Mô hình Production: Hub-Spoke Split-Horizon

Kiến trúc phổ biến nhất dành cho môi trường doanh nghiệp (enterprise):

Dự án trung tâm (Hub Project - quản lý DNS tập trung)
├── Shared VPC
├── Public Zone: api.example.com (internet-facing)
├── Private Zone: api.example.com → 10.0.1.5 (liên kết với Shared VPC)
└── Private Zone: internal.example.com (liên kết với Shared VPC)

Service Project A
├── Tài nguyên nằm trong Shared VPC
└── Kết quả phân giải (Resolve):
    - api.example.com → 10.0.1.5 (thông qua private zone)
    - Người dùng công cộng (public) phân giải → 35.201.100.50 (thông qua public zone)

Service Project B
├── Tài nguyên nằm trong Shared VPC
└── Quá trình phân giải (Resolution) diễn ra giống như Dự án A (Project A)

Triển khai bằng Terraform (Terraform Implementation)

hcl
# Public zone
resource "google_dns_managed_zone" "api_public" {
  name        = "api-public-zone"
  dns_name    = "api.example.com."
  description = "Public zone for api.example.com"
  visibility  = "public"
}

# Private zone
resource "google_dns_managed_zone" "api_private" {
  name        = "api-private-zone"
  dns_name    = "api.example.com."
  description = "Private zone for internal resolution"
  visibility  = "private"
  
  private_visibility_config {
    networks_list {
      network_url = google_compute_network.shared_vpc.id
    }
  }
}

# Public records
resource "google_dns_record_set" "api_public" {
  name            = "api.example.com."
  type            = "A"
  ttl             = 300
  managed_zone    = google_dns_managed_zone.api_public.name
  rrdatas         = ["35.201.100.50"]
}

# Private records (internal)
resource "google_dns_record_set" "api_private" {
  name            = "api.example.com."
  type            = "A"
  ttl             = 300
  managed_zone    = google_dns_managed_zone.api_private.name
  rrdatas         = ["10.0.1.5"]
}

Những thách thức về tính nhất quán (Consistency Challenges)

Thách thức 1: Đồng bộ hóa thủ công (Manual Synchronization)

Khi duy trì đồng thời 2 zone, dữ liệu cần phải luôn đảm bảo tính nhất quán (remain consistent):

Public Zone:        api.example.com A 35.201.100.50
Private Zone:       api.example.com A 10.0.1.5

Kịch bản (Scenario): Cập nhật IP của load balancer từ 35.201.100.50 sang 35.201.100.51

Quy trình thực hiện thủ công (Manual process):
  Bước 1: Cập nhật public zone
  Bước 2: Chờ cấu hình đồng bộ (mất vài giây)
  Bước 3: Cập nhật IP nội bộ của private zone? (nếu có thay đổi)
  
Vấn đề: Xảy ra tình trạng bất đồng bộ (Race condition) - các máy khách bên ngoài nhìn thấy IP mới, trong khi máy khách nội bộ vẫn thấy IP cũ

Giải pháp: Sử dụng mô hình Infrastructure-as-Code (Terraform) để cập nhật đồng thời (atomically update) cả hai zone.

Thách thức 2: Phối hợp thời gian sống (TTL Coordination)

Giá trị TTL giữa hai zone public và private có thể khác nhau (differ):

Public Zone:  api.example.com TTL=300
Private Zone: api.example.com TTL=60

Kịch bản (Scenario): Thay đổi địa chỉ IP nội bộ
  Thời điểm T+0: Cập nhật private zone với TTL=60 → các bản ghi cũ trong bộ nhớ đệm (cached entries) sẽ hết hạn sau 60 giây
  Thời điểm T+0: Các máy khách nội bộ vẫn thấy IP cũ (do dữ liệu được cache và TTL chưa hết hạn)
  Thời điểm T+60: Bộ nhớ đệm (Cache) hết hạn, IP mới được phân giải thành công

Tuy nhiên nếu:
  Public Zone: api.example.com TTL=3600
  
Các máy khách bên ngoài có thể sẽ nhìn thấy IP cũ (stale IP) trong vòng 1 giờ sau khi cập nhật.

Khuyến nghị: Sử dụng cùng một giá trị TTL cho cùng một domain (300 giây là giá trị mặc định tối ưu).

Thách thức 3: Tính đầy đủ của bản ghi (Record Completeness)

Hai zone public và private không nhất thiết phải sở hữu các bản ghi giống nhau (same records):

Public Zone (api.example.com):
  - api.example.com A 35.201.100.50 (public endpoint)

Private Zone (api.example.com):
  - api.example.com A 10.0.1.5 (internal IP)
  - api-admin.example.com A 10.0.1.100 (chỉ dùng cho quản trị - admin only, không cấu hình ở public)
  - api-debug.example.com A 10.0.1.101 (chỉ dùng để gỡ lỗi - debug only, không cấu hình ở public)

Kết quả (Result):
  Nội bộ (Internal): Cả 4 tên miền đều phân giải thành công
  Bên ngoài (External): Chỉ phân giải được duy nhất api.example.com

Hệ quả (Implication): Làm tăng độ phức tạp vận hành, do đó tài liệu hướng dẫn đóng vai trò cực kỳ quan trọng.

Khả năng dự phòng và chuyển vùng sự cố (Failover & Redundancy)

Kịch bản 1: Chuyển vùng sự cố đơn giản (Simple Failover - Active-Standby)

Primary Load Balancer: 35.201.100.50 (active)
Standby Load Balancer: 35.201.100.51 (standby)

Cấu hình 1 (Không sử dụng split-horizon):
  Public Zone: api.example.com A 35.201.100.50
  
  Nếu máy chủ chính (primary) bị sập:
    → Thực hiện cập nhật public zone
    → TTL=300 có nghĩa là mất 5 phút trước khi người dùng bên ngoài nhìn thấy IP mới
    → Vấn đề: Gây ra gián đoạn dịch vụ (outage) 5 phút đối với người dùng bên ngoài

Cấu hình 2 (Có sử dụng split-horizon kết hợp health checks):
  Public Zone: api.example.com A 35.201.100.50
  Private Zone: api.example.com A 10.0.1.5 (internal LB)
  
  Nếu bộ cân bằng tải công cộng (public LB) bị sập:
    → Lưu lượng nội bộ không bị ảnh hưởng (nhờ định tuyến nội bộ - routed internal)
    → Người dùng bên ngoài sẽ thấy lỗi dịch vụ (có thể giảm thiểu bằng cách đặt TTL ngắn hơn)
  
  Nếu IP nội bộ bị sập:
    → Người dùng nội bộ sẽ thấy lỗi dịch vụ
    → Có thể thực hiện thử lại/chuyển vùng thông qua logic của ứng dụng

Kịch bản 2: Chuyển vùng sự cố đa vùng (Multi-Region Failover)

Vung (Region): us-central1
  Public IP: 35.201.100.50
  Private IP: 10.0.1.5

Vung (Region): us-east1
  Public IP: 35.201.100.51
  Private IP: 10.0.2.5

Cấu hình (Sử dụng GCP Cloud Load Balancer):
  Public Zone:
    - api.example.com (Quy mô toàn cầu - Global) → Trỏ tới Cloud LB (anycast)
    - Cloud LB sẽ tự động định tuyến đến vùng hoạt động khỏe mạnh gần nhất
  
  Private Zone:
    - api.example.com A 10.0.1.5 (thuộc us-central1, mặc định)
    - api-us-east1.example.com A 10.0.2.5 (chuyển vùng tường minh - explicit failover)

Các ứng dụng nội bộ (Internal applications):
  - Trạng thái bình thường: Phân giải api.example.com → trả về 10.0.1.5
  - Kịch bản vùng bị sập (Region-down scenario): Sử dụng tường minh api-us-east1.example.com

Các thiết kế phản mẫu cần tránh (Anti-Patterns)

❌ Thiết kế sai 1: Chỉ cấu hình Public Zone, không có Private Zone

Zone "example.com" (public only):
  api.example.com A 10.0.1.5 (làm lộ địa chỉ IP nội bộ - PRIVATE IP!)
  
Hệ quả:
  ✗ Lưu lượng nội bộ bắt buộc phải đi vòng qua internet
  ✗ Domain có thể bị phân giải bởi bất kỳ ai (rò rỉ thông tin qua DNS enumeration)
  ✗ Gây lãng phí băng thông không đáng có

Giải pháp: Sử dụng private zone riêng biệt cho các địa chỉ IP nội bộ.

❌ Thiết kế sai 2: Bản ghi không nhất quán (Inconsistent Records)

Public Zone: api.example.com A 35.201.100.50
Private Zone: (bỏ trống, không có bản ghi api.example.com)
  
Hệ quả:
  ✗ Các máy khách nội bộ không thể phân giải được api.example.com
  ✗ Phải sử dụng một tên miền nội bộ khác (ví dụ: api-internal.example.com)
  ✗ Mã nguồn của ứng dụng phải tự xử lý cả hai tên miền khác nhau

Giải pháp: Private zone cần phải có các bản ghi trùng khớp tương ứng.

❌ Thiết kế sai 3: Cấu hình các giá trị TTL khác nhau

Public: api.example.com TTL=3600
Private: api.example.com TTL=0 (luôn làm mới - always refresh)
  
Hệ quả:
  ✗ Người dùng bên ngoài nhìn thấy IP cũ trong vòng 1 giờ
  ✗ Các truy cập nội bộ liên tục gửi yêu cầu DNS gây quá tải

Giải pháp: Cấu hình cùng một giá trị TTL (khuyến nghị là 300 giây).

❌ Thiết kế sai 4: Thiếu tài liệu hướng dẫn và quản lý phiên bản

Zone "api.example.com" (VPC nào đang được liên kết?)
Zone "api-internal.example.com" (tên này có giống tên phía trên không?)
Zone "api.prod.example.com" (ứng dụng prod sẽ sử dụng tên nào?)
  
Hệ quả:
  ✗ Gây hỗn loạn khi tiếp nhận kỹ sư mới (on-boarding)
  ✗ Rất khó khăn trong việc bảo trì hệ thống

Giải pháp: Đặt tên rõ ràng, viết tài liệu chi tiết, bổ sung chú thích trong mã Terraform.

Giám sát và Xử lý sự cố (Monitoring & Troubleshooting)

Sự cố 1: Máy khách nội bộ nhận IP bên ngoài (External IP)

Các bước gỡ lỗi (Debugging):
  From internal VM: nslookup api.example.com
  → Trả về kết quả 35.201.100.50 (SAI, kết quả đúng phải là 10.0.1.5)

Các nguyên nhân có thể xảy ra:
  1. Private zone chưa được liên kết với VPC của VM
  2. Private zone không tồn tại bản ghi api.example.com
  3. VM đang sử dụng public DNS bên ngoài (ví dụ 8.8.8.8) thay vì VPC resolver mặc định

Cách giải quyết:
  gcloud dns managed-zones describe api-private --format="value(privateVisibilityConfig.networks[].networkUrl)"
  # Kết quả phải hiển thị VPC của bạn

  gcloud dns record-sets list --zone=api-private --filter="name:api.example.com"
  # Kết quả phải hiển thị 10.0.1.5
  
  # Cách xử lý nếu VM đang sử dụng public DNS
  # Trong file /etc/resolv.conf of VM, hãy đảm bảo cấu hình trỏ về VPC resolver (169.254.169.254)

Sự cố 2: Máy khách bên ngoài không thể phân giải được

Các bước gỡ lỗi (Debugging):
  From external: dig api.example.com @8.8.8.8
  → Trả về lỗi NXDOMAIN (không tìm thấy bản ghi)

Các nguyên nhân có thể xảy ra:
  1. Public zone không tồn tại bản ghi api.example.com
  2. Thông tin nameservers chưa được cập nhật chính xác tại nhà đăng ký (registrar)
  3. Quá trình đồng bộ hóa zone chưa hoàn tất

Cách giải quyết:
  gcloud dns record-sets list --zone=api-public
  # Xác minh xem api.example.com đã tồn tại chưa
  
  gcloud dns managed-zones describe api-public --format="value(nameServers)"
  # Lấy danh sách nameservers và xác minh tại nhà đăng ký (GoDaddy, v.v.)
  
  dig api.example.com @ns-123.googledomains.com
  # Thực hiện kiểm tra trực tiếp với nameserver cụ thể

Sự cố 3: Mâu thuẫn giá trị TTL gây ra hiện tượng nhận IP cũ

Triệu chứng (Symptom): Sau khi thay đổi bản ghi, một số máy khách vẫn tiếp tục nhận IP cũ

Cách giải quyết:
  1. Monitor với: watch -n 1 'dig +short api.example.com'
  2. Nếu bị cũ (stale): Hãy giảm TTL xuống hoặc yêu cầu phía client xóa bộ nhớ đệm (clear cache)
  3. Đối với các thay đổi tiếp theo: Lên kế hoạch trước về thời điểm TTL hết hạn

Mô hình Split-Horizon đặc thù trong GKE

Các GKE services sẽ tự động nhận được các DNS entries tương ứng:

Dịch vụ "api" nằm trong namespace "default":
  DNS nội bộ (Internal DNS): api.default.svc.cluster.local (10.4.0.50)
  
Được công khai ra bên ngoài (Exposed) thông qua Google Cloud LB:
  DNS công cộng (Public DNS): api.example.com (35.201.100.50)

Cấu trúc Split-horizon sẽ được tự động xử lý bởi kube-dns hoặc CoreDNS:
  ✓ Các pod nội bộ (Internal pods) → api.default.svc.cluster.local
  ✓ Người dùng bên ngoài (External users) → api.example.com

Danh sách kiểm tra trước khi lên Production (Production Checklist)

  • [ ] Public zone đã được tạo và khai báo thành công tại nhà đăng ký domain
  • [ ] Private zone đã được tạo và liên kết với các VPC tương ứng
  • [ ] Cả hai zone đều có đầy đủ các bản ghi A/AAAA trùng khớp cho các dịch vụ chính
  • [ ] TTL cùng giá trị giữa các zone (khuyến nghị là 300 giây)
  • [ ] Có tài liệu giải thích rõ ràng bản ghi nào thuộc zone nào
  • [ ] Sử dụng Terraform để quản lý cả hai zone một cách đồng thời (atomically)
  • [ ] Thiết lập cảnh báo giám sát nếu IP nội bộ và bên ngoài bị lệch nhau ngoài ý muốn
  • [ ] Thực hiện kiểm tra định kỳ: Sử dụng lệnh gcloud dns record-sets list cho cả hai zone
  • [ ] Kịch bản chuyển vùng sự cố (Failover) đã được kiểm thử (thay đổi bản ghi thủ công, kiểm tra đồng bộ)
  • [ ] Đội ngũ đã được đào tạo: Cách gỡ lỗi các vấn đề liên quan đến split-horizon

Tài liệu tham khảo (References)