0. Contents

1. Overview

이름 → 주소(4·5절)go-web Pod10.244.1.5db:5432 로 연결CoreDNSkube-dns 10.96.0.10db ?110.96.45.72search 가 db 를db.cn4.svc.cluster.local로 채워 묻고, CoreDNS 는 API 에서 본Service 의 가상 IP 를 답한다주소 → Pod(3절)go-web Pod10.244.1.5dst 10.96.45.7:5432노드 A 의 커널KUBE-SERVICES → DNAT3postgres Pod10.244.2.7LISTEN :5432dst 10.244.2.7:54324① resolver 가 search 로 이름을 채워 묻는다 ② CoreDNS 가 Service 의 가상 IP 를 답한다③ 보내는 노드의 커널이 가상 IP 를 준비된 Pod IP 로 바꾼다 ④ 그 뒤는 04-1 의 Pod 네트워크가 나른다
10.96.45.7 은 어느 문에도 붙어 있지 않은 가상 주소다 — 이름이 그것을 가리키고, 보내는 노드의 커널이 그것을 진짜 Pod 의 문으로 바꾼다

04-1 에서 이어지는 것

  • 04-1 은 Pod 가 IP 를 얻고, 그 IP 로 노드를 넘어 닿는 이야기였다 — 문제 ①②
  • 이 글은 문제 ③ 바뀌는 방이다
    • Pod 는 죽고 다시 뜨면 IP 가 바뀐다
    • 그래서 바뀌지 않는 이름 하나와 바뀌지 않는 가상 주소 하나를 앞에 세운다

두 단계로 나뉜다

  • 이름 → 주소: db 라고 부르면 CoreDNS 가 Service 의 가상 IP 를 돌려준다 — 4·5절
  • 주소 → Pod: 가상 IP 로 보낸 패킷을 노드의 커널 규칙이 진짜 Pod IP 로 바꾼다 — 3절
  • 둘은 다른 단계라서 이름은 되는데 연결이 안 되는 경우가 따로 있다 — 7.1 의 진단 순서

그 밖에

  • 2절은 실습 환경이다 — 노드 셋을 컨테이너 셋으로 만들고 Cilium 을 부품 하나로 끼운다
  • 6절은 주소 계열이 둘(IPv4·IPv6)일 때 위 두 단계의 필드가 어떻게 늘어나는지다

2. Lab — 노드 셋을 컨테이너 셋으로

2.1 kind 와 Cilium 의 자리

VM 한 대 — Ubuntu · 커널 하나 · rootful Docker · cgroup v2관리 셸 — VM 호스트kubectl --context kind-cn4 · helmAPI server :6443 으로만 말한다클러스터 cn4 — kind 네트워크 172.18.0.0/16kind create cluster --name cn4 한 번cn4-control-plane172.18.0.2에이전트 셋 — 각각 노드마다 하나 · 노드 IPkubeletPod 띄움 · probe프로세스kube-proxyService 규칙 (3절)Podcilium-agentveth · 경로 · 정책Pod컨트롤 플레인 Pod — 노드 IP (hostNetwork)API server · etcdscheduler · ctrl-managercilium-operator클러스터에 1개cn4-worker172.18.0.31노드 = Docker 컨테이너 하나에이전트 셋 — 각각 노드마다 하나 · 노드 IP2kubeletPod 띄움 · probe프로세스kube-proxyService 규칙 (3절)Podcilium-agentveth · 경로 · 정책PodPod — 빈 노드 아무 데나 · Pod IP3coredns10.244.1.2이름 → 주소 (4절)go-web10.244.1.5앱cn4-worker2172.18.0.4에이전트 셋 — 각각 노드마다 하나 · 노드 IPkubeletPod 띄움 · probe프로세스kube-proxyService 규칙 (3절)Podcilium-agentveth · 경로 · 정책PodPod — 빈 노드 아무 데나 · Pod IPcoredns10.244.2.2이름 → 주소 (4절)postgres10.244.2.7앱① 노드 = kind 가 만든 컨테이너 ② 에이전트 = 노드마다 하나, 노드 IP 를 씀 ③ Pod = 빈 노드 아무 데나, 10.244.x 를 받음Cilium 은 노드마다 cilium-agent 하나 + 클러스터에 cilium-operator 하나로 끼운 부품이다 — 범위 나눔은 controller-manager, Service 는 kube-proxy 가 맡는다
클러스터는 하나고 요소는 셋이다 — 노드, 노드마다 붙는 에이전트, 노드 위에 뜨는 Pod. 서버를 사지 않아도 노드가 셋이라 노드 사이의 길을 확인할 수 있다

kind 가 하는 일

  • Docker 컨테이너 하나를 쿠버네티스 노드 하나로 쓴다
  • 제어 평면 1 + 워커 2 — 노드가 셋이라서 다른 노드 사이의 통신을 확인할 수 있다
  • 셋이 한 컴퓨터의 자원을 나눠 쓴다 — 물리 서버 셋의 성능이나 장애 격리는 재현하지 않는다
구성 요소고정한 버전이유
kindv0.33.0공식 릴리스가 제공하는 노드 이미지를 쓴다
노드 이미지kindest/node:v1.36.4 + digest태그가 아니라 정확한 빌드까지 고정한다
Cilium Helm chart1.20.3공식 안정 릴리스 — 호환 범위가 쿠버네티스 1.33~1.36
  • kind 0.33.0 의 기본 노드 이미지는 1.37.0 이지만 Cilium 1.20.3 의 보장 범위 밖이라 1.36.4 를 명시한다
  • 버전을 생략해서 각 도구의 기본값에 맡기는 것과 다르다 — 어느 조합을 썼는지가 재현의 전제다

호스트에 필요한 것

  • Linux 의 rootful Docker, kind, kubectl, Helm
  • Cilium 은 커널 5.10 이상, 이 쿠버네티스 버전은 cgroup v2
  • 노드가 컨테이너라서 커널과 cgroup 은 호스트 것 하나다 — 노드 이미지를 바꿔도 커널은 안 바뀐다
docker version
docker info --format '{{.CgroupVersion}}'   # 2 여야 한다
kind version
kubectl version --client
helm version
uname -r

Cilium 의 자리

  • 이 실습에서 Cilium 은 EKS 의 VPC CNI 처럼 가져다 쓰는 부품이다 — 04-1 7.5
  • 특색 둘을 끈다
    • ipam.mode=kubernetes — Pod IP 범위는 kube-controller-manager 가 노드마다 나눈 podCIDR 을 그대로 쓴다 (04-1 5.2 의 1단계)
    • kubeProxyReplacement=false — Service 는 kube-proxy 가 맡는다. 3절에서 iptables 규칙을 읽기 위해서다
  • 남는 일은 veth 를 꽂고 노드 사이 경로를 깔고 NetworkPolicy 를 집행하는 것뿐이다
  • Hubble 과 연결성 테스트는 선택 사항이라 건너뛴다

2.2 만드는 순서

클러스터 설정

# kind-cn4.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  disableDefaultCNI: true      # kindnet 대신 Cilium 을 넣는다
  kubeProxyMode: "iptables"    # 3절에서 읽을 규칙을 iptables 로 고정
nodes:
  - role: control-plane
  - role: worker
  - role: worker
  • disableDefaultCNI: true — kind 의 기본 CNI(kindnet)를 깔지 않는다. CNI 가 없으니 노드는 NotReady 로 시작한다
  • kubeProxyMode: "iptables" — 3.4 의 네 모드 중 하나를 못 박는다
  • IPv4 단일 스택이다. 이중 스택은 6절에서 별도로 다룬다

생성

