Khi cụm máy chủ chỉ có vài máy, nhớ IP từng máy không khó. Khi con số lên vài chục, gồm balancer, minion, database, máy build, máy giám sát, việc ghi IP vào script, vào file .env hay vào đầu người vận hành bắt đầu sinh lỗi. Tài liệu này hướng dẫn dựng DNS nội bộ bằng BIND để đặt tên và quản lý hàng chục Cloud VPS: cấu trúc zone, quy ước đặt tên, cách đẩy DNS cho máy làm việc qua VPN, quy trình sửa zone và những lỗi hay gặp khi quản lý zone bằng tay.

1. Vì sao cần DNS nội bộ

Trong mô hình ba tầng Balancer – Minion – Database, chỉ balancer có IP public. Minion và database chỉ có IP private, mọi giao tiếp giữa các tầng đi qua mạng private. Nếu không có tên, mỗi nơi cần biết "database của site này ở đâu" sẽ phải chép một địa chỉ 10.0.0.x:

  • Khối upstream trong file cấu hình site trên balancer.
  • DB_HOST trong .env của từng website.
  • Script backup, script deploy, file ~/.ssh/config của từng người.

Một IP đổi (dựng lại máy, chuyển sang Cloud VPS mới) là phải đi sửa ở tất cả những chỗ đó. Tệ hơn, IP không nói gì về vai trò: nhìn 10.0.0.37 không ai biết đó là balancer hay database. DNS nội bộ giải quyết cả hai: tên mang nghĩa, và khi IP đổi chỉ cần sửa một bản ghi.

2. DNS nội bộ hoạt động thế nào

DNS nội bộ là một (hoặc vài) máy chủ DNS chỉ phục vụ trong mạng private, giữ một zone riêng không công khai ra Internet. Trong tài liệu này, zone của DNS nội bộ được gọi là internal.example. Ba khái niệm cần nắm:

  • Zone thuận (forward zone): ánh xạ tên sang IP bằng bản ghi A, và tên phụ sang tên chính bằng CNAME.
  • Zone ngược (reverse zone): ánh xạ IP sang tên bằng bản ghi PTR, dùng khi log hay công cụ cần hiển thị tên thay vì IP.
  • Master và secondary: zone được sửa trên máy master. Máy secondary kéo bản sao qua zone transfer và tiếp tục phục vụ khi master gặp sự cố. Secondary biết zone đã đổi nhờ số serial trong bản ghi SOA tăng lên.

Khi một máy hỏi db-1.internal.example, resolver trên máy đó gửi truy vấn tới máy chủ DNS nội bộ khai trong /etc/resolv.conf, nhận về IP private và cache kết quả trong khoảng thời gian bằng TTL của bản ghi.

3. Cấu trúc repo và zone DNS nội bộ

Nên quản lý zone của DNS nội bộ trong một repo git ngay từ đầu, để mọi thay đổi đều có lịch sử và có thể review. Cấu trúc tối thiểu:

dns-repo/
├── named.conf.local          # khai báo 2 zone
└── zones/
    ├── internal.example      # zone thuận: A, CNAME
    └── 10.in-addr.arpa       # zone ngược: PTR

File named.conf.local trên máy master của DNS nội bộ:

zone "internal.example" {
    type master;
    file "/etc/bind/zones/internal.example";
    notify yes;
    also-notify { 10.0.0.3; };        # báo secondary ngay khi zone đổi
    allow-transfer { 10.0.0.3; };     # chỉ secondary được kéo zone
};

zone "10.in-addr.arpa" {
    type master;
    file "/etc/bind/zones/10.in-addr.arpa";
    allow-transfer { 10.0.0.3; };
};

Dòng allow-transfer là bắt buộc: nếu bỏ trống, bất kỳ ai trong mạng cũng có thể tải toàn bộ danh bạ máy bằng một lệnh dig AXFR. Phần đầu zone thuận mẫu:

$TTL    3600
@       IN      SOA     internal.example.    dns1.internal.example. (
                        42           ; serial: tăng mỗi lần sửa
                        3600         ; refresh
                        900          ; retry
                        864000       ; expire
                        300 )        ; negative cache TTL
        IN      NS      dns1.internal.example.
        IN      NS      dns2.internal.example.

dns1                IN  A      10.0.0.2
dns2                IN  A      10.0.0.3
vpn                 IN  CNAME  dns2

