쿠버네티스 네트워크 흐름에 대한 이해 (1/2) (Flannel CNI)

온프레미스 K8s 클러스터 구축하기 - 이론 및 검증 1
이민석's avatar
Mar 26, 2025
쿠버네티스 네트워크 흐름에 대한 이해 (1/2) (Flannel CNI)

본 문서는 FlannelCNI 기반으로 구축된 온프레미스 클러스터에서
파드 - 파드 || 서비스 || 헤드리스 서비스 간의 통신 경로에 대한 내용을 다룹니다.

FlannelCNI에 대한 딥다이브 내용은 아래 문서에서 다루고 있습니다.

  • 쿠버네티스 네트워크 흐름에 대한 이해 (2/2) (Flannel CNI) [1]

A. 단일 호스트

단일 호스트에서 직접 통신, 서비스 통신, 헤드리스 서비스 통신 등을 실습하면서
가상 이더넷 페어, 브릿지, CoreDNS, Kube-proxy 등을 이해하고자 합니다.

  1. 단일 호스트 내 파드(Pods) 간 네트워크 통신

  2. 단일 호스트 내 파드(Pod)에서 서비스, 디플로이먼트(Service, Deploy)로 네트워크 통신

  3. 단일 호스트 내 파드(Pod)에서 헤드리스 서비스(StatefulSet)로 네트워크 통신

A.1. 파드 간 통신

단일 호스트 내 파드(Pods) 간 네트워크 통신

단일 호스트에서 한 파드(b)에서 다른 파드(a)로 요청을 보내면
가상 이더넷 페어(eth0 — veth0)과 브릿지(cni0)를 거쳐서 도착하게 됩니다.

[이론] 단일 노드 파드 간 네트워크 통신 경로
[이론] 단일 노드 내, 파드에서 파드로 통신

요청에 대한 가상 이더넷 페어, 브릿지를 확인하고자 [실습 1]를 진행하였으나
실험 환경에서 TTL Hop을 줄이는 구간이 없어 traceroute에서 감지되지 않았습니다.

[실습 1] traceroute를 사용한 통신 흐름

1. 테스트 파드 생성하기

kubectl run a-pod --image=busybox --restart=Never -- sleep 3600 kubectl run b-pod --image=busybox --restart=Never -- sleep 3600

2. 테스트 파드 확인하기

kubectl get pods -o=custom-columns=NAME:.metadata.name,IP:.status.podIP
NAME IP a-pod 10.244.0.53 b-pod 10.244.0.54

3. 테스트 파드 접속하고 네트워크 테스트하기

kubectl exec -it b-pod -- /bin/sh # traceroute 10.244.0.26

4. 테스트 파드 정리하기

kubectl delete pod/a-pod --force kubectl delete pod/b-pod --force

💡

[traceroute 작동방식*] TTL(Time-To-Live)을 증가시키면서 패킷을 전송하고 TTL이 0이 되는 시점에서 ICMP Time Exceeded 메세지를 수집 따라서 TTL을 줄이는 네트워크 홉이 없는 경우에는 감지되지 않음 * 운영 체제에 따라서 ICMP가 아닌UDP를 사용할 수도 있음

세부적인 가상 이더넷 페어, 브릿지를 확인하기 위해 [실습 2]를 진행하였고
tcpdump를 통해서 파드 B에서 파드 A로 가는 흐름을 확인할 수 있었습니다.

  • Pod A netns (eth0)
    → Root netnes (veth75a7f7d6 → cni0 → vethded7ef9f)
    → Pod B netns (eth0)

[실습 2] tcpdump를 사용한 이더넷, 브릿지 확인

1. 테스트 파드 생성하기

kubectl run a-pod --image=busybox --restart=Never -- sleep 3600 kubectl run b-pod --image=busybox --restart=Never -- sleep 3600

2. 테스트 파드 IP 확인하기

kubectl get pods -o=custom-columns=NAME:.metadata.name,IP:.status.podIP | grep -E "(NAME|app)"
NAME IP a-pod 10.244.0.53 b-pod 10.244.0.54

3. 파드 A의 패킷 캡쳐 시작하기

tcpdump -i any host 10.244.0.53

4. 파드 B에서 파드 A로 ping 전송하기
(4번은 별도의 터미널을 열어서 진행해주세요.)

kubectl exec -it b-pod -- /bin/sh / # ping -c 1 10.244.0.53
PING 10.244.0.53 (10.244.0.53): 56 data bytes 64 bytes from 10.244.0.53: seq=0 ttl=64 time=0.297 ms --- 10.244.0.53 ping statistics --- 1 packets transmitted, 1 packets received, 0% packet loss round-trip min/avg/max = 0.297/0.297/0.297 ms

5. 파드 A의 패킷 캡처 확인하기(3번을 실행한 터미널)