NODE_IMAGE="kindest/node:v1.36.4"
NODE_DIGEST="sha256:099e049362a1526b2db71494e1947aae99bd16290d7c895f2b7ea312e3cbfaed"

kind create cluster --name cn4 --config kind-cn4.yaml \
  --image "$NODE_IMAGE@$NODE_DIGEST"

k() { kubectl --context kind-cn4 "$@"; }
k get nodes -o wide        # 셋 다 NotReady — CNI 전이라 정상
  • 이 이미지는 노드 전체의 이미지다 — 나중에 Pod 에 넣는 앱 이미지와 용도가 다르다
  • k 는 항상 kind-cn4 컨텍스트를 쓰는 셸 함수다. 새 터미널에서는 다시 정의한다
  • 이 시점에 podCIDR 은 이미 노드마다 적혀 있다 — CNI 가 없어도 1단계 IPAM 은 끝나 있다

Cilium 설치

helm --kube-context kind-cn4 repo add cilium https://helm.cilium.io/
helm --kube-context kind-cn4 repo update cilium

helm --kube-context kind-cn4 install cilium cilium/cilium \
  --version 1.20.3 \
  --namespace kube-system \
  --set image.pullPolicy=IfNotPresent \
  --set ipam.mode=kubernetes \
  --set kubeProxyReplacement=false \
  --wait --timeout 10m
  • --wait 는 필요한 리소스가 준비될 때까지 기다린다 — 이미지 다운로드가 느리면 제한 시간 안에 못 끝날 수 있다
  • 명령과 옵션은 Cilium 의 kind 설치 가이드를 그대로 따른다

확인

k wait --for=condition=Ready nodes --all --timeout=300s
k get nodes -o wide
k -n kube-system get daemonset cilium kube-proxy
k -n kube-system get deployment cilium-operator
k -n kube-system get pods -o wide
k create namespace cn4
  • 셋 다 Ready, cilium DaemonSet 이 노드마다 하나, CoreDNS 같은 시스템 Pod 가 준비되면 끝이다
  • 문제가 있으면 재설치하기 전에 이벤트와 로그를 읽는다
k -n kube-system get events --sort-by=.lastTimestamp
k -n kube-system logs daemonset/cilium -c cilium-agent --tail=80
k -n kube-system exec daemonset/cilium -c cilium-agent -- cilium-dbg status
  • cilium-dbg 는 Pod 하나의 agent 상태다 — 관리 PC 의 cilium CLI 가 보는 클러스터 전체 상태와 다르다

3. Service — 가상 IP 가 Pod IP 가 되는 길

3.1 Service 의 종류와 이 글의 범위

apiVersion: v1
kind: Service
metadata:
  name: db
  namespace: cn4
spec:
  selector:
    app: db            # 이 레이블을 가진 Pod 들이 뒤에 선다
  ports:
    - port: 5432       # Service 포트
      targetPort: 5432 # Pod 포트
  • 만들면 10.96.45.7 같은 ClusterIP 가 하나 붙는다
  • 이 주소는 어느 노드의 어느 인터페이스에도 없다 — 그래서 “이 주소로 보낸 패킷이 어떻게 Pod 에 가는가” 가 3절 전체의 질문이다
  • 뒤의 Pod 가 바뀌어도 이 주소는 Service 를 지울 때까지 그대로다
종류ClusterIP(가상 IP)를 받나다루는 곳
ClusterIP + selector있음3절 (기본형)
Headless없음 — DNS 가 Pod IP 목록을 그대로 준다4.1
ExternalName없음 — 밖의 이름으로 바꿔 준다4.1
NodePort · LoadBalancer있음 + 밖에서 들어오는 문문제 ④, 범위 밖

3.2 의도에서 규칙까지

API server — 객체가 사는 곳. 노드에는 없다1Service dbselector app=dbClusterIP 10.96.45.7port 5432사람이 적는다Pod app=db x2db-1 10.244.2.7✓db-2 10.244.1.9✗kubelet 의 probe 결과 (04-1 6절)2EndpointSlice db-xxxx컨트롤러가 적는다10.244.2.7:5432 ready: true10.244.1.9:5432 ready: false주소는 남기고 ready 만 바꾼다3노드마다 있는 kube-proxy 셋이 같은 것을 보고, 각자 자기 노드에 똑같이 쓴다cn4-control-planekube-proxy씀iptables nat10.96.45.7:5432 → 10.244.2.7:5432커널이 규칙대로 바꿔 보낸다 — kube-proxy 안 지남cn4-workerkube-proxy씀iptables nat10.96.45.7:5432 → 10.244.2.7:5432커널이 규칙대로 바꿔 보낸다 — kube-proxy 안 지남4cn4-worker2kube-proxy씀iptables nat10.96.45.7:5432 → 10.244.2.7:5432커널이 규칙대로 바꿔 보낸다 — kube-proxy 안 지남①② 는 API server 의 객체, ③ 은 노드마다 복사된 규칙, ④ 는 커널 — 10.244.1.9 는 목록에 남아 있지만 ready: false 라 규칙에는 없다
kube-proxy 의 일은 ③ 까지다 — 패킷은 kube-proxy 를 지나지 않고, 그 프로세스가 죽어도 ③ 의 규칙으로 ④ 는 계속 돈다

네 단계

  • ① 사람이 Service 와 Pod 를 적는다 — API server 에 객체로 저장된다. 노드에는 아무것도 없다
  • ② 컨트롤러가 selector 로 Pod 를 골라 주소·포트·준비 여부를 EndpointSlice 에 적는다 — 역시 API server 의 객체다
  • ③ 노드마다 있는 kube-proxy 가 ①② 를 지켜보다가 바뀌면 자기 노드의 nat 테이블을 고쳐 쓴다 — 노드 셋에 같은 규칙이 복사된다
  • ④ 패킷은 커널이 그 규칙대로 바꿔 보낸다 — 새 연결에서 백엔드를 고르고, 그 뒤 패킷은 conntrack 이 같은 Pod 로 보낸다

ready: false 는 삭제가 아니다

  • 준비 안 된 Pod 가 생겨도 EndpointSlice 에서 주소가 바로 지워지지는 않는다 — 주소는 남고 ready: false 로 표시된다
  • 보통은 준비된 주소만 ③ 에 들어간다
  • 예외가 있다 — publishNotReadyAddresses 를 켠 Service, 종료 중(terminating)인 주소의 처리
  • 그래서 IP 목록만 보고 대상이 살아 있다고 읽지 않는다 — 조건 칸까지 본다
  • readiness 가 ①→④ 를 타고 규칙에서 빠지는 네 단계는 04-1 6.5 의 그림에 있다

kube-proxy 는 규칙만 쓴다

  • 패킷이 kube-proxy 프로세스를 지나는 그림은 틀렸다 — kube-proxy 는 규칙을 유지하고, 커널이 규칙대로 나른다
  • 그렇게 그려지는 이유는 옛 userspace 모드다 — 프로세스가 연결을 중계했고 1.26 에서 제거됐다
  • Cilium 으로 kube-proxy 를 대체하면 eBPF 가 ③④ 를 맡는다 — 별도 전제 조건이 있고, 이 실습은 끈 상태다

3.3 두 입구 — PREROUTING 과 OUTPUT

