파드 네트워크 이해하기
삽질을 통해 파드 네트워크 이해하기
여기서는 쿠버네티스 네트워크 중 파드의 네트워크에 대해 정리한다. 네트워크 관련 포스팅은 여기에서 확인할 수 있습니다.
Pod Network
파드에서 사용되는 네트워크 모델에 대해 알아본다. 파드 네트워크를 담당하는 것은 CNI이며 CNI 구현체에 따라 네트워크 동작방식이 다르다. k3s를 통해 실습을 진행했기에 Flannel을 기준으로 진행한다.(k3s이기에 세부적인 내용은 k8s와 다를 수 있습니다.) 앞으로 “컨테이너 통신”, “같은 노드 내의 파드 통신”, “다른 노드에 위치하는 파드 간 통신”에 대해 순차적으로 정리해본다.
파드 내부 통신(Container 내부 네트워크 확인)
CNI에 의해 각 파드에 고유한 IP 주소가 할당된다. 네트워크 인터페이스도 생성된다. Docker와 다르게 파드는 내부의 모든 컨테이너는 IP 주소와 네트워크 포트를 포함한 모든 네트워크 스택을 공유한다. 즉, 파드 내부의 컨테이너는 LocalHost를 통해 서로 통신할 수 있다. 그렇기에 만약 같은 포트를 사용하는 컨테이너를 중복 생성하면 오류가 발생한다.
파드 내의 컨테이너를 외부로 노출하기 위해서 Container Port를 사용한다. 해당 포트를 통해 외부와 통신할 수 있다.
파드 생성
1. network.yml 파일을 생성한다.
1
vi network.yml
2. 아래의 내용을 복사한 뒤 저장한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
name: test
spec:
containers:
- name: debug
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
- name: web
image: nginx:1.14.2
ports:
- containerPort: 80
3. 파일을 이용하여 파드를 생성한다.
1
2
$ kubectl create -f test.yml
pod/test created
4. 명령어를 통해 생성된 파드를 확인한다.
1
2
3
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
test 2/2 Running 0 16s 10.42.1.36 ip-10-32-7-224.ap-northeast-2.compute.internal <none> <none>
테스트
이제 debug 컨테이너에 접속하여, 컨테이너끼리 정말 네트워크를 공유하고 있는지 확인한다.
1. debug 컨테이너에 접속한다.
1
2
3
4
5
6
7
8
9
10
$ kubectl exec test -it -c debug -- zsh
dP dP dP
88 88 88
88d888b. .d8888b. d8888P .d8888b. 88d888b. .d8888b. .d8888b. d8888P
88' `88 88ooood8 88 Y8ooooo. 88' `88 88' `88 88' `88 88
88 88 88. ... 88 88 88 88 88. .88 88. .88 88
dP dP `88888P' dP `88888P' dP dP `88888P' `88888P' dP
Welcome to Netshoot! (github.com/nicolaka/netshoot)
Version: 0.11
2. netstat
명령어를 통해 포트를 확인한다.
1
2
3
4
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:56286 127.0.0.1:80 TIME_WAIT -
Nginx가 사용하는 port::80를 확인할 수 있습니다.
3. Web 컨테이너에 접속하여 포트를 추가로 확장한다.
1
kubectl exec test -it -c web -- /bin/bash
추가적으로 8080포트도 개방한다.
1
echo 'server { listen 8080; }' > /etc/nginx/conf.d/default.conf && nginx
4. debug 컨테이너로 돌아와서, 네트워크 구성을 확인한다.
web 컨테이너의 변경사항이 같이 적용된 것을 확인할 수 있다.
1
2
3
4
5
$ kubectl exec test -it -c debug -- netstat -anp
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
추가적으로 localhost를 통해 Nginx를 호출하면 정상적으로 통신되는 것을 확인할 수 있다.
1
2
3
4
5
6
7
8
9
10
$ curl localhost:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
이와 같이 파드내의 컨테이너들은 네트워크 스택을 공유하는 것을 확인할 수 있다. 이제 테스트가 끝났으니, 파드를 삭제한다.
1
2
$ kubectl delete -f test.yml
pod "test" deleted
같은 노드 파드 통신
같은 노드 내의 파드는 브릿지 형태로 연결되어있다. 아래의 그림과 같이 각 파드에는 veth0이 하나씩 붙어있으며 해당 이더넷은 cni0을 통해 브릿지 형태로 연결되어있다. (Calico는 브릿지 형태가 아닌 터미널 형태로 통신을 지원한다.)
[출처: https://medium.com/finda-tech/kubernetes-네트워크-정리-fccd4fd0ae6]
네트워크 구성 확인
우선 워커노드에 접속하여 네트워크 구성을 살펴본다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP group default qlen 1000
link/ether 02:97:bb:b7:70:10 brd ff:ff:ff:ff:ff:ff
inet 10.32.7.224/20 brd 10.32.15.255 scope global dynamic eth0
valid_lft 2443sec preferred_lft 2443sec
inet6 fe80::97:bbff:feb7:7010/64 scope link
valid_lft forever preferred_lft forever
3: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8951 qdisc noqueue state UNKNOWN group default
link/ether 2e:98:28:11:0b:8e brd ff:ff:ff:ff:ff:ff
inet 10.42.1.0/32 scope global flannel.1
valid_lft forever preferred_lft forever
inet6 fe80::2c98:28ff:fe11:b8e/64 scope link
valid_lft forever preferred_lft forever
4: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8951 qdisc noqueue state UP group default qlen 1000
link/ether 22:dd:de:d2:ec:55 brd ff:ff:ff:ff:ff:ff
inet 10.42.1.1/24 brd 10.42.1.255 scope global cni0
valid_lft forever preferred_lft forever
inet6 fe80::20dd:deff:fed2:ec55/64 scope link
valid_lft forever preferred_lft forever
5: vetha2194963@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8951 qdisc noqueue master cni0 state UP group default
link/ether a2:d8:d0:e5:8e:59 brd ff:ff:ff:ff:ff:ff link-netns cni-3cb42f8b-53d8-58ac-1f0a-b379faa755c5
inet6 fe80::a0d8:d0ff:fee5:8e59/64 scope link
valid_lft forever preferred_lft forever
우리가 여기서 확인할 부분은 3번부터 5번이다. 1번과 2번은 모든 컴퓨터에 있는 기본적인 인터페이스이며, 각 요소에 대해 설명은 다음과 같다.
💡 With ChatGPT-4
lo
: 루프백 인터페이스입니다. 루프백 인터페이스는 컴퓨터가 자기 자신과 통신하는 데 사용되며, 주로 네트워크 소프트웨어 테스트에 사용됩니다.
eth0
: 첫 번째 이더넷 인터페이스를 나타냅니다. 이 인터페이스는 일반적으로 물리적 네트워크 연결에 사용됩니다.
flannel.1
: flannel 네트워크 인터페이스입니다. 오버레이 네트워크 솔루션 중 하나로, 다른 노드의 파드와 통신할 수 있게 합니다.
cni0
: Container Network Interface (CNI) 네트워크 인터페이스입니다.
vetha2194963@if2
: 이것은 가상 이더넷 (veth) 페어 중 하나입니다. veth 페어는 두 개의 가상 네트워크 인터페이스로 구성되며, 하나는 컨테이너 내부에, 다른 하나는 컨테이너 외부에 위치합니다. 이들은 서로에게 패킷을 전달하여 컨테이너와 외부 세계 사이의 네트워크 통신을 가능하게 합니다. 이 인터페이스는cni0
에 연결되어 있습니다.
1
2
3
4
5
1. `lo`: 루프백 인터페이스입니다. 루프백 인터페이스는 컴퓨터가 자기 자신과 통신하는 데 사용되며, 주로 네트워크 소프트웨어 테스트에 사용됩니다.
2. `eth0`: 첫 번째 이더넷 인터페이스를 나타냅니다. 이 인터페이스는 일반적으로 물리적 네트워크 연결에 사용됩니다.
3. `flannel.1`: flannel 네트워크 인터페이스입니다. 오버레이 네트워크 솔루션 중 하나로, 다른 노드의 파드와 통신할 수 있게 합니다.
4. `cni0`: Container Network Interface (CNI) 네트워크 인터페이스입니다.
5. `vetha2194963@if2`: 이것은 가상 이더넷 (veth) 페어 중 하나입니다. veth 페어는 두 개의 가상 네트워크 인터페이스로 구성되며, 하나는 컨테이너 내부에, 다른 하나는 컨테이너 외부에 위치합니다. 이들은 서로에게 패킷을 전달하여 컨테이너와 외부 세계 사이의 네트워크 통신을 가능하게 합니다. 이 인터페이스는 `cni0`에 연결되어 있습니다.
위의 그림에서 확인한 veth과 cni0을 확인할 수 있다. 5번에서 자세히 확인하면 “master cni0”를 볼 수 있다. 이는 일반적으로 cni0과 연결되어 있고 네트워크 브릿지를 나타낸다. 리눅스 브릿지 VLAN 구성을 출력하는 bridge vlan
명령어를 통해 확인하면 두 인터페이스가 브릿지 형태로 연결된 것을 볼 수 있다.
1
2
3
4
$ bridge vlan
port vlan-id
cni0 1 PVID Egress Untagged
vetha2194963 1 PVID Egress Untagged
veth은 각 파드에 위치하며, cni를 통해 각 veth은 브릿지 형태로 연결되어있다. 위의 veth도 어떤 파드에 연결되어있습니다. 어떤 파드인지 확인한다. 아래와 같이 워커노드에 배포된 파드를 확인할 수 있다. 이는 모든 노드에 배포되는 프록시 서버역할을 하는 파드이다.
1
2
3
4
$ k get pods -A -o wide
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
kube-system svclb-traefik-549b62e4-dqvvz 2/2 Running 6 (97m ago) 18h 10.42.1.34 ip-10-32-7-224.ap-northeast-2.compute.internal <none> <none>
통신 확인
이제 실제로 파드를 배포하고, 통신을 테스트한다.
파일을 생성하고 내용을 복사한 뒤 저장한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ vi network-pod.yml
apiVersion: v1
kind: Pod
metadata:
name: pod1
spec:
containers:
- name: test1
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
---
apiVersion: v1
kind: Pod
metadata:
name: pod2
spec:
containers:
- name: test2
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
파드를 생성한다.
1
2
3
$ kubectl network-pod.yml
pod/pod1 created
pod/pod2 created
파드의 생성유무를 확인한다.
1
2
3
4
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
pod2 1/1 Running 0 94s
pod1 1/1 Running 0 94s
tcpdump
를 통해 워커노드의 veth 두개와 cni0의 내용을 확인한다.
먼저, 하나의 파드에 접속하여 다른 파드로 ping 통신을 시도한다.
1
2
3
4
5
6
$ ping -c 4 10.42.1.38
PING 10.42.1.38 (10.42.1.38) 56(84) bytes of data.
64 bytes from 10.42.1.38: icmp_seq=1 ttl=64 time=0.102 ms
64 bytes from 10.42.1.38: icmp_seq=2 ttl=64 time=0.080 ms
64 bytes from 10.42.1.38: icmp_seq=3 ttl=64 time=0.087 ms
64 bytes from 10.42.1.38: icmp_seq=4 ttl=64 time=0.088 ms
ping을 보내는 동안, 워커노드의 cni에서 확인되는 패킷을 확인하면 아래와 같이 Ping 통신이 이뤄지는 것을 확인할 수 있다. 이를 통해 cni0을 통해 각 veth가 통신이 이뤄진다는 것을 다시한번 알 수 있다.
1
2
3
4
5
6
7
8
9
$ tcpdump -i cni0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on cni0, link-type EN10MB (Ethernet), capture size 262144 bytes
02:54:42.086081 ARP, Request who-has ip-10-42-1-38.ap-northeast-2.compute.internal tell ip-10-42-1-37.ap-northeast-2.compute.internal, length 28
02:54:42.086099 ARP, Reply ip-10-42-1-38.ap-northeast-2.compute.internal is-at 6a:74:6a:77:30:3a (oui Unknown), length 28
02:54:42.086127 IP ip-10-42-1-37.ap-northeast-2.compute.internal > ip-10-42-1-38.ap-northeast-2.compute.internal: ICMP echo request, id 5, seq 1, length 64
02:54:42.086138 IP ip-10-42-1-38.ap-northeast-2.compute.internal > ip-10-42-1-37.ap-northeast-2.compute.internal: ICMP echo reply, id 5, seq 1, length 64
02:54:43.097596 IP ip-10-42-1-37.ap-northeast-2.compute.internal > ip-10-42-1-38.ap-northeast-2.compute.internal: ICMP echo request, id 5, seq 2, length 64
02:54:43.097650 IP ip-10-42-1-38.ap-northeast-2.compute.internal > ip-10-42-1-37.ap-northeast-2.compute.internal: ICMP echo reply, id 5, seq 2, length 64
테스트가 끝났으니 파드를 삭제한다.
1
2
3
$ kubectl delete -f network-pod.yml
pod "pod1" deleted
pod "pod2" deleted
다른 노드에 위치하는 파드 간 통신
파드는 Private IP로 노드의 IP 네트워크와 다르기에 다른 노드에 존재한 파드와 통신하려면 노드의 대역폭을 한번 거쳐가야 한다. 같은 노드 상의 파드와 통신하는 건 문제가 없어도, 다른 노드 상의 파드와 통신하는 건 문제가 된다. 이를 위해 터널링 기술을 사용한다. 다른 노드에 위치한 파드에게 트래픽을 전송할 땐 터널링을 통해 기존의 src/dst를 숨기고 노드의 IP를 src/dst로 트래픽을 감싼다. 이 과정을 CNI가 진행하며, 캡슐화를 통해 파드의 트래픽이 호스트 네트워크로 전송되며, 목적지 노드에 위치한 CNI에 의해 디캡슐화를 진행한 후 목적지 파드로 전달된다.
[출처: https://www.sobyte.net/post/2023-02/k8s-openshift/]
파드를 배포한다.
각 파드는 서로 다른 노드(컨트롤 플레인, 워커노드)에 위치해야 한다. nodeName을 통해 각 파드의 노드를 지정해준다. 아래에서 ‘nodeName’에 해당하는 부분을 환경에 맞게 수정한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ vi network-node.yml
apiVersion: v1
kind: Pod
metadata:
name: pod1
spec:
nodeName: ip-10-32-7-224.ap-northeast-2.compute.internal
containers:
- name: test1
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
---
apiVersion: v1
kind: Pod
metadata:
name: pod2
spec:
nodeName: ip-10-32-11-62.ap-northeast-2.compute.internal
containers:
- name: test2
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
파일을 이용하여 파드를 생성한다.
1
2
3
$ kubectl create -f network-node.yml
pod/pod1 created
pod/pod2 created
파드의 정보를 확인한다.
1
2
3
4
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod2 1/1 Running 0 5s 10.42.0.37 ip-10-32-11-62.ap-northeast-2.compute.internal <none> <none>
pod1 1/1 Running 0 5s 10.42.1.39 ip-10-32-7-224.ap-northeast-2.compute.internal <none> <none>
네트워크 구성을 확인한다.
네트워크 구성 중 flannel.1 인터페이스의 정보를 다시 한번 확인한다.
1
2
3
4
5
6
7
$ ip addr show
3: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8951 qdisc noqueue state UNKNOWN group default
link/ether 2e:98:28:11:0b:8e brd ff:ff:ff:ff:ff:ff
inet 10.42.1.0/32 scope global flannel.1
valid_lft forever preferred_lft forever
inet6 fe80::2c98:28ff:fe11:b8e/64 scope link
valid_lft forever preferred_lft forever
컨트롤 플레인의 라우팅 테이블을 확인한다. 10.42.1.0/24
, 워커노드에 위치한 파드 네트워크 대역은 flannel.1 인터페이스로 라우팅한다.
1
2
3
4
5
6
7
8
9
$ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 10.32.0.1 0.0.0.0 UG 0 0 0 eth0
10.32.0.0 0.0.0.0 255.255.240.0 U 0 0 0 eth0
10.42.0.0 0.0.0.0 255.255.255.0 U 0 0 0 cni0
10.42.1.0 10.42.1.0 255.255.255.0 UG 0 0 0 flannel.1
169.254.169.254 0.0.0.0 255.255.255.255 UH 0 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
워커노드의 라우팅 테이블을 확인한다. 유사하게 10.42.0.0/24
컨트롤 플레인의 파드 네트워크 대역은 flannel.1 인터페이스로 라우팅한다.
1
2
3
4
5
6
7
8
$ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 10.32.0.1 0.0.0.0 UG 0 0 0 eth0
10.32.0.0 0.0.0.0 255.255.240.0 U 0 0 0 eth0
10.42.0.0 10.42.0.0 255.255.255.0 UG 0 0 0 flannel.1
10.42.1.0 0.0.0.0 255.255.255.0 U 0 0 0 cni0
169.254.169.254 0.0.0.0 255.255.255.255 UH 0 0 0 eth0
이제 파드끼리 통신을 진행한다.
우선, pod2(컨트롤 플레인에 위치)에 접속하여 pod1의 ip로 ping
을 보낸다.
1
2
kubectl exec pod2 -it -- zsh
ping -c 4 10.42.1.33
이전과는 다르게, 응답이 오지않고 패킷을 잃는다. flannel이 포장하는 과정에서 별도의 포트를 사용하기 때문인데, 포트를 확인하고 보안그룹에서 해당 포트를 개방한다.
1
2
3
4
5
$ netstat -a | grep udp
udp 0 0 0.0.0.0:bootpc 0.0.0.0:*
udp 0 0 0.0.0.0:sunrpc 0.0.0.0:*
udp 0 0 0.0.0.0:otv 0.0.0.0:*
...
여기서 otv(overlay Transport Virtualization)
서비스의 정보를 확인하면 8472 UDP 포트를 사용하는 것을 확인할 수 있다.
1
2
3
$ grep 'otv' /etc/services
otv 8472/tcp # Overlay Transport Virtualization (OTV)
otv 8472/udp # Overlay Transport Virtualization (OTV)
AWS 보안그룹에서, 아래와 사진과 같이 포트를 개방한다.
설정을 완료한 후, 다시 Ping 테스트를 진행합니다. 정상적으로 작동하는 것을 확인할 수 있다.
1
2
3
4
5
6
7
8
9
10
$ ping -c 4 10.42.1.39
PING 10.42.1.39 (10.42.1.39) 56(84) bytes of data.
64 bytes from 10.42.1.39: icmp_seq=1 ttl=62 time=0.655 ms
64 bytes from 10.42.1.39: icmp_seq=2 ttl=62 time=0.541 ms
64 bytes from 10.42.1.39: icmp_seq=3 ttl=62 time=0.629 ms
64 bytes from 10.42.1.39: icmp_seq=4 ttl=62 time=0.552 ms
--- 10.42.1.39 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3065ms
rtt min/avg/max/mdev = 0.541/0.594/0.655/0.048 ms
컨트롤 노드의 eth0 인터페이스에서 UDP 8472로 빠져나가는 패킷을 확인해본다.
아래와 같이 컨트롤 노드의 파드에서 워커 노드의 파드로 패킷이 전송되고 있다. 이를 통해 오버레이 가상화 기술로 서로 다른 노드에 위치한 파드끼리 통신을 진행한다는 것을 한번 더 확인할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ tcpdump -i eth0 -nn udp port 8472
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
14:12:33.865018 IP 10.32.11.62.41373 > 10.32.7.224.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP 10.42.0.37 > 10.42.1.39: ICMP echo request, id 4, seq 1, length 64
14:12:33.865543 IP 10.32.7.224.51951 > 10.32.11.62.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP 10.42.1.39 > 10.42.0.37: ICMP echo reply, id 4, seq 1, length 64
14:12:34.881738 IP 10.32.11.62.41373 > 10.32.7.224.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP 10.42.0.37 > 10.42.1.39: ICMP echo request, id 4, seq 2, length 64
14:12:34.882245 IP 10.32.7.224.51951 > 10.32.11.62.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP 10.42.1.39 > 10.42.0.37: ICMP echo reply, id 4, seq 2, length 64
14:12:35.905774 IP 10.32.11.62.41373 > 10.32.7.224.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP 10.42.0.37 > 10.42.1.39: ICMP echo request, id 4, seq 3, length 64
14:12:35.906901 IP 10.32.7.224.51951 > 10.32.11.62.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP 10.42.1.39 > 10.42.0.37: ICMP echo reply, id 4, seq 3, length 64
14:12:36.907095 IP 10.32.11.62.41373 > 10.32.7.224.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP 10.42.0.37 > 10.42.1.39: ICMP echo request, id 4, seq 4, length 64
14:12:36.907617 IP 10.32.7.224.51951 > 10.32.11.62.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP 10.42.1.39 > 10.42.0.37: ICMP echo reply, id 4, seq 4, length 64
참고
https://medium.com/finda-tech/kubernetes-네트워크-정리-fccd4fd0ae6