Docker, Kubernetes 네트워크

인터넷 검색을 하다보면 Docker, Kubernetes 네트워크에 관한 글이 많이 보인다. 기본적인 이론에서부터 응용까지 잘 설명된 글들이 꽤 많은데, 나는 눈에 보이는 상태를 한번 살펴보기로 했다.

Docker 네트워크

Docker 를 처음 설치하면 어떤 상태일까? 먼저 Docker 를 설치한 리눅스 시스템의 네트워크 상태는 다음과 같다.

$ ip -c -br link 
lo               UNKNOWN        00:00:00:00:00:00  
enp0s3           UP             08:00:27:e3:f6:8b  
docker0          DOWN           02:42:be:90:93:52 

$ ip a
1: lo:  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
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s3:  mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:e3:f6:8b brd ff:ff:ff:ff:ff:ff
    inet 192.168.96.39/20 brd 192.168.111.255 scope global dynamic noprefixroute enp0s3
       valid_lft 4524sec preferred_lft 4524sec
    inet6 fe80::d52d:e84a:d820:3cf/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: docker0:  mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:be:90:93:52 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever

docker0 라는 네트워크 인터페이스가 생성되면서 172.17.0.1/16 아이피가 할당되었다. 그리고 이 인터페이스는 Bridge 다.하지만 인터페이스는 DOWN 상태다. Docker 를 막 설치하고 난 후에 이런 모습이다.

Bridge 상태는 다음의 명령으로 확인이 가능하다.

$ nmcli connection show --active
NAME     UUID                                  TYPE      DEVICE  
enp0s3   9d696977-82ab-4f36-b2be-fccaf5bcee5c  ethernet  enp0s3  
docker0  fd2e456f-93ea-43cf-8fd7-46aefe11a4a4  bridge    docker0
$ bridge link show
$

nmcli 를 보면 TYPE 에 bridge 라고 나온다. 그리고 이 docker0 인터페이스는 docker 네트워크에서 Bridge 네트워크에 해당한다고 다음과 같이 확인해 볼수 있다.

$ sudo docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
11c5b343044e   bridge    bridge    local
7019c2852833   host      host      local
11786973bc6f   none      null      local
$ sudo docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "11c5b343044e1c1e84a1e0cd241b580bdae8e5cfffcce4428931f4fe553aa188",
        "Created": "2021-04-28T20:38:11.485806876+09:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "6d93a395f09203085ae7789d9b38825c4cbcfaa7b92cbcdeca1c567f1aa2fb74": {
                "Name": "doc1",
                "EndpointID": "15227e807ea614fd2adfa752470b776f8783157a48a738422a018fbb80de8342",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

“com.docker.network.bridge.name”: “docker0” 로 Docker Bridge 가 docker0 호스트 네트워크 인터페이스라는 걸 말해주고 있으며 여기에 붙은 Container 인 doc1 에 대한 정보를 보여주고 있다. doc1 컨테이너의 IP 는 172.17.0.2/16 이다.

bridge 명령어에서는 아무것도 안나온다. 이제 Docker 컨테이너를 하나 실행해 본다.

$ docker pull alpine
$ docker run -dit --name doc1 alpine ash
6d93a395f09203085ae7789d9b38825c4cbcfaa7b92cbcdeca1c567f1aa2fb74
$ sudo docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED         STATUS         PORTS     NAMES
6d93a395f092   alpine    "ash"     9 seconds ago   Up 7 seconds             doc1

이렇게 Docker 컨테이너를 하나 생성하면 docker0 브릿지 인터페이스는 Down -> Up 상태로 변경되며 여기에 veth 가상의 인터페이스가 하나 붙는다.

]$ ip -c a
1: lo:  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
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s3:  mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:e3:f6:8b brd ff:ff:ff:ff:ff:ff
    inet 192.168.96.39/20 brd 192.168.111.255 scope global dynamic noprefixroute enp0s3
       valid_lft 1577sec preferred_lft 1577sec
    inet6 fe80::d52d:e84a:d820:3cf/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: docker0:  mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:be:90:93:52 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:beff:fe90:9352/64 scope link 
       valid_lft forever preferred_lft forever
