Kubernetes

[스터디] Kubernetes Container Networking 정리

mokpolar 2023. 3. 18. 12:55
반응형

PKOS2주차의 주제는 Networking 이다.

그 중에서도 Container networking 에 대해 정리해보려고 한다. 

Kubernetes networking의 고려사항

이 문서를 보면 어떤 고민들을 갖고 쿠버네티스의 네트워킹 모델이 등장했는지를 알 수 있다.

https://kubernetes.io/ko/docs/concepts/cluster-administration/networking/

 💡 쿠버네티스는 애플리케이션 간에 머신을 공유하는 것이 핵심입니다. 일반적으로 머신을 공유하려면 두 애플리케이션이 동일한 포트를 사용하려고 시도하지 않도록 해야 합니다. 여러 개발자가 포트를 조정하는 것은 대규모로 수행하기가 매우 어렵고 사용자가 제어할 수 없는 클러스터 수준의 문제에 노출될 수 있습니다.
동적 포트 할당은 모든 애플리케이션이 포트를 플래그로 사용해야 하고, API 서버가 동적 포트 번호를 구성 블록에 삽입하는 방법을 알아야 하며, 서비스가 서로를 찾는 방법을 알아야 하는 등 시스템에 많은 복잡성을 가져옵니다. 이를 처리하는 대신 Kubernetes는 다른 접근 방식을 취합니다.

쿠버네티스의 네트워크는 CNI (컨테이너 네트워크 인터페이스) 플러그인을 사용해서 네트워크를 관리한다.

Docker container 의 Bridge network

쿠버네티스 네트워크 모델을 살펴보기 전에 좀 더 간단한 구조를 보고 싶어서

쿠버네티스와는 다르지만 먼저 도커 컨테이너의 네트워크부터 살펴보았다.

아래는 도커 컨테이너의 bridge network에 대한 설명이다.

https://dockerlabs.collabnix.com/intermediate/DiffBridgeVsOverlay.html

💡 브리지 드라이버는 호스트 내부에 프라이빗 네트워크를 생성하여 이 네트워크의 컨테이너가 통신할 수 있도록 합니다. 외부 액세스는 컨테이너에 포트를 노출하여 부여됩니다. Docker는 서로 다른 Docker 네트워크 간의 연결을 차단하는 규칙을 관리하여 네트워크를 보호합니다.
백그라운드에서 Docker 엔진은 이러한 연결을 가능하게 하는 데 필요한 Linux 브리지, 내부 인터페이스, iptables 규칙 및 호스트 경로를 생성합니다. 아래 강조 표시된 예에서는 Docker 브리지 네트워크가 생성되고 두 개의 컨테이너가 여기에 연결됩니다.

 

출처 : https://dockerlabs.collabnix.com/intermediate/DiffBridgeVsOverlay.html

 

위 예시를 보면 애플리케이션이 8000번 포트에서 서비스되고 있다.

bridge network를 이용하면 우측의 web app이 컨테이너의 이름으로 좌측의 db와 통신할 수 있다.

bridge driver는 동일한 네트워크에 있기 때문에 자동으로 service discovery가 되는 것이다.

 

아래와 같이 docker network 를 보면 bridge가 존재하는 것을 볼 수 있다.

별다른 옵션이 없으면 컨테이너 생성시 이 브릿지 네트워크에 연결되는 것 같다.

sudo docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
46ab3bba2c79   bridge    bridge    local
d420a6c23cce   host      host      local
fe690a89937b   none      null      local

 

bridge를 확인해보면, 네트워크 대역을 확인할 수 있다.

이 범위 내에서 컨테이너의 IP 가 할당이 되는 것 같다.

sudo docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "46ab3bba2c7931414227340f5ce61816609f4ffb1251f62f773d0dad1ddee803",
        "Created": "2023-03-15T15:00:06.493599526Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },

아래처럼 인터페이스를 확인할 수 있다. 컨테이너간 연결을 위한 virtual ethernet이다.

ifconfig | grep docker
docker0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500

 

Docker Overlay network

https://dockerlabs.collabnix.com/intermediate/DiffBridgeVsOverlay.html

Overlay network는 두 개의 호스트 사이에 가상 네트워크를 만드는 데 사용된다.

가상인 이유는 기존 네트워크 위에 구축이 되기 때문이다.