; BALANCER -------------------------------------------------
lb-1                IN  A      10.0.0.11
; MINION ---------------------------------------------------
app-php74-1         IN  A      10.0.0.21
app-php82-1         IN  A      10.0.0.22
; DATABASE -------------------------------------------------
db-mysql80-1        IN  A      10.0.0.31
; DỊCH VỤ --------------------------------------------------
ops                 IN  A      10.0.0.40
build               IN  CNAME  ops

Serial chỉ cần là số nguyên tăng dần. Ví dụ trên dùng bộ đếm đơn giản; nhiều nơi chọn định dạng YYYYMMDDNN (ngày sửa cộng hai chữ số thứ tự trong ngày) để nhìn vào là biết zone được sửa lần cuối khi nào. Chọn kiểu nào cũng được, miễn là không bao giờ giảm.

4. Quy ước đặt tên máy trong DNS nội bộ

Cách đặt tên đơn giản nhất là vai trò + số thứ tự (minion1, db3). Cách này ổn khi cụm còn nhỏ, nhưng sẽ bộc lộ ba điểm yếu khi cụm lớn lên:

  1. Tên không nói phiên bản. Máy chạy PHP 7.2 hay 7.4? Phải SSH vào mới biết, trong khi đây là câu hỏi đầu tiên khi deploy một site.
  2. Số bị "thủng". Máy gỡ đi để lại khoảng trống, còn quy ước ngầm "minion số N đi cặp với database số N" bị phá khi một database phục vụ nhiều minion.
  3. Tên theo dự án lẫn vào. Tên kiểu minion<tên-dự-án> dễ nhớ với người làm dự án đó nhưng không theo khuôn nào, script không đoán được.

Quy ước khuyến nghị có dạng vai-trò-runtime-số:

Mẫu tênVí dụĐọc tên biết gì
lb-Nlb-1Balancer có IP public, chạy nginx và certbot
app-phpXY-Napp-php74-1Minion chạy PHP 7.4, máy thứ nhất của nhóm
db-mysqlXY-Ndb-mysql80-1Máy database MySQL 8.0
Tên dịch vụops, vpn, buildMáy có một chức năng duy nhất

Đổi tên một máy đang chạy không thể làm một lần, vì script và cấu hình đang tham chiếu tên cũ. Cách an toàn là thêm tên mới dạng CNAME trỏ về tên cũ, chuyển dần các nơi tham chiếu sang tên mới, rồi mới đảo ngược: tên mới thành bản ghi A, tên cũ thành alias, cuối cùng xoá alias khi không còn ai dùng.

5. DNS nội bộ cho máy làm việc qua VPN

Người vận hành không ngồi trong mạng private. Máy làm việc kết nối vào cụm qua VPN, và để gõ được ssh app-php74-1 thì máy đó cũng phải hỏi được DNS nội bộ. Với OpenVPN, máy chủ đẩy DNS cho client bằng push:

# /etc/openvpn/server.conf (trích)
push "route 10.0.0.0 255.255.0.0"
push "dhcp-option DNS 10.0.0.2"
push "dhcp-option DNS 10.0.0.3"
push "dhcp-option DOMAIN internal.example"

Dòng DOMAIN cho phép gõ tên ngắn: ssh app-php74-1 được resolver tự nối thành app-php74-1.internal.example. Hai lưu ý:

  • Cách client áp dụng dhcp-option khác nhau theo hệ điều hành. Windows nhận trực tiếp. Trên Linux thường phải có script cập nhật resolver (ví dụ update-resolv-conf), thiếu nó thì VPN vẫn lên nhưng tên không resolve được.
  • Trên chính các Cloud VPS, /etc/resolv.conf phải khai DNS nội bộ trước DNS public, kèm search internal.example. Nếu không, lệnh mysql -h db-mysql80-1 sẽ báo không tìm thấy host.

6. Lợi ích cho script vận hành

Khi tên đã ổn định trong DNS nội bộ, script chỉ cần biết tên máy, không cần biết IP:

# ~/.ssh/config: một khối cho mọi máy trong zone
Host lb-* app-* db-*
    User deploy
    IdentityFile ~/.ssh/id_ops

# chạy cùng một lệnh trên cả nhóm minion PHP 7.4
for h in app-php74-1 app-php74-2 app-php74-3; do
    ssh "$h" 'hostname; systemctl is-active nginx'
done

# .env của website trỏ database bằng tên
DB_HOST=db-mysql80-1.internal.example

