Certified Kubernetes Administrator (CKA) #20 Networking 3: CoreDNS, NetworkPolicy

#19 Networking 2에서 Ingress로 외부 트래픽을 host와 path로 갈라 보냈습니다. 그런데 클러스터 안에서 Pod끼리는 서로를 어떻게 찾을까요. Pod IP는 재시작마다 바뀌므로 IP를 직접 쓸 수 없고, Service에 붙은 IP조차 매니페스트에 박아 두기엔 불안정합니다. 그래서 쿠버네티스는 이름으로 서로를 찾는 클러스터 내부 DNS를 기본 제공합니다.

이번 글의 두 주제는 그 이름 해석을 담당하는 CoreDNS와, Pod 사이의 트래픽을 누가 누구에게 보낼 수 있는지 통제하는 NetworkPolicy입니다. 하나는 “어떻게 찾는가”, 다른 하나는 “누구와 통신할 수 있는가"를 다룹니다. 둘 다 시험의 Services and Networking도메인에서 자주 출제되고, 운영에서도 장애 추적의 출발점이 자주 되는 지점이므로 동작 원리와 디버깅 흐름에 무게를 두겠습니다.

CoreDNS: 클러스터의 이름 해석기 #

CoreDNS는 쿠버네티스 클러스터의 기본 DNS 서버입니다. kubeadm으로 설치한 클러스터에는 kube-system 네임스페이스에 CoreDNS가 Deployment로 떠 있고, 그 앞에 kube-dns라는 이름의 Service가 붙어 고정 ClusterIP를 가집니다. Pod 안의 /etc/resolv.conf는 이 ClusterIP를 nameserver로 가리키므로, Pod에서 발생하는 이름 해석 요청은 모두 CoreDNS로 흘러갑니다.

# CoreDNS Deployment와 Pod 확인
k get deploy coredns -n kube-system
k get pods -n kube-system -l k8s-app=kube-dns

# CoreDNS 앞단 Service (이름은 kube-dns로 유지됨)
k get svc kube-dns -n kube-system

CoreDNS Pod가 죽어 있거나 ClusterIP가 바뀌면 클러스터 전체의 이름 해석이 멎습니다. 그래서 “어떤 앱이 다른 앱에 못 붙는다"는 증상의 1차 용의자가 늘 DNS입니다.

Service와 Pod의 DNS 이름 #

CoreDNS는 클러스터 객체에 규칙적인 이름을 부여합니다. 가장 자주 쓰는 것은 Service의 DNS 이름입니다.

<service>.<namespace>.svc.cluster.local

예를 들어 default 네임스페이스의 web Service는 web.default.svc.cluster.local로 해석됩니다. 같은 네임스페이스 안에서는 짧게 web만 써도 닿고, 다른 네임스페이스의 Service를 부를 때는 web.default처럼 네임스페이스를 붙입니다. 이 단축이 가능한 이유는 Pod의 resolv.conf에 search도메인이 들어 있기 때문입니다.

이름 형태의미
web같은 네임스페이스의 web Service
web.defaultdefault 네임스페이스의 web Service
web.default.svc.cluster.localFQDN(완전한 이름). 어디서나 동일하게 해석

Pod에도 DNS 이름이 붙지만 형태가 다릅니다. Pod IP의 점을 하이픈으로 바꾼 10-244-1-5.default.pod.cluster.local 형태이며, 실무에서 직접 쓰는 일은 드뭅니다. 다만 Headless Service에 묶인 StatefulSet Pod는 <pod>.<service>.<namespace>.svc.cluster.local이라는 안정적인 이름을 얻어, 이 이름으로 개별 Pod를 지목할 수 있습니다.

Corefile: CoreDNS의 설정 #

CoreDNS의 동작은 Corefile이라는 설정으로 정의되며, 이 Corefile은 kube-system 네임스페이스의 coredns ConfigMap에 들어 있습니다. 설정을 보거나 고칠 때는 이 ConfigMap을 다룹니다.

# Corefile 내용 확인
k get configmap coredns -n kube-system -o yaml

ConfigMap 안의 Corefile은 대략 다음 모양입니다.

.:53 {
    errors
    health
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {
        pods insecure
        fallthrough in-addr.arpa ip6.arpa
    }
    prometheus :9153
    forward . /etc/resolv.conf       # 클러스터 밖 도메인은 상위 DNS로
    cache 30
    loop
    reload
    loadbalance
}