예 — 노드 A 의 go-web 이 db(10.96.45.7:5432) 로 보낸 요청이 노드 B 의 postgres(10.244.2.7) 에 닿기까지노드 A1go-web Pod 방보내는 패킷의 봉투src 10.244.1.5내 Pod IPdst 10.96.45.7ClusterIPeth0 로 내보낸다호스트 방 — 커널과 노드 프로세스입구 1 — veth 로 들어온 패킷vethPREROUTING노드 셸curl 10.96.45.7입구 2 — 노드가 만든 패킷OUTPUTDNAT 10.96.45.7 → 10.244.2.72ClusterIP · 문 없음Pod IP · 진짜 문목적지 칸을 고쳐 쓰는 줄 — kube-proxy 가 썼다3라우팅→ 노드 BPOSTROUTING문만 고른다 · 패킷은 안 건드림도착이 같은 노드의 Pod 면 ③ 이 그 Pod 의 veth 를 고른다 — 나머지는 같다응답은 거꾸로 — conntrack 이 출발지 10.244.2.7 을 10.96.45.7 로 되돌린다ens3노드 B4postgresPod 방10.244.2.7Pod IP — eth0 에붙은 진짜 문veth 로 받는다① 목적지에 가상 IP 를 적고 출발 ② 호스트 방 입구에서 진짜 Pod IP 로 바뀜 ③ 바뀐 주소로 라우팅 ④ 노드 B 의 진짜 문에 도착
먼저 바꾸고 그다음 라우팅한다 — 그래서 가상 IP 는 라우팅 표에 없어도 되고, 어느 문으로 나갈지는 바뀐 주소를 보고 고른다

그림의 예

  • 노드 A 의 go-web Pod 가 db Service 로 연결 요청을 보낸다 — 목적지에 가상 IP 10.96.45.7 을 적는다
  • 그 요청이 노드 A 의 호스트 방을 지나 노드 B 의 postgres Pod 10.244.2.7 에 닿는 길이다
  • 응답은 같은 길을 거꾸로 온다 — conntrack 이 출발지를 10.96.45.7 로 되돌려 주니 go-web 은 가상 IP 와 얘기한 줄 안다

입구가 둘인 이유

  • netfilter 의 다섯 자리 중 라우팅 결정 전에 패킷을 잡는 자리는 둘이다
    • PREROUTING — NIC 로 들어온 패킷. Pod 가 veth 로 보낸 것은 호스트 방 입장에서 “들어온” 패킷이다
    • OUTPUT — 노드 자신의 프로세스가 만든 패킷. 노드 셸의 curl, hostNetwork Pod
  • kube-proxy 는 같은 KUBE-SERVICES 체인을 두 자리에 모두 건다 — 어느 입구로 들어와도 같은 규칙에서 바뀐다
  • 클러스터 밖의 기계는 어느 입구도 못 쓴다 — 가상 IP 로 가는 길이 없고, 바꿔 줄 규칙도 그 기계에 없다

라우팅 결정 전에 바꾸는 이유

  • 가상 IP 는 라우팅 표에 없다 — 그대로 두면 default 로 빠져나가 버린다
  • DNAT 로 목적지를 먼저 Pod IP 로 바꿔 두면 라우팅 결정은 평범한 Pod IP 를 본다
    • 같은 노드의 Pod 면 FORWARD → 그 Pod 의 veth
    • 다른 노드의 Pod 면 FORWARD → POSTROUTING → ens3
  • kubelet 의 probe 는 이 길을 안 지난다 — Pod IP 로 직접 두드리기 때문이다 (04-1 6.3)

3.4 kube-proxy 의 네 모드

모드어디서지금의 의미
iptablesLinux이 실습이 고정한 모드. 많은 배포판의 기본
nftablesLinuxnftables 를 직접 쓴다. 1.33 에서 Stable, 커널 5.13 이상
ipvsLinuxIPVS 와 보조 규칙. 1.35 부터 deprecated — 기존 환경을 읽는 용도
kernelspaceWindowsWindows 노드의 네트워크 기능. iptables 명령으로 조사할 수 없다

헷갈리는 둘

  • iptables 명령이 속으로 nft 백엔드를 쓰는 환경과, kube-proxy 를 nftables 모드로 설정한 환경은 다르다 — 앞은 명령의 구현, 뒤는 kube-proxy 의 모드다
  • IPVS 모드가 deprecated 라는 것과 커널의 IPVS 기능이 사라졌다는 것은 다르다

모드를 바꿀 때

  • 조회 도구만 바뀌는 게 아니다 — NodePort 동작과 방화벽 연동 조건까지 확인한다
  • nftables 모드는 커널 5.13 이상이다 — 2.1 의 Cilium 기준(5.10)만으로 되는지 판단하지 않는다

3.5 확률 사슬 — 25% 네 번이 아니다

KUBE-SERVICESnat 테이블 · PREROUTING 과 OUTPUT 에서 점프KUBE-SVC-DB (dst = ClusterIP 10.96.45.7:5432)1/4KUBE-SEP-A--probability 0.25000DNAT → Pod IP 10.244.2.7:5432남은 것의 1/3KUBE-SEP-B--probability 0.33333DNAT → Pod IP 10.244.1.9:5432남은 것의 1/2KUBE-SEP-C--probability 0.50000DNAT → Pod IP 10.244.2.8:5432나머지 전부KUBE-SEP-D(조건 없음)DNAT → Pod IP 10.244.1.10:5432100 연결을 보내면 —1/4 · 1/3 · 1/2 · 전부같은 25% 네 번25 연결25 연결25 연결18.8 연결25 연결14.1 연결25 연결42.2 연결둘째 줄은 첫 줄에서 빠져나온 75 만 본다 — 거기서 25% 를 떼면 18.8 뿐이라, 75 의 1/3 이어야 25 가 된다. 뒤로 갈수록 줄고 마지막만 몰린다주사위는 새 연결의 첫 패킷에만 — conntrack 장부에 '이 연결 = B' 라고 적고, 뒤 패킷과 응답은 장부만 보고 B 로 간다
iptables -t nat -S KUBE-SERVICES 로 읽히는 모양이다 — 체인 이름은 iptables 모드의 관찰 지점이지 API 의 약속이 아니라서, nftables 나 eBPF 에서는 없어도 장애가 아니다

같은 25% 를 네 번 쓰면 — 100 연결로 세어 보면

  • iptables 는 규칙을 위에서 아래로 본다. 각 줄은 자기까지 내려온 연결에 대해서만 주사위를 굴린다
  • 첫 줄 25%: 100 중 25 가 A 로 간다. 75 가 아래로 내려간다
  • 둘째 줄 25%: 75 중 25% 인 18.8 만 B 로 간다
  • 셋째 줄 25%: 56 중 14.1 만 C 로 간다
  • 넷째 줄: 남은 42.2 가 전부 D 로 간다 — 뒤로 갈수록 줄고 마지막에 몰린다

그래서 남은 몫 기준으로 적는다

  • 첫 줄 1/4 → 25. 남은 75
  • 둘째 줄 남은 것의 1/3 → 75 × 1/3 = 25. 남은 50
  • 셋째 줄 남은 것의 1/2 → 25. 남은 25
  • 넷째 줄 조건 없음 → 25
  • kube-proxy 가 이 분수를 계산해 --probability 값으로 써 넣는다 — 그림의 0.25 · 0.33333 · 0.5 가 그것이다

주사위는 연결의 첫 패킷에만

  • 패킷마다 굴리면 같은 연결의 패킷이 서로 다른 Pod 로 흩어져 연결이 깨진다
  • 그래서 새 연결의 첫 패킷에만 굴리고, conntrack 이 장부에 “이 연결 = B” 라고 적는다
  • 뒤 패킷과 응답은 장부만 보고 같은 Pod 로 간다 — 3.6 의 “연결 단위” 가 여기서 나온다

보장이 아니라 원리다

  • 실제 규칙은 준비 상태, 트래픽 정책, 세션 선호도의 영향을 받는다
  • 연결을 몇 개만 보냈을 때 Pod 마다 정확히 같은 개수가 간다는 보장은 없다

3.6 분산의 단위는 연결

