Docker, Kubernetes 네트워크
인터넷 검색을 하다보면 Docker, Kubernetes 네트워크에 관한 글이 많이 보인다. 기본적인 이론에서부터 응용까지 잘 설명된 글들이 꽤 많은데, 나는 눈에 보이는 상태를 한번 살펴보기로 했다.
Docker 네트워크
Docker 를 처음 설치하면 어떤 상태일까? 먼저 Docker 를 설치한 리눅스 시스템의 네트워크 상태는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
$ ip -c -br link lo UNKNOWN 00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP> enp0s3 UP 08:00:27:e3:f6:8b <BROADCAST,MULTICAST,UP,LOWER_UP> docker0 DOWN 02:42:be:90:93:52 <NO-CARRIER,BROADCAST,MULTICAST,UP> $ ip a 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 valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> 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: <NO-CARRIER,BROADCAST,MULTICAST,UP> 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 상태는 다음의 명령으로 확인이 가능하다.
1 2 3 4 5 6 |
$ 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 네트워크에 해당한다고 다음과 같이 확인해 볼수 있다.
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
$ 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 컨테이너를 하나 실행해 본다.
1 2 3 4 5 6 |
$ 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 가상의 인터페이스가 하나 붙는다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
]$ ip -c a 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 valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> 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: <BROADCAST,MULTICAST,UP,LOWER_UP> 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: <BROADCAST,MULTICAST,UP,LOWER_UP> 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 과는 무슨 상관일까?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ sudo docker exec 6d93a395f092 ip a 1: lo: <LOOPBACK,UP,LOWER_UP> 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: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> 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: <BROADCAST,MULTICAST,UP,LOWER_UP> 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 컨테이너의 네트워크 인터페이스를 말한다.
1 2 3 4 5 |
$ 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 |
그러니까 네트워크 인터페이스에 번호로 서로 연관성을 보여주고 있다는 걸 알수 있다.
1 2 3 |
$ ip -c link show type veth 9: veth46d3de6@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> 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 을 사용했다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$ 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 <none> <none> kube-system coredns-558bd4d5db-g97bd 1/1 Running 0 138m 10.31.0.3 ubuntu <none> <none> kube-system etcd-ubuntu 1/1 Running 0 138m 192.168.96.37 ubuntu <none> <none> kube-system kube-apiserver-ubuntu 1/1 Running 0 138m 192.168.96.37 ubuntu <none> <none> kube-system kube-controller-manager-ubuntu 1/1 Running 0 138m 192.168.96.37 ubuntu <none> <none> kube-system kube-flannel-ds-6bl8f 1/1 Running 0 16m 192.168.96.39 vknode <none> <none> kube-system kube-flannel-ds-k9cpz 1/1 Running 0 16m 192.168.96.37 ubuntu <none> <none> kube-system kube-proxy-6lz88 1/1 Running 0 138m 192.168.96.37 ubuntu <none> <none> kube-system kube-proxy-md4s7 1/1 Running 0 133m 192.168.96.39 vknode <none> <none> kube-system kube-scheduler-ubuntu 1/1 Running 0 138m 192.168.96.37 ubuntu <none> <none> |
정상적으로 설치되었다면 Flannel 이 Pod 로 올라온다. 더불어서 CoreDNS 도 정상으로 나온다.
한가지 쿠버네티스에 대해서 짚고 넘어가야 할게 있는데, 쿠버네티스는 마스터 노드라 불리는 Controller 와 워커 노드로 나뉜다. 적어도 2대의 호스트 서버가 필요하고 여기에 설치가 진행 된다.
Flannel 을 설치하고 nginx 파드(pod) 를 생성한 후에 워커 노드 상태다.
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 27 28 29 30 31 32 33 |
ip -c a 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 valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> 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: <NO-CARRIER,BROADCAST,MULTICAST,UP> 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: <BROADCAST,MULTICAST,UP,LOWER_UP> 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: <BROADCAST,MULTICAST,UP,LOWER_UP> 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: <BROADCAST,MULTICAST,UP,LOWER_UP> 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 다.
1 2 3 4 5 6 |
$ ip -c link show type bridge ip -c link show type bridge 3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> 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: <BROADCAST,MULTICAST,UP,LOWER_UP> 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 에 연결된 네트워크 인터페이스는 다음과 같이 확인할 수 있다.
1 2 3 |
$ ip -c link show master cni0 6: veth2b86d0e3@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> 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 인터페이스는 대체 무엇일까? 다음의 명령어로 확인해 보자.
1 2 3 4 |
ip -c -d link show flannel.1 4: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> 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 기반으로 호스트가 서로 다른 파드에 대한 연결을 만들어주게 되어 있다. 마스터 노드에서 다음의 명령어를 실행해 보자.
1 2 3 4 5 |
$ 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 보다) 훨씬 간단하다.