9: veth46d3de6@if8:  mtu 1500 qdisc noqueue master docker0 state UP group default 
    link/ether 3a:59:a3:03:e4:a9 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::3859:a3ff:fe03:e4a9/64 scope link 
       valid_lft forever preferred_lft forever

“veth46d3de6@if8” 가 보인다. 이것은 필시 doc1 컨테이너가 실행되면서 생성되었을게 분명하다. 이것과 doc1 과는 무슨 상관일까?

$ sudo docker exec 6d93a395f092 ip a
1: lo:  mtu 65536 qdisc noqueue state UNKNOWN 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
       valid_lft forever preferred_lft forever
8: eth0@if9:  mtu 1500 qdisc noqueue state UP 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
$ ip -c a
9: veth46d3de6@if8:  mtu 1500 qdisc noqueue master docker0 state UP group default 
    link/ether 3a:59:a3:03:e4:a9 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::3859:a3ff:fe03:e4a9/64 scope link 
       valid_lft forever preferred_lft forever

doc1 컨테이너의 네트워크 인터페이스 eth0 는 8번이다. 그리고 if9 로 인터페이스 9번을 가리키고 있다고 명시하고 있다. 아래 호스트 네트워크 인터페이스를 보면 veth 라고 나오는데, if8 로 인터페이스 8번을 가리고 있다. 인터페이스 8번은 doc1 컨테이너의 네트워크 인터페이스를 말한다.

$ sudo docker exec 6d93a395f092 route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         172.17.0.1      0.0.0.0         UG    0      0        0 eth0
172.17.0.0      *               255.255.0.0     U     0      0        0 eth0

그러니까 네트워크 인터페이스에 번호로 서로 연관성을 보여주고 있다는 걸 알수 있다.

$ ip -c link show type veth
9: veth46d3de6@if8:  mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default 
    link/ether 3a:59:a3:03:e4:a9 brd ff:ff:ff:ff:ff:ff link-netnsid 0

ip 명령어를 통해서 veth 인터페이스만 뽑아 볼 수 있다. veth 는 가상 이더넷(Virtual Ethernet) 인데, Docker 컨테이너가 생성될때마다 가상 이더넷 카드가 호스트에 하나 생성되고 이런 가상 이더넷은 Docker 컨테이너의 기본 네트워크 인터페이스 카드와 연결된다.

docker0 는 Docker 에 기본 Bridge 인터페이스이며 Docker 컨테이너가 하나도 없으면 DOWN 상태가 되며 단 하나의 컨테이너가 실행될 경우에 UP 상태로 변경되고 가상 이더넷을 생성하고 Bridge 에 붙이게 된다.

Kubernetes 네트워크

Kubernetes 를 설치하고 난후에 상태는 Docker 만 설치한 것과 동일하다. Kubernetes 는 CNI 를 설치를 해줘야 한다. CNI 는 Kubernetes 에 네트워크를 담당할 기능을 붙이기 위한 인터페이스로 다양한 네트워크 기능들을 제공하는 프로그램들이 있다.

Flannel, Weave, Calico 등등이 자주 쓰인다.

한가지 의문(?) 혹은 문제는 이 쿠버네티스 CNI 들은 구조가 모두 다르다. 테스트를 위해서 Flannel 을 사용했다.

$ kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml
Warning: policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
podsecuritypolicy.policy/psp.flannel.unprivileged created
clusterrole.rbac.authorization.k8s.io/flannel created
clusterrolebinding.rbac.authorization.k8s.io/flannel created
serviceaccount/flannel created
configmap/kube-flannel-cfg created
daemonset.apps/kube-flannel-ds created
$ kubectl get pod -o wide -A
NAMESPACE     NAME                             READY   STATUS    RESTARTS   AGE    IP              NODE     NOMINATED NODE   READINESS GATES
kube-system   coredns-558bd4d5db-5hhxr         1/1     Running   0          138m   10.31.0.2       ubuntu              
kube-system   coredns-558bd4d5db-g97bd         1/1     Running   0          138m   10.31.0.3       ubuntu              
kube-system   etcd-ubuntu                      1/1     Running   0          138m   192.168.96.37   ubuntu              
kube-system   kube-apiserver-ubuntu            1/1     Running   0          138m   192.168.96.37   ubuntu              
kube-system   kube-controller-manager-ubuntu   1/1     Running   0          138m   192.168.96.37   ubuntu              
kube-system   kube-flannel-ds-6bl8f            1/1     Running   0          16m    192.168.96.39   vknode              
kube-system   kube-flannel-ds-k9cpz            1/1     Running   0          16m    192.168.96.37   ubuntu              
kube-system   kube-proxy-6lz88                 1/1     Running   0          138m   192.168.96.37   ubuntu              
kube-system   kube-proxy-md4s7                 1/1     Running   0          133m   192.168.96.39   vknode              
kube-system   kube-scheduler-ubuntu            1/1     Running   0          138m   192.168.96.37   ubuntu              

정상적으로 설치되었다면 Flannel 이 Pod 로 올라온다. 더불어서 CoreDNS 도 정상으로 나온다.

한가지 쿠버네티스에 대해서 짚고 넘어가야 할게 있는데, 쿠버네티스는 마스터 노드라 불리는 Controller 와 워커 노드로 나뉜다. 적어도 2대의 호스트 서버가 필요하고 여기에 설치가 진행 된다.

Flannel 을 설치하고 nginx 파드(pod) 를 생성한 후에 워커 노드 상태다.

ip -c a
1: lo:  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
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s3:  mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:e3:f6:8b brd ff:ff:ff:ff:ff:ff
    inet 192.168.96.39/20 brd 192.168.111.255 scope global dynamic noprefixroute enp0s3
       valid_lft 5059sec preferred_lft 5059sec
    inet6 fe80::d52d:e84a:d820:3cf/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: docker0:  mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:e4:22:b1:58 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
4: flannel.1:  mtu 1450 qdisc noqueue state UNKNOWN group default 
    link/ether 82:14:e6:a1:21:43 brd ff:ff:ff:ff:ff:ff
    inet 10.31.1.0/32 brd 10.31.1.0 scope global flannel.1
       valid_lft forever preferred_lft forever
    inet6 fe80::8014:e6ff:fea1:2143/64 scope link 
       valid_lft forever preferred_lft forever
5: cni0:  mtu 1450 qdisc noqueue state UP group default qlen 1000
    link/ether 56:a6:dd:5f:19:b3 brd ff:ff:ff:ff:ff:ff
    inet 10.31.1.1/24 brd 10.31.1.255 scope global cni0
       valid_lft forever preferred_lft forever
    inet6 fe80::54a6:ddff:fe5f:19b3/64 scope link 
       valid_lft forever preferred_lft forever
6: veth2b86d0e3@if3:  mtu 1450 qdisc noqueue master cni0 state UP group default 
    link/ether 92:07:4d:10:b7:9b brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::9007:4dff:fe10:b79b/64 scope link 
       valid_lft forever preferred_lft forever

위 상태를 보면, cni0 네트워크 인터페이스가 보인다. 이는 Flannel 에서 생성한 것으로 Bridge 다.

$ ip -c link show type bridge
ip -c link show type bridge
3: docker0:  mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default 
    link/ether 02:42:e4:22:b1:58 brd ff:ff:ff:ff:ff:ff
5: cni0:  mtu 1450 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 56:a6:dd:5f:19:b3 brd ff:ff:ff:ff:ff:ff

위에 보면 Bridge 네트워크 인터페이스를 볼 수 있는데, Docker0 는 DOWN 이며 cni0 는 UP 상태다. 이 cni0 에 연결된 네트워크 인터페이스는 다음과 같이 확인할 수 있다.

$ ip -c link show master cni0
6: veth2b86d0e3@if3:  mtu 1450 qdisc noqueue master cni0 state UP mode DEFAULT group default 
    link/ether 92:07:4d:10:b7:9b brd ff:ff:ff:ff:ff:ff link-netnsid 0

이 인터페이스는 필히 nginx 파드에 것이다. master 브릿지로 cni0 를 사용하고 있다.