짧은 연결 — 질문마다 새로 건다클라이언트Pod A질문 2통화 2Pod B질문 2통화 2Pod C질문 2통화 2Pod D질문 2통화 2질문 8 → Pod 마다 2. 걸 때마다 교환대가 고르니 고르다긴 연결 — 끊지 않고 계속 묻는다 (keep-alive · HTTP/2)클라이언트Pod A질문 100통화 1 · 질문 100Pod B질문 100통화 1 · 질문 100Pod C질문 0Pod D질문 0질문 200 → A·B 만. 통화 중인 손님은 옮겨지지 않는다요청 단위로 나누려면 — 둘 중 하나. ClusterIP 는 그대로 둔다클라이언트L7 프록시 — 안내 데스크Envoy · 서비스 메시 사이드카질문마다A · B · C · D통화는 데스크가 받고, 질문마다 상담원에게 넘긴다클라이언트 쪽 LB — 직통번호gRPC 채널 · headless DNSPod IP 넷을 알고질문마다 다른 번호로직통번호부 = headless Service 의 DNS (4.1절)연결 = 통화, 요청 = 통화 중의 질문 하나 — 교환대(iptables)는 전화를 연결할 때만 상담원을 고르고, 질문을 나누는 일은 안내 데스크(L7)나 손님의 몫이다
Pod 를 넷으로 늘렸는데 둘만 바쁘면 십중팔구 긴 연결이다 — iptables 는 연결을 나눴지 요청을 나누지 않았다

연결과 요청 — 통화와 질문

  • 연결은 클라이언트와 Pod 사이의 통화, 요청은 그 통화 중에 하는 질문 하나다
  • iptables 는 교환대다 — 전화를 연결하는 순간에만 어느 상담원(Pod)에게 붙일지 고른다. 3.5 의 주사위가 그것이다
  • 통화가 이어지는 동안 질문을 하나 하든 백 개 하든 교환대는 다시 개입하지 않는다
  • 질문마다 전화를 새로 거는 짧은 연결이면 걸 때마다 고르니 고르게 나뉜다

긴 연결이 쏠림을 만든다

  • HTTP keep-alive — 질문이 끝나도 끊지 않고 같은 통화로 다음 질문을 한다
  • gRPC(HTTP/2) — 한 통화에 질문 여럿을 동시에 싣는다
  • 통화는 적고 질문은 많은 구조다. 긴 통화 둘이 A·B 에 붙으면 Pod 를 넷으로 늘려도 C·D 는 논다 — 통화 중인 손님은 옮겨지지 않는다
  • “gRPC 는 언제나 연결 하나” 도 틀렸다 — gRPC 채널은 서브채널로 통화 여럿을 열고 자체 로드밸런싱을 할 수 있다

질문 단위로 나누려면

  • L7 프록시 — Envoy 같은 프록시(서비스 메시의 사이드카)가 안내 데스크처럼 통화를 대신 받고, 질문마다 상담원에게 넘긴다. ClusterIP 를 버리는 게 아니라 그 앞에 데스크를 하나 더 두는 것이다
  • 클라이언트 쪽 로드밸런싱 — 손님이 상담원 직통번호(Pod IP) 넷을 알고 질문마다 다른 번호로 건다. 직통번호부는 headless Service 의 DNS 가 준다 (4.1 절)
  • 둘 다 질문의 내용을 읽는 L7 의 일이다 — 주소만 바꾸는 교환대(iptables, L4)로는 할 수 없다

3.7 실습 노드에서 규칙 읽기

k -n kube-system get configmap kube-proxy -o yaml          # mode: iptables
k -n kube-system logs daemonset/kube-proxy --tail=40
k -n kube-system get service kube-dns -o wide              # 가상 IP 10.96.0.10
k -n kube-system get endpointslices \
  -l kubernetes.io/service-name=kube-dns -o wide           # 뒤에 선 CoreDNS Pod IP

docker exec cn4-worker iptables -t nat -S KUBE-SERVICES
docker exec cn4-worker iptables-save -t nat | \
  rg 'kube-dns|KUBE-SVC-|KUBE-SEP-|DNAT'
  • 이미 있는 DNS Service 를 대상으로 설정 → 대상 목록 → 커널 규칙 순서로 읽는다 — 3.2 의 ①②③ 그대로다
  • docker exec cn4-worker 는 노드 컨테이너의 호스트 방에서 명령을 치는 것이다 — Pod 안이 아니다
  • 출력의 IP 와 체인 이름 뒤의 해시는 실행 시점의 값이다

체인 이름은 관찰 지점이다

  • KUBE-SERVICES → KUBE-SVC-… → KUBE-SEP-… → DNAT 의 모양은 iptables 모드의 구현이다
  • KUBE-SERVICES 는 kube-proxy 가 iptables 를 쓸 때 붙인 내부 이름표다 — 쿠버네티스 API 의 약속이 아니다
  • nftables 모드나 eBPF 대체 환경으로 바꾸면 이 이름은 사라지고, 같은 일을 다른 자리에서 한다 — 체인이 안 보인다고 장애가 아니다
  • DNAT 뒤 Pod 까지의 길은 CNI 가 만든 네트워크다 — 04-1 의 이야기로 돌아간다

3.8 IPVS 의 알고리즘 여섯

표기이름고르는 기준
rrRound Robin차례대로
lcLeast Connection연결 수가 적은 쪽
dhDestination Hashing목적지 주소의 해시
shSource Hashing출발지 주소의 해시
sedShortest Expected Delay연결 수와 가중치로 계산한 예상 지연이 작은 쪽
nqNever Queue노는 백엔드 우선, 없으면 sed
  • 기존 IPVS 환경의 설정을 읽을 때 필요한 여섯이다 — 지원되는 전체 목록은 아니다
  • “연결이 가장 적은” 이나 “예상 지연” 이 들어 있어도 앱의 CPU 사용률이나 RPC 하나의 처리 비용은 모른다
  • 어떤 정보로 무엇을 나누는지를 먼저 구분해야 관측한 편중을 바르게 읽는다

4. DNS — 이름이 주소가 되는 길

4.1 이름의 모양

go-web Pod아는 것은 이름 하나db이름 — 들어가는 쪽클라이언트 → Service (4절)db → CoreDNS → 10.96.45.7Service dbClusterIP 10.96.45.7selector app=db이름과 레이블이 만나는 곳Pod db-110.244.2.7labels: app=dbPod db-210.244.1.9labels: app=db레이블 — 뒤로 나가는 쪽Service → Pod (3.2절)app=db 인 Pod 를 골라 EndpointSlice → iptables 규칙클라이언트는 레이블을 모른다 — 이름만 부른다. 레이블은 패킷이 올 때 보는 게 아니라, 미리 규칙으로 바뀌어 있다
주소 둘이 하는 일이 다르다 — 이름은 Service 를 찾을 때, 레이블은 Service 가 Pod 를 찾을 때
dbService 이름cn4네임스페이스svc고정cluster.local클러스터 도메인 — 설정값...= db 의 전체 이름클라이언트가 쓰는 네 가지 모양db같은 네임스페이스에서 — search 가 뒤를 채운다db.cn4다른 네임스페이스에서 — 역시 search 가 svc.cluster.local 을 채운다db.cn4.svc.cluster.local.끝에 점 — 절대 이름. search 를 안 붙인다kubernetes.defaultAPI server 의 Service — 어느 클러스터에나 있어 DNS 확인용으로 쓴다
cluster.local 은 설정값이지 약속이 아니다 — 짧은 이름이 통하는 것은 Pod 의 search 목록이 뒤를 채워 주기 때문이다
조회 대상보통의 응답
IPv4 ClusterIP ServiceService IPv4 주소의 A 레코드
IPv6 ClusterIP ServiceService IPv6 주소의 AAAA 레코드
Headless Service준비된 endpoint 들의 주소 — 가상 IP 없이 Pod IP 목록
이름 있는 Service 포트포트와 대상을 적은 SRV 레코드

