[Kubernetes] CoreDNS “no next plugin found”: forward 한 줄에서 빠진 점(.) 하나


업무로 만지는 폐쇄망 RKE2 클러스터에서 격은 일입니다. Go로 만든 앱이 사내 Keycloak을 호출하다가 DNS 단계에서 막혔는데, 원인이 Corefile의 점 하나였습니다. 도메인과 IP는 바꿔 적었습니다.

🚨 증상

Get "https://sso.example.internal/...": dial tcp: lookup sso.example.internal on 10.43.0.10:53: server misbehaving

10.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.conf

CoreDNS 파드 안에서 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을 나누는 게 가장 확실합니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다