핵심 블록은 두 가지입니다. kubernetes cluster.local ...cluster.local도메인을 쿠버네티스 API와 연동해 Service,Pod 이름을 해석하는 플러그인이고, forward . /etc/resolv.conf는 클러스터 내부 도메인이 아닌 외부 이름(예: google.com)을 노드의 상위 DNS로 넘기는 설정입니다. Corefile을 고친 뒤에는 CoreDNS가 reload 플러그인으로 자동 반영하지만, 즉시 적용하려면 CoreDNS Pod를 재시작하면 됩니다.

# Corefile 변경 후 즉시 반영하려면 CoreDNS 롤아웃 재시작
k rollout restart deploy coredns -n kube-system

DNS 디버깅 #

시험과 운영 모두에서 “이름이 안 풀린다"를 가르는 가장 빠른 방법은 임시 Pod를 띄워 직접 조회해 보는 것이며, 표준 도구는 busybox 이미지의 nslookup입니다.

# 일회용 Pod로 Service 이름 조회 (끝나면 자동 삭제)
k run -it --rm test --image=busybox:1.28 --restart=Never -- nslookup web

# FQDN으로 다른 네임스페이스 Service 조회
k run -it --rm test --image=busybox:1.28 --restart=Never -- \
  nslookup web.default.svc.cluster.local

busybox 이미지는 버전에 따라 nslookup 동작이 다릅니다. 1.28 태그가 DNS 조회에서 가장 말썽이 적어 디버깅의 사실상 표준으로 쓰입니다.

조회가 실패하면 다음 순서로 좁힙니다.

  • CoreDNS Pod 상태: k get pods -n kube-system -l k8s-app=kube-dns로 Running인지 확인합니다. CrashLoop이면 k logs로 원인을 봅니다.
  • kube-dns Service: k get svc kube-dns -n kube-system으로 ClusterIP가 있는지, endpoints가 비어 있지 않은지 봅니다.
  • Pod의 resolv.conf: k exec로 들어가 cat /etc/resolv.conf를 확인합니다. nameserver가 kube-dns ClusterIP를 가리켜야 합니다.
  • 이름 자체: 네임스페이스를 빠뜨렸거나 FQDN 철자가 틀렸을 수 있습니다. 같은 네임스페이스가 아니라면 <service>.<namespace> 형태로 부릅니다.

DNS 트러블슈팅은 #25에서 인증서,RBAC와 함께 다시 종합하겠습니다.

NetworkPolicy: Pod 사이 트래픽 통제 #

기본 상태의 쿠버네티스 클러스터에서 모든 Pod는 서로 자유롭게 통신할 수 있습니다. 네임스페이스가 달라도, 라벨이 달라도 IP만 알면 닿습니다. 이 전부 허용(all-allow) 상태를 좁히는 도구가 NetworkPolicy입니다.

NetworkPolicy의 작동 방식에는 한 가지 핵심 규칙이 있습니다. 어떤 Pod에도 정책이 걸려 있지 않으면 그 Pod는 계속 전부 허용입니다. 그런데 어떤 Pod에 정책이 하나라도 걸리는 순간, 그 Pod는 명시적으로 허용한 트래픽만 받는 화이트리스트 방식으로 바뀝니다. 즉 NetworkPolicy는 차단 목록이 아니라 허용 목록입니다.

상태동작
Pod에 정책 없음모든 인바운드/아웃바운드 허용
Pod에 정책 1개 이상해당 방향(ingress/egress)은 정책에 적힌 것만 허용, 나머지 차단

NetworkPolicy의 구조 #

핵심 필드는 다음과 같습니다.

  • podSelector: 이 정책이 적용될 대상 Pod를 라벨로 고릅니다. 비우면({}) 네임스페이스의 모든 Pod가 대상입니다.
  • policyTypes: 이 정책이 Ingress(들어오는 트래픽), Egress(나가는 트래픽), 또는 둘 다를 다루는지 선언합니다.
  • ingress.from: 누구로부터의 인바운드를 허용할지. podSelector, namespaceSelector, ipBlock으로 출처를 지정합니다.
  • egress.to: 어디로의 아웃바운드를 허용할지. 마찬가지로 세 selector로 목적지를 지정합니다.
  • ports: 허용할 포트와 프로토콜을 좁힙니다.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-api
  namespace: default
spec:
  podSelector:              # 이 정책의 적용 대상: app=api Pod
    matchLabels:
      app: api
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:          # 같은 네임스페이스의 app=frontend Pod만 허용
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080

위 정책은 app=api Pod에 적용되며, 같은 네임스페이스의 app=frontend Pod가 TCP 8080으로 보내는 인바운드만 허용합니다. 그 외 모든 인바운드는 차단됩니다. egress는 policyTypes에 없으므로 손대지 않아 계속 전부 허용입니다.

from/to의 selector 세 가지 #