일반화하지 않는 것

  • cluster.local 은 흔한 설정값이지 모든 클러스터의 약속이 아니다
  • 직접 만든 Pod 의 이름이 언제나 같은 모양의 A 레코드를 만들지는 않는다 — hostname·subdomain·Service·endpoint 준비 상태 같은 조건이 붙는다
  • StatefulSet 의 안정적인 DNS 이름도 이 구조 위에 있다 — 이름은 안정적이지만 교체된 Pod 의 IP 는 달라진다

4.2 CoreDNS 와 kube-dns 라는 이름

1go-web Pod 의 resolver/etc/resolv.confnameserver 10.96.0.10search cn4.svc.cluster.localsvc.cluster.local cluster.localoptions ndots:5앱이 db 를 찾으면 이 설정을 읽는다⑥ 답을 받으면 10.96.45.7:5432 로 연결 → 3절② 점이 5개 미만 → search 를 붙여 전체 이름으로db.cn4.svc.cluster.local ?③ kube-dns 10.96.0.10 에 묻는다iptables 가 CoreDNS Pod 로 바꿈 (3절)⑤ 답 — db 는 10.96.45.7 (A 레코드)CoreDNS Pod (kube-system)4번호부 — API server 에서 베낀 것db.cn410.96.45.7web.cn410.96.12.3kube-dns.kube-system10.96.0.10Service 마다 한 줄 — 사람이 등록하지 않는다④ 번호부에서 찾는다 → 10.96.45.7없는 이름(api.example.com)은 상위 DNS 로 forwardAPI serverService 가 생기면 한 줄 추가검색 후보(db → db.cn4.svc.cluster.local)는 Pod 안의 resolver 가 만든다 — CoreDNS 는 받은 이름에 답할 뿐이다kube-dns 는 Service 이름이다 — DNS 서버 자신도 Service 뒤의 Pod 라 ③ 에서 3절의 길을 먼저 탄다DNS 성공과 연결 성공은 다른 단계다 — ⑤ 에서 주소를 받아도 Service 에 준비된 백엔드가 없으면 ⑥ 은 실패한다
이름 → 주소는 Pod 의 resolver 와 CoreDNS 의 번호부 둘이 나눠 한다 — 뒤를 채우는 건 Pod, 답하는 건 CoreDNS, 번호부를 채우는 건 API server 다
1Pod 안의 /etc/resolv.confnameserver 10.96.0.10search cn4.svc.cluster.local …k -n cn4 exec diagnostic -c tools --cat /etc/resolv.conf2Service kube-dnsClusterIP 10.96.0.10selector k8s-app=kube-dnsk -n kube-system get svc kube-dns3CoreDNS Pod 둘coredns-a 10.244.1.2coredns-b 10.244.2.2labels: k8s-app=kube-dnsk -n kube-system get pod-l k8s-app=kube-dns -o wide같은 주소같은 레이블① 의 주소가 ② 의 Service 이고, ② 의 selector 가 ③ 의 Pod 를 고른다 — 쿠버네티스는 자기 DNS 도 같은 공식으로 운영한다질의 패킷은 10.96.0.10 으로 나가 3절의 규칙에서 CoreDNS Pod 하나로 바뀐다 (주사위 1/2 · 나머지) — CoreDNS 가 전부 죽으면 새로 이름 찾기만 멈춘다
DNS 서버로 가는 길도 Service 다 — resolver → kube-dns Service → iptables 규칙 → CoreDNS Pod
k -n cn4 exec diagnostic -c tools -- cat /etc/resolv.conf   # nameserver 10.96.0.10
k -n kube-system get svc kube-dns                           # 10.96.0.10 = kube-dns Service 의 ClusterIP
k -n kube-system get pod -l k8s-app=kube-dns -o wide        # 그 뒤에 선 진짜 CoreDNS Pod 둘과 Pod IP

CoreDNS 의 자리

  • 공식 분류는 애드온이다 — API server · etcd 같은 컨트롤 플레인 층은 아니지만 kube-system 에 살고, 없으면 클러스터가 제 구실을 못 한다
  • CoreDNS 가 전부 죽으면 멈추는 것은 “새로 이름 찾기” 다 — 이미 맺은 연결과 캐시된 답은 그대로 간다. 그래서 복제본을 둘 이상 두고 5.4 절의 메트릭을 본다

이름이 kube-dns 인 이유

  • 옛 구현이 kube-dns · dnsmasq · sidecar 세 컨테이너였다. CoreDNS 로 바뀐 뒤에도 Service 이름은 호환을 위해 그대로 두었다
  • 이름만 보고 옛 구현이라고 판단하지 않는다

CoreDNS 의 세 조각

  • Deployment — CoreDNS Pod 둘을 유지한다. 복제본 수는 배포판마다 다를 수 있다
  • ConfigMap 의 Corefile — 무엇을 어떻게 답할지 정한다. 번호부에 없는 이름은 노드의 상위 DNS 로 넘긴다(forward). 5.1 절
  • Service kube-dns — 클라이언트의 접근점. 위의 10.96.0.10

4.3 dnsPolicy 넷

go-web Pod 안앱go-web 코드resolverglibc · musl · Go 라이브러리/etc/resolv.confnameserver 10.96.0.10search cn4.svc.cluster.local …① db 어디야?② 설정을 읽는다③ db.cn4.svc.cluster.local ?꼬리표는 resolver 가 붙인다CoreDNSkube-dns 10.96.0.10 뒤의 Pod④ 10.96.45.7⑤ 10.96.45.7kubeletPod 를 띄울 때 파일을 써 준다dnsPolicy 대로resolver 는 쿠버네티스 부품이 아니라 앱이 쓰는 라이브러리의 기능이다 — 그래서 구현마다 질의 순서와 재시도가 다르다 (4.5절)
앱 → resolver → CoreDNS → resolver → 앱. 파일은 kubelet 이 쓰고, 읽는 것은 Pod 안의 resolver 다
값뜻
ClusterFirst보통의 기본값. 클러스터 DNS 를 쓰고 외부 이름은 DNS 서버의 상위 전달을 따른다
Default노드의 DNS 설정을 물려받는다. 필드를 생략했을 때의 기본값이라는 뜻이 아니다
ClusterFirstWithHostNethostNetwork Pod 에서도 클러스터 DNS 를 쓰려 할 때 (Linux)
NonednsConfig 로 직접 준다. nameserver 가 하나 이상 있어야 한다
  • spec.dnsPolicy 는 kubelet 이 Pod 의 /etc/resolv.conf 를 어떻게 쓸지 정한다
  • 실제로 질의를 보내는 것은 앱이나 런타임의 resolver 다 — kubelet 이 아니다
  • hostNetwork: true 인 Linux Pod 는 ClusterFirstWithHostNet 을 명시해야 클러스터 이름을 찾는다 — Windows 에는 그대로 적용하지 않는다
apiVersion: v1
kind: Pod
metadata:
  name: host-dns-example
  namespace: cn4
spec:
  hostNetwork: true
  dnsPolicy: ClusterFirstWithHostNet
  containers:
    - name: tools
      image: busybox:1.37.0
      command: ["sleep", "3600"]
  • 보통의 실습용 클라이언트는 hostNetwork 를 쓰지 않는다 — 위는 모양을 보여 주는 예다

4.4 dnsConfig 와 search 목록

