안녕하세요.
Private Cloud를 메인으로 IaaS, PaaS, DevOps 등 모든 것을 설계하고 구축/유지보수까지하고 있는 오픈소스컨설팅 Playce Cloud팀 이성림입니다.
Public Cloud에서 편하게 사용할 수 있는 부분들을 Private Cloud는 모든 분야를 고려하여 원활한 서비스를 제공해야 합니다. 그 중에서 트러블슈팅하기 가장 까다로운 부분이 네트워크인데 오늘은 쿠버네티스의 가장 기본이 되는 POD 네트워크에 대해서 얘기해 보겠습니다.
저도 서비스 단에서 일하다가 쿠버네티스를 처음 접하고 가장 어려웠던 부분이 네트워크였기 때문에 쿠버네티스의 네트워크가 실제로 동작하는 것을 보여드리기 위해 주제를 선정하였습니다.
포스팅에 나오는 노드는 총 5대 이며, 역할 및 버전은 아래와 같으며, CNI 플러그인은 Calico를 사용합니다.
root@sung-ubuntu01:~# kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP …
sung-ubuntu01 Ready control-plane,master 18d v1.22.8 192.168.110.100
sung-ubuntu02 Ready control-plane,master 18d v1.22.8 192.168.110.101
sung-ubuntu03 Ready control-plane,master 18d v1.22.8 192.168.110.102
sung-ubuntu04 Ready 18d v1.22.8 192.168.110.103
sung-ubuntu05 Ready 18d v1.22.8 192.168.110.104
root@sung-ubuntu01:~# calicoctl version
Client Version: v3.20.3
Git commit: dcb4b76a
Cluster Version: v3.20.3
Cluster Type: kubespray,bgp,kubeadm,kdd,k8s
쿠버네티스 네트워크의 가장 기본인 파드 네트워크를 살펴보겠습니다.
[그림1]은 worker 노드에 하나의 파드가 존재하는 경우를 도식화 한 것입니다. 호스트 물리 네트워크 인터페이스(ens3)와 파드 인터페이스(veth0)가 터널 인터페이스(tunl0)를 통해 연결되어 있습니다.
도커 네트워크를 공부해 보신 분들은 이상한 점을 눈치 채셨나요?
도커 네트워크를 사용하게 되면 tunl0 인터페이스가 아닌 docker0 인터페이스를 사용하지만, Calico에서는 tunl0 이름의 인터페이스를 생성합니다.
쿠버네티스 클러스터가 준비되신 분들은 실습 예제를 따라서 같이 진행해보세요!
쿠버네티스 클러스터는 VMWare, VirtualBox로 VM 을 생성한 후 쿠버네티스 공식 홈페이지 가이드 대로 kubeadm을 이용하여 생성 할 수가 있으며, kubespray 또는 rancher 같은 툴을 사용할 수도 있습니다.
Playground를 제공하는 온라인 사이트에서는 환경이 다를 수 있습니다.
실습1
첫 번째 실습은 파드와 노드 인터페이스를 살펴보겠습니다.
ubuntu 20.04 이미지를 사용하는 파드를 생성합니다.
root@sung-ubuntu01:~/tmp# cat ubuntu.yaml
apiVersion: v1
kind: Pod
metadata:
name: ubuntu-test
spec:
containers:
- name: ubuntu
image: ubuntu:20.04
command: ["/bin/sleep", "3650d"]
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: /cache
name: cache-volume
volumes:
- name: cache-volume
emptyDir: {}
restartPolicy: Always
dnsConfig:
nameservers:
- 8.8.8.8
root@sung-ubuntu01:~/tmp# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE ...
ubuntu-test 1/1 Running 0 6m20s 10.233.99.1 sung-ubuntu04
ubuntu-test 파드가 정상적으로 생성이 되었는지 확인합니다.
제 파드는 sung-ubuntu04 노드에 생성이 되었네요. ubuntu-test 파드에 들어가서 인터페이스를 확인해 보겠습니다.
### ubuntu 컨테이너 접속
root@sung-ubuntu01:~/tmp# kubectl exec -it ubuntu-test -- bash
root@ubuntu-test:/#
### 패키지 설치
# apt update
# apt install -y net-tools iputils-ping
### 인터넷 연결 확인
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=54 time=39.6 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=54 time=38.1 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=54 time=38.8 ms
64 bytes from 8.8.8.8: icmp_seq=4 ttl=54 time=39.3 ms
### 네트워크 인터페이스 확인
# root@ubuntu-test:/# ifconfig -a
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1480
inet 10.233.99.1 netmask 255.255.255.255 broadcast 0.0.0.0
ether 06:55:84:5a:ac:6b txqueuelen 0 (Ethernet)
RX packets 5718 bytes 24026416 (24.0 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 3690 bytes 250168 (250.1 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1000 (Local Loopback)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
tunl0: flags=128<NOARP> mtu 1480
tunnel txqueuelen 1000 (IPIP Tunnel)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
ubuntu-test 파드 내 ubuntu 컨테이너에서 인터넷 접속이 되는 것과 IP가 10.233.99.1로 할당된 것을 확인하였습니다.
해당 파드가 위치한 sung-ubuntu04 노드의 인터페이스도 확인해 볼까요?
root@sung-ubuntu04:~# ifconfig -a
...
tunl0: flags=193<UP,RUNNING,NOARP> mtu 1480
inet 10.233.99.0 netmask 255.255.255.255
tunnel txqueuelen 1000 (IPIP Tunnel)
RX packets 60 bytes 8528 (8.5 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 66 bytes 4476 (4.4 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
파드와 호스트 eth0의 브릿지 역할을 하는 tunl0 인터페이스 대역이 10.233.99.0으로 설정되어있네요. 위에서 ubuntu-test 파드의 IP는 10.233.99.1 이었습니다.
상관관계를 파악하셨나요? sung-ubuntu04 노드도 [그림1]과 구조가 같다는 것을 생각해보세요.
제가 배포한 클러스터의 파드 IP 대역은 10.233.64.0/18을 사용합니다.
기본적으로 노드마다 IP의 3번째 Octet이 고유하게 할당되고 그 대역으로 파드가 생성이 됩니다. 네트워크 대역을 16비트로 할당했으면 보기가 편했겠지만 18비트로 나누었기 때문에 IP를 2진수로 변환하였을 때 18번째까지 네트워크 부분이고 뒤 14자리가 호스트 부분이 됩니다.
00001010.11101001.11000011.00000000
네트워크 부분은 고정이기 때문에 세 번째 Octet은 1100011(99)부터 11111111(255)까지 중에 255를 제외하고 156개 노드까지 한 개의 클러스터로 구성할 수 있습니다.
잠시 얘기가 경로를 이탈했지만, 호스트의 tunl0 인터페이스가 10.233.99.0이면 해당 호스트에 위치한 파드의 IP는 10.233.99.1 ~254중에 할당이 됩니다. ubuntu-test 파드는 10.233.99.1로 할당이 된 것을 다시 확인해보세요.
실습2
실습1에서 단일 컨테이너를 가지고 있는 파드와 해당 파드가 위치하고 있는 노드의 네트워크 인터페이스를 확인하였습니다.
이번에는 멀티 컨테이너를 가지고 있는 파드를 생성하여 확인해보도록 하겠습니다. 이번에 생성할 파드는 multi-container라는 이름이고 ubuntu, nginx 컨테이너를 포함하고 있습니다.
root@sung-ubuntu01:~/tmp# cat multi-con.yaml
apiVersion: v1
kind: Pod
metadata:
name: multi-container
spec:
containers:
- name: ubuntu
image: ubuntu:20.04
command: ["/bin/sleep", "3650d"]
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: /cache
name: cache-volume
- name: nginx
image: nginx
ports:
- containerPort: 80
volumes:
- name: cache-volume
emptyDir: {}
restartPolicy: Always
dnsConfig:
nameservers:
- 8.8.8.8
root@sung-ubuntu01:~/tmp# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE ...
multi-container 2/2 Running 0 25m 10.233.78.3 sung-ubuntu05
ubuntu-test 1/1 Running 0 57m 10.233.99.1 sung-ubuntu04
이번에 생성한 파드는 sung-ubuntu05 노드에 위치하고 있네요. 생성한 multi-container 파드 내 ubuntu, nginx 파드에 각각 접속하여 네트워크 인터페이스를 확인해 보세요
###ubuntu 컨테이너 접속
root@sung-ubuntu01:~/tmp# kubectl exec -it multi-container -c ubuntu -- bash
### POD 안에서
# apt update
# apt install -y net-tools iputils-ping
root@multi-container:/# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1480
inet 10.233.78.3 netmask 255.255.255.255 broadcast 0.0.0.0
ether ce:de:b3:90:c1:a7 txqueuelen 0 (Ethernet)
RX packets 5206 bytes 23989810 (23.9 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 3160 bytes 213900 (213.9 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1000 (Local Loopback)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
### nginx 컨테이너 접속
root@sung-ubuntu01:~/tmp# kubectl exec -it multi-container -c nginx -- bash
### POD 안에서
# apt update
# apt install -y net-tools iputils-ping
root@multi-container:/# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1480
inet 10.233.78.3 netmask 255.255.255.255 broadcast 0.0.0.0
ether ce:de:b3:90:c1:a7 txqueuelen 0 (Ethernet)
RX packets 6287 bytes 33013014 (31.4 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 3935 bytes 267591 (261.3 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1000 (Local Loopback)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
두 개의 컨테이너의 eth0인터페이스가 10.233.78.3 이라는 같은 IP를 가지고 있습니다.
그 이유는 [그림1]에서 알 수 있듯이 하나의 파드는 하나의 인터페이스를 가지고 내부에 있는 컨테이너들은 모두 인터페이스를 공유하기 때문입니다. 파드가 생성될 때 pause 컨테이너가 생성되며, pause 컨테이너를 부모 프로세스 삼아서 사용자가 생성한 컨테이너들이 하나의 인터페이스를 사용하도록 설정됩니다. 이렇게 되면 파드 내 컨테이너들은 localhost 통신으로 데이터를 주고 받을 수 있으며, 당연히 같은 포트를 사용하지 못합니다.
파드 내부의 인터페이스를 확인하였으니 다시 multi-container 파드가 위치한 sung-ubuntu05 파드의 인터페이스를 확인해 볼까요?
이번에는 내용을 자르지 않고 ifconfig 명령어 결과 전체로 확인해보겠습니다.
root@sung-ubuntu05:~# ifconfig -a
calib4cfe5eb958: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1480
inet6 fe80::ecee:eeff:feee:eeee prefixlen 64 scopeid 0x20<link>
ether ee:ee:ee:ee:ee:ee txqueuelen 0 (Ethernet)
RX packets 3935 bytes 267591 (267.5 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 6287 bytes 33013014 (33.0 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
docker0: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255
ether 02:42:6a:17:c5:80 txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
ens3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.110.104 netmask 255.255.0.0 broadcast 192.168.255.255
inet6 fe80::f816:3eff:fe54:bc4 prefixlen 64 scopeid 0x20<link>
ether fa:16:3e:54:0b:c4 txqueuelen 1000 (Ethernet)
RX packets 1353299 bytes 1304887824 (1.3 GB)
RX errors 0 dropped 88603 overruns 0 frame 0
TX packets 191206 bytes 20789350 (20.7 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
kube-ipvs0: flags=130<BROADCAST,NOARP> mtu 1500
inet 10.233.0.1 netmask 255.255.255.255 broadcast 0.0.0.0
ether 66:2d:b3:6c:50:9a txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 162061 bytes 22298211 (22.2 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 162061 bytes 22298211 (22.2 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
nodelocaldns: flags=130<BROADCAST,NOARP> mtu 1500
inet 169.254.25.10 netmask 255.255.255.255 broadcast 0.0.0.0
ether 16:84:53:46:fe:65 txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
tunl0: flags=193<UP,RUNNING,NOARP> mtu 1480 #터널 인터페이스
inet 10.233.78.0 netmask 255.255.255.255
tunnel txqueuelen 1000 (IPIP Tunnel)
RX packets 69 bytes 9380 (9.3 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 76 bytes 5125 (5.1 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
첫 번째로 cali로 시작하는 인터페이스는 [실습2]에서 생성한 multi-container 파드에서 사용하는 인터페이스입니다. 이 부분은 Calico에서 자동으로 생성하는 것으로 “그렇구나~’하고 이해해 주세요.
두 번째로 docker0 인터페이스는 배포된 쿠버네티스 클러스터의 런타임으로 도커를 사용하기 때문에 생성되었으며, 다른 런타임을 사용할 경우 생성되지 않습니다. 현재 쿠버네티스 클러스터에서는 사용하지 않습니다.
kube-ipvs0, nodelocaldns도 이번 포스팅 주제와는 맞지 않기 때문에 바로 tunl0인터페이스를 살펴보겠습니다. multi-container 파드 IP가 10.233.78.3 이었기 때문에 tunl0 인터페이스가 어떤 대역으로 설정되었는지는 이제 보지 않고도 파악이 가능하게 되었습니다.
실습1, 2의 내용을 종합해보면 파드의 IP를 알고 있으면 해당 파드가 어느 노드에 위치하고 있는지 알 수가 있습니다.
실습1, 2를 진행한 의도가 파악되시나요? 서로 다른 노드에 위치한 파드가 어떻게 통신이 되는지 설명하기 위해 오랜 시간이 걸렸네요. 이제 마무리 지을 실습을 해보도록 하겠습니다.
실습3
root@sung-ubuntu01:~/tmp# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE ...
multi-container 2/2 Running 0 25m 10.233.78.3 sung-ubuntu05
ubuntu-test 1/1 Running 0 57m 10.233.99.1 sung-ubuntu04
실습1,2에서 생성한 두 개의 파드는 다른 노드에 위치하고 있습니다.
sung-ubuntu04노드에 위치한 ubuntu-test 파드에서 sung-ubuntu05 노드에 위치한 multi-container 파드로 통신을 할 때 어떻게 찾아 갈 수 있을까요? ubuntu-test 파드에서 확인해보도록 하겠습니다.
#ubuntu-test
root@ubuntu-test:/# apt install traceroute
root@ubuntu-test:/# traceroute 10.233.78.3
traceroute to 10.233.78.3 (10.233.78.3), 30 hops max, 60 byte packets
1 192.168.110.103 (192.168.110.103) 0.202 ms 0.032 ms 0.028 ms #sung-ubuntu04 veth0
2 10.233.78.0 (10.233.78.0) 1.169 ms 0.990 ms 0.928 ms #sung-ubuntu05 tunl0
3 10.233.78.3 (10.233.78.3) 1.096 ms 1.111 ms 1.087 ms #multi-container IP
ubuntu-test 파드에서 multi-container로 traceroute 명령어를 실행하였습니다. 두 개의 파드는 서로 다른 IP 대역을 가지고 있는데 어떻게 찾아 갔는지 이해가 되시나요?
traceroute 명령어 결과 오른쪽에 주석으로 해당 IP 주석과 [그림2]를 같이 보시면 쉽게 이해가 됩니다.
파드와 호스트 인터페이스가 tunl0 인터페이스를 통한다는 것은 이미 위에서 설명을 하였습니다. 또한, 파드 IP를 알면 어느 호스트에 위치하는지도 알 수 있다고 하였습니다. ‘이미 알고있다’고 표현한 이유는 해당 작업을 CNI Plugin인 Calico에서 자동으로 OS의 route table에 설정을 해주기 때문입니다.
ubuntu-test 파드가 위치한 sung-ubuntu04 노드의 route 설정을 확인해 보겠습니다.
root@sung-ubuntu04:~# route
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
default _gateway 0.0.0.0 UG 100 0 0 ens3
10.233.78.0 sung-ubuntu05.c 255.255.255.0 UG 0 0 0 tunl0
10.233.91.0 sung-ubuntu02.c 255.255.255.0 UG 0 0 0 tunl0
10.233.95.0 sung-ubuntu01.c 255.255.255.0 UG 0 0 0 tunl0
10.233.99.0 0.0.0.0 255.255.255.0 U 0 0 0 *
10.233.99.1 0.0.0.0 255.255.255.255 UH 0 0 0 calie3df4d89b13
10.233.99.2 0.0.0.0 255.255.255.255 UH 0 0 0 calia85a668c715
10.233.112.0 sung-ubuntu03.c 255.255.255.0 UG 0 0 0 tunl0
169.254.169.254 192.168.51.110 255.255.255.255 UGH 100 0 0 ens3
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
192.168.0.0 0.0.0.0 255.255.0.0 U 0 0 0 ens3
root@sung-ubuntu04:~# cat /etc/hosts
...
# Ansible inventory hosts BEGIN
192.168.110.100 sung-ubuntu01.cluster.local sung-ubuntu01
192.168.110.101 sung-ubuntu02.cluster.local sung-ubuntu02
192.168.110.102 sung-ubuntu03.cluster.local sung-ubuntu03
192.168.110.103 sung-ubuntu04.cluster.local sung-ubuntu04
192.168.110.104 sung-ubuntu05.cluster.local sung-ubuntu05
route table을 보면 [traceroute 10.233.78.3] 명령어가 어디로 가야하는지 확인 할 수 있습니다. 이미 목적지인 10.233.78.3을 알고 있기 때문에 클러스터 내 특정 노드의 tunl0 인터페이스가 10.233.78.0으로 설정되어 있을 것을 Calico는 알고 있습니다. 10.233.78.0으로 가려면 Gateway 항목으로 가도록 설정되어 있습니다. 단어가 길어서 cluster 단어 일부가 안보이지만, sung-ubuntu05 노드로 가라고 정의가 되어 있습니다.
이제 [그림2]가 완전히 이해가 되시나요?
쿠버네티스 네트워크의 가장 기본인 파드 간 통신에 대해 잘 설명한 글이 많지만 실제 클러스터에서 동작하는 방법을 실습으로 구성하여 설명해 보았습니다.
이번 포스팅에서 설명한 내용을 이해하셨다면, 쿠버네티스 각 노드에서 파드로 트래픽을 전달하는 kube-proxy와 CNI 동작 방법에 대해서도 공부해 보기를 추천합니다.
감사합니다.
참고 및 추천 사이트
https://www.docker.com/products/container-runtime/
https://kubernetes.io/ko/docs/concepts/cluster-administration/networking/
https://projectcalico.docs.tigera.io/reference/architecture/data-path
https://coffeewhale.com/k8s/network/2019/04/19/k8s-network-01/