tcpdump: data link type LINUX_SLL2 tcpdump: verbose output suppressed, use -v[v]... for full protocol decode listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes 21:18:36.504305 vethded7ef9f P IP 10.244.0.54 > 10.244.0.53: ICMP echo request, id 39, seq 0, length 64 21:18:36.504380 veth75a7f7d6 Out IP 10.244.0.54 > 10.244.0.53: ICMP echo request, id 39, seq 0, length 64 21:18:36.504440 veth75a7f7d6 P IP 10.244.0.53 > 10.244.0.54: ICMP echo reply, id 39, seq 0, length 64 21:18:36.504459 vethded7ef9f Out IP 10.244.0.53 > 10.244.0.54: ICMP echo reply, id 39, seq 0, length 64 21:18:41.740639 veth75a7f7d6 P ARP, Request who-has 10.244.0.54 tell 10.244.0.53, length 28 21:18:41.740646 vethded7ef9f Out ARP, Request who-has 10.244.0.54 tell 10.244.0.53, length 28 21:18:41.740649 vethded7ef9f P ARP, Request who-has 10.244.0.53 tell 10.244.0.54, length 28 21:18:41.740653 veth75a7f7d6 Out ARP, Request who-has 10.244.0.53 tell 10.244.0.54, length 28 21:18:41.740699 vethded7ef9f P ARP, Reply 10.244.0.54 is-at ae:cb:f3:eb:3a:b9 (oui Unknown), length 28 21:18:41.740703 veth75a7f7d6 Out ARP, Reply 10.244.0.54 is-at ae:cb:f3:eb:3a:b9 (oui Unknown), length 28 21:18:41.740707 veth75a7f7d6 P ARP, Reply 10.244.0.53 is-at 7e:45:1f:49:82:91 (oui Unknown), length 28 21:18:41.740709 vethded7ef9f Out ARP, Reply 10.244.0.53 is-at 7e:45:1f:49:82:91 (oui Unknown), length 28

6. 파드 A, B의 브릿지 확인하기
아래 구문을 vethded7ef9f, veth75a7f7d6으로 실행해볼 것

# Host ip link | grep vethded7ef9f
4178: vethded7ef9f@if2: BROADCAST,MULTICAST,UP,LOWER_UP mtu 1450 qdisc noqueue master cni0 state UP mode DEFAULT group default

7. 테스트 파드 정리하기

kubectl delete pod/a-pod --force kubectl delete pod/b-pod --force
[증명] 단일 노드 파드 간 네트워크 통신 경로
[증명] 단일 노드 내, 파드에서 파드로 통신

이를 통해서
일반적으로 알려진 파드 간 네트워크 통신을 증명할 수 있었습니다.

A.2. 파드에서 서비스로 통신

단일 호스트 내 파드(Pod)에서 서비스, 디플로이먼트(Service, Deploy)로 네트워크 통신

단일 호스트에서 한 파드(b)에서 다른 서비스(a)로 요청을 보내면
가상 이더넷 페어, 브릿지 외에 coredns, kube-proxy를 경유한다 알고 있습니다.
(가설의 편의를 위해서 kube-proxy는 iptables 방식을 사용하며 외부 변수는 통제)

이를 확인하기 위해 [실습 3]을 진행하였으나
TCP(HTTP/S)를 사용하는 wget은 정상적으로 NGINX을 응답 받았으나,
UDP/ICMP를 사용하는 traceroute는 요청이 외부로 나가는 현상이 보였습니다.
즉, Private IP를 직접 명시하지 않고 도메인 이름을 사용하는 경우에는 traceroute가 기능하지 않음을 알 수 있었습니다.

[실습 3] wget, traceroute를 이용한 통신 흐름 확인

1. 테스트 앱 배포하기

kubectl create deploy a-app --image=nginx --replicas=2 kubectl run b-app --image=busybox -- sleep 3600 kubectl expose deployment a-app --port=80 --target-port=80 --type=ClusterIP

2. 테스트 앱 확인하기

kubectl get svc,endpoints,deploy,pod -o=custom-columns=TYPE:.kind,NAME:.metadata.name | grep -E "(TYPE|app)"
TYPE NAME Service a-app Endpoints a-app Deployment a-app Pod a-app-57974fdb4-mcz5n Pod a-app-57974fdb4-mv7qn Pod b-app

3. 테스트 앱 요청 전송하기

kubectl exec b-app -- wget -qO- http://a-app.default.svc.cluster.local
... Welcome to nginx!

4. 테스트 앱 요청 경로 확인하기

kubectl exec -it b-app -- traceroute -m 10 a-app.default.svc.cluster.local
traceroute to a-app.default.svc.cluster.local (10.103.129.86), 10 hops max, 46 byte packets 1 10.244.0.1 (10.244.0.1) 0.015 ms 0.014 ms 0.013 ms 2 172.30.1.254 (172.30.1.254) 1.416 ms 1.498 ms 1.327 ms 3 61.77.108.1 (61.77.108.1) 2.935 ms * 19.481 ms 4 125.141.249.140 (125.141.249.140) 2.617 ms 3.073 ms 3.046 ms 5 * * * 6 * * * 7 * * * 8 * * * 9 * * * 10 * * *

5. 테스트 앱 정리하기

kubectl delete deploy/a-app --force kubectl delete pod/b-app --force kubectl delete svc/a-app