dnsPolicy: None
dnsConfig:
  nameservers:
    - 10.96.0.10                  # 예시 — 실제 kube-dns 주소로 바꾼다
  searches:
    - cn4.svc.cluster.local
    - svc.cluster.local
    - cluster.local
  options:
    - name: ndots
      value: "2"
  • dnsConfig 는 nameservers · searches · options 를 받는다
  • None 이 아닌 dnsPolicy 와도 같이 쓸 수 있다 — 기본 설정과 합치고 중복을 지운다
  • 한도 — nameserver 3개, search 32개, search 목록 전체 2048자
    • 책의 “search 는 최대 6개” 는 지금 한도가 아니다
    • resolver 나 오래된 런타임의 제약은 따로 본다
  • 공용 DNS 만 적으면 클러스터 안의 Service 이름을 못 찾는다
k -n kube-system get service kube-dns -o wide               # 클러스터 DNS 주소
k -n cn4 exec diagnostic -c tools -- cat /etc/resolv.conf  # 클라이언트가 실제로 받은 설정

4.5 search 와 ndots

db — 점 0 개, ndots 5 미만✓db.cn4.svc.cluster.localA 레코드첫 후보에서 끝 — 질의 2 (A + AAAA)api.example.com — 점 2 개, 역시 5 미만✗api.example.com.cn4.svc.cluster.localNXDOMAIN✗api.example.com.svc.cluster.localNXDOMAIN✗api.example.com.cluster.localNXDOMAIN✓api.example.com상위 DNS 가 답후보 넷 × (A + AAAA) = 질의 8api.example.com. 처럼 끝에 점을 찍으면 4 번만 — 단 HTTP Host·TLS 검증 같은 앱의 이름 처리도 같이 본다
짧은 이름이 편한 대가로 외부 이름이 네 번 묻는다 — 느리다고 느끼면 ndots 를 일괄로 바꾸기 전에 그 앱의 실제 질의를 먼저 센다

후보를 만드는 쪽

  • 검색 목록은 Pod 안의 resolver 가 참조하는 설정이다 — CoreDNS 가 매번 만드는 것이 아니다
  • ndots:5 면 점이 5개보다 적은 이름은 접미사를 붙인 뒤 마지막에 원래 이름으로 묻는다
  • 순서와 재시도는 구현마다 다르다 — libc, Go resolver, JVM

조사할 때 나누는 것

  • A 와 AAAA 질의, 검색 접미사, 재시도, 응답 코드, 캐시
  • 짧은 이름을 썼다고 반드시 느린 것은 아니다
  • 모든 Pod 의 ndots 를 일괄로 바꾸지 않는다 — 그 앱의 실제 질의와 지연을 본 뒤 조정한다

5. CoreDNS — 설정과 관찰

5.1 Corefile 읽는 순서

.:53 {errorshealth1ready2kubernetes cluster.local in-addr.arpa ip6.arpa {3pods disabledfallthrough in-addr.arpa ip6.arpattl 30}prometheus :91534forward . /etc/resolv.conf5cache 306loopreload7loadbalance}1health · readykubelet 이 두드리는 창구 — 04-1 6절의 probe2kubernetescluster.local 과 역방향 영역은 API 에서 본 것으로 답한다3pods disabledIP 를 이름으로 만드는 Pod 레코드만 끈다 — Service DNS 는 그대로4forward나머지 이름은 노드의 /etc/resolv.conf 의 상위 DNS 로5cache 30응답을 30초 재사용한다6loopforward 가 자기에게 돌아오면 멈춘다 — systemd-resolved stub 함정7loadbalance응답 안의 주소 순서만 섞는다 — kube-proxy 의 연결 분산과 다르다나열 순서가 실행 순서는 아니다 — 플러그인 순서는 CoreDNS 빌드가 정한다
읽는 순서는 '어느 영역을 · 누가 답하고 · 못 답하면 어디로' 다 — 복붙해서 기존 ConfigMap 을 덮지 않는다
  • 학습용 대표 구성이다 — 실제 ConfigMap 은 설치 도구와 배포판에 따라 다르다
  • 읽으라고 보여 주는 것이지 기존 설정을 통째로 덮는 명령이 아니다
  • 배포판 기본 설정에는 옛 호환용 pods insecure 가 들어 있을 수 있다

5.2 플러그인 — 이름 몇 개

플러그인중심 역할
kubernetesAPI 기반 레코드
forward상위 DNS 로 전달
cache응답 캐시
errors · log오류 기록 · 질의 기록
health · ready프로세스 상태 · 준비 상태
prometheus메트릭 노출
loop · reload전달 루프 탐지 · 설정 재적재
loadbalance응답 주소 순서 조정
autopath서버 쪽 검색 접미사 탐색
hosts · filehosts 파일 · zone 파일
rewrite · template이름 변경 · 동적 응답
  • 긴 목록은 DNS 서버가 확장 가능한 구조라는 뜻이다 — 전부 기본으로 켜져 있다는 뜻이 아니다
  • 책의 오탈자 — host → hosts, loadbalancer → loadbalance, router53 → route53
  • 책의 proxy 와 federation 은 옛 항목이다 — 상위 전달은 forward 로 읽는다

5.3 autopath 와 NodeLocal DNSCache

Pod 의 resolversearch · ndots · 재시도NodeLocal DNSCache노드마다 · 169.254.20.10CoreDNS (중앙)kube-dns 10.96.0.10상위 DNS외부 이름질의 개수를 정한다ndots · search 는 여기서 바뀐다반복 질의를 노드 안에서 끝낸다Service IP 경로와 UDP conntrack 을 줄인다autopath 는 여기서 검색을 대신pods verified 와 원래 Pod IP 가 필요forward 의 목적지셋은 다른 자리에서 다른 것을 줄인다 — '느리다' 하나로 뭉뚱그려 ndots 를 일괄로 바꾸지 않는다
NodeLocal 을 넣으면 Pod 의 nameserver 가 169.254.20.10 으로 바뀌고 CoreDNS 는 원래 Pod IP 를 못 본다 — autopath 와는 같이 쓰기 어렵다

autopath

  • 클라이언트가 접미사를 바꿔 가며 반복 질의하는 상황에서 서버가 후보를 대신 탐색해 돌려준다
  • 질의 다섯 번이 항상 한 번이 되는 것은 아니다 — 못 찾으면 클라이언트가 다시 검색한다
  • 쿠버네티스와 연동하려면 pods verified 와 요청 Pod 의 원래 IP 가 CoreDNS 에 보여야 한다 — Pod 정보 추적은 메모리도 더 쓴다
  • Pod IP 가 재사용될 때 API watch 보다 질의가 먼저 와서 이전 네임스페이스 정보로 답할 수 있다 — Windows 제약도 있다
  • 기본 처방으로 켜지 않는다 — 지금 질의 경로에 맞는지 먼저 본다

NodeLocal DNSCache

  • 노드마다 캐시 에이전트를 두어 반복 질의를 가까이서 끝낸다
  • DNS Service 로 가는 길과 UDP conntrack 부담이 준다
  • 대신 Pod 의 nameserver 주소가 바뀌고, 정책이 허용해야 하는 경로도 바뀐다
  • 중앙 CoreDNS 가 원래 Pod IP 를 못 보게 되는 구성이라 autopath 와는 같이 쓰기 어렵다

5.4 메트릭의 실제 이름

메트릭보는 것
coredns_build_info실행 버전과 빌드
coredns_dns_requests_total질의 수 — 프로토콜·질의 종류별
coredns_dns_responses_totalNOERROR · NXDOMAIN · SERVFAIL 등 응답 코드별
coredns_dns_request_duration_seconds처리 지연 분포
coredns_dns_request_size_bytes질의 크기 분포
coredns_plugin_enabled켜진 플러그인
  • 총량만 보지 않는다 — 실패 응답과 지연을 같이 본다
  • 질의가 는 것인지, 상위 DNS 가 느린 것인지, CoreDNS 의 CPU·메모리가 모자란 것인지를 나눈다
  • _seconds 히스토그램은 수집 결과에서 _bucket · _sum · _count 로 보인다
  • 책의 공백 들어간 이름은 실제 Prometheus 식별자가 아니다
  • CoreDNS 복제본이 언제나 2 라는 전제도 두지 않는다