Khi .env trỏ database bằng tên, việc dựng lại database trên Cloud VPS mới chỉ còn là đổi một bản ghi A và đợi hết TTL. Các website tiếp tục kết nối đúng mà không cần sửa từng file. Tên cũng xuất hiện trong output của script, nên log kiểu backup db-mysql80-1: OK dễ đọc hơn nhiều so với một dãy IP.

7. Quy trình sửa zone

Mọi thay đổi DNS nội bộ nên đi qua git, sau đó mới áp dụng lên master:

# 1. sửa zone, TĂNG SERIAL ở cả zone thuận và zone ngược
# 2. kiểm tra cú pháp trước khi nạp
named-checkconf /etc/bind/named.conf
named-checkzone internal.example /etc/bind/zones/internal.example
named-checkzone 10.in-addr.arpa  /etc/bind/zones/10.in-addr.arpa

# 3. nạp lại zone, không cần restart named
rndc reload internal.example
rndc reload 10.in-addr.arpa

# 4. hỏi thẳng cả hai máy DNS, so serial
dig @10.0.0.2 internal.example SOA +short
dig @10.0.0.3 internal.example SOA +short
dig @10.0.0.2 app-php74-1.internal.example +short
dig @10.0.0.2 -x 10.0.0.21 +short

8. Lỗi thường gặp khi tự quản DNS nội bộ

  • Quên tăng serial. Master đã có bản ghi mới nhưng secondary không kéo về, vì serial không đổi. Dấu hiệu: dig vào hai máy DNS cho hai kết quả khác nhau.
  • Sửa zone thuận nhưng quên zone ngược (hoặc ngược lại). Tên resolve được nhưng reverse lookup trả rỗng, hoặc ngược lại.
  • A và PTR lệch nhau. Bản ghi A trỏ một IP, còn PTR lại gán tên đó cho IP của máy khác. Tên thuận vẫn đúng nên khó phát hiện, nhưng log hiển thị theo reverse DNS sẽ báo sai tên máy.
  • TTL dài. TTL tính bằng ngày làm máy khác giữ IP cũ rất lâu. Với hạ tầng nội bộ hay đổi IP, nên dùng TTL vừa phải, và hạ TTL của riêng bản ghi sắp đổi xuống vài phút từ trước khi chuyển máy.
  • Secondary không nằm trong NS. Với notify yes, BIND chỉ gửi NOTIFY cho các máy có trong bản ghi NS hoặc khai bằng also-notify. Thiếu cả hai, secondary chỉ cập nhật theo chu kỳ refresh.
  • Máy đã gỡ nhưng tên vẫn còn. Script vẫn SSH vào một IP mà sau này có thể được cấp cho máy khác. Gỡ máy phải đi kèm xoá cả bản ghi A lẫn PTR.

Các khuyến nghị về serial, TTL và PTR trong RFC 1912 về các lỗi cấu hình DNS thường gặp vẫn còn nguyên giá trị.

9. Checklist và đánh đổi khi tự quản DNS nội bộ

Checklist mỗi lần sửa zone DNS nội bộ:

  • Tăng serial ở mọi zone có thay đổi.
  • Thêm hoặc xoá đồng thời bản ghi A và PTR.
  • Chạy named-checkconf và named-checkzone trước khi reload.
  • So serial trên master và secondary, kiểm tra dig thuận và ngược.
  • Commit thay đổi vào git kèm mô tả ngắn lý do.
ĐượcMất
Script, cấu hình, .env dùng tên thay vì IPThêm một dịch vụ phải giữ sống: DNS chết thì mọi lệnh gọi bằng tên đều lỗi
Lịch sử thay đổi đầy đủ trong gitSửa zone bằng tay, dễ quên serial, quên PTR
Hoàn toàn trong mạng private, không phụ thuộc bên ngoàiMáy hay dịch vụ không dùng resolver nội bộ sẽ không gọi được bằng tên

Với một đội vận hành nhỏ, BIND cộng git là đủ dùng lâu dài. Điều quan trọng nhất không nằm ở phần mềm mà ở kỷ luật: mọi máy mới đều có tên trong DNS nội bộ trước khi được dùng, và mọi máy gỡ đi đều bị xoá tên.

Bài tiếp theo

Tầng Balancer: Nginx reverse proxy, SSL offload và định tuyến domain. Cấu trúc thư mục cấu hình trên balancer, các header proxy bắt buộc, timeout, gzip, HTTPS, và cách phân biệt 502 với 504.