결국 세부적인 흐름을 확인하기 위해 [실습 4]를 진행하면서
4-10 과정을 통해 B Pod에서 cluster.local 질의 흐름을 보고
11-15 과정을 통해 B Pod에서 a-app.default.svc.cluster.local 질의 흐름을 보고
16-20 과정을 통해 B Pod에서 A Pod에 도달하기까지 가상 이더넷 페어, 브릿지 등을 볼 수 있었습니다.

풀어서 설명하면 대략적으로 아래와 같은 흐름인 것 같습니다.

  1. 네임서버 위치를 찾는 과정 (nameserver + iptables)

  2. 특정한 앤드포인트를 찾는 과정 (nameserver + iptables)

  3. 특정한 엔드포인트로 요청이 가는 과정

    Pod B netns (eth0)
    → Root netnes (vetha999267c → cni0 → vethedfea565)
    → Pod A netns (eth0)

[실습 4] wget 시 도메인 질의와 네트워크 흐름

1. 테스트 앱 배포하기

kubectl create deploy a-app --image=nginx --replicas=2 kubectl run b-app --image=busybox -- sleep 3600 kubectl expose deployment a-app --port=80 --target-port=80 --type=ClusterIP

2. 테스트 앱 확인하기

kubectl get svc a-app
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE a-app ClusterIP 10.103.129.8680/TCP 52m

3. 테스트 앱 IP 확인하기

kubectl get pods -o=custom-columns=NAME:.metadata.name,IP:.status.podIP | grep -E "(NAME|app)"
NAME IP a-app-57974fdb4-mcz5n 10.244.0.143 a-app-57974fdb4-mv7qn 10.244.0.144 b-app 10.244.0.147

4. b-pod의 /etc/hosts 확인하기
IPv4, IPv6와 도메인 간의 연결고리를 최초로 확인할 수 있습니다.

kubectl exec b-app -- cat /etc/hosts /etc/resolv.conf
# Kubernetes-managed hosts file. 127.0.0.1 localhost ::1 localhost ip6-localhost ip6-loopback fe00::0 ip6-localnet fe00::0 ip6-mcastprefix fe00::1 ip6-allnodes fe00::2 ip6-allrouters 10.244.0.147 b-app

5. b-pod의 /etc/resolv.conf 확인하기
/etc/hosts에 선언되지 않은 경우 네임서버로 질의하도록 설정이 가능합니다.

search default.svc.cluster.local svc.cluster.local cluster.local nameserver 10.96.0.10 options ndots:5

6. b-pod에서 cluster.local에 대한 nslookup 실행해보기
실제로 cluster.local을 호출하면 네임서버 IP가 반환되는지 확인합니다.

kubectl exec -it b-app -- nslookup cluster.local
Server: 10.96.0.10 Address: 10.96.0.10:53

7. 네임서버(CoreDNS) 위치 확인하기
쿠버네티스의 기본 네임서버는 CoreDNS Service의 ClusterIP를 향합니다.

kubectl get svc -A -o=custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,CLUSTER-IP:.spec.clusterIP | grep -E "(NAME|10.96.0.1)"
NAMESPACE NAME CLUSTER-IP default kubernetes 10.96.0.1 kube-system kube-dns 10.96.0.10

8. 네임서버의 패킷 포워딩 경로 확인하기
CoreDNS ClusterIP에 도메인 질의를 하면 패킷 포워딩 체인을 볼 수 있습니다.

sudo iptables -t nat -L KUBE-SERVICES -n | grep 10.96.0.1
KUBE-SVC-JD5MR3NA4I4DYORP tcp -- 0.0.0.0/0 10.96.0.10 /* kube-system/kube-dns:metrics cluster IP */ KUBE-SVC-NPX46M4PTMTKRN6Y tcp -- 0.0.0.0/0 10.96.0.1 /* default/kubernetes:https cluster IP */ KUBE-SVC-TCOU7JCQXEZGVUNU udp -- 0.0.0.0/0 10.96.0.10 /* kube-system/kube-dns:dns cluster IP */ KUBE-SVC-ERIFXISQEP7F7OF4 tcp -- 0.0.0.0/0 10.96.0.10 /* kube-system/kube-dns:dns-tcp cluster IP */

9. 네임서버의 패킷 포워딩 체인 확인하기
패킷 포워딩 체인에는 두 아이피(10.244.0.105, 10.244.0.107)가 보입니다.