5.5 실습에서 확인하기

k -n cn4 exec diagnostic -c tools -- nslookup db
k -n cn4 exec diagnostic -c tools -- nslookup db.cn4.svc.cluster.local
k -n cn4 exec diagnostic -c tools -- nslookup kubernetes.default

k -n kube-system get pods -l k8s-app=kube-dns -o wide
k -n kube-system get service kube-dns -o wide
k -n kube-system get endpointslice -l kubernetes.io/service-name=kube-dns
k -n kube-system get configmap coredns -o yaml
k -n kube-system logs deployment/coredns --tail=50
  • -c tools 는 같은 Pod 의 다른 컨테이너와 구분하려고 적는다
  • diagnostic Pod 와 db Service 는 앱 실습에서 만드는 것이다 — 이름 해석만 보려면 kubernetes.default 로 충분하다
내부 이름 (db)외부 이름연결✓✓✗Service 의 selector · EndpointSlice 의 ready · NetworkPolicy · 실제 서버 포트✓✗–CoreDNS 의 forward 설정 · 상위 DNS 에 닿는가✗✗–Pod 의 nameserver · 53 UDP/TCP 경로 · kube-dns 의 EndpointSlice · CoreDNS Ready증거가 어느 단계에 있는지로 나눈다 — 이름 해석과 연결은 다른 단계다
nslookup db 가 되면 DNS 는 끝난 것이다 — 그 다음 실패는 3절의 문제이지 4절의 문제가 아니다

노드의 resolv.conf 함정

  • 노드의 /etc/resolv.conf 가 systemd-resolved 의 stub 주소(127.0.0.53)를 가리키면 CoreDNS 의 forward 가 자기 자신으로 돌아오는 루프가 생긴다 — loop 플러그인이 잡는 그 경우다
  • 실제 resolver 파일과 kubelet 의 resolvConf 설정을 본다
  • 무조건 공용 DNS 로 바꾸면 사내 DNS 에 의존하는 이름이 깨진다 — 원래 경로를 먼저 파악한다

6. Dual-stack — 주소 계열이 둘일 때

6.1 Stable 과 준비됨은 다르다

  • IPv4·IPv6 이중 스택은 1.23 부터 Stable 이다 — 책의 alpha 설명과 IPv6DualStack=true feature gate 는 지금 절차가 아니다
  • Stable 이라는 것과 지금 클러스터에 두 계열이 준비됐다는 것은 다르다
  • 같이 맞춰야 하는 것 — 노드 주소와 라우팅, CNI 와 IPAM 의 지원, Pod CIDR 과 Service CIDR, Service 처리 구현, 외부 로드밸런서의 주소 계열
  • Service YAML 한 줄로 IPv4 전용 네트워크에 IPv6 길이 생기지는 않는다
  • cn4 는 IPv4 단일 스택이다 — 아래는 거기에 PreferDualStack Service 를 넣어 한 계열로 동작하는 대체 동작을 보는 것이다

6.2 주소 필드와 ipFamilyPolicy

Podstatus.podIP 10.244.1.5status.podIPs- 10.244.1.5- fd00:10:244:1::5Service web-dualspec.clusterIP 10.96.45.7spec.clusterIPs- 10.96.45.7- fd00:10:96::2aipFamilies [IPv4, IPv6]EndpointSlice ×2addressType: IPv410.244.1.5 readyaddressType: IPv6fd00:10:244:1::5 readySingleStack → 한 계열PreferDualStack → 되면 둘, 안 되면 하나RequireDualStack → 둘 아니면 거절IPv4 만 있는 cn4 에 PreferDualStack 을 내면 → clusterIPs 하나, IPv4 EndpointSlice 하나두 주소를 받아도 앱이 0.0.0.0:8080 (IPv4) 에만 귀 기울이면 IPv6 로는 못 받는다
단일 필드(podIP · clusterIP)는 목록의 첫 칸이다 — 기능이 Stable 인 것과 지금 클러스터에 두 계열이 준비된 것은 다른 이야기다
ipFamilyPolicy단일 스택 클러스터이중 스택 클러스터
SingleStack한 계열한 계열
PreferDualStack지원되는 한 계열로 동작두 계열
RequireDualStack생성 거절두 계열
  • Pod — status.podIP 는 주 주소, status.podIPs 는 목록
  • Service — spec.clusterIP 는 첫 주소와 연결된 옛 단일 필드, spec.clusterIPs 는 목록, ipFamilies 는 계열과 순서 (IPv6 우선도 된다)
  • 책의 “PreferDualStack 은 SingleStack 과 같다” 는 틀렸다 — 두 계열을 쓸 수 있으면 쓰고, 단일 스택에서는 한 계열로 동작하는 정책이다. RequireDualStack 은 그 대체 동작을 허용하지 않는다
  • 표는 유효한 일반 ClusterIP Service 기준이다 — 명시한 ipFamilies 의 지원 여부와 다른 필드 조합도 맞아야 한다

6.3 Service 예제와 관찰

# web-dual.yaml
apiVersion: v1
kind: Service
metadata:
  name: web-dual
  namespace: cn4
spec:
  ipFamilyPolicy: PreferDualStack   # 되면 둘, 안 되면 하나
  selector:
    app: web
  ports:
    - name: http
      protocol: TCP
      port: 8080
      targetPort: 8080
# IPv6 를 먼저, 둘 다 반드시 — 두 계열을 지원하는 클러스터에서만
ipFamilyPolicy: RequireDualStack
ipFamilies:
  - IPv6
  - IPv4
k -n cn4 apply -f web-dual.yaml
k -n cn4 get pods -o wide
k -n cn4 get pod -l app=web \
  -o jsonpath='{range .items[*]}{.status.podIPs}{"\n"}{end}'
k -n cn4 get service web-dual -o yaml           # clusterIPs 가 하나
k -n cn4 get endpointslice \
  -l kubernetes.io/service-name=web-dual -o yaml  # addressType 확인
  • cn4 에서는 IPv4 ClusterIP 하나가 붙는 것이 예상 결과다 — ipFamilies 를 생략해 클러스터의 기본 순서를 따랐다
  • EndpointSlice 는 주소 계열별로 나뉜다 — addressType 을 본다
  • 두 계열을 가진 Service 도 실제 백엔드와 경로가 있어야 통한다 — LoadBalancer 의 외부 IPv6 는 클라우드나 LB 구현의 지원이 필요하다

앱 쪽에서 막히는 것

  • 0.0.0.0:8080 으로 듣는 웹 서버는 IPv4 에서만 받는다 — 같은 이미지를 이중 스택으로 옮겨도 IPv6 HTTP 접속이 저절로 되지 않는다
  • IPv6 전용 Pod 가 IPv4 전용 외부 서버와 통하려면 프록시나 DNS64·NAT64 같은 변환 경로가 필요하다
  • IPv6 라고 모든 외부 통신이 NAT 없이 성립하는 것도 아니다 — 주소 할당, 기본 경로, 방화벽, 앱의 수신 주소를 각각 본다

7. Discussion

7.1 진단 순서 — 어느 단계의 증거인가