그러면, flannel.1 인터페이스는 대체 무엇일까? 다음의 명령어로 확인해 보자.

ip -c -d link show flannel.1
4: flannel.1:  mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default 
    link/ether 82:14:e6:a1:21:43 brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 68 maxmtu 65535 
    vxlan id 1 local 192.168.96.39 dev enp0s3 srcport 0 0 dstport 8472 nolearning ttl auto ageing 300 udpcsum noudp6zerocsumtx noudp6zerocsumrx addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535

“vxlan id 1 local 192.168.96.39 dev enp0s3” 로컬 호스트의 enp0s3 에 연결된 vxlan 이라고 나온다.

Flannel 은 VXLAN 기반으로 호스트가 서로 다른 파드에 대한 연결을 만들어주게 되어 있다. 마스터 노드에서 다음의 명령어를 실행해 보자.

$ kubectl describe node vknode | grep -A3 Annotations
Annotations:        flannel.alpha.coreos.com/backend-data: {"VNI":1,"VtepMAC":"82:14:e6:a1:21:43"}
                    flannel.alpha.coreos.com/backend-type: vxlan
                    flannel.alpha.coreos.com/kube-subnet-manager: true
                    flannel.alpha.coreos.com/public-ip: 192.168.96.39

backend-type: vxlan 이라고 나오고 있으며 VTEP 도 보인다. 이 장치에 대한 Mac 주소도 나오는데, 이것은 flannel.1 에 있는것과 같다.

파드의 네트워크는 veth 장치를 통해서 패킷이 나가고 이것을 cni0 브릿지가 받는다. 그리고 flannel 이 이것을 flannel.1 장치로 보내게 된다.

이렇게 하는 이유가 있는데, Flannel 은 L2 Layer 스위치다. L2 Layer 스위치는 ARP 라우팅만 가능하다. 파드(Pod) 가 같은 호스트에 있는 경우에는 ARP 라우팅만으로 서로 통신이 가능하겠지만 호스트가 다를 경우, IP 대역이 변경될 경우에는 원격 호스트에 있는 파드를 ARP 라우팅만으로 찾을 수 없다.

그래서 Flannel 은 L3 Layer 계층의 가상의 스위칭 장비를 만들고, 이렇게 만들어진 각 호스트의 가상의 스위칭을 하나로 연결하는데 이것이 바로 VXLAN 이다. 가상의 스위칭 장비가 flannel.1 네트워크 인터페이스 이다. 이때, cni0 에서 올라온 데이터는 L2 Layer 에서 만든 프레임(Frame)으로 이것을 UDP 패킷형태로 IP 를 붙여 캡슐화 한다. 그리면 flannel.1 인터페이스는 다른 호스트와 연결된 또 다른 flannel.1 인터페이스로 브로드캐스팅을 한다.

다른 호스트로 받은 UDP 패킷은 flannel.1 장치에 의해서 까지고(디캡슐화) 자신의 네트워크 대역과 비교하는데, 맞으면 받은 프레임에 destination mac address 를 자신의 mac address 로 넣고 다시 UDP로 캡슐화해 돌려보낸다. 이렇게 함으로써 호스트가 다른 파드의 이더넷 주소(맥주소)를 얻게되어 연결이 이루어지는데, 이게 VXLAN 의 동작 방법이다.

VXLAN 는 L2 Layer 프레임을 UDP L3 Layer 로 캡슐화해 브로드캐스팅하고 목적지 맥주소를 얻는데 있다.

Flannel 은 이렇게 작동하지만 Calico 는 또 다르다. Calico 는 BGP 연결을 통해서 아예 L3 Layer 를 구현 한다. 완전한 Pure L3 Switch 기능을 제공하기 때문에 처음부터 IP 라우팅이 가능해진다. Calico 는 Tunnel.0 인터페이스를 통해서 서로 다른 호스트와 Peer to Peer 연결되어 있어서 IP 라우팅만으로 대상 호스트의 파드를 알아낼 수 있다. 그래서 동작 방법은 (Flannel 보다) 훨씬 간단하다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다