업무로 만지는 폐쇄망 RKE2 클러스터에서 격은 일입니다. Go로 만든 앱이 사내 Keycloak을 호출하다가 DNS 단계에서 막혔는데, 원인이 Corefile의 점 하나였습니다. 도메인과 IP는 바꿔 적었습니다.
🚨 증상
Get "https://sso.example.internal/...": dial tcp: lookup sso.example.internal on 10.43.0.10:53: server misbehaving10.43.0.10은 RKE2 기본 클러스터 DNS(CoreDNS) 서비스 IP입니다. server misbehaving은 CoreDNS가 제대로 된 응답을 못 줬다는 뜻이라 CoreDNS부터 봤습니다.
🔍 CoreDNS 로그
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=200[ERROR] plugin/errors: 2 sso.example.internal. AAAA: plugin/forward: no next plugin found
[ERROR] plugin/errors: 2 sso.example.internal. A: plugin/forward: no next plugin found🧩 무슨 일이 있었나
처음 Corefile은 기본값인 forward . /etc/resolv.conf였습니다. 그런데 노드의 resolv.conf가 사내 DNS를 보고 있지 않아서 no such host가 났고, 사내 named 서버(여기선 10.0.0.53)를 넣으려고 이렇게 바꿨습니다.
forward 10.0.0.53 /etc/resolv.confCoreDNS 파드 안에서 nslookup sso.example.internal 10.0.0.53을 하면 잘 풀리는데, 앱은 계속 실패합니다. upstream은 멀쩡하다는 뜻이죠.
💡 원인: forward의 첫 인자는 ‘upstream’이 아니라 ‘zone’
forward 플러그인 문법은 이렇습니다.
forward FROM TO...첫 인자 FROM은 이 forward가 맡을 도메인(zone)이고, 그 뒤가 upstream입니다. 그러니 forward 10.0.0.53 /etc/resolv.conf는 CoreDNS 입장에서 “10.0.0.53이라는 이름의 도메인을 resolv.conf로 보내라”입니다. sso.example.internal은 그 도메인이 아니니 forward는 다음 플러그인으로 넘기려는데, 체인 끝이라 넘길 곳이 없으니 no next plugin found.
처음에 뜬 .은 그냥 붙어 있는 기호가 아니라 “모든 도메인”이라는 zone이었던 겁니다.
🛠️ 해결
점 하나만 넣으면 됩니다.
forward . 10.0.0.53수정 후 reload 플러그인이 켜져 있으면 잠시 뒤 자동 반영되고, 급하면 재시작합니다. RKE2에서는 디플로이먼트 이름이 조금 깁니다.
kubectl -n kube-system rollout restart deployment rke2-coredns-rke2-coredns그런데 원래 하고 싶었던 건 “사내 DNS로 먼저 물어보고, 안 되면 노드 resolv.conf”였습니다. 그래서 fallback까지 제대로 구성했습니다.
⚠️ upstream을 여러 개 적는 것만으로는 fallback이 아니다
forward . 10.0.0.53 /etc/resolv.conf처럼 적으면 “앞에 거 안 되면 뒤에 거”로 보이지만, forward의 기본 policy는 random입니다. 쿼리마다 upstream을 랜덤으로 골라서 보내요. 게다가 사내 도메인을 모르는 upstream이 돌려주는 NXDOMAIN도 ‘정상 응답’이라 다른 upstream으로 재시도하지 않습니다. 사내 도메인이 간헐적으로만 풀리는, 더 골치 아픈 상황이 됩니다.
✅ 진짜 fallback: policy sequential + failover
forward . 10.0.0.53 /etc/resolv.conf {
policy sequential
failover SERVFAIL REFUSED
}policy sequential: 항상 첫 번째 upstream부터 씁니다. 응답이 없거나 unhealthy일 때만 다음으로 넘어갑니다.failover SERVFAIL REFUSED: 응답은 왔는데 SERVFAIL이나 REFUSED면 다음 upstream에 다시 물어봅니다.- NXDOMAIN은 failover 대상에 넣지 않았습니다. 넣으면 진짜로 없는 도메인을 모든 upstream에 다 물어보게 됩니다.
그래서 사내 도메인을 아는 DNS를 앞에 두는 게 중요합니다. 순서를 반대로 적으면 resolv.conf 쪽 DNS가 사내 도메인에 NXDOMAIN을 주고 거기서 끝납니다.
🏠 참고: 집 클러스터의 Corefile
집 RKE2 클러스터도 같은 패턴으로 쓰고 있습니다. 일반 도메인은 노드 resolv.conf → 퍼블릭 DNS 순서로 fallback하고, 특정 도메인(OCI VCN 내부 도메인)은 server block을 따로 나눠서 Tailscale 너머 OCI 노드의 DNS로 보냅니다. (관련 없는 부분은 생략하고 주소는 바꿨습니다.)
.:53 {
errors
# ... hosts, kubernetes, prometheus 등 생략
forward . /etc/resolv.conf 1.1.1.1 1.0.0.1 {
policy sequential
failover SERVFAIL REFUSED
}
cache 30 {
serve_stale 4m immediate
}
loop
reload
loadbalance
}
<vcn>.oraclevcn.com:53 {
errors
forward . 100.64.0.2 100.64.0.3 {
policy sequential
failover SERVFAIL REFUSED
}
cache 30
loop
}- 특정 도메인만 다른 DNS가 알고 있다면, 이처럼 server block을 나누는 게 fallback보다 더 확실합니다. 그 도메인 쿼리는 애초에 그쪽 DNS로만 갑니다.
serve_stale 4m immediate는 upstream이 전부 죽어도 만료된 캐시를 최대 4분간 돌려주고, 갱신은 뒤에서 하는 옵션입니다. upstream이 잠깐 흔들릴 때 클러스터 전체가 같이 흔들리는 걸 막아줍니다.
⚠️ RKE2라면 ConfigMap 직접 수정은 임시방편
RKE2의 CoreDNS는 rke2-coredns Helm 차트로 관리되기 때문에, ConfigMap을 직접 고친 건 차트가 다시 적용될 때(RKE2 업그레이드 등) 덮어써질 수 있습니다. 오래 가져갈 설정이면 서버 노드의 /var/lib/rancher/rke2/server/manifests/에 HelmChartConfig로 넣는 쪽이 안전합니다.
📝 정리
no next plugin found는 “이 쿼리를 처리할 플러그인이 없다”는 뜻. upstream 장애가 아니라 Corefile 문법을 의심하세요.forward의 첫 인자는 zone입니다. 전체 도메인이면forward . <upstream>.- upstream을 여러 개 적으면 기본은 랜덤 분산입니다. fallback을 원하면
policy sequential+failover SERVFAIL REFUSED, 그리고 사내 DNS를 앞에. - 특정 도메인만 다른 DNS로 보내려면 server block을 나누는 게 가장 확실합니다.

답글 남기기