GitLab 的網頁能開,不代表 Argo CD 能 clone repository;NUC 上能解析 Registry,也不代表 Pod 或 container runtime 走相同的 DNS 路徑。把家中服務分成公開瀏覽器入口與內部機器入口後,這幾個差異就不能再靠同一個「DNS 正常」概括。
我的內部 DNS 設定由 router 的 dnsmasq 管理,NUC 使用 router 作為 upstream,RKE2 裡的 CoreDNS 再替 Pods 解析名稱。這裡的分流使用不同的公開與內部名稱,不是讓同一個 hostname 在內外網回傳不同 IP 的 split-horizon 設定。
本文用以下網段示範,部署時需換成自己的固定租約或靜態位址。home.arpa 是 RFC 8375指定給家用網路的特殊用途 domain。
| 用途 | 範例 |
|---|---|
| Router DNS | 192.168.50.1 |
| NUC | 192.168.50.10 |
| 內部 Git/Registry | gitlab.home.arpa、registry.home.arpa |
| 內部 Vault | vault.home.arpa |
先畫出誰在問誰
一般使用 ClusterFirst 的 application Pod 先問 cluster DNS;CoreDNS 自己的 upstream 則來自它的設定與 kubelet 提供的 resolver。Container runtime 拉 image 通常走 node 端的解析路徑,不是 application Pod 的 DNS。
application Pod → CoreDNS → router dnsmasq → public upstream
└→ 直接回答 home.arpa 名稱
node/container runtime ───→ router dnsmasq
所以「host 成功、Pod 失敗」不應直接改 application。「image pull 失敗、Pod 內查詢成功」也不能排除 node DNS。先辨認出問題的是哪一個 client,才能選對觀察位置。
讓 router 成為內部名稱的來源
在 Asuswrt-Merlin 啟用 JFFS custom scripts/configs 後,將下列內容加入 /jffs/configs/dnsmasq.conf.add,保留原有設定:
local=/home.arpa/
host-record=gitlab.home.arpa,192.168.50.10
host-record=registry.home.arpa,192.168.50.10
host-record=vault.home.arpa,192.168.50.10
host-record 對這幾個確定名稱建立 records;local 則避免把該 zone 未知名稱的查詢轉送到 public upstream。這比把所有未知內部名稱也指到同一台主機更容易排錯。dnsmasq manual有兩項設定的完整語意;Merlin 的持久化路徑見 custom config files。
安排 router DNS 可短暫中斷的時間後,在 router 上重載服務:
service restart_dnsmasq
接著從 LAN client 直接問 router,不先經過其他 DNS cache:
nslookup gitlab.home.arpa 192.168.50.1
nslookup registry.home.arpa 192.168.50.1
nslookup vault.home.arpa 192.168.50.1
三個名稱都應指向範例 NUC 位址。若這一步不成立,先處理 router config、租約與 DNS 服務,不要繼續改 CoreDNS。DHCP client 也必須取得 router DNS;自行使用 public DNS 或 DoH 的 client 不一定能查到這些名稱。
確認 NUC 實際使用哪個 resolver
在使用 NetworkManager 的 Linux node 上先查看狀態:
nmcli connection show --active
resolvectl status
readlink -f /etc/resolv.conf
cat /etc/resolv.conf
cat /run/systemd/resolve/resolv.conf
若正確的 connection 名稱是 LAN,可把 IPv4 DNS 指向 router。以下只適用於由 NetworkManager 管理的 connection;若使用 systemd-networkd 或其他 backend,請修改該主機對應的設定來源:
sudo nmcli connection modify "LAN" \
ipv4.ignore-auto-dns yes \
ipv4.dns "192.168.50.1"
IPv6 DHCP/RA 也可能提供另一個 DNS;若有使用 IPv6,須一併確認它能解析內部 zone,不能只修 IPv4。套用 connection 變更可能中斷 SSH,應先保留 console 或其他維護路徑,再用適合該主機的方式重新啟用 connection。
重新檢查 resolvectl status,並同時測試 system resolver 和一般程式的名稱解析:
resolvectl query gitlab.home.arpa
getent hosts gitlab.home.arpa
getent hosts registry.home.arpa
resolvectl 成功只證明 systemd-resolved 的查詢路徑可用。還要看 kubelet 使用的 resolver file,因為 host 的查詢 API 與 Pod 取得的 /etc/resolv.conf 並非同一件事。
為什麼 127.0.0.53 會成為陷阱
Host 上的 127.0.0.53 是 systemd-resolved 的 local stub。對 host 程式而言,它可以是合法的 DNS 入口;但在一般 Pod 的 network namespace 裡,loopback 指向 Pod 自己,不是 host。
如果 kubelet 把含有這個 loopback nameserver 的檔案交給 CoreDNS,而 Corefile 又使用 forward . /etc/resolv.conf,就可能形成 forwarding loop。看到 plugin/loop: Loop ... detected 時,應檢查這條 forwarding 關係,而不是把 loop plugin 刪掉。該 plugin 只偵測部分啟動時的簡單迴圈,沒有報錯也不等於整個 DNS 拓撲正確。CoreDNS loop 文件列出了原因與限制。
修正前,先確認非 stub 檔案真的包含可從 node/Pod 網路到達的 router DNS,且 router 不會把同樣的查詢再送回 CoreDNS。常見路徑是 /run/systemd/resolve/resolv.conf,不能只憑檔名就假設其內容正確。
明確指定 RKE2 給 kubelet 的 resolver file
原本的主機 runbook 採用調整 /etc/resolv.conf symlink 的方法。若希望保留 host 的 stub 設定,可以改在 RKE2 層指定 kubelet 的 resolver 來源;兩種方法不需要一起做。
在每個相關 node 的 /etc/rancher/rke2/config.yaml 合併以下 key,保留其他設定:
resolv-conf: /run/systemd/resolve/resolv.conf
這是 RKE2 的設定 key,不是直接貼入 kubelet config 的 resolvConf 欄位。可對照 RKE2 agent configuration reference。如果該檔案列出多個來自 VPN 或不同介面的 DNS,它不一定能保留原本的 per-domain routing;此時應提供一份由設定管理維護、只包含合適 upstream 的 resolver file,再讓 RKE2 指向它。
Node service 重新啟動應安排維護時段,依 node 角色選 rke2-server 或 rke2-agent,逐台操作。單節點 control plane 尤其要預期 API 暫時不可用。設定生效後,確認實際 CoreDNS Deployment 名稱,再重建受影響的 Pods:
kubectl -n kube-system get deployments
kubectl -n kube-system rollout restart deployment/<coredns-deployment>
kubectl -n kube-system rollout status deployment/<coredns-deployment>
只 restart CoreDNS 而沒有先修正它取得的 upstream,新 Pod 還是會拿到同一個錯誤設定。
用新的測試 Pod 分開驗證 cluster、內部與公開名稱
下列測試會建立短生命週期 Pod;先確認 cluster 能取得這個測試 image,避免把 image pull 問題誤認成測試內的 DNS 失敗:
kubectl run dns-check --rm -it --restart=Never --image=busybox:1.36 -- sh
在測試 Pod 內執行:
cat /etc/resolv.conf
nslookup kubernetes.default.svc.cluster.local
nslookup gitlab.home.arpa
nslookup registry.home.arpa
nslookup example.com
exit
Cluster Service、內部名稱與 public 名稱都要查。只測 example.com,可能漏掉 router 的內部 zone;只測 GitLab,又可能漏掉 CoreDNS 自己的 Kubernetes service discovery。測試使用預設 cluster.local,若 cluster domain 不同應隨之調整。
| 觀察結果 | 下一個檢查位置 |
|---|---|
| 直接問 router 就失敗 | dnsmasq records、DNS service、router 可達性 |
| Router 成功,node 失敗 | Node 的 DNS 設定、cache、VPN routing |
| Node 成功,Pod 內部名稱失敗 | CoreDNS upstream、node-local resolver、網路規則 |
| CoreDNS 出現 loop error | Forward 目標是否指向自己或繞回自己 |
| DNS 成功,Registry 仍無法拉取 | TLS、port、container runtime trust 與登入憑證 |
DNS 只回答「去哪裡」,不授予 Git 或 Registry 的存取權。改用內部 hostname 後,TLS certificate、SSH known hosts 和 registry credential 的 hostname 也要對應。公開入口可繼續走 Cloudflare Tunnel;VPN 則需同時提供路由與內部 DNS,不能只建立網路連線就期待名稱自然可用。