sudo iptables -t nat -L KUBE-SVC-JD5MR3NA4I4DYORP -n echo "==================================================" sudo iptables -t nat -L KUBE-SVC-NPX46M4PTMTKRN6Y -n echo "==================================================" sudo iptables -t nat -L KUBE-SVC-TCOU7JCQXEZGVUNU -n echo "==================================================" sudo iptables -t nat -L KUBE-SVC-ERIFXISQEP7F7OF4 -n echo "=================================================="
Chain KUBE-SVC-JD5MR3NA4I4DYORP (1 references) target prot opt source destination KUBE-MARK-MASQ tcp -- !10.244.0.0/16 10.96.0.10 /* kube-system/kube-dns:metrics cluster IP */ KUBE-SEP-3FUHYLOKDTXSLTUI all -- 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:metrics -> 10.244.0.105:9153 */ statistic mode random probability 0.50000000000 KUBE-SEP-LINGYYJVFJ44OFRC all -- 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:metrics -> 10.244.0.107:9153 */ ================================================== Chain KUBE-SVC-NPX46M4PTMTKRN6Y (1 references) target prot opt source destination KUBE-MARK-MASQ tcp -- !10.244.0.0/16 10.96.0.1 /* default/kubernetes:https cluster IP */ KUBE-SEP-XJNE7KJ7C5OK5DHV all -- 0.0.0.0/0 0.0.0.0/0 /* default/kubernetes:https -> 172.30.1.48:6443 */ ================================================== Chain KUBE-SVC-TCOU7JCQXEZGVUNU (1 references) target prot opt source destination KUBE-MARK-MASQ udp -- !10.244.0.0/16 10.96.0.10 /* kube-system/kube-dns:dns cluster IP */ KUBE-SEP-SMSEVC7UC3TMMLEX all -- 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:dns -> 10.244.0.105:53 */ statistic mode random probability 0.50000000000 KUBE-SEP-D2LNDAFJVFQ3SYLN all -- 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:dns -> 10.244.0.107:53 */ ================================================== Chain KUBE-SVC-ERIFXISQEP7F7OF4 (1 references) target prot opt source destination KUBE-MARK-MASQ tcp -- !10.244.0.0/16 10.96.0.10 /* kube-system/kube-dns:dns-tcp cluster IP */ KUBE-SEP-RJ76AAHLMPF4DXXJ all -- 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:dns-tcp -> 10.244.0.105:53 */ statistic mode random probability 0.50000000000 KUBE-SEP-I6P6J5WVH6DMN5I3 all -- 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:dns-tcp -> 10.244.0.107:53 */ ==================================================

10. 네임서버(CoreDNS) 파드 확인하기
두 아이피는 CoreDNS Pod에 할당된 IP와 동일합니다.

kubectl get pods -o=custom-columns=NAME:.metadata.name,IP:.status.podIP -A | grep -E "(NAME|coredns)"
NAME IP coredns-7c65d6cfc9-g2qxv 10.244.0.105 coredns-7c65d6cfc9-sscfz 10.244.0.107

11. b-pod에서 a-app.default.svc.cluster.local에 대한 nslookup 실행해보기
네임서버(CoreDNS, 6번 참고)에 질의해 가상IP(10.103.129.86)를 봤습니다.

kubectl exec -it b-app -- nslookup a-app.default.svc.cluster.local
Server: 10.96.0.10 Address: 10.96.0.10:53 Name: a-app.default.svc.cluster.local Address: 10.103.129.86

12. a-app.default.svc.cluster.local에 대한 패킷 포워딩 경로 확인하기
실제로 iptables에서 포워딩 체인을 향하도록 하는 것을 볼 수 있습니다.

sudo iptables -t nat -L KUBE-SERVICES -n | grep 10.103.129.86
KUBE-SVC-CHTNYTLOWMWCGZWR tcp -- 0.0.0.0/0 10.103.129.86 /* default/a-app cluster IP */

13. a-app.default.svc.cluster.local에 대한 패킷 포워딩 체인 확인하기해당 포워딩 체인에서는 두 아이피(10.244.0.143, 10.244.0.144)가 보입니다.