fromto 안의 selector는 출처,목적지를 고르는 방식이 셋입니다. 이 셋을 정확히 구분하는 것이 시험에서 자주 갈리는 지점입니다.

selector의미
podSelector같은 네임스페이스 안에서 라벨로 Pod를 고름
namespaceSelector라벨로 고른 네임스페이스(들)의 Pod를 고름
ipBlockCIDR 범위로 IP를 고름(클러스터 밖 출처 등)
  ingress:
  - from:
    - namespaceSelector:     # team=prod 라벨이 붙은 네임스페이스에서 오는 트래픽 허용
        matchLabels:
          team: prod
    - ipBlock:               # 특정 CIDR에서 오는 트래픽 허용 (단, 10.0.5.0/24 제외)
        cidr: 10.0.0.0/16
        except:
        - 10.0.5.0/24

여기서 미묘하지만 시험에 단골로 나오는 함정이 있습니다. podSelectornamespaceSelector한 from 항목 안에 같이 두면 “그 네임스페이스 안의 그 라벨 Pod"라는 AND 조건이 됩니다. 반면 별도 항목으로 - 두 개로 나누면 “그 네임스페이스의 모든 Pod” 또는 “그 라벨 Pod"라는 OR 조건이 됩니다.

  ingress:
  - from:
    - namespaceSelector:     # AND: team=prod 네임스페이스의 app=frontend Pod만
        matchLabels:
          team: prod
      podSelector:           # (앞 항목과 같은 - 아래, 들여쓰기로 묶임)
        matchLabels:
          app: frontend

- namespaceSelectorpodSelector 사이에 -가 없다는 점에 주목합니다. 하이픈 하나의 유무가 AND와 OR을 가르므로, 매니페스트를 적은 뒤 k describe networkpolicy로 의도대로 묶였는지 반드시 확인하겠습니다.

default deny 패턴 #

운영에서 가장 흔하게 쓰는 패턴은 먼저 네임스페이스 전체를 차단한 뒤, 필요한 통신만 별도 정책으로 열어 주는 방식입니다. 전부 막는 default deny 정책은 podSelector를 비우고 ingress 규칙을 두지 않는 것으로 만듭니다.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: default
spec:
  podSelector: {}            # 네임스페이스의 모든 Pod에 적용
  policyTypes:
  - Ingress
  # ingress 규칙이 없으므로 = 인바운드 전부 차단

podSelector: {}로 네임스페이스의 모든 Pod를 대상으로 잡고, ingress 항목 자체를 두지 않으면 허용 목록이 비어 모든 인바운드가 막힙니다. egress까지 막으려면 policyTypesEgress를 추가하고 egress 규칙을 비웁니다. 인바운드,아웃바운드를 모두 막는 완전 차단은 다음과 같습니다.

spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

이렇게 default deny를 깔아 두면, 앞의 allow-frontend-to-api 같은 허용 정책을 하나씩 더해 가며 꼭 필요한 경로만 열린 클러스터를 만들 수 있습니다. NetworkPolicy는 같은 Pod에 여러 개가 걸리면 **합집합(OR)**으로 동작하므로, 차단 정책과 허용 정책을 함께 두면 허용 정책이 연 경로가 통합니다.

default deny 네임스페이스에서는 CoreDNS로 가는 DNS 질의(UDP/TCP 53)도 막힙니다. egress를 차단했다면 kube-system의 kube-dns로 향하는 53 포트 egress를 따로 허용해야 이름 해석이 동작합니다. 이름이 안 풀리는 증상을 만났을 때 NetworkPolicy를 의심하는 이유입니다.

CNI가 지원해야 동작한다 #

NetworkPolicy에서 가장 자주 놓치는 사실이 있습니다. NetworkPolicy 객체를 만들어도 CNI 플러그인이 이를 시행(enforce)하지 않으면 아무 일도 일어나지 않습니다. NetworkPolicy는 규칙 선언일 뿐이고, 실제 패킷을 막는 것은 CNI입니다.

  • 시행함: Calico, Cilium, Weave Net, Antrea 등
  • 시행 안 함: Flannel(기본 구성)처럼 NetworkPolicy를 지원하지 않는 CNI

즉 Flannel만 깔린 클러스터에서는 NetworkPolicy를 아무리 정확히 써도 트래픽이 그대로 흐릅니다. “정책을 만들었는데 차단이 안 된다"는 증상의 첫 확인 지점이 CNI입니다. Pod 네트워킹 모델과 CNI의 역할은 #3 클러스터 아키텍처 2에서 다뤘습니다.

검증과 트러블슈팅 #

NetworkPolicy를 만든 뒤에는 실제로 막히는지,통하는지를 임시 Pod로 확인합니다.