물을 것볼 것읽는 범위
Pod 가 준비됐나Pod conditions · probe · eventsRunning 만으로 Ready 를 판단하지 않는다
이름이 주소로 바뀌나resolv.conf · DNS 응답이름 해석 성공과 연결 성공을 구별한다
Service 에 백엔드가 있나selector · EndpointSlice주소 · ready · 포트까지 본다
Pod IP 로 직접 되나수신 주소 · CNI 경로Service 처리 문제와 기본 연결 문제를 분리한다
양쪽 정책이 허용하나보내는 쪽 egress · 받는 쪽 ingress같은 방향의 허용 합집합까지 본다
앱 요청이 성공하나HTTP 응답 · 실제 SQLTCP 연결 성공을 업무 성공으로 넓히지 않는다
  • “쿠버네티스 네트워크가 안 된다” 한 문장 대신 어느 단계의 증거가 있는지로 나눈다
  • 순서를 반드시 지킬 필요는 없다 — 이미 확인한 사실이 있으면 다음 의문으로 바로 간다
  • 같은 Pod 의 localhost, Pod IP, Service IP, 노드 IP, 외부 IP 는 서로 다른 접속점이다 — 어느 주소에서 무엇을 시험했는지 적어야 결과를 비교할 수 있다
  • 클러스터 밖에서 들어오는 길(문제 ④)에는 외부 라우팅, 로드밸런서, 방화벽, SNAT 와 반환 경로가 더해진다 — NodePort · LoadBalancer · Ingress · Gateway API 는 전부 3절의 Service 위에 얹는 문이다

핵심 메시지

노드 셋 = 컨테이너 셋
kind 는 커널 하나 위에 노드 셋을 올린다. Cilium 은 범위 나눔과 Service 처리를 끄고 끼운 부품이다
가상 IP 는 문이 아니다
어느 인터페이스에도 없다. 보내는 노드의 커널이 PREROUTING 이나 OUTPUT 에서, 라우팅 결정 전에 진짜 Pod IP 로 바꾼다
kube-proxy 는 규칙만
Service 와 EndpointSlice 를 보고 nat 테이블을 고친다. 패킷은 커널이 나르고, 주소는 ready: false 로 남는다
연결을 나눈다
1/4 · 1/3 · 1/2 · 전부는 새 연결에 한 번 굴리는 주사위다. keep-alive 와 gRPC 의 요청은 나뉘지 않는다
후보는 Pod 가 만든다
db 를 db.cn4.svc.cluster.local 로 채우는 것은 resolver 의 search 와 ndots 다. CoreDNS 는 받은 이름에 답할 뿐이다
단계를 나눠 읽는다
이름 해석 · 백엔드 목록 · 직접 연결 · 정책 · 앱 응답. 이중 스택도 이 단계마다 필드가 하나씩 늘어난 것이다

8. References

kube-proxy 가 데이터 경로에 있지 않다는 것, iptables 모드가 netfilter 규칙으로 전달을 구성한다는 것, 모드별 상태(nftables Stable 1.33, IPVS deprecated 1.35)와 nftables 의 커널 요구는 가상 IP 문서와 kube-proxy 설정 참조에 있다.
"In this mode, kube-proxy configures packet forwarding rules using the iptables API of the kernel netfilter subsystem."
[해석] "이 모드에서 kube-proxy 는 커널 netfilter 의 iptables API 로 패킷 전달 규칙을 구성한다." — 3.2 의 "규칙만 쓴다" 와 3.3 그림의 근거다. 연결 단위로 백엔드를 고르고 이후 패킷이 연결 추적을 따른다는 것도 같은 문서에 있다. 책의 "kube-proxy 가 모든 패킷을 중계한다" 는 제거된 userspace 모드의 설명이고, "IPVS 가 새 권장" 은 지금 적용하지 않는다.
→ kubernetes.io — Virtual IPs and Service Proxies
→ kubernetes.io — kube-proxy Configuration (v1alpha1)
→ kubernetes.io — Service
EndpointSlice 의 ready · serving · terminating 조건과 주소가 남은 채 ready 만 바뀐다는 것, publishNotReadyAddresses 의 예외는 EndpointSlice 문서에 있다.
"ready is a condition that maps to a Pod's Ready condition."
[해석] "ready 는 Pod 의 Ready 조건에 대응하는 조건이다." — 3.2 의 "ready: false 는 삭제가 아니다" 와 sv42 그림 ② 의 근거다. 종료 중인 Pod 의 주소를 serving · terminating 으로 따로 표시한다는 것도 여기 있다.
→ kubernetes.io — EndpointSlices
Service 이름의 모양, A/AAAA/SRV 레코드, dnsPolicy 네 값과 Default 가 기본값이 아니라는 것, dnsConfig 의 한도(nameserver 3, search 32, 2048자), ndots 의 동작은 DNS 문서에 있다.
"Kubernetes allows up to 32 search domains and a list of up to 2048 characters."
[해석] "쿠버네티스는 search 도메인 32개, 목록 전체 2048자까지 허용한다." — 4.4 의 한도와 "책의 6개는 지금 한도가 아니다" 의 근거다. 4.1 그림의 이름 형식(my-svc.my-namespace.svc.cluster-domain.example)과 4.3 의 표도 같은 문서에서 가져왔다.
→ kubernetes.io — DNS for Services and Pods
→ kubernetes.io — Customizing DNS Service
kubernetes 플러그인의 pods 옵션(disabled · insecure · verified), autopath 의 조건과 제약, loadbalance 가 응답 순서만 바꾼다는 것, 메트릭 이름은 CoreDNS 플러그인 문서에 있다.
"autopath allows for server-side search path completion."
[해석] "autopath 는 서버 쪽에서 검색 경로를 완성해 준다." — 5.3 의 근거다. Pod IP 재사용 때 이전 네임스페이스 정보로 답할 수 있다는 경고와 pods verified 요구도 같은 문서에 있다. 책의 host · loadbalancer · router53 은 hosts · loadbalance · route53 의 오탈자다.
→ coredns.io — kubernetes plugin
→ coredns.io — autopath plugin
→ coredns.io — metrics (prometheus) plugin
→ kubernetes.io — Using NodeLocal DNSCache
이중 스택이 1.23 부터 Stable 이라는 것, podIPs · clusterIPs · ipFamilies 필드, ipFamilyPolicy 세 값의 동작은 dual-stack 문서와 Service API 레퍼런스에 있다.
"PreferDualStack (two IP families on dual-stack configured clusters or a single IP family on single-stack clusters)"
[해석] "PreferDualStack — 이중 스택 클러스터에서는 두 계열, 단일 스택 클러스터에서는 한 계열." — 6.2 의 표와 "책의 PreferDualStack = SingleStack 은 틀렸다" 의 근거다. 책의 IPv6DualStack feature gate 는 Stable 승격과 함께 사라졌다.
→ kubernetes.io — IPv4/IPv6 dual-stack
→ kubernetes.io — Service API reference (ipFamilyPolicy)
kind 의 disableDefaultCNI · kubeProxyMode 설정과 노드 이미지 고정, Cilium 의 kind 설치 옵션(ipam.mode=kubernetes)과 Helm 값, 커널 요구는 kind 설정 문서와 Cilium 설치 문서에 있다.
"kind ships with a simple networking implementation ("kindnetd") based around standard CNI plugins"
[해석] "kind 는 표준 CNI 플러그인 위에 만든 단순한 네트워크 구현(kindnetd)을 함께 배포한다." — 2.2 의 disableDefaultCNI 와 "NotReady 로 시작한다" 의 근거다. Helm 값의 기본과 kubeProxyReplacement 의 전제 조건은 Cilium Helm 참조에 있다.
→ kind.sigs.k8s.io — Configuration
→ docs.cilium.io — Installation using kind
→ docs.cilium.io — Helm Reference
→ docs.cilium.io — System Requirements