sudo iptables -t nat -L KUBE-SVC-CHTNYTLOWMWCGZWR -n
Chain KUBE-SVC-CHTNYTLOWMWCGZWR (1 references) target prot opt source destination KUBE-MARK-MASQ tcp -- !10.244.0.0/16 10.103.129.86 /* default/a-app cluster IP */ KUBE-SEP-ZZRQGRW2BIJ6VNXP all -- 0.0.0.0/0 0.0.0.0/0 /* default/a-app -> 10.244.0.143:80 */ statistic mode random probability 0.50000000000 KUBE-SEP-BJVKNWLDIWYBU2WJ all -- 0.0.0.0/0 0.0.0.0/0 /* default/a-app -> 10.244.0.144:80 *

14. a-app 파드 확인하기두 아이피는 a-app Pod에 할당된 IP와 동일합니다.

kubectl get pods -o=custom-columns=NAME:.metadata.name,IP:.status.podIP -A | grep -E "(NAME|a-app)"
NAME IP a-app-57974fdb4-mcz5n 10.244.0.143 a-app-57974fdb4-mv7qn 10.244.0.144

15. a-app-57974fdb4-mcz5n Pod (10.244.0.143)의 tcpdump 확인하기

sudo tcpdump -i any host 10.244.0.143 -n

16. a-app-57974fdb4-mcz5 Pod (10.244.0.144)의 tcpdump 확인하기

sudo tcpdump -i any host 10.244.0.144 -n

17. a-app Service (10.103.129.86)의 tcpdump 확인하기

sudo tcpdump -i any host 10.103.129.86 -n

18. b-app Pod에서 a-app Service의 도메인으로 질의하기

kubectl exec b-app -- wget -qO- http://a-app.default.svc.cluster.local
... Welcome to nginx! ...

19. a-app-* Pod tcpdump 둘 중 하나(15번, 16번)에 패킷 덤프가 기록될 것
b-pod netns eth — root netns vetha999267c
a-pod netns eth — root netns vethedfea565

tcpdump: data link type LINUX_SLL2 tcpdump: verbose output suppressed, use -v[v]... for full protocol decode listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes 14:44:36.756039 cni0 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756048 vetha999267c Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756051 vethedfea565 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756054 veth53932873 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756056 vethf290baee Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756059 veth6bcce521 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756062 veth8a8d0f26 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756064 vethf201ff42 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756067 veth8aa03fe0 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756070 veth396efc3f Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756072 vethd435ec95 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756075 vethb68ca92d Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756078 vethbd42ba02 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756080 vetheeff4d57 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756083 vethab0e2c30 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756086 veth9d176d07 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756089 vethb41c5c9a Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756091 veth86736a4f Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756094 veth725de8a5 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756096 veth672167ea Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756099 veth6b029ff9 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756101 veth17423715 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756104 veth902ab610 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756107 veth73448725 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756109 vethd1954510 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756111 vetha204b856 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756114 vethf751aa00 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756116 vethe6bf6e5d Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756118 veth8385c227 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756121 vethfb36eb94 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756153 vethedfea565 P IP 10.244.0.144.80 > 10.244.0.147.55042: Flags [S.], seq 3569082682, ack 4003348581, win 64308, options [mss 1410,sackOK,TS val 3133631796 ecr 2321197533,nop,wscale 7], length 0 14:44:36.756174 cni0 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [.], ack 1, win 507, options [nop,nop,TS val 2321197533 ecr 3133631796], length 0 14:44:36.756175 vethedfea565 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [.], ack 1, win 507, options [nop,nop,TS val 2321197533 ecr 3133631796], length 0 14:44:36.756216 cni0 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [P.], seq 1:95, ack 1, win 507, options [nop,nop,TS val 2321197533 ecr 3133631796], length 94: HTTP: GET / HTTP/1.1 14:44:36.756218 vethedfea565 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [P.], seq 1:95, ack 1, win 507, options [nop,nop,TS val 2321197533 ecr 3133631796], length 94: HTTP: GET / HTTP/1.1 14:44:36.756222 vethedfea565 P IP 10.244.0.144.80 > 10.244.0.147.55042: Flags [.], ack 95, win 502, options [nop,nop,TS val 3133631796 ecr 2321197533], length 0 14:44:36.756322 vethedfea565 P IP 10.244.0.144.80 > 10.244.0.147.55042: Flags [P.], seq 1:234, ack 95, win 502, options [nop,nop,TS val 3133631796 ecr 2321197533], length 233: HTTP: HTTP/1.1 200 OK 14:44:36.756345 cni0 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [.], ack 234, win 506, options [nop,nop,TS val 2321197534 ecr 3133631796], length 0 14:44:36.756347 vethedfea565 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [.], ack 234, win 506, options [nop,nop,TS val 2321197534 ecr 3133631796], length 0 14:44:36.756363 vethedfea565 P IP 10.244.0.144.80 > 10.244.0.147.55042: Flags [P.], seq 234:849, ack 95, win 502, options [nop,nop,TS val 3133631797 ecr 2321197534], length 615: HTTP 14:44:36.756371 cni0 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [.], ack 849, win 502, options [nop,nop,TS val 2321197534 ecr 3133631797], length 0 14:44:36.756372 vethedfea565 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [.], ack 849, win 502, options [nop,nop,TS val 2321197534 ecr 3133631797], length 0 14:44:36.756398 vethedfea565 P IP 10.244.0.144.80 > 10.244.0.147.55042: Flags [F.], seq 849, ack 95, win 502, options [nop,nop,TS val 3133631797 ecr 2321197534], length 0 14:44:36.756443 cni0 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [F.], seq 95, ack 850, win 502, options [nop,nop,TS val 2321197534 ecr 3133631797], length 0 14:44:36.756447 vethedfea565 Out IP 10.244.0.147.55042 > 10.244.0.144.80: Flags [F.], seq 95, ack 850, win 502, options [nop,nop,TS val 2321197534 ecr 3133631797], length 0 14:44:36.756457 vethedfea565 P IP 10.244.0.144.80 > 10.244.0.147.55042: Flags [.], ack 96, win 502, options [nop,nop,TS val 3133631797 ecr 2321197534], length 0 14:44:42.105516 cni0 Out ARP, Request who-has 10.244.0.144 tell 10.244.0.1, length 28 14:44:42.105533 vethedfea565 Out ARP, Request who-has 10.244.0.144 tell 10.244.0.1, length 28 14:44:42.105644 vethedfea565 P ARP, Request who-has 10.244.0.147 tell 10.244.0.144, length 28 14:44:42.105658 vetha999267c Out ARP, Request who-has 10.244.0.147 tell 10.244.0.144, length 28 14:44:42.105696 vethedfea565 P ARP, Reply 10.244.0.144 is-at 52:13:49:ee:00:09, length 28 14:44:42.105701 cni0 In ARP, Reply 10.244.0.144 is-at 52:13:49:ee:00:09, length 28 14:44:42.105717 vetha999267c P ARP, Reply 10.244.0.147 is-at 56:c4:29:ee:b8:b5, length 28 14:44:42.105720 vethedfea565 Out ARP, Reply 10.244.0.147 is-at 56:c4:29:ee:b8:b5, length 28

20. a-app Service로 향하는 tcpdump 확인하기

tcpdump: data link type LINUX_SLL2 tcpdump: verbose output suppressed, use -v[v]... for full protocol decode listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes 14:44:36.756026 vetha999267c P IP 10.244.0.147.55042 > 10.103.129.86.80: Flags [S], seq 4003348580, win 64860, options [mss 1410,sackOK,TS val 2321197533 ecr 0,nop,wscale 7], length 0 14:44:36.756161 vetha999267c Out IP 10.103.129.86.80 > 10.244.0.147.55042: Flags [S.], seq 3569082682, ack 4003348581, win 64308, options [mss 1410,sackOK,TS val 3133631796 ecr 2321197533,nop,wscale 7], length 0 14:44:36.756171 vetha999267c P IP 10.244.0.147.55042 > 10.103.129.86.80: Flags [.], ack 1, win 507, options [nop,nop,TS val 2321197533 ecr 3133631796], length 0 14:44:36.756213 vetha999267c P IP 10.244.0.147.55042 > 10.103.129.86.80: Flags [P.], seq 1:95, ack 1, win 507, options [nop,nop,TS val 2321197533 ecr 3133631796], length 94: HTTP: GET / HTTP/1.1 14:44:36.756224 vetha999267c Out IP 10.103.129.86.80 > 10.244.0.147.55042: Flags [.], ack 95, win 502, options [nop,nop,TS val 3133631796 ecr 2321197533], length 0 14:44:36.756334 vetha999267c Out IP 10.103.129.86.80 > 10.244.0.147.55042: Flags [P.], seq 1:234, ack 95, win 502, options [nop,nop,TS val 3133631796 ecr 2321197533], length 233: HTTP: HTTP/1.1 200 OK 14:44:36.756342 vetha999267c P IP 10.244.0.147.55042 > 10.103.129.86.80: Flags [.], ack 234, win 506, options [nop,nop,TS val 2321197534 ecr 3133631796], length 0 14:44:36.756366 vetha999267c Out IP 10.103.129.86.80 > 10.244.0.147.55042: Flags [P.], seq 234:849, ack 95, win 502, options [nop,nop,TS val 3133631797 ecr 2321197534], length 615: HTTP 14:44:36.756369 vetha999267c P IP 10.244.0.147.55042 > 10.103.129.86.80: Flags [.], ack 849, win 502, options [nop,nop,TS val 2321197534 ecr 3133631797], length 0 14:44:36.756401 vetha999267c Out IP 10.103.129.86.80 > 10.244.0.147.55042: Flags [F.], seq 849, ack 95, win 502, options [nop,nop,TS val 3133631797 ecr 2321197534], length 0 14:44:36.756438 vetha999267c P IP 10.244.0.147.55042 > 10.103.129.86.80: Flags [F.], seq 95, ack 850, win 502, options [nop,nop,TS val 2321197534 ecr 3133631797], length 0 14:44:36.756462 vetha999267c Out IP 10.103.129.86.80 > 10.244.0.147.55042: Flags [.], ack 96, win 502, options [nop,nop,TS val 3133631797 ecr 2321197534], length 0

21. 테스트 앱 정리하기

kubectl delete deploy/a-app --force kubectl delete pod/b-app --force kubectl delete svc/a-app

위의 1, 2번을 주황색으로 표현하였고 3번은 붉은 색으로 표현하였습니다.
(물론 실제로 네임서버 질의 또한 아래와 같은 순서대로 진행 될 것입니다.)
(순서 : Pod netns → Root netns veth, cni, veth → Pod netns)

[증명] 단일 노드 내, 파드에서서비스로 통신

A.3. 파드에서 헤드리스 서비스로 통신

단일 호스트 내 파드(Pod)에서 헤드리스 서비스(StatefulSet)로 네트워크 통신

데이터베이스 등의 특징을 가진 앱의 경우,
헤드리스 서비스*를 사용하여 파드 별 도메인을 사용할 수 있습니다.

  • {name}-{ordinal}.{service}.{namespace}.svc.cluster.local

  • a-app-0.a-app.default.svc.cluster.local

⚠️

[헤드리스 서비스 생성 방법*] StatefulSet의 .spec.serviceName을 명시 Service의 .spec.type을 ClusterIP로 .spec.clusterIP는 None으로 명시

공식문서[A.3-1]에서는 헤드리스 서비스는 로드 밸런싱을 하지 않는다고 설명합니다.

⚠️

헤드리스 서비스의 경우, 클러스터 IP가 할당되지 않고, kube-proxy가 이러한 서비스를 처리하지 않으며, 플랫폼에 의해 로드 밸런싱 또는 프록시를 하지 않는다. DNS가 자동으로 구성되는 방법은 서비스에 셀렉터가 정의되어 있는지 여부에 달려있다.

하지만 헤드리스 서비스도 로드밸런싱을 지원함을 [실습 5]에서 볼 수 있습니다.
이것이 특정 버전, 기능에서 발생하는 문제인지는 다루지 않고 있습니다.

[실습 5] 헤드리스 서비스의 로드 밸런싱 검증

1. 테스트 앱 배포하기

mkdir -p ~/network-test cd ~/network-test cat <statefulset.yaml --- apiVersion: apps/v1 kind: StatefulSet metadata: name: a-app labels: app: a-app spec: selector: matchLabels: app: a-app serviceName: "a-app" replicas: 3 template: metadata: labels: app: a-app spec: containers: - name: my-container image: nginx:1.20 ports: - containerPort: 80 name: web --- apiVersion: v1 kind: Service metadata: name: a-app labels: app: a-app spec: clusterIP: None # 헤드리스 서비스로 만들기 위한 설정 selector: app: a-app ports: - port: 80 targetPort: web name: http EOF kubectl apply -f statefulset.yaml kubectl run b-app --image=busybox -- sleep 3600

2. 테스트 앱 확인하기

kubectl get svc,statefulset,pod | grep -E "(NAME|-app)"
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/a-app ClusterIP None none 80/TCP 2d7h NAME READY AGE statefulset.apps/a-app 3/3 2d7h NAME READY STATUS RESTARTS AGE pod/a-app-0 1/1 Running 0 2d7h pod/a-app-1 1/1 Running 0 2d7h pod/a-app-2 1/1 Running 0 2d7h pod/b-app 1/1 Running 55 (22m ago) 2d7h

3. a-app-0 로그 감시하기(tail)

kubectl logs a-app-0 -f

4. a-app-1 로그 감시하기(tail)

kubectl logs a-app-1 -f

5. a-app-2 로그 감시하기(tail)

kubectl logs a-app-2 -f

6. a-app 앤드포인트로 요청 전송하기

kubectl exec b-app -- wget -qO- a-app.default.svc.cluster.local kubectl exec b-app -- wget -qO- a-app.default.svc.cluster.local kubectl exec b-app -- wget -qO- a-app.default.svc.cluster.local

7. 3,4,5번 터미널의 로그를 확인하면 로드 밸런싱이 기능함을 알 수 있습니다.


8. 테스트 앱 삭제하기

kubectl delete -f statefulset.yaml kubectl delete pod/b-app

헤드리스 서비스가 파드 별 하위 도메인을 제공함을 [실습 6]에서 확인했습니다.
동시에 a-app.default.svc.cluster.local에서 차이점을 발견할 수 있었습니다.

  1. Service : nslookup의 결과 ClusterIP를 반환

  2. Headless Service : nslookup의 결과 PodIP를 반환

[실습 6] 헤드리스 서비스의 하위 도메인

1. 테스트 앱 배포하기

mkdir -p ~/network-test cd ~/network-test cat <statefulset.yaml --- apiVersion: apps/v1 kind: StatefulSet metadata: name: a-app labels: app: a-app spec: selector: matchLabels: app: a-app serviceName: "a-app" replicas: 3 template: metadata: labels: app: a-app spec: containers: - name: my-container image: nginx:1.20 ports: - containerPort: 80 name: web --- apiVersion: v1 kind: Service metadata: name: a-app labels: app: a-app spec: clusterIP: None # 헤드리스 서비스로 만들기 위한 설정 selector: app: a-app ports: - port: 80 targetPort: web name: http EOF kubectl apply -f statefulset.yaml

2. 테스트 앱 확인하기

kubectl get svc,statefulset,pod | grep -E "(NAME|-app)"
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/a-app ClusterIP None none 80/TCP 2d7h NAME READY AGE statefulset.apps/a-app 3/3 2d7h NAME READY STATUS RESTARTS AGE pod/a-app-0 1/1 Running 0 2d7h pod/a-app-1 1/1 Running 0 2d7h pod/a-app-2 1/1 Running 0 2d7h

3. nslookup 확인하기

kubectl run nslookup-pod-0 --image=busybox -it --rm -- nslookup a-app-0.a-app.default.svc.cluster.local kubectl run nslookup-pod-1 --image=busybox -it --rm -- nslookup a-app-1.a-app.default.svc.cluster.local kubectl run nslookup-pod-2 --image=busybox -it --rm -- nslookup a-app-2.a-app.default.svc.cluster.local kubectl run nslookup-pod --image=busybox -it --rm -- nslookup a-app.default.svc.cluster.local
kubectl logs nslookup-pod-0 kubectl logs nslookup-pod-1 kubectl logs nslookup-pod-2 kubectl logs nslookup-pod
# nslookup-pod-0 # nslookup a-app-0.a-app.default.svc.cluster.local Server: 10.96.0.10 Address: 10.96.0.10:53 Name: a-app-0.a-app.default.svc.cluster.local # nslookup-pod-1 # nslookup a-app-1.a-app.default.svc.cluster.local Server: 10.96.0.10 Address: 10.96.0.10:53 Name: a-app-1.a-app.default.svc.cluster.local Address: 10.244.0.221 # nslookup-pod-2 # nslookup a-app-2.a-app.default.svc.cluster.local Server: 10.96.0.10 Address: 10.96.0.10:53 Name: a-app-2.a-app.default.svc.cluster.local Address: 10.244.0.222 # nslookup-pod # nslookup a-app.default.svc.cluster.local Server: 10.96.0.10 Address: 10.96.0.10:53 Name: a-app.default.svc.cluster.local Address: 10.244.0.221 Name: a-app.default.svc.cluster.local Address: 10.244.0.222 Name: a-app.default.svc.cluster.local Address: 10.244.0.219

4. dig 확인하기

kubectl run dig-pod-0 --image=tutum/dnsutils -it --rm -- dig a-app-0.a-app.default.svc.cluster.local kubectl run dig-pod-1 --image=tutum/dnsutils -it --rm -- dig a-app-1.a-app.default.svc.cluster.local kubectl run dig-pod-2 --image=tutum/dnsutils -it --rm -- dig a-app-2.a-app.default.svc.cluster.local kubectl run dig-pod --image=tutum/dnsutils -it --rm -- dig a-app.default.svc.cluster.local
kubectl logs dig-pod-0 kubectl logs dig-pod-1 kubectl logs dig-pod-2 kubectl logs dig-pod
# dig-pod-0 # dig a-app-0.a-app.default.svc.cluster.local ; <<>> DiG 9.9.5-3ubuntu0.2-Ubuntu <<>> a-app-0.a-app.default.svc.cluster.local ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 56600 ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; WARNING: recursion requested but not available ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;a-app-0.a-app.default.svc.cluster.local. IN A ;; ANSWER SECTION: a-app-0.a-app.default.svc.cluster.local. 26 IN A 10.244.0.219 ;; Query time: 0 msec ;; SERVER: 10.96.0.10#53(10.96.0.10) ;; WHEN: Sun Mar 30 13:33:56 UTC 2025 ;; MSG SIZE rcvd: 123 # dig-pod-1 # dig a-app-1.a-app.default.svc.cluster.local ; <<>> DiG 9.9.5-3ubuntu0.2-Ubuntu <<>> a-app-1.a-app.default.svc.cluster.local ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 15398 ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; WARNING: recursion requested but not available ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;a-app-1.a-app.default.svc.cluster.local. IN A ;; ANSWER SECTION: a-app-1.a-app.default.svc.cluster.local. 30 IN A 10.244.0.221 ;; Query time: 0 msec ;; SERVER: 10.96.0.10#53(10.96.0.10) ;; WHEN: Sun Mar 30 13:34:28 UTC 2025 ;; MSG SIZE rcvd: 123 # dig-pod-2 # dig a-app-2.a-app.default.svc.cluster.local ; <<>> DiG 9.9.5-3ubuntu0.2-Ubuntu <<>> a-app-2.a-app.default.svc.cluster.local ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29238 ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; WARNING: recursion requested but not available ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;a-app-2.a-app.default.svc.cluster.local. IN A ;; ANSWER SECTION: a-app-2.a-app.default.svc.cluster.local. 25 IN A 10.244.0.222 ;; Query time: 0 msec ;; SERVER: 10.96.0.10#53(10.96.0.10) ;; WHEN: Sun Mar 30 13:35:29 UTC 2025 ;; MSG SIZE rcvd: 123 # dig-pod # dig a-app.default.svc.cluster.local ; <<>> DiG 9.9.5-3ubuntu0.2-Ubuntu <<>> a-app.default.svc.cluster.local ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29717 ;; flags: qr aa rd; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1 ;; WARNING: recursion requested but not available ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;a-app.default.svc.cluster.local. IN A ;; ANSWER SECTION: a-app.default.svc.cluster.local. 30 IN A 10.244.0.222 a-app.default.svc.cluster.local. 30 IN A 10.244.0.219 a-app.default.svc.cluster.local. 30 IN A 10.244.0.221 ;; Query time: 0 msec ;; SERVER: 10.96.0.10#53(10.96.0.10) ;; WHEN: Sun Mar 30 13:35:55 UTC 2025 ;; MSG SIZE rcvd: 201

5. 테스트 앱 정리하기

kubectl delete -f statefulset.yaml --force

ClusterIP는 논리적 NAT으로서 kube-proxy가 생성함을 [실습 4]에서 배웠습니다.
따라서 a-app.default.svc.cluster.local의 네트워크 흐름 또한 차이가 발생합니다.

  1. Service : CoreDNS → Kube-Proxy(iptables) → Pod

  2. Headless Service : CoreDNS → Pod

[증명] 단일 노드 내, 파드에서 헤드리스 서비스로 통신

참고 문서

  1. T-Story (구구달스) — [2주차] 쿠버네티스에서 파드 간 통신은 어떻게 작동하나요?

  2. Medium (Hyukjun, Nam) — Kubernetes Network에 대한 이해

  3. 요즘 IT (Finda) — 쿠버네티스(Kubernetes) 네트워크 정리

  4. T-Story (집주변이 최고야) — [쿠버네티스] 쿠버네티스 스테이트풀셋 (StatefulSet) 소개 및 관리

  5. T-Story (악분) — 쿠버네티스 Headless 서비스

Share article

Unchaptered