0. Contents
1. Overview
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 의 자리
클러스터는 하나고 요소는 셋이다 — 노드, 노드마다 붙는 에이전트, 노드 위에 뜨는 Pod. 서버를 사지 않아도 노드가 셋이라 노드 사이의 길을 확인할 수 있다
kind 가 하는 일
- Docker 컨테이너 하나를 쿠버네티스 노드 하나로 쓴다
- 제어 평면 1 + 워커 2 — 노드가 셋이라서 다른 노드 사이의 통신을 확인할 수 있다
- 셋이 한 컴퓨터의 자원을 나눠 쓴다 — 물리 서버 셋의 성능이나 장애 격리는 재현하지 않는다
| 구성 요소 | 고정한 버전 | 이유 |
|---|---|---|
| kind | v0.33.0 | 공식 릴리스가 제공하는 노드 이미지를 쓴다 |
| 노드 이미지 | kindest/node:v1.36.4 + digest | 태그가 아니라 정확한 빌드까지 고정한다 |
| Cilium Helm chart | 1.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 의ciliumCLI 가 보는 클러스터 전체 상태와 다르다
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 의도에서 규칙까지
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
먼저 바꾸고 그다음 라우팅한다 — 그래서 가상 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 의 네 모드
| 모드 | 어디서 | 지금의 의미 |
|---|---|---|
| iptables | Linux | 이 실습이 고정한 모드. 많은 배포판의 기본 |
| nftables | Linux | nftables 를 직접 쓴다. 1.33 에서 Stable, 커널 5.13 이상 |
| ipvs | Linux | IPVS 와 보조 규칙. 1.35 부터 deprecated — 기존 환경을 읽는 용도 |
| kernelspace | Windows | Windows 노드의 네트워크 기능. iptables 명령으로 조사할 수 없다 |
헷갈리는 둘
iptables명령이 속으로 nft 백엔드를 쓰는 환경과, kube-proxy 를 nftables 모드로 설정한 환경은 다르다 — 앞은 명령의 구현, 뒤는 kube-proxy 의 모드다- IPVS 모드가 deprecated 라는 것과 커널의 IPVS 기능이 사라졌다는 것은 다르다
모드를 바꿀 때
- 조회 도구만 바뀌는 게 아니다 — NodePort 동작과 방화벽 연동 조건까지 확인한다
- nftables 모드는 커널 5.13 이상이다 — 2.1 의 Cilium 기준(5.10)만으로 되는지 판단하지 않는다
3.5 확률 사슬 — 25% 네 번이 아니다
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 를 넷으로 늘렸는데 둘만 바쁘면 십중팔구 긴 연결이다 — 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 의 알고리즘 여섯
| 표기 | 이름 | 고르는 기준 |
|---|---|---|
| rr | Round Robin | 차례대로 |
| lc | Least Connection | 연결 수가 적은 쪽 |
| dh | Destination Hashing | 목적지 주소의 해시 |
| sh | Source Hashing | 출발지 주소의 해시 |
| sed | Shortest Expected Delay | 연결 수와 가중치로 계산한 예상 지연이 작은 쪽 |
| nq | Never Queue | 노는 백엔드 우선, 없으면 sed |
- 기존 IPVS 환경의 설정을 읽을 때 필요한 여섯이다 — 지원되는 전체 목록은 아니다
- “연결이 가장 적은” 이나 “예상 지연” 이 들어 있어도 앱의 CPU 사용률이나 RPC 하나의 처리 비용은 모른다
- 어떤 정보로 무엇을 나누는지를 먼저 구분해야 관측한 편중을 바르게 읽는다
4. DNS — 이름이 주소가 되는 길
4.1 이름의 모양
주소 둘이 하는 일이 다르다 — 이름은 Service 를 찾을 때, 레이블은 Service 가 Pod 를 찾을 때
cluster.local 은 설정값이지 약속이 아니다 — 짧은 이름이 통하는 것은 Pod 의 search 목록이 뒤를 채워 주기 때문이다
| 조회 대상 | 보통의 응답 |
|---|---|
| IPv4 ClusterIP Service | Service IPv4 주소의 A 레코드 |
| IPv6 ClusterIP Service | Service 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 라는 이름
이름 → 주소는 Pod 의 resolver 와 CoreDNS 의 번호부 둘이 나눠 한다 — 뒤를 채우는 건 Pod, 답하는 건 CoreDNS, 번호부를 채우는 건 API server 다
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 넷
앱 → resolver → CoreDNS → resolver → 앱. 파일은 kubelet 이 쓰고, 읽는 것은 Pod 안의 resolver 다
| 값 | 뜻 |
|---|---|
| ClusterFirst | 보통의 기본값. 클러스터 DNS 를 쓰고 외부 이름은 DNS 서버의 상위 전달을 따른다 |
| Default | 노드의 DNS 설정을 물려받는다. 필드를 생략했을 때의 기본값이라는 뜻이 아니다 |
| ClusterFirstWithHostNet | hostNetwork Pod 에서도 클러스터 DNS 를 쓰려 할 때 (Linux) |
| None | dnsConfig 로 직접 준다. 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
짧은 이름이 편한 대가로 외부 이름이 네 번 묻는다 — 느리다고 느끼면 ndots 를 일괄로 바꾸기 전에 그 앱의 실제 질의를 먼저 센다
후보를 만드는 쪽
- 검색 목록은 Pod 안의 resolver 가 참조하는 설정이다 — CoreDNS 가 매번 만드는 것이 아니다
ndots:5면 점이 5개보다 적은 이름은 접미사를 붙인 뒤 마지막에 원래 이름으로 묻는다- 순서와 재시도는 구현마다 다르다 — libc, Go resolver, JVM
조사할 때 나누는 것
- A 와 AAAA 질의, 검색 접미사, 재시도, 응답 코드, 캐시
- 짧은 이름을 썼다고 반드시 느린 것은 아니다
- 모든 Pod 의 ndots 를 일괄로 바꾸지 않는다 — 그 앱의 실제 질의와 지연을 본 뒤 조정한다
5. CoreDNS — 설정과 관찰
5.1 Corefile 읽는 순서
읽는 순서는 '어느 영역을 · 누가 답하고 · 못 답하면 어디로' 다 — 복붙해서 기존 ConfigMap 을 덮지 않는다
- 학습용 대표 구성이다 — 실제 ConfigMap 은 설치 도구와 배포판에 따라 다르다
- 읽으라고 보여 주는 것이지 기존 설정을 통째로 덮는 명령이 아니다
- 배포판 기본 설정에는 옛 호환용
pods insecure가 들어 있을 수 있다
5.2 플러그인 — 이름 몇 개
| 플러그인 | 중심 역할 |
|---|---|
| kubernetes | API 기반 레코드 |
| forward | 상위 DNS 로 전달 |
| cache | 응답 캐시 |
| errors · log | 오류 기록 · 질의 기록 |
| health · ready | 프로세스 상태 · 준비 상태 |
| prometheus | 메트릭 노출 |
| loop · reload | 전달 루프 탐지 · 설정 재적재 |
| loadbalance | 응답 주소 순서 조정 |
| autopath | 서버 쪽 검색 접미사 탐색 |
| hosts · file | hosts 파일 · zone 파일 |
| rewrite · template | 이름 변경 · 동적 응답 |
- 긴 목록은 DNS 서버가 확장 가능한 구조라는 뜻이다 — 전부 기본으로 켜져 있다는 뜻이 아니다
- 책의 오탈자 —
host→hosts,loadbalancer→loadbalance,router53→route53 - 책의
proxy와federation은 옛 항목이다 — 상위 전달은forward로 읽는다
5.3 autopath 와 NodeLocal DNSCache
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_total | NOERROR · 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 의 다른 컨테이너와 구분하려고 적는다diagnosticPod 와dbService 는 앱 실습에서 만드는 것이다 — 이름 해석만 보려면kubernetes.default로 충분하다
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=truefeature gate 는 지금 절차가 아니다 - Stable 이라는 것과 지금 클러스터에 두 계열이 준비됐다는 것은 다르다
- 같이 맞춰야 하는 것 — 노드 주소와 라우팅, CNI 와 IPAM 의 지원, Pod CIDR 과 Service CIDR, Service 처리 구현, 외부 로드밸런서의 주소 계열
- Service YAML 한 줄로 IPv4 전용 네트워크에 IPv6 길이 생기지는 않는다
- cn4 는 IPv4 단일 스택이다 — 아래는 거기에
PreferDualStackService 를 넣어 한 계열로 동작하는 대체 동작을 보는 것이다
6.2 주소 필드와 ipFamilyPolicy
단일 필드(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 · events | Running 만으로 Ready 를 판단하지 않는다 |
| 이름이 주소로 바뀌나 | resolv.conf · DNS 응답 | 이름 해석 성공과 연결 성공을 구별한다 |
| Service 에 백엔드가 있나 | selector · EndpointSlice | 주소 · ready · 포트까지 본다 |
| Pod IP 로 직접 되나 | 수신 주소 · CNI 경로 | Service 처리 문제와 기본 연결 문제를 분리한다 |
| 양쪽 정책이 허용하나 | 보내는 쪽 egress · 받는 쪽 ingress | 같은 방향의 허용 합집합까지 본다 |
| 앱 요청이 성공하나 | HTTP 응답 · 실제 SQL | TCP 연결 성공을 업무 성공으로 넓히지 않는다 |
- “쿠버네티스 네트워크가 안 된다” 한 문장 대신 어느 단계의 증거가 있는지로 나눈다
- 순서를 반드시 지킬 필요는 없다 — 이미 확인한 사실이 있으면 다음 의문으로 바로 간다
- 같은 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 가 새 권장" 은 지금 적용하지 않는다.
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 으로 따로 표시한다는 것도 여기 있다.
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 플러그인의 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 의 오탈자다.
이중 스택이 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 승격과 함께 사라졌다.
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 참조에 있다.