또한 VXLAN 데이터 플레인을 통해 컨테이너 네트워크를 기본 물리적 네트워크에서 분리한다고 한다.

 

 

출처 : https://dockerlabs.collabnix.com/intermediate/DiffBridgeVsOverlay.html

 

이 예시에서는 포트 8000에서 여전히 서비스 하고 있지만 앱애 여러 호스트에 있는 구조이다.

오버레이를 통해 연결해서 이전과 같은 효과를 볼 수 있는 것 같다.

 

 

Kubernetes networking model

컨테이너간 통신은 저런 형태도 있을 수도 있다.. 라는 이해 위에 얹어서 다시 Kubernetes networking 을 이해하려고 시도한다.

Kubernetes networking model의 공식 문서에 가보면 아래와 같이 설명이 되어있다.

 

https://kubernetes.io/ko/docs/concepts/services-networking/

💡 클러스터의 모든 [파드](<https://kubernetes.io/ko/docs/concepts/workloads/pods/>)는 고유한 IP 주소를 갖는다. 이는 즉 파드간 연결을 명시적으로 만들 필요가 없으며 또한 컨테이너 포트를 호스트 포트에 매핑할 필요가 거의 없음을 의미한다. 이로 인해 포트 할당, 네이밍, 서비스 디스커버리, 로드 밸런싱, 애플리케이션 구성, 마이그레이션 관점에서 파드를 VM 또는 물리 호스트처럼 다룰 수 있는 깔끔하고 하위 호환성을 갖는 모델이 제시된다.
쿠버네티스는 모든 네트워킹 구현에 대해 다음과 같은 근본적인 요구사항을 만족할 것을 요구한다. (이를 통해, 의도적인 네트워크 분할 정책을 방지)
파드는 NAT 없이 노드 상의 모든 파드와 통신할 수 있다.노드 상의 에이전트(예: 시스템 데몬, kubelet)는 해당 노드의 모든 파드와 통신할 수 있다. 

 

또한 이 네 가지의 문제에 대응할 수 있어야 한다고 한다.

💡 쿠버네티스 네트워킹은 다음의 네 가지 문제를 해결한다.
* 파드 내의 컨테이너는 루프백(loopback)을 통한 네트워킹을 사용하여 통신한다.
* 클러스터 네트워킹은 서로 다른 파드 간의 통신을 제공한다.
* 서비스 리소스를 사용하면 파드에서 실행 중인 애플리케이션을 클러스터 외부에서 접근할 수 있다.
* 또한 서비스를 사용하여 서비스를 클러스터 내부에서만 사용할 수 있도록 게시할 수 있다. 
...

 

아래 문서에 이런 구조를 설명하는 굉장히 깔끔한 그림이 있어서 참고 했다.

https://opensource.com/article/22/6/kubernetes-networking-fundamentals

 

출처 : https://opensource.com/article/22/6/kubernetes-networking-fundamentals

 

먼저 Pod간 통신이 어떻게 일어나는 지에 대한 부분이다.

Pod간 통신을 위해서는 Pod의 네트워크 네임스페이스에서 루트 네트워크 네임스페이스를 타고 넘어가야 한다는 모습을 보여준다.

pod1의 eth0 → veth0 → virtual bridge → veth1 → pod2의 eth0

 

 

출처 : https://opensource.com/article/22/6/kubernetes-networking-fundamentals

 

그 다음은 클러스터 네트워크를 통해서 node1에서 node2로 어떻게 넘어가는지에 대한 부분이다.

앞서와 다르게 iptable/IPVS 가 들어가있는 모습이 보인다. 이건 클러스터 내 로드밸런싱을 어떤 모드로 할 건지에 따라 달라지는 것 같다.

 

CNI란?

CNCF의 프로젝트 중 하나로, Container Network Interface 라는 것이다.

깃허브의 공식 설명을 살펴보면,

 

https://github.com/containernetworking/cni

💡 CNCF프로젝트인 CNI(컨테이너 네트워크 인터페이스)는 리눅스 컨테이너에서 네트워크 인터페이스를 구성하기 위한 플러그인을 작성하기 위한 사양과 라이브러리, 그리고 지원되는 여러 플러그인으로 구성되어 있습니다. CNI는 컨테이너의 네트워크 연결과 컨테이너가 삭제될 때 할당된 리소스를 제거하는 것만 다룹니다. 이러한 초점 때문에 CNI는 광범위한 지원을 제공하며 사양을 구현하기가 간단합니다.

라고 되어있다.

 

CNI는 그러니까 네트워크 인터페이스 설정을 기술한 명세서이다.

실제 동작은 Calico, Flannel, Cilium 등 다양한 CNI 플러그인에 의해서 구현된다.

IP 할당관리를 수행하고, 파드 간 통신을 위한 라우팅 설정을 처리한다.

 

CNI가 동작하는 과정은 아래와 같다.

  • Pod 생성 : K8S에서 Pod가 생성되면, Pod 내의 컨테이너들은 동일한 네트워크 네임스페이스 안에서 실행된다. 이때 각 컨테이너에는 가상의 이더넷 인터페이스가 생성된다.
  • CNI 호출 : 각 컨테이너에 할당된 가상의 이더넷 인터페이스에 IP 주소를 할당하고, 다른 컨테이너와 통신할 수 있도록 네트워크를 설정하기 위해 CNI 가 호출된다.
  • CNI 플러그인 실행 : CNI 호출에 의해 실행되는 플러그인은 각 컨테이너에 IP 주소를 할당하고, 라우팅 정보를 설정하여 네트워크를 구성한다. 이 때, CNI는 CNI 구성 파일을 사용하여 설정 정보를 받아온다.
  • 네트워크 구성: CNI가 설정한 IP 주소와 라우팅 정보를 기반으로, 각 컨테이너는 다른 컨테이너나 호스트 시스템과 통신할 수 있게 된다. 이를 통해 Kubernetes 클러스터 내의 모든 컨테이너는 서로 통신이 가능해지며, 서비스나 외부 네트워크와도 통신할 수 있다.

CNI 플러그인은 컨테이너 런타임 대신 원하는 작업을 실행하고나서 실행 상태를 반환한다.

성공할 경우에는 0이 반환되는데, 조작한 IP, 라우트, DNS의 세부사항도 반환한다.

 

Calico 동작 확인

CNI의  종류에는 여러가지가 있는데 링크참고

Calico, Flannel, Cilium 등이 있고 AWS 를 사용하면 AWS VPC CNI가 있다.

 

그 중에서 설치되어있는 calico의 동작을 확인해보자.

calico pod들은 kube-system namespace에 배포되어 있다.

kubectl get pod -n kube-system
k get po -n kube-system | grep calico
calico-kube-controllers-8575b76f66-p9j2r   1/1     Running   2          3d10h
calico-node-2g5c2                          1/1     Running   0          3d9h
calico-node-946wl                          1/1     Running   0          3d9h
calico-node-dvtj4                          1/1     Running   0          2d20h
....

그리고 이건 daemonset이다.

kubectl get daemonset -A | grep calico
kube-system      calico-node                           19        19        19      19           19          kubernetes.io/os=linux                            4d12h

 

그럼 확인하고자 하는 노드에 nodeselector를 걸고 그 노드에 있는 calico의 로그를 확인해보자.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  selector:
    matchLabels:
      app: my-app
  replicas: 1
  template:
    metadata:
      labels:
        app: my-app
    spec:
      nodeSelector:
        xxxxx : xxxxxx
      containers:
      - name: my-app
        image: nginx
kubectl apply -f test-deploy.yaml

 

해당 노드의 Calico 에 접근해서 보면 로그들이 막 생성되면서 동작하고 있는 모습을 볼 수 있다. 

 

해당 pod 로 접근해서 확인해보자. 가상의 eth0에 붙어있는 ip이다.

kubectl exec my-app-7cd7745bcb-67gnp -- ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1480
        inet 10.233.98.65  netmask 255.255.255.255  broadcast 10.233.98.65
        ether 32:f2:e2:ff:b8:01  txqueuelen 0  (Ethernet)
        RX packets 638  bytes 9375832 (8.9 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 597  bytes 41469 (40.4 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

 

 

그리고 지워보자. 

kubectl delete -f test-deploy.yaml

마찬 가지로 여러가지 동작들이 일어나는 모습을 볼 수 있다. 

 

배포된 pod 의 ip와 해당 노드의 ip도 비교해보자. 

kubectl get node -o wide | grep node9
node9    Ready    <none>                 4d11h   v1.21.6   172.xx.xx.xx   <none>        Ubuntu 20.04.1 LTS   5.4.0-58-generic    docker://20.10.8
kubectl get pod -o wide | grep my
my-app-7cd7745bcb-67gnp           1/1     Running   0          5m21s   10.233.98.65   node9    <none>           <none>

 

AWS VPC CNI와의 비교

사실 스터디에서는 AWS 환경에서 작업을 수행 중이기 때문에 AWS VPC CNI를 사용하며 이와 비교한다.

아래 그림이 아주 깔끔하게 위에서의 동작 호출과 뭐가 다른지를 알 수 있을 것이다.

위에서 보면 노드 네트워크 대역과 pod 의 네트워크 대역이 다르다는 사실을 알 수 있다.

하지만 AWS VPC는 이것이 같다는 것 같다.

이렇게 노드와 파드의 네트워크 대역을 동일하게 설정하면 네트워크 통신의 최적화(성능, 지연)가 된다고 한다.

 

또한 pod간 통신 시 일반적으로 K8S CNI는 오버레이(VXLAN, IP-IP 등) 통신을 하고, AWS VPC CNI는 동일 대역으로 직접 통신을 한다고 한다.

 

Pod 간 통신 확인

테스트용 pod를 배포하고

cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: **Deployment**
metadata:
  name: netshoot-pod
spec:
  **replicas: 2**
  selector:
    matchLabels:
      app: netshoot-pod
  template:
    metadata:
      labels:
        app: netshoot-pod
    spec:
      containers:
      - name: netshoot-pod
        image: **nicolaka/netshoot**
        command: ["tail"]
        args: ["-f", "/dev/null"]
      terminationGracePeriodSeconds: 0
EOF

# 파드 이름 변수 지정
PODNAME1=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[0].metadata.name})
PODNAME2=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[1].metadata.name})