# 정책과 적용 대상,규칙 확인
k get networkpolicy
k describe networkpolicy allow-frontend-to-api

# 허용 출처 라벨을 단 Pod로 접속 시도 (통해야 정상)
k run probe --image=busybox:1.28 --restart=Never --labels=app=frontend \
  -- wget -qO- --timeout=2 api:8080

# 허용되지 않은 Pod로 접속 시도 (막혀야 정상)
k run probe2 --image=busybox:1.28 --restart=Never \
  -- wget -qO- --timeout=2 api:8080

막히는 경우 wget이 타임아웃으로 떨어지고, 통하는 경우 응답이 돌아옵니다. 의도와 결과가 어긋나면 다음을 봅니다.

  • 대상 Pod 라벨: podSelector가 고른 라벨이 실제 대상 Pod에 붙어 있는지 k get pods --show-labels로 대조합니다.
  • 출처 라벨/네임스페이스: from의 selector가 실제 출처 Pod,네임스페이스 라벨과 맞는지 봅니다.
  • AND vs OR: from 항목의 하이픈 구조가 의도한 조건인지 다시 확인합니다.
  • CNI: 차단이 전혀 안 되면 CNI가 NetworkPolicy를 시행하는지 봅니다.
  • DNS egress: egress를 막은 정책이라면 53 포트가 열려 있는지 확인합니다.

시험 포인트 #

CKA의 Services and Networking도메인(20%)에서 CoreDNS와 NetworkPolicy는 함께 자주 출제됩니다. 다음을 손에 익혀 두겠습니다.

  • Service의 DNS 이름은 <service>.<namespace>.svc.cluster.local입니다. 같은 네임스페이스는 짧은 이름, 다른 네임스페이스는 <service>.<namespace>로 부릅니다.
  • DNS 디버깅은 k run -it --rm test --image=busybox:1.28 -- nslookup <이름>이 표준입니다. CoreDNS는 kube-system의 Deployment이고, 설정은 coredns ConfigMap의 Corefile입니다.
  • NetworkPolicy는 화이트리스트입니다. 정책이 걸린 Pod는 명시적으로 허용한 것만 받습니다. 정책이 없는 Pod는 전부 허용입니다.
  • podSelector: {} + 규칙 없음 = default deny. policyTypes로 ingress/egress 방향을 선택합니다.
  • from/topodSelector,namespaceSelector,ipBlock을 구분하고, 한 항목 안의 AND와 별도 항목의 OR을 가릅니다.
  • NetworkPolicy는 CNI가 시행할 때만 동작합니다. Flannel 기본 구성에서는 무시됩니다.

NetworkPolicy는 명령형으로 만들기 어려워 매니페스트 작성이 사실상 필수입니다. 공식 문서의 예시를 빠르게 복사해 라벨과 포트만 바꾸는 것이 시험장에서 가장 빠릅니다. 클러스터 내부 통신과 DNS의 더 넓은 맥락은 K8s 중급 #7에서 함께 복습하면 좋습니다.

정리 #

이번 글에서 잡은 것:

  • CoreDNS는 클러스터 기본 DNS입니다. kube-system의 Deployment로 떠 있고 앞단 Service 이름은 kube-dns이며, Service는 <service>.<namespace>.svc.cluster.local로 해석됩니다.
  • CoreDNS 설정은 coredns ConfigMap의 Corefile입니다. kubernetes 플러그인이 클러스터 이름을, forward가 외부 이름을 처리합니다.
  • DNS 디버깅은 busybox 1.28의 nslookup을 일회용 Pod로 띄워 시작합니다.
  • NetworkPolicy는 화이트리스트입니다. 정책이 없으면 전부 허용, 정책이 걸리면 그 방향은 허용 목록만 통합니다.
  • podSelector/policyTypes/ingress.from/egress.to/namespaceSelector/ipBlock/ports로 규칙을 짜고, default deny로 막은 뒤 필요한 경로만 엽니다.
  • NetworkPolicy는 CNI가 시행해야 동작합니다. Flannel 기본 구성에서는 효과가 없습니다.

다음: Helm과 Kustomize #

네트워킹 세 편으로 Service,Ingress,DNS,NetworkPolicy까지 클러스터의 통신 계층을 정리했습니다. 다음은 이 모든 매니페스트를 효율적으로 관리하는 도구입니다.

#21 Helm과 Kustomize: 매니페스트 관리에서는 매니페스트를 템플릿과 값으로 다루는 Helm(차트,릴리스,helm install/upgrade)과, 베이스에 패치를 겹쳐 환경별 변형을 만드는 Kustomize(kustomization.yaml,overlay)를 운영 관점에서 비교하며 정리하겠습니다.

X