안녕하세요.

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.10
4

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] POD 네트워크

쿠버네티스 네트워크의 가장 기본인 파드 네트워크를 살펴보겠습니다.

[그림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

[그림2] POD 간 네트워크
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/

https://ikcoo.tistory.com/10

이성림
Cloud Engineer

오픈소스컨설팅 Playce Cloud팀 소속으로 폭 넓은 지식과 깊이 있는 지식 모두 포기할 수 없는 이성림입니다. 하나 알았다 싶으면 다시 새로운 두 개가 나오는 세상에서 살아남아 보도록 하겠습니다.

Leave a Reply

Your email address will not be published. Required fields are marked *