그리고 아래 처럼 Pod간 통신이 이루어지는지 확인할 수 있다.

# 파드1 Shell 에서 파드2로 ping 테스트
kubectl exec -it $PODNAME1 -- ping -c 2 $PODIP2
PING 10.233.108.33 (10.233.108.33) 56(84) bytes of data.
64 bytes from 10.233.108.33: icmp_seq=1 ttl=63 time=0.079 ms

64 bytes from 10.233.108.33: icmp_seq=2 ttl=63 time=0.017 ms

--- 10.233.108.33 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1023ms
rtt min/avg/max/mdev = 0.017/0.048/0.079/0.031 ms

# 파드2 Shell 에서 파드1로 ping 테스트
kubectl exec -it $PODNAME2 -- ping -c 2 $PODIP1
PING 10.233.108.34 (10.233.108.34) 56(84) bytes of data.
64 bytes from 10.233.108.34: icmp_seq=1 ttl=63 time=0.099 ms

64 bytes from 10.233.108.34: icmp_seq=2 ttl=63 time=0.023 ms

--- 10.233.108.34 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1011ms
rtt min/avg/max/mdev = 0.023/0.061/0.099/0.038 ms

해당 노드에서 tcpdump를 떠보면 아래와 같은 내용으로 pod 통신이 정상적으로 이루어지는 것을 확인할 수 있다.

sudo tcpdump -i any -nn icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked v1), capture size 262144 bytes
12:42:12.344554 IP 10.233.108.34 > 10.233.108.33: ICMP echo request, id 63346, seq 1, length 64
12:42:12.344638 IP 10.233.108.34 > 10.233.108.33: ICMP echo request, id 63346, seq 1, length 64
12:42:12.344657 IP 10.233.108.33 > 10.233.108.34: ICMP echo reply, id 63346, seq 1, length 64
12:42:12.344664 IP 10.233.108.33 > 10.233.108.34: ICMP echo reply, id 63346, seq 1, length 64
12:42:13.372521 IP 10.233.108.34 > 10.233.108.33: ICMP echo request, id 63346, seq 2, length 64
12:42:13.372543 IP 10.233.108.34 > 10.233.108.33: ICMP echo request, id 63346, seq 2, length 64
12:42:13.372551 IP 10.233.108.33 > 10.233.108.34: ICMP echo reply, id 63346, seq 2, length 64
12:42:13.372557 IP 10.233.108.33 > 10.233.108.34: ICMP echo reply, id 63346, seq 2, length 64

 

 

